Current section

8 Versions

Jump to

Compare versions

5 files changed
+124 additions
-11 deletions
  @@ -1,5 +1,11 @@
1 1 # Changelog
2 2
3 + ## v0.3.2
4 +
5 + - Improve compile-time icon discovery for wrapper component `icon` attributes and same-line recoverable HEEx inside EEx blocks
6 + - Discover literal icon names returned by icon helper functions
7 + - Include `priv/**/*.heex` templates in scanner source paths
8 +
3 9 ## v0.3.1
4 10
5 11 - Fix manifest decoding for Mix tasks when persisted icon field atoms are not loaded yet
  @@ -38,7 +38,7 @@ Add the dependency:
38 38 ```elixir
39 39 def deps do
40 40 [
41 - {:phoenix_iconify, "~> 0.1.0"}
41 + {:phoenix_iconify, "~> 0.3.2"}
42 42 ]
43 43 end
44 44 ```
  @@ -1,7 +1,7 @@
1 1 {<<"links">>,
2 2 [{<<"GitHub">>,<<"https://github.com/elixir-volt/phoenix_iconify">>}]}.
3 3 {<<"name">>,<<"phoenix_iconify">>}.
4 - {<<"version">>,<<"0.3.1">>}.
4 + {<<"version">>,<<"0.3.2">>}.
5 5 {<<"description">>,
6 6 <<"Phoenix components for Iconify icons with compile-time discovery">>}.
7 7 {<<"elixir">>,<<"~> 1.14">>}.
  @@ -22,10 +22,11 @@ defmodule PhoenixIconify.Scanner do
22 22 content
23 23 |> tokenize_heex("nofile")
24 24 |> icons_from_tokens()
25 + |> Enum.uniq()
25 26 end
26 27
27 28 defp source_paths do
28 - ["lib/**/*.ex", "lib/**/*.heex"]
29 + ["lib/**/*.ex", "lib/**/*.heex", "priv/**/*.heex"]
29 30 |> Enum.flat_map(&Path.wildcard/1)
30 31 end
31 32
  @@ -49,11 +50,18 @@ defmodule PhoenixIconify.Scanner do
