Return to generating the icons from the filesystem

This commit is contained in:
Max Veytsman
2022-09-01 16:37:50 -04:00
parent b5200596c4
commit 0fef953625
9 changed files with 95 additions and 37363 deletions

View File

@ -0,0 +1,80 @@
defmodule Heroicons.Generator do
defmacro __using__(icon_dir: icon_dir) 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)
else
Module.put_attribute(__CALLER__.module, :assign_mod, Phoenix.LiveView)
Module.put_attribute(__CALLER__.module, :assigns_to_attrs_mod, Phoenix.LiveView.Helpers)
end
for path <- icon_paths do
generate(path)
end
end
@doc false
def generate(path) do
name =
Path.basename(path, ".svg")
|> String.replace("-", "_")
|> String.to_atom()
doc = """
![](assets/#{Path.relative_to(path, :code.priv_dir(:heroicons))}) {: width=24px}
## Examples
Use as a `Phoenix.Component`
<.#{name} />
<.#{name} class="h-6 w-6 text-gray-500" />
or as a function
<%= #{name}() %>
<%= #{name}(class: "h-6 w-6 text-gray-500") %>
"""
quote do
@doc unquote(doc)
def unquote(name)(assigns_or_opts \\ [])
def unquote(name)(var!(assigns)) when is_map(var!(assigns)) do
var!(attrs) = @assigns_to_attrs_mod.assigns_to_attributes(var!(assigns))
var!(assigns) = @assign_mod.assign(var!(assigns), :attrs, var!(attrs))
EEx.compile_string("<svg {@attrs}" <> Heroicons.IconCache.icon_body(unquote(path)),
engine: Phoenix.LiveView.HTMLEngine,
file: __ENV__.file,
line: __ENV__.line + 1,
module: __ENV__.module,
indentation: 0,
assigns: var!(assigns)
)
end
def unquote(name)(opts) when is_list(opts) do
attrs =
for {k, v} <- opts do
safe_k =
k |> Atom.to_string() |> String.replace("_", "-") |> Phoenix.HTML.Safe.to_iodata()
safe_v = v |> Phoenix.HTML.Safe.to_iodata()
{:safe, [?\s, safe_k, ?=, ?", safe_v, ?"]}
end
{:safe,
[
"<svg",
Phoenix.HTML.Safe.to_iodata(attrs),
" ",
Heroicons.IconCache.icon_body(unquote(path))
]}
end
end
end
end

View File

@ -0,0 +1,12 @@
defmodule Heroicons.IconCache do
@doc "Get's an icon's body from the filesystem"
# TODO implement ETS-based caching & benchmark
def icon_body(path) do
icon = File.read!(path)
<<"<svg ", body::binary>> = icon
body
end
end

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
defmodule Mix.Heroicons.GeneratorHelpers do
alias Mix.Heroicons.SvgProcessor
def icon_name(path) do
Path.basename(path, ".svg")
|> String.replace("-", "_")
|> String.to_atom()
end
def icon_body(path, svg_opts) do
icon =
File.read!(path)
|> SvgProcessor.process(svg_opts)
<<"<svg ", body::binary>> = icon
body
end
end

View File

@ -1,20 +0,0 @@
defmodule Mix.Heroicons.SvgProcessor do
alias Mix.Heroicons.SvgProcessor.Handler
@moduledoc """
An SVG parser loosly based on https://github.com/svg/svgo
## Options
Currently supports the following options:
* `:remove_dimensions` - remove the `width` and `height` attributes. Defaults to false.
* `:sort_attributes` - sort the svg attributes by name. Default to false.
* `:remove_attributes` - list of attributes to remove
* `:add_attributes` - list of `{"name", "value"}` pairs of attributes to add
"""
def process(svg, opts \\ []) do
{:ok, stack} = Saxy.parse_string(svg, Handler, {[], opts})
Saxy.encode!(stack)
end
end

View File

