defmodule Unicode.Set.Property do @moduledoc false # Canonical block lookup keyed by the fully normalized (downcased, separator # stripped) block name. `Unicode.Block.fetch/1` cannot resolve some blocks # whose canonical alias is absent from the dependency's alias table (e.g. # digit-bearing names such as "Latin-1 Supplement" -> "latin1supplement"), # because on an alias miss it looks the still-string name up against the # atom-keyed block map and fails. We rebuild a canonical name -> atom key map # directly from `Unicode.Block.blocks/0` so every real block resolves. @block_by_canonical_name Unicode.Block.blocks() |> Map.keys() |> Map.new(fn key -> {Unicode.Utils.downcase_and_remove_whitespace(key), key} end) # Of this list, only the following are unknown to Unicode # * xdigit # * word # * blank # * print # * alnum @doc false def fetch_property(:script_or_category, "alpha") do {:ok, Unicode.Set.parse!("\\p{Alphabetic}")} end def fetch_property(:script_or_category, "lower") do {:ok, Unicode.Set.parse!("\\p{Lowercase}")} end def fetch_property(:script_or_category, "upper") do {:ok, Unicode.Set.parse!("\\p{Uppercase}")} end # `punct` uses the UTS #18 Annex C "POSIX-compatible" definition # (`gc=Punctuation` + `gc=Symbol` - `alpha`, ~9,300 codepoints), which matches # ICU's POSIX `[:punct:]` and the behaviour most POSIX tooling expects. # # Tradeoff: this deliberately deviates from the UTS #18 *Standard* recommendation, # which is just `gc=Punctuation` (~850 codepoints). The POSIX definition includes # symbols such as `+ $ < = > ^ | ~` \`` that the Standard definition excludes. # We keep POSIX behaviour for ICU/POSIX compatibility; callers wanting the # Standard set should use `\p{gc=Punctuation}` explicitly. See the README # "Compatibility Property Names" section. def fetch_property(:script_or_category, "punct") do {:ok, Unicode.Set.parse!("[\\p{gc=Punctuation}\\p{gc=Symbol}-\\p{alpha}]")} end def fetch_property(:script_or_category, "digit") do {:ok, Unicode.Set.parse!("\\p{gc=Decimal_Number}")} end def fetch_property(:script_or_category, "xdigit") do {:ok, Unicode.Set.parse!("[\\p{gc=Decimal_Number}\\p{Hex_Digit}]")} end def fetch_property(:script_or_category, "alnum") do {:ok, Unicode.Set.parse!("[\\p{alpha}\\p{digit}]")} end def fetch_property(:script_or_category, "space") do {:ok, Unicode.Set.parse!("\\p{Whitespace}")} end def fetch_property(:script_or_category, "blank") do {:ok, Unicode.Set.parse!("[\\p{gc=Space_Separator}\\t]")} end def fetch_property(:script_or_category, "cntrl") do {:ok, Unicode.Set.parse!("\\p{gc=Control}")} end def fetch_property(:script_or_category, "print") do {:ok, Unicode.Set.parse!("[\\p{graph}\\p{blank}-\\p{cntrl}]")} end def fetch_property(:script_or_category, "word") do {:ok, Unicode.Set.parse!( "[\\p{alpha}\\p{gc=Mark}\\p{digit}\\p{gc=Connector_Punctuation}\\p{Join_Control}]" )} end def fetch_property(:script_or_category, "graph") do {:ok, Unicode.Set.parse!("[^\\p{Whitespace}\\p{gc=Control}\\p{gc=Surrogate}\\p{gc=Unassigned}]")} end # `LC` / `Cased_Letter` is the derived general-category group `Lu | Ll | Lt`. # The `unicode` library only derives the single-letter super-categories # (`L`, `C`, ...), so resolve this one here for both `\p{Cased_Letter}` and # `\p{gc=LC}`. Returns a flat range list (not a set) so both the bare and the # `gc=` reduction paths wrap it correctly. def fetch_property(:script_or_category, value) when value in ["lc", "cased_letter"] do {:ok, cased_letter_ranges()} end def fetch_property(:script_or_category, value) do range_list = Unicode.Script.get(value) || Unicode.GeneralCategory.get(value) || Unicode.Property.get(value) if range_list do {:ok, range_list} else {:error, "The unicode script, category or property #{inspect(value)} is not known"} end end @doc false # `\p{gc=LC}` / `\p{General_Category=Cased_Letter}` — the `LC` group category # (`Lu | Ll | Lt`), which the `unicode` library does not derive. def fetch_property(property, value) when property in ["gc", "general_category"] and value in ["lc", "cased_letter"] do {:ok, cased_letter_ranges()} end def fetch_property("block" = property, value) do case Unicode.Block.fetch(value) do {:ok, range_list} -> {:ok, range_list} :error -> normalized = Unicode.Utils.downcase_and_remove_whitespace(value) case Map.fetch(@block_by_canonical_name, normalized) do {:ok, block_key} -> Unicode.Block.fetch(block_key) :error -> {:error, "The unicode property #{inspect(property)} with value #{inspect(value)} is not known"} end end end def fetch_property(property, value) do with {:ok, module} <- Unicode.fetch_property(property), {:ok, range_list} <- module.fetch(value) do {:ok, range_list} else :error -> {:error, "The unicode property #{inspect(property)} with value #{inspect(value)} is not known"} end end def fetch_property!(property, value) do case fetch_property(property, value) do {:ok, range_list} -> range_list {:error, reason} -> raise Regex.CompileError, reason end end defp cased_letter_ranges do {:in, ranges} = Unicode.Set.parse_and_reduce!("[\\p{Lu}\\p{Ll}\\p{Lt}]").parsed ranges end @doc false # Resolution for the `Is` prefix: try the name as a script, general # category or binary property first, and only fall back to a block. Raises a # `Regex.CompileError` if the name matches none of them. def fetch_script_category_or_block(value) do case fetch_property(:script_or_category, value) do {:ok, result} -> result {:error, _reason} -> fetch_property!("block", value) end end @doc false # Resolution for a bare `\p{name}` / `[:name:]`. Tries the name as a script, # category or binary property; if that fails and the name has a Java-style # `In` prefix, tries the remainder as a block (so `\p{InBasicLatin}` resolves) # while leaving genuine `In...` scripts/properties (e.g. `Inherited`) alone, # since those already resolve at the script/category/property step. def fetch_script_category_or_in_block(value) do case fetch_property(:script_or_category, value) do {:ok, result} -> result {:error, reason} -> fetch_in_block_or_raise(value, reason) end end defp fetch_in_block_or_raise("in" <> block, reason) when block != "" do case fetch_property("block", block) do {:ok, ranges} -> ranges {:error, _} -> raise Regex.CompileError, reason end end defp fetch_in_block_or_raise(_value, reason) do raise Regex.CompileError, reason end end