49 50
50 51 defp scan_ex(content) do
51 52 case Code.string_to_quoted(content) do
52 - {:ok, ast} -> ast |> heex_sigil_sources() |> Enum.flat_map(&scan_heex_content/1)
53 + {:ok, ast} -> scan_ast(ast)
53 54 {:error, _} -> []
54 55 end
55 56 end
56 57
58 + defp scan_ast(ast) do
59 + heex_icons = ast |> heex_sigil_sources() |> Enum.flat_map(&scan_heex_content/1)
60 + icon_function_icons = ast |> icon_function_string_literals()
61 +
62 + heex_icons ++ icon_function_icons
63 + end
64 +
57 65 defp heex_sigil_sources(ast) do
58 66 {_ast, sources} =
59 67 Macro.prewalk(ast, [], fn
  @@ -69,10 +77,29 @@ defmodule PhoenixIconify.Scanner do
69 77 end
70 78
71 79 defp tokenize_heex(content, path) do
80 + do_tokenize_heex(content, path)
81 + rescue
82 + Phoenix.LiveView.Tokenizer.ParseError -> tokenize_heex_lines(content, path)
83 + end
84 +
85 + defp tokenize_heex_lines(content, path) do
86 + content
87 + |> String.split("\n")
88 + |> Enum.with_index(1)
89 + |> Enum.flat_map(fn {line, line_number} ->
90 + try do
91 + do_tokenize_heex(line, path, line_number)
92 + rescue
93 + Phoenix.LiveView.Tokenizer.ParseError -> []
94 + end
95 + end)
96 + end
97 +
98 + defp do_tokenize_heex(content, path, line \\ 1) do
72 99 state = Tokenizer.init(0, path, content, Phoenix.LiveView.HTMLEngine)
73 100
74 101 {tokens, _cont} =
75 - Tokenizer.tokenize(content, [line: 1, column: 1], [], {:text, :enabled}, state)
102 + Tokenizer.tokenize(content, [line: line, column: 1], [], {:text, :enabled}, state)
76 103
77 104 tokens
78 105 end
  @@ -85,28 +112,108 @@ defmodule PhoenixIconify.Scanner do
85 112
86 113 defp icon_from_token({:local_component, "icon", attrs, _meta}) do
87 114 attrs
88 - |> Enum.find_value(&name_attr/1)
115 + |> Enum.find_value(&icon_name_attr/1)
116 + |> List.wrap()
117 + end
118 +
119 + defp icon_from_token({:local_component, _name, attrs, _meta}) do
120 + attrs
121 + |> Enum.find_value(&component_icon_attr/1)
89 122 |> List.wrap()
90 123 end
91 124
92 125 defp icon_from_token(_token), do: []
93 126
94 - defp name_attr({"name", {:string, name, _meta}, _attr_meta}) do
127 + defp icon_name_attr({"name", {:string, name, _meta}, _attr_meta}) do
95 128 normalize_name(name)
96 129 end
97 130
98 - defp name_attr({"name", {:expr, expr, _meta}, _attr_meta}) do
131 + defp icon_name_attr({"name", {:expr, expr, _meta}, _attr_meta}) do
99 132 case Code.string_to_quoted(expr) do
100 133 {:ok, name} when is_binary(name) -> normalize_name(name)
101 134 _ -> nil
102 135 end
103 136 end
104 137
105 - defp name_attr(_attr), do: nil
138 + defp icon_name_attr(_attr), do: nil
139 +
140 + defp component_icon_attr({"icon", {:string, name, _meta}, _attr_meta}) do
141 + normalize_name(name)
142 + end
143 +
144 + defp component_icon_attr({"icon", {:expr, expr, _meta}, _attr_meta}) do
145 + case Code.string_to_quoted(expr) do
146 + {:ok, name} when is_binary(name) -> normalize_name(name)
147 + _ -> nil
148 + end
149 + end
150 +
151 + defp component_icon_attr(_attr), do: nil
152 +
153 + defp icon_function_string_literals(ast) do
154 + {_ast, icons} =
155 + Macro.prewalk(ast, [], fn
156 + {kind, _meta, [{name, _name_meta, args}, [do: body]]} = node, icons
157 + when kind in [:def, :defp] and is_atom(name) and is_list(args) ->
158 + if icon_function?(name) do
159 + {node, string_literals(body) ++ icons}
160 + else
161 + {node, icons}
162 + end
163 +
164 + {kind, _meta, [{name, _name_meta, args}, body]} = node, icons
165 + when kind in [:def, :defp] and is_atom(name) and is_list(args) ->
166 + if icon_function?(name) do
167 + {node, string_literals(body) ++ icons}
168 + else
169 + {node, icons}
170 + end
171 +
172 + node, icons ->
173 + {node, icons}
174 + end)
175 +
176 + icons
177 + |> Enum.map(&normalize_name/1)
178 + |> Enum.reject(&is_nil/1)
179 + end
180 +
181 + defp icon_function?(name) do
182 + name
183 + |> Atom.to_string()
184 + |> String.contains?("icon")
185 + end
186 +
187 + defp string_literals(ast) do
188 + {_ast, strings} =
189 + Macro.prewalk(ast, [], fn
190 + string, strings when is_binary(string) -> {string, [string | strings]}
191 + node, strings -> {node, strings}
192 + end)
193 +
194 + strings
195 + end
106 196
107 197 defp normalize_name("hero-" <> _rest = name), do: PhoenixIconify.normalize_name(name)
108 198
109 199 defp normalize_name(name) do
110 - if String.contains?(name, ":"), do: name
200 + if valid_iconify_name?(name), do: name
201 + end
202 +
203 + defp valid_iconify_name?(name) when is_binary(name) do
204 + case String.split(name, ":", parts: 2) do
205 + [prefix, icon] -> valid_name_part?(prefix) and valid_name_part?(icon)
206 + _ -> false
207 + end
208 + end
209 +
210 + defp valid_iconify_name?(_name), do: false
211 +
212 + defp valid_name_part?(part) do
213 + part != "" and part |> String.to_charlist() |> Enum.all?(&valid_name_character?/1)
214 + end
215 +
216 + defp valid_name_character?(character) do
217 + character in ?a..?z or character in ?A..?Z or character in ?0..?9 or character in [?-, ?_]
111 218 end
112 219 end
  @@ -1,7 +1,7 @@
1 1 defmodule PhoenixIconify.MixProject do
2 2 use Mix.Project
3 3
4 - @version "0.3.1"
4 + @version "0.3.2"
5 5 @source_url "https://github.com/elixir-volt/phoenix_iconify"
6 6
7 7 def project do