@ -1,107 +0,0 @@
defmodule Mix.Heroicons.SvgProcessor.Handler do
@moduledoc false
@behaviour Saxy.Handler
@impl Saxy.Handler
def handle_event(:start_document, _prolog, {stack, opts}) do
{:ok, {stack, opts}}
end
@impl Saxy.Handler
def handle_event(:start_element, {"svg", attributes}, {stack, opts}) do
attributes =
remove_dimensions(attributes, Keyword.get(opts, :remove_dimensions))
|> remove_attributes(Keyword.get(opts, :remove_attributes))
|> add_attributes(Keyword.get(opts, :add_attributes))
|> sort_attributes(Keyword.get(opts, :sort_attributes))
tag = {"svg", attributes, []}
{:ok, {[tag | stack], opts}}
end
def handle_event(:start_element, {"path", attributes}, {stack, opts}) do
attributes = remove_attributes(attributes, Keyword.get(opts, :remove_path_attributes, []))
tag = {"path", 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 remove_dimensions(attributes, true) do
Enum.reject(attributes, fn {attr, _value} ->
attr == "width" || attr == "height"
end)
end
defp remove_dimensions(attributes, _) do
attributes
end
defp remove_attributes(attributes, nil) do
attributes
end
defp remove_attributes(attributes, remove_attrs) do
Enum.reject(attributes, fn {attr, _value} ->
attr in remove_attrs
end)
end
defp add_attributes(attributes, nil) do
attributes
end
defp add_attributes(attributes, add_attrs) do
attributes ++ add_attrs
end
defp sort_attributes(attributes, true) do
Enum.sort_by(attributes, fn {attr, _value} -> attr end)
end
defp sort_attributes(attributes, nil) do
attributes
end
end

View File

@ -1,88 +0,0 @@
defmodule Mix.Tasks.Heroicons.Generate do
use Mix.Task
import Mix.Heroicons.GeneratorHelpers
@shortdoc "Generate heroicons"
@icon_sets [
%{
module: Heroicons.Outline,
path: "lib/heroicons/outline.ex",
moduledoc:
"Outline style icons drawn with a stroke, packaged as Phoenix Components.\n\n For primary navigation and marketing sections, with an outlined appearance,\n designed to be rendered at 24x24.",
icon_dir: "icons/outline/",
# Following https://github.com/tailwindlabs/heroicons/blob/b933d51df1f27c35414389fea185e9bac0097481/svgo.24.outline.yaml
svg_opts: [
remove_dimensions: true,
sort_attributes: true,
remove_attributes: ["stroke"],
add_attributes: [
{"stroke", "currentColor"}
]
]
},
%{
module: Heroicons.Solid,
path: "lib/heroicons/solid.ex",
moduledoc:
"Solid style icons drawn with fills, packaged as Phoenix Components.\n\n For primary navigation and marketing sections, with a filled appearance,\n designed to be rendered at 24x24.",
icon_dir: "icons/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"}
]
]
},
%{
module: Heroicons.Mini,
path: "lib/heroicons/mini.ex",
moduledoc:
"Solid style icons drawn with fills, packaged as Phoenix Components.\n\n For smaller elements like buttons, form elements, and to support text,\n designed to be rendered at 20x20.",
icon_dir: "icons/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"}
]
]
}
]
@impl Mix.Task
def run(_args) do
for %{
module: module,
path: path,
moduledoc: moduledoc,
icon_dir: icon_dir,
svg_opts: svg_opts
} <-
@icon_sets do
icon_paths =
Path.absname(icon_dir, :code.priv_dir(:heroicons))
|> Path.join("*.svg")
|> Path.wildcard()
Mix.Generator.create_file(
path,
EEx.eval_file(
"priv/templates/icon_set.ex",
[
module: module,
moduledoc: moduledoc,
icon_paths: icon_paths,
svg_opts: svg_opts
],
functions: __ENV__.functions
)
)
end
end
end