Packages

Pure-Elixir implementation of UTS #46 (IDNA 2008 Compatibility Processing) with Punycode (RFC 3492), bidi (RFC 5893) and CONTEXTJ joiner rules.

Current section

Files

Jump to
unicode_idna lib unicode idna context.ex
Raw

lib/unicode/idna/context.ex

defmodule Unicode.IDNA.Context do
@moduledoc """
RFC 5892 Appendix A *CONTEXTJ* rules for the join-control
characters Zero Width Non-Joiner (U+200C) and Zero Width Joiner
(U+200D).
These rules govern when a join control may appear in a label
based on the surrounding code points' `Joining_Type` and
`Canonical_Combining_Class` properties.
"""
alias Unicode.JoiningType
@zwnj 0x200C
@zwj 0x200D
@virama 9
@doc """
Validates a label against the CONTEXTJ rules for ZWJ and ZWNJ.
### Arguments
* `label` is a `t:String.t/0` representing one already-mapped
domain label.
### Returns
* `:ok` if every join control in the label is in a permitted
context (or if the label contains no join controls).
* `{:error, :context}` otherwise.
### Examples
iex> Unicode.IDNA.Context.validate("hello")
:ok
iex> Unicode.IDNA.Context.validate("hello\\u200Cworld")
{:error, :context}
"""
@spec validate(String.t()) :: :ok | {:error, :context}
def validate(label) when is_binary(label) do
codepoints = String.to_charlist(label)
walk(codepoints, [])
end
defp walk([], _before), do: :ok
defp walk([@zwnj | after_cps], before_reversed) do
if zwnj_allowed?(before_reversed, after_cps) do
walk(after_cps, [@zwnj | before_reversed])
else
{:error, :context}
end
end
defp walk([@zwj | after_cps], before_reversed) do
if zwj_allowed?(before_reversed) do
walk(after_cps, [@zwj | before_reversed])
else
{:error, :context}
end
end
defp walk([cp | after_cps], before_reversed) do
walk(after_cps, [cp | before_reversed])
end
# RFC 5892 A.1 — ZWNJ allowed if:
# * Canonical_Combining_Class(Before(cp)) == Virama, OR
# * label matches (L|D) T* ZWNJ T* (R|D) around the join control.
defp zwnj_allowed?(before_reversed, after_cps) do
previous_is_virama?(before_reversed) or zwnj_joiner_context?(before_reversed, after_cps)
end
defp previous_is_virama?([]), do: false
defp previous_is_virama?([prev | _]) do
Unicode.CanonicalCombiningClass.combining_class(prev) == @virama
end
defp zwnj_joiner_context?(before_reversed, after_cps) do
has_left_joiner?(before_reversed) and has_right_joiner?(after_cps)
end
defp has_left_joiner?(before_reversed) do
case Enum.drop_while(before_reversed, &(JoiningType.joining_type(&1) == :t)) do
[cp | _] -> JoiningType.joining_type(cp) in [:l, :d]
[] -> false
end
end
defp has_right_joiner?(after_cps) do
case Enum.drop_while(after_cps, &(JoiningType.joining_type(&1) == :t)) do
[cp | _] -> JoiningType.joining_type(cp) in [:r, :d]
[] -> false
end
end
# RFC 5892 A.2 — ZWJ allowed only if Canonical_Combining_Class(Before(cp)) == Virama.
defp zwj_allowed?(before_reversed) do
previous_is_virama?(before_reversed)
end
end