Current section

Files

Jump to
canonical_tailwind lib canonical_tailwind.ex
Raw

lib/canonical_tailwind.ex

defmodule CanonicalTailwind do
@moduledoc false
@behaviour Mix.Tasks.Format
@impl Mix.Tasks.Format
def features(_opts), do: [sigils: [:TW]]
@impl Mix.Tasks.Format
def format(contents, opts) do
# Leave a body with interpolation untouched: the sigil_TW macro rejects it
# at compile time, and we must not reorder tokens across a `#{...}`.
if String.contains?(contents, "\#{") do
contents
else
contents
|> canonicalize(opts)
|> preserve_heredoc_newline(opts[:opening_delimiter])
end
end
def render_attribute({_name, nil, _meta} = attr, _opts), do: attr
def render_attribute({name, {:string, value, meta}, attr_meta}, opts) do
{name, {:string, canonicalize(value, opts), meta}, attr_meta}
end
def render_attribute({name, {:expr, value, meta}, attr_meta}, opts) do
line_length = opts[:heex_line_length] || opts[:line_length] || 98
{name, {:expr, canonicalize_expr(value, opts, line_length), meta}, attr_meta}
end
defp canonicalize_expr(expr, opts, line_length) do
{quoted, comments} =
Code.string_to_quoted_with_comments!(expr,
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
token_metadata: true,
unescape: false
)
quoted
|> canonicalize_ast(opts)
|> Code.quoted_to_algebra(escape: false, comments: comments)
|> Inspect.Algebra.format(line_length)
|> IO.iodata_to_binary()
end
defp canonicalize_ast({:__block__, meta, [value]}, opts) when is_binary(value) do
case meta[:delimiter] do
delim when delim in ["\"", "\"\"\""] ->
canonicalized =
value
|> canonicalize(opts)
|> preserve_heredoc_newline(delim)
{:__block__, meta, [canonicalized]}
_ ->
{:__block__, meta, [value]}
end
end
defp canonicalize_ast({sigil, meta, [{:<<>>, bin_meta, [content]}, mods]}, opts)
when sigil in [:sigil_s, :sigil_S] and is_binary(content) do
canonicalized =
content
|> canonicalize(opts)
|> preserve_heredoc_newline(meta[:delimiter])
{sigil, meta, [{:<<>>, bin_meta, [canonicalized]}, mods]}
end
defp canonicalize_ast({:<<>>, meta, segments}, opts) do
segments =
Enum.map(segments, fn
seg when is_binary(seg) -> seg
interp -> canonicalize_ast(interp, opts)
end)
canonicalized =
segments
|> Enum.with_index()
|> Enum.map(fn
{binary, i} when is_binary(binary) ->
prev_interp? = i > 0 and not is_binary(Enum.at(segments, i - 1))
next_interp? = i < length(segments) - 1 and not is_binary(Enum.at(segments, i + 1))
canonicalize_segment(binary, prev_interp?, next_interp?, opts)
{interp, _i} ->
interp
end)
{:<<>>, meta, canonicalized}
end
defp canonicalize_ast({left, right}, opts) do
{canonicalize_ast(left, opts), canonicalize_ast(right, opts)}
end
defp canonicalize_ast({form, meta, args}, opts) when is_list(args) do
{form, meta, Enum.map(args, &canonicalize_ast(&1, opts))}
end
defp canonicalize_ast(list, opts) when is_list(list) do
Enum.map(list, &canonicalize_ast(&1, opts))
end
defp canonicalize_ast(other, _opts), do: other
defp preserve_heredoc_newline(value, delim) when delim in ["\"\"\"", "'''"] do
if String.ends_with?(value, "\n"), do: value, else: value <> "\n"
end
defp preserve_heredoc_newline(value, _delim), do: value
defp canonicalize_segment(binary, prev_interp?, next_interp?, opts) do
words = String.split(binary)
if words == [] do
binary
else
{prefix, words, suffix} = split_boundary_words(binary, words, prev_interp?, next_interp?)
canonicalized = canonicalize(Enum.join(words, " "), opts)
leading = if String.match?(binary, ~r/^\s/), do: " ", else: ""
trailing = if String.match?(binary, ~r/\s$/), do: " ", else: ""
parts =
Enum.reject(
[prefix, if(canonicalized != "", do: canonicalized), suffix],
&is_nil/1
)
leading <> Enum.join(parts, " ") <> trailing
end
end
defp split_boundary_words(binary, words, prev_interp?, next_interp?) do
ignore_first = prev_interp? and not String.match?(binary, ~r/^\s/)
ignore_last = next_interp? and not String.match?(binary, ~r/\s$/)
{prefix, words} =
if ignore_first, do: {hd(words), tl(words)}, else: {nil, words}
{words, suffix} =
if ignore_last and words != [],
do: {Enum.drop(words, -1), List.last(words)},
else: {words, nil}
{prefix, words, suffix}
end
defp canonicalize(class_string, opts) do
if String.trim(class_string) == "" do
class_string
else
class_string
|> String.replace("\n", " ")
|> CanonicalTailwind.Canonicalizer.canonicalize(opts)
end
end
end