/
tailwind_formatter.ex
150 lines (124 loc) · 4.41 KB
/
tailwind_formatter.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
defmodule TailwindFormatter do
@external_resource "README.md"
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
alias TailwindFormatter.{Order, HEExTokenizer}
@behaviour Mix.Tasks.Format
@placeholder "💧"
def features(_opts) do
[sigils: [:H], extensions: [".heex"]]
end
def format(contents, _opts) do
contents
|> HEExTokenizer.tokenize()
|> Enum.reduce([contents], fn
{:tag, _name, attrs, _meta}, contents ->
Enum.reduce(attrs, contents, fn
{"class", class_attr, _meta}, [remainder | acc] ->
[attr, remainder] = String.split(remainder, old_classes(class_attr), parts: 2)
[remainder, sort_classes(class_attr), attr | acc]
_, contents ->
contents
end)
_, contents ->
contents
end)
|> Enum.reverse()
|> Enum.join()
end
defp old_classes({_type, classes, _meta}), do: classes
defp sort_classes({:string, classes, _meta}), do: sort(classes)
defp sort_classes({:expr, expr_class, _meta}) do
expr_class
|> Code.string_to_quoted!(literal_encoder: &{:ok, {:__block__, &2, [&1]}})
|> sort_expr()
|> Code.quoted_to_algebra()
|> Inspect.Algebra.format(:infinity)
|> IO.iodata_to_binary()
end
defp sort_expr({:<<>>, meta, children}), do: {:<<>>, meta, handle_interpolation(children)}
defp sort_expr({a, b, c}), do: {sort_expr(a), sort_expr(b), sort_expr(c)}
defp sort_expr({a, b}), do: {sort_expr(a), sort_expr(b)}
defp sort_expr(list) when is_list(list), do: Enum.map(list, &sort_expr/1)
defp sort_expr(text) when is_binary(text), do: sort(text)
defp sort_expr(node), do: node
defp handle_interpolation(children) do
{classes_with_placeholders, {placeholder_map, _index}} =
Enum.map_reduce(children, {%{}, 0}, fn
str, acc when is_binary(str) ->
{str, acc}
node, {placeholder_map, index} ->
{"#{@placeholder}#{index}#{@placeholder}",
{Map.put(placeholder_map, "#{index}", sort_expr(node)), index + 1}}
end)
classes_with_placeholders
|> Enum.reduce("", fn class, acc ->
if placeholder?(class) or String.starts_with?(class, "-"),
do: acc <> class,
else: "#{acc} #{class}"
end)
|> sort()
|> String.split()
|> weave_in_code(placeholder_map)
end
defp weave_in_code(classes, placeholder_map) do
classes
|> Enum.map(fn class ->
if placeholder?(class) do
[prefix, index, suffix] = String.split(class, @placeholder)
[prefix, Map.fetch!(placeholder_map, index), suffix]
else
class
end
end)
|> Enum.intersperse(" ")
|> List.flatten()
end
defp sort_variant_chains(classes) do
classes
|> String.split()
|> Enum.map(fn class ->
class
|> String.split(":")
|> Enum.sort_by(&variant_position/1, :desc)
|> Enum.join(":")
end)
end
defp sort(classes) when is_binary(classes) do
leading_space = if classes =~ ~r/\A\s/, do: " "
trailing_space = if classes =~ ~r/\s\z/, do: " "
classes =
classes
|> sort_variant_chains()
|> sort()
|> Enum.join(" ")
Enum.join([leading_space, classes, trailing_space])
end
defp sort([]), do: []
defp sort(class_list) when is_list(class_list) do
{variants, base_classes} = Enum.split_with(class_list, &variant?/1)
Enum.sort_by(base_classes, &class_position/1) ++ sort_variant_classes(variants)
end
defp placeholder?(class), do: String.contains?(class, @placeholder)
defp variant?(class), do: String.contains?(class, ":")
defp prose?(class), do: String.contains?(class, "prose")
defp class_position(class),
do: if(placeholder?(class), do: -1_000_000, else: Map.get(Order.classes(), class, -1))
# prose variant order matters, thus push to front
defp variant_position(variant),
do: if(prose?(variant), do: 0, else: Map.get(Order.variants(), variant, -1))
defp sort_variant_classes(variants) do
variants
|> group_by_first_variant()
|> Enum.sort_by(fn {variant, _rest} -> variant_position(variant) end)
|> Enum.map(fn {variant, rest} -> {variant, sort(rest)} end)
|> Enum.flat_map(fn {variant, rest} -> Enum.map(rest, &"#{variant}:#{&1}") end)
end
defp group_by_first_variant(variants) do
variants
|> Enum.map(&String.split(&1, ":", parts: 2))
|> Enum.group_by(&List.first/1, &List.last/1)
end
end