Last active
October 17, 2020 13:37
-
-
Save lnr0626/20714c185d79dfdc8795726a64ad8061 to your computer and use it in GitHub Desktop.
A module for handling svg icons both with and without surface
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule HeroiconWithSurface do | |
@moduledoc """ | |
This assumes https://github.com/tailwindlabs/heroicons has been cloned as a submodule to svgs/heroicons | |
Examples: | |
<#HeroiconWithSurface variant="outline" icon="phone" class="w-5 h-5" /> | |
<%= HeroiconWithSurface.svg({"outline", "phone"}, class: "w-5 h-5") %> | |
""" | |
use SvgIcons, | |
path: ["svgs/heroicons", {:variant, [:outline, :solid]}, :icon] | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule HeroiconWithoutSurface do | |
@moduledoc """ | |
This assumes https://github.com/tailwindlabs/heroicons has been cloned as a submodule to svgs/heroicons | |
Examples: | |
<%= HeroiconWithoutSurface.svg({"outline", "phone"}, class: "w-5 h-5") %> | |
""" | |
use SvgIcons, | |
path: ["svgs/heroicons", {:variant, [:outline, :solid]}, :icon], | |
surface: false | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule SvgIcons do | |
@moduledoc """ | |
This module is used to Handle creating modules for SVG icons using macros to insert the | |
icons at compile time. This allows using inline svgs without having to maintain the svgs | |
inline. | |
""" | |
defmacro __using__(opts) do | |
path_parts = Keyword.get(opts, :path) | |
extension = Keyword.get(opts, :ext, ".svg") | |
path_sep = Keyword.get(opts, :path_sep, "/") | |
base_dir = Keyword.get(opts, :base_dir, Path.dirname(__CALLER__.file)) | |
surface = Keyword.get(opts, :surface, true) | |
pattern_parts = collect_pattern_parts(path_parts) | |
regex = | |
((pattern_parts | |
|> Enum.map(fn {_, pattern, _, _} -> pattern end) | |
|> Enum.join(path_sep)) <> Regex.escape(extension)) | |
|> Regex.compile!() | |
capture_names = | |
pattern_parts | |
|> Enum.map(fn {name, _, _, _} -> name end) | |
|> Enum.reject(&is_nil/1) | |
capture_name_strings = Enum.map(capture_names, &to_string/1) | |
files = | |
((pattern_parts | |
|> Enum.map(fn {_, _, wildcard, _} -> wildcard end) | |
|> Enum.join(path_sep)) <> extension) | |
|> Path.expand(base_dir) | |
|> Path.wildcard() | |
|> Enum.sort() | |
props = | |
for {name, _, _, default} <- pattern_parts do | |
quote do | |
prop(unquote({name, [], Elixir}), :string, default: unquote(default)) | |
end | |
end | |
defaults = | |
for {name, _, _, default} <- pattern_parts, into: %{} do | |
{name, default} | |
end | |
surface_code = | |
quote do | |
@default_props unquote(Macro.escape(defaults)) | |
use Surface.MacroComponent | |
unquote(props) | |
prop(id, :string) | |
prop(class, :string) | |
prop(opts, :keyword, default: []) | |
def expand(attributes, _children, meta) do | |
props = Surface.MacroComponent.eval_static_props!(__MODULE__, attributes, meta.caller) | |
id = | |
unquote(capture_names) | |
|> Enum.map(fn name -> props[name] || Map.get(@default_props, name) end) | |
|> List.to_tuple() | |
class = props[:class] || "" | |
opts = props[:opts] || [] | |
attrs = | |
opts ++ | |
[class: class] ++ | |
Enum.map(unquote(capture_names), fn name -> | |
{"data-#{to_string(name)}", props[name]} | |
end) | |
%Surface.AST.Literal{ | |
value: render_svg(id, attrs) |> IO.iodata_to_binary() | |
} | |
end | |
end | |
quote do | |
@svgs (for path <- unquote(files), | |
relative_path = Path.relative_to(path, unquote(Macro.escape(base_dir))), | |
captures = Regex.named_captures(unquote(Macro.escape(regex)), relative_path), | |
captures != nil, | |
id = | |
unquote(Macro.escape(capture_name_strings)) | |
|> Enum.map(fn name -> captures[name] end) | |
|> List.to_tuple(), | |
into: %{} do | |
@external_resource Path.relative_to_cwd(path) | |
"<svg" <> contents = | |
path | |
|> File.read!() | |
|> String.replace("\n", "") | |
|> String.trim() | |
{id, ["<svg", contents]} | |
end) | |
unquote(if surface, do: surface_code) | |
defmacro svg(id, attrs \\ []) do | |
Phoenix.HTML.raw(render_svg(id, attrs)) | |
end | |
def render_svg(id, attrs) do | |
if Map.has_key?(@svgs, id) do | |
[head, tail] = Map.get(@svgs, id) | |
[head, translate_attrs(attrs), tail] | |
else | |
IO.warn("Could not find icon for #{inspect(id)}") | |
["<span>", "Failed to load icon ", inspect(id), "</span>"] | |
end | |
end | |
defp translate_attrs([]) do | |
[] | |
end | |
defp translate_attrs([{key, true} | tail]) do | |
[" ", to_string(key), translate_attrs(tail)] | |
end | |
defp translate_attrs([{_, value} | tail]) when is_nil(value) or value == false do | |
translate_attrs(tail) | |
end | |
defp translate_attrs([{key, value} | tail]) do | |
[" ", to_string(key), ~S(="), value, ~S("), translate_attrs(tail)] | |
end | |
end | |
end | |
def collect_pattern_parts(path_parts) do | |
Enum.map(path_parts, fn | |
path when is_binary(path) -> | |
path_segment(path) | |
name when is_atom(name) -> | |
named_path_segment(name) | |
{name, default} when is_atom(name) and is_binary(default) -> | |
named_path_segment(name, default) | |
{name, options} when is_atom(name) and is_list(options) -> | |
enum_path_segment(name, options) | |
{name, {options, default}} | |
when is_atom(name) and is_list(options) and is_binary(default) -> | |
enum_path_segment(name, options, default) | |
end) | |
end | |
def path_segment(path), do: {nil, Regex.escape(path), path, nil} | |
def named_path_segment(name, default \\ nil), | |
do: {name, "(?<#{to_string(name)}>[^/]+)", "*", default} | |
def enum_path_segment(name, values, default \\ nil) do | |
regex_or = | |
values | |
|> Enum.map(&to_string/1) | |
|> Enum.map(&Regex.escape/1) | |
|> Enum.join("|") | |
wildcard_or = | |
values | |
|> Enum.map(&to_string/1) | |
|> Enum.join(",") | |
{name, "(?<#{to_string(name)}>#{regex_or})", "{#{wildcard_or}}", default} | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment