Current section
8 Versions
Jump to
Current section
8 Versions
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 |