Skip to content

Instantly share code, notes, and snippets.

@capitalist
Forked from lnr0626/heroicons_with_surface.ex
Created October 17, 2020 13:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save capitalist/0555e138a7039a560ae7d5fc4d7e7ed8 to your computer and use it in GitHub Desktop.
Save capitalist/0555e138a7039a560ae7d5fc4d7e7ed8 to your computer and use it in GitHub Desktop.
A module for handling svg icons both with and without surface
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
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
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