defmodule FLHook.XMLText do
@moduledoc """
The XML text module allows composing formatted text to be sent via a chat
command.
"""
require Bitwise
alias FLHook.Utils
defstruct chardata: ""
@char_map %{
"<" => "<",
">" => ">",
"&" => "&"
}
@flags %{
bold: 1,
italic: 2,
underline: 4,
big: 8,
big_wide: 16,
very_big: 32,
smoothest: 64,
smoother: 128,
small: 144
}
@type t :: %__MODULE__{chardata: IO.chardata()}
@type color ::
{red :: non_neg_integer, green :: non_neg_integer,
blue :: non_neg_integer}
| String.t()
@type flag ::
:bold
| :italic
| :underline
| :big
| :big_wide
| :very_big
| :smoothest
| :smoother
| :small
@doc """
Creates a new XML text struct with the specified content.
"""
@spec new([
{:format, color}
| {:format, color, flag | [flag]}
| {:text, String.Chars.t()}
| String.Chars.t()
]) :: t
def new(nodes \\ []) do
Enum.reduce(nodes, %__MODULE__{}, fn
{:format, color}, xml_text ->
format(xml_text, color)
{:format, color, flags}, xml_text ->
format(xml_text, color, flags)
{:text, text}, xml_text ->
text(xml_text, text)
text, xml_text ->
text(xml_text, text)
end)
end
@doc """
Adds a format node the specified XML text struct. You can specify a color and
optional format flags.
"""
@spec format(t, color, flag | [flag]) :: t
def format(%__MODULE__{} = xml_text, color, flags \\ []) do
color = build_color(color)
flags = build_flags(flags)
node = ~s()
%{xml_text | chardata: [xml_text.chardata, node]}
end
@doc """
Adds a text node to the specified XML text struct.
"""
@spec text(t, String.Chars.t()) :: t
def text(%__MODULE__{} = xml_text, text) do
text = text |> Kernel.to_string() |> Utils.map_chars(@char_map)
node = "#{text}"
%{xml_text | chardata: [xml_text.chardata, node]}
end
@doc """
Converts the XML text struct to a string.
"""
@spec to_string(t) :: String.t()
def to_string(%__MODULE__{} = xml_text) do
IO.chardata_to_string(xml_text.chardata)
end
defp build_flags(flags) do
flags
|> List.wrap()
|> Enum.reduce(0, fn flag, value ->
case Map.fetch(@flags, flag) do
{:ok, flag_value} ->
Bitwise.bor(value, flag_value)
:error ->
raise ArgumentError, "invalid format flag (#{inspect(flag)})"
end
end)
|> to_hex()
end
defp build_color(color) do
color
|> normalize_color()
|> Enum.map_join(&to_hex/1)
end
defguardp is_rgb_value(value) when value in 0..255
defp normalize_color({red, green, blue})
when is_rgb_value(red) and is_rgb_value(green) and is_rgb_value(blue) do
[blue, green, red]
end
defp normalize_color(<<"#", code::binary-size(6)>>) do
normalize_color(code)
end
defp normalize_color(<<"#", code::binary-size(3)>>) do
normalize_color(code)
end
defp normalize_color(
<>
) do
Enum.map([blue, green, red], &String.to_integer(&1, 16))
end
defp normalize_color(
<>
) do
[blue, green, red]
|> Enum.map(&String.duplicate(&1, 2))
|> Enum.map(&String.to_integer(&1, 16))
end
defp normalize_color(_) do
raise ArgumentError, "invalid RGB color"
end
defp to_hex(value) do
value
|> Integer.to_string(16)
|> String.upcase()
|> String.pad_leading(2, "0")
end
defimpl String.Chars do
defdelegate to_string(xml_text), to: FLHook.XMLText
end
end