diff --git a/lib/heroicons.ex b/lib/heroicons.ex index 5012d29..9087ae6 100644 --- a/lib/heroicons.ex +++ b/lib/heroicons.ex @@ -25,7 +25,19 @@ defmodule Heroicons do designed to be rendered at 24x24. """ - use Heroicons.Generator, icon_dir: "outline/" + use Heroicons.Generator, + icon_dir: "outline/", + # Following https://github.com/tailwindlabs/heroicons/blob/b933d51df1f27c35414389fea185e9bac0097481/svgo.24.outline.yaml + svg_opts: [ + remove_dimensions: true, + sort_attributes: true, + remove_attributes: ["stroke", "path:stroke-width"], + add_attributes: [ + {"stroke-widt", "1.5"}, + {"stroke", "currentColor"}, + {"aria-hidden", "true"} + ] + ] end defmodule Solid do @@ -36,7 +48,18 @@ defmodule Heroicons do designed to be rendered at 24x24. """ - use Heroicons.Generator, icon_dir: "solid/" + use Heroicons.Generator, + icon_dir: "solid/", + # Following https://github.com/tailwindlabs/heroicons/blob/b933d51df1f27c35414389fea185e9bac0097481/svgo.24.solid.yaml + svg_opts: [ + remove_dimensions: true, + sort_attributes: true, + remove_attributes: ["fill"], + add_attributes: [ + {"fill", "currentColor"}, + {"aria-hidden", "true"} + ] + ] end defmodule Mini do @@ -47,6 +70,17 @@ defmodule Heroicons do designed to be rendered at 20x20. """ - use Heroicons.Generator, icon_dir: "mini/" + use Heroicons.Generator, + icon_dir: "mini/", + # Following https://github.com/tailwindlabs/heroicons/blob/b933d51df1f27c35414389fea185e9bac0097481/svgo.20.solid.yaml + svg_opts: [ + remove_dimensions: true, + sort_attributes: true, + remove_attributes: ["fill"], + add_attributes: [ + {"fill", "currentColor"}, + {"aria-hidden", "true"} + ] + ] end end diff --git a/lib/heroicons/generator.ex b/lib/heroicons/generator.ex index 4ad02b9..4ab5dec 100644 --- a/lib/heroicons/generator.ex +++ b/lib/heroicons/generator.ex @@ -1,11 +1,14 @@ defmodule Heroicons.Generator do - defmacro __using__(icon_dir: icon_dir) do + alias Heroicons.SvgProcessor + + defmacro __using__(icon_dir: icon_dir, svg_opts: svg_opts) do icon_paths = Path.absname(icon_dir, :code.priv_dir(:heroicons)) |> Path.join("*.svg") |> Path.wildcard() require Phoenix.Component + if function_exported?(Phoenix.Component, :assigns_to_attributes, 2) do Module.put_attribute(__CALLER__.module, :assign_mod, Phoenix.Component) Module.put_attribute(__CALLER__.module, :assigns_to_attrs_mod, Phoenix.Component) @@ -15,20 +18,23 @@ defmodule Heroicons.Generator do end for path <- icon_paths do - generate(path) + generate(path, svg_opts) end end @doc false - def generate(path) do + def generate(path, svg_opts) do name = Path.basename(path, ".svg") |> String.replace("-", "_") |> String.to_atom() - icon = File.read!(path) - {i, _} = :binary.match(icon, ">") - {head, body} = String.split_at(icon, i) + icon = + File.read!(path) + |> SvgProcessor.process(svg_opts) + + <<"> = icon + head = " add_attributes(opts) + |> sort_attributes(opts) + + tag = {"svg", attributes, []} + {:ok, {[tag | stack], opts}} + end + + def handle_event(:start_element, {tag_name, attributes}, {stack, opts}) do + tag = {tag_name, attributes, []} + {:ok, {[tag | stack], opts}} + end + + @impl Saxy.Handler + def handle_event(:characters, chars, {stack, opts}) do + [{tag_name, attributes, content} | stack] = stack + + current = {tag_name, attributes, [chars | content]} + + {:ok, {[current | stack], opts}} + end + + @impl Saxy.Handler + def handle_event(:cdata, chars, {stack, opts}) do + [{tag_name, attributes, content} | stack] = stack + + current = {tag_name, attributes, [{:cdata, chars} | content]} + + {:ok, {[current | stack], opts}} + end + + @impl Saxy.Handler + def handle_event(:end_element, tag_name, {[{tag_name, attributes, content} | stack], opts}) do + current = {tag_name, attributes, Enum.reverse(content)} + + case stack do + [] -> + {:ok, {current, opts}} + + [parent | rest] -> + {parent_tag_name, parent_attributes, parent_content} = parent + parent = {parent_tag_name, parent_attributes, [current | parent_content]} + {:ok, {[parent | rest], opts}} + end + end + + @impl Saxy.Handler + def handle_event(:end_document, _, {stack, _opts}) do + {:ok, stack} + end + + defp filter_attributes(attributes, opts) do + remove_dimensions = Keyword.get(opts, :remove_dimensions) + remove_attrs = Keyword.get(opts, :remove_attributes, []) + + Enum.reject(attributes, fn {attr, _value} -> + (remove_dimensions && (attr == "width" || attr == "height")) || + attr in remove_attrs + end) + end + + defp add_attributes(attributes, opts) do + attributes ++ Keyword.get(opts, :add_attributes, []) + end + + defp sort_attributes(attributes, opts) do + if Keyword.get(opts, :sort_attributes) do + Enum.sort_by(attributes, fn {attr, _value} -> attr end) + else + attributes + end + end +end diff --git a/mix.exs b/mix.exs index 06c02da..16bc255 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule HeroiconsElixir.MixProject do def project do [ app: :heroicons, - version: "0.3.2", + version: "0.4.0", elixir: "~> 1.11", start_permanent: Mix.env() == :prod, deps: deps(), @@ -28,7 +28,8 @@ defmodule HeroiconsElixir.MixProject do [ {:phoenix_html, "~> 2.14 or ~> 3.0"}, {:phoenix_live_view, ">= 0.16.0", optional: true}, - {:ex_doc, "~> 0.23", only: :dev, runtime: false} + {:ex_doc, "~> 0.23", only: :dev, runtime: false}, + {:saxy, "~> 1.4"} ] end diff --git a/mix.lock b/mix.lock index 76956ca..ced9769 100644 --- a/mix.lock +++ b/mix.lock @@ -16,6 +16,7 @@ "plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"}, "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, "rustler": {:hex, :rustler, "0.21.1", "5299980be32da997c54382e945bacaa015ed97a60745e1e639beaf6a7b278c65", [:mix], [{:toml, "~> 0.5.2", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "6ee1651e10645b2b2f3bb70502bf180341aa058709177e9bc28c105934094bc6"}, + "saxy": {:hex, :saxy, "1.4.0", "c7203ad20001f72eaaad07d08f82be063fa94a40924e6bb39d93d55f979abcba", [:mix], [], "hexpm", "3fe790354d3f2234ad0b5be2d99822a23fa2d4e8ccd6657c672901dac172e9a9"}, "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, "toml": {:hex, :toml, "0.5.2", "e471388a8726d1ce51a6b32f864b8228a1eb8edc907a0edf2bb50eab9321b526", [:mix], [], "hexpm", "f1e3dabef71fb510d015fad18c0e05e7c57281001141504c6b69d94e99750a07"}, }