defmodule Unicode.Set do @moduledoc File.read!("README.md") |> String.split("") |> Enum.at(1) import NimbleParsec import Unicode.Set.Parser alias Unicode.Set.{Operation, Transform} defstruct [:set, :parsed] @doc """ Parses a Unicode Set binary into an internal AST-like representation ## Example iex> Unicode.Set.parse("[[:Zs:]]") {:ok, %#{inspect __MODULE__}{ set: "[[:Zs:]]", parsed: [ in: [ {32, 32}, {160, 160}, {5760, 5760}, {8192, 8202}, {8239, 8239}, {8287, 8287}, {12288, 12288} ] ] }} """ defparsec( :parse_one, parsec(:one_set) |> eos() ) @doc false defparsec( :parse_many, parsec(:one_set) |> ignore(optional(whitespace())) |> repeat(parsec(:one_set)) |> eos() ) @doc false @dialyzer {:nowarn_function, one_set: 1} defparsec(:one_set, unicode_set()) def parse(unicode_set) do case parse_one(unicode_set) do {:ok, parsed, "", _, _, _} -> {:ok, struct(__MODULE__, [set: unicode_set, parsed: parsed])} {:error, message, rest, _, _, _} -> {:error, parse_error(unicode_set, message, rest)} end end def parse!(unicode_set) do case parse(unicode_set) do {:ok, result} -> result {:error, {exception, reason}} -> raise exception, reason end end @doc """ Parses a unicode set and expands the set expressions then compacts the character ranges. """ def parse_and_expand(unicode_set) do with {:ok, parsed} <- parse(unicode_set) do {:ok, Operation.expand(parsed)} end end @doc """ Returns a boolean based upon whether `var` matches the provided `unicode_set`. ## Arguments * `var` is any integer variable (since codepoints are integers) * `unicode_set` is a binary representation of a unicode set. An exception will be raised if `unicode_set` is not a compile time binary ## Returns `true` or `false` ## Examples * `Unicode.match?/2` can be used in as `defguard` argument. For example: defguard is_lower(codepoint) when Unicode.Set.match?(codepoint, "[[:Lu:]]") * Or as a guard clause itself: def my_function(<< codepoint :: utf8, _rest :: binary>>) #=> when Unicode.Set.match?(codepoint, "[[:Lu:]]") """ defmacro match?(var, unicode_set) do assert_binary_parameter!(unicode_set) parse!(unicode_set) |> Operation.expand() |> Operation.traverse(var, &Transform.guard_clause/3) end def pattern(unicode_set) when is_binary(unicode_set) do with {:ok, parsed} <- parse(unicode_set) do parsed |> Operation.expand() |> Operation.traverse(&Transform.pattern/3) end end def compile_pattern(unicode_set) when is_binary(unicode_set) do with pattern when is_list(pattern) <- pattern(unicode_set) do :binary.compile_pattern(pattern) end end def utf8_char(unicode_set) when is_binary(unicode_set) do with {:ok, parsed} <- parse(unicode_set) do parsed |> Operation.expand() |> Operation.traverse(&Transform.utf8_char/3) end end def character_class(unicode_set) when is_binary(unicode_set) do with {:ok, parsed} <- parse(unicode_set) do parsed |> Operation.expand() |> Operation.traverse(&Transform.character_class/3) |> Enum.join end end def character_class!(unicode_set) when is_binary(unicode_set) do case character_class(unicode_set) do {:error, {exception, reason}} -> raise exception, reason class -> class end end defp assert_binary_parameter!(unicode_set) do unless is_binary(unicode_set) do raise ArgumentError, "unicode_set must be a compile-time binary. Found #{inspect(unicode_set)}" end end defp parse_error(unicode_set, message, "") do {Unicode.Set.ParseError, "Unable to parse #{inspect unicode_set}. " <> "#{message}." } end defp parse_error(unicode_set, message, rest) do {Unicode.Set.ParseError, "Unable to parse #{inspect unicode_set}. " <> "#{message}. Detected at #{inspect rest}." } end end