Skip to content

Commit d89ad59

Browse files
committed
Release v0.3.2
1 parent 0990da8 commit d89ad59

5 files changed

Lines changed: 135 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

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+
39
## v0.3.1
410

511
- Fix manifest decoding for Mix tasks when persisted icon field atoms are not loaded yet

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Add the dependency:
3838
```elixir
3939
def deps do
4040
[
41-
{:phoenix_iconify, "~> 0.1.0"}
41+
{:phoenix_iconify, "~> 0.3.2"}
4242
]
4343
end
4444
```

lib/phoenix_iconify/scanner.ex

Lines changed: 115 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ defmodule PhoenixIconify.Scanner do
2222
content
2323
|> tokenize_heex("nofile")
2424
|> icons_from_tokens()
25+
|> Enum.uniq()
2526
end
2627

2728
defp source_paths do
28-
["lib/**/*.ex", "lib/**/*.heex"]
29+
["lib/**/*.ex", "lib/**/*.heex", "priv/**/*.heex"]
2930
|> Enum.flat_map(&Path.wildcard/1)
3031
end
3132

@@ -49,11 +50,18 @@ defmodule PhoenixIconify.Scanner do
4950

5051
defp scan_ex(content) do
5152
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)
5354
{:error, _} -> []
5455
end
5556
end
5657

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+
5765
defp heex_sigil_sources(ast) do
5866
{_ast, sources} =
5967
Macro.prewalk(ast, [], fn
@@ -69,10 +77,29 @@ defmodule PhoenixIconify.Scanner do
6977
end
7078

7179
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
7299
state = Tokenizer.init(0, path, content, Phoenix.LiveView.HTMLEngine)
73100

74101
{tokens, _cont} =
75-
Tokenizer.tokenize(content, [line: 1, column: 1], [], {:text, :enabled}, state)
102+
Tokenizer.tokenize(content, [line: line, column: 1], [], {:text, :enabled}, state)
76103

77104
tokens
78105
end
@@ -85,28 +112,108 @@ defmodule PhoenixIconify.Scanner do
85112

86113
defp icon_from_token({:local_component, "icon", attrs, _meta}) do
87114
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)
89122
|> List.wrap()
90123
end
91124

92125
defp icon_from_token(_token), do: []
93126

94-
defp name_attr({"name", {:string, name, _meta}, _attr_meta}) do
127+
defp icon_name_attr({"name", {:string, name, _meta}, _attr_meta}) do
95128
normalize_name(name)
96129
end
97130

98-
defp name_attr({"name", {:expr, expr, _meta}, _attr_meta}) do
131+
defp icon_name_attr({"name", {:expr, expr, _meta}, _attr_meta}) do
99132
case Code.string_to_quoted(expr) do
100133
{:ok, name} when is_binary(name) -> normalize_name(name)
101134
_ -> nil
102135
end
103136
end
104137

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
106196

107197
defp normalize_name("hero-" <> _rest = name), do: PhoenixIconify.normalize_name(name)
108198

109199
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 [?-, ?_]
111218
end
112219
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule PhoenixIconify.MixProject do
22
use Mix.Project
33

4-
@version "0.3.1"
4+
@version "0.3.2"
55
@source_url "https://github.com/elixir-volt/phoenix_iconify"
66

77
def project do

test/phoenix_iconify/scanner_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ defmodule PhoenixIconify.ScannerTest do
3535
assert "heroicons:sun-20-solid" in icons
3636
end
3737

38+
test "finds literal icon strings passed through component attributes" do
39+
content = ~s(<.nav_item icon="lucide:messages-square">Sessions</.nav_item>)
40+
icons = Scanner.scan_heex_content(content)
41+
assert "lucide:messages-square" in icons
42+
end
43+
44+
test "recovers same-line icons when the surrounding HEEx cannot be tokenized as a whole" do
45+
content = ~s(<%= if true do %>\n<.icon name="lucide:circle-check" />\n<% end %>)
46+
icons = Scanner.scan_heex_content(content)
47+
assert "lucide:circle-check" in icons
48+
end
49+
3850
test "ignores icons without prefix:name format" do
3951
content = ~s(<.icon name="just-a-name" />)
4052
icons = Scanner.scan_heex_content(content)

0 commit comments

Comments
 (0)