Current section
Files
Jump to
Current section
Files
lib/twemoji.ex
defmodule Twemoji do
@moduledoc """
Documentation for `Twemoji`.
"""
@twemoji_cdn "https://twemoji.maxcdn.com/2/72x72/"
# TODO: custom settings for cdn/format (e.g., svg emoji instead of png, or selfhosted/alt cdn)
@spec parse(String.t()) :: String.t()
def parse(input) do
input
|> parse_as_ast()
|> case do
{:ok, ast} -> Floki.raw_html(ast)
{:error, _} -> input
end
end
@spec parse_as_ast(String.t()) :: {:ok, Floki.html_tree()} | {:error, String.t()}
def parse_as_ast(input) do
with {:ok, ast} <- Floki.parse_fragment(input),
p_ast <- process_ast(ast),
p when is_list(p) <- List.flatten(p_ast) do
{:ok, p}
else
{:error, _} = e -> e
end
end
defp process_ast([_ | _] = ast) do
Enum.map(ast, fn
{tag, attrs, children} ->
{tag, attrs, children |> process_ast() |> List.flatten()}
data when is_binary(data) ->
data
|> Emojix.scan()
|> case do
[] ->
data
_ = emoji ->
emoji
|> Enum.map(fn em ->
%{
point: String.downcase(em.hexcode),
description: em.description,
re: Regex.compile!(em.unicode)
}
end)
|> Enum.map(&process_emoji(&1, data))
end
end)
end
defp process_ast([]), do: []
defp process_emoji(emoji, input) do
input
|> String.split(emoji.re, include_captures: true, trim: true)
|> Enum.map(fn part ->
cond do
Regex.match?(emoji.re, part) ->
{"img",
[
{"draggable", "false"},
{"class", "twemoji"},
{"alt", emoji.description},
{"aria-label", emoji.description},
{"src", to_cdn(emoji.point)}
], []}
true ->
part
end
end)
end
defp to_cdn(point, ext \\ ".png"), do: @twemoji_cdn <> point <> ext
end