defmodule Iconify do
use Phoenix.Component
use Arrows
import Phoenix.LiveView.TagEngine
# import Phoenix.LiveView.HTMLEngine
require Logger
# this is executed at compile time
@cwd File.cwd!()
def iconify(assigns) do
with {_, fun, assigns} <- prepare(assigns, assigns[:mode]) do
component(
fun,
assigns,
{__ENV__.module, __ENV__.function, __ENV__.file, __ENV__.line}
)
end
end
def prepare(assigns, opts \\ [])
def prepare(assigns, opts) when is_map(assigns) and is_list(opts) do
assigns =
Map.put_new_lazy(assigns, :class, fn ->
Application.get_env(:iconify_ex, :default_class, "w-4 h-4")
end)
icon = Map.fetch!(assigns, :icon)
case opts[:mode] || mode(emoji?(icon)) do
:set ->
href = href_for_prepared_set_icon(icon, opts)
{:set, &render_svg_for_sprite/1, assigns |> Enum.into(%{href: href})}
:img_url ->
maybe_prepare_icon_img(icon, opts)
:img ->
src = prepare_icon_img(icon, opts)
{:img, &render_svg_with_img/1, assigns |> Enum.into(%{src: src})}
:inline ->
{:inline, &prepare_icon_component(icon, opts).render/1, assigns}
:data ->
{:data, prepare_icon_data(icon, opts)}
_ ->
# :css by default
icon_name = prepare_icon_css(icon, opts)
{:css, &render_svg_with_css/1, assigns |> Enum.into(%{icon_name: icon_name})}
end
end
def prepare(icon, opts) when is_binary(icon) do
prepare(%{icon: icon}, opts)
end
def prepare(icon, mode) when is_atom(mode) do
prepare(icon, mode: mode)
end
def manual(icon, opts \\ nil) do
assigns = Map.put(opts[:assigns] || %{}, :icon, icon)
mode = opts[:mode]
case prepare(assigns, opts[:mode]) do
{_, fun, assigns} when is_function(fun) ->
fun.(assigns)
{_, other} ->
other
other ->
other
end
end
# TODO: configurable
def fallback_icon, do: "heroicons-solid:question-mark-circle"
def dev_env?, do: Code.ensure_loaded?(Mix)
def path, do: Application.get_env(:iconify_ex, :generated_icon_modules_path, "./lib/web/icons")
def static_path,
do:
Application.get_env(
:iconify_ex,
:generated_icon_static_path,
"./assets/static/images/icons"
)
def static_url, do: Application.get_env(:iconify_ex, :generated_icon_static_url, "")
defp mode(true), do: :img
defp mode(_), do: Application.get_env(:iconify_ex, :mode, false)
def using_svg_inject?, do: Application.get_env(:iconify_ex, :using_svg_inject, false)
# def css_class, do: Application.get_env(:iconify_ex, :css_class, "iconify_icon")
@doc "Icon is part of an known emoji set (or another set which doesn't support CSS mode)"
def emoji?(icon),
do:
String.starts_with?(to_string(icon), [
"emoji",
"noto",
"openmoji",
"twemoji",
"fluent-emoji",
"fxemoji",
"streamline-emoji",
"meteocons"
])
defp href_for_prepared_set_icon(icon, opts) do
with [family_name, icon_name] <- family_and_icon(icon) do
icon_name = String.trim_trailing(icon_name, "-icon")
if dev_env?() do
do_prepare_set_icon_img(family_name, icon_name, opts)
end
"#{static_url()}/#{family_name}.svg##{icon_name}"
else
_ ->
nil
end
catch
{:fallback, fallback_icon} when is_binary(fallback_icon) -> prepare_icon_img(fallback_icon)
other -> raise other
end
defp prepare_svg_for_set(family_name, icon_name, opts) do
json_path = json_path(family_name)
svg = svg_for_sprite(json_path, icon_name, opts)
# |> IO.inspect()
end
defp do_prepare_set_icon_img(family_name, icon_name, opts \\ []) do
path = "#{static_path()}"
src = "#{path}/#{family_name}.svg"
if not File.exists?(src) do
svg = opts[:svg] || prepare_svg_for_set(family_name, icon_name, opts)
sprite = """
"""
File.mkdir_p(path)
File.write!(src, sprite)
IO.inspect(src,
label: "Iconify set created: #{family_name} and icon added on family sprite: #{icon_name}"
)
else
IO.inspect(src, label: "Iconify found existing family icon set: #{family_name}")
{:ok, file} = file_open(src, [:read, :utf8])
case read_file(src, file)
# |> IO.inspect
|> Floki.parse_fragment() do
{:ok, content} ->
svgs =
content
|> Floki.find("defs")
|> List.first()
|> Floki.children()
# |> IO.inspect
if Floki.find(svgs, "[id=#{icon_name}]") |> Enum.count() > 0 do
IO.inspect(src, label: "Iconify icon already exists in set: #{icon_name}")
else
IO.inspect(src,
label: "Iconify look for icon #{icon_name} in iconify icon set: #{family_name}"
)
svg = opts[:svg] || prepare_svg_for_set(family_name, icon_name, opts)
# |> IO.inspect()
sprite = """
"""
File.write!(src, sprite)
cache_contents(src, sprite)
IO.inspect(src, label: "Iconify icon added on family sprite: #{family_name}")
end
{:error, err} ->
IO.inspect(err)
end
end
end
defp prepare_icon_img(icon, opts \\ []) do
with img when is_binary(img) <- maybe_prepare_icon_img(icon, opts) do
img
else
_ ->
icon_error(icon, "Could not process family_and_icon")
end
catch
{:fallback, fallback_icon} when is_binary(fallback_icon) -> prepare_icon_img(fallback_icon)
other -> raise other
end
defp maybe_prepare_icon_img(icon, opts) do
with [family_name, icon_name] <- family_and_icon(icon) do
icon_name = String.trim_trailing(icon_name, "-icon")
if dev_env?() do
do_prepare_icon_img(family_name, icon_name, opts)
end
"#{static_url()}/#{family_name}/#{icon_name}.svg"
else
_ ->
nil
end
end
defp do_prepare_icon_img(family_name, icon_name, opts) do
path = "#{static_path()}/#{family_name}"
src = "#{path}/#{icon_name}.svg"
if not File.exists?(src) do
IO.inspect(src, label: "Iconify new icon found")
json_path = json_path(family_name)
svg = opts[:svg] || svg_as_is(json_path, icon_name, opts)
# |> IO.inspect()
File.mkdir_p(path)
File.write!(src, svg)
IO.inspect(src, label: "Iconify icon added")
else
IO.inspect(src, label: "Iconify icon already exists")
end
end
defp prepare_icon_component(icon \\ fallback_icon(), opts \\ [])
defp prepare_icon_component(icon, opts) when is_binary(icon) do
with [family_name, icon_name] <- family_and_icon(icon) do
do_prepare_icon_component(family_name, icon_name, opts)
else
_ ->
icon_error(icon, "Could not process family_and_icon")
end
catch
{:fallback, fallback_icon} when is_binary(fallback_icon) ->
prepare_icon_component(fallback_icon)
other ->
raise other
end
defp prepare_icon_component(icon, _opts) when is_atom(icon) do
if Code.ensure_loaded?(icon) do
icon
else
icon_error(
icon,
"No component module is available in your app for this icon: `#{inspect(icon)}`. Using the binary icon name instead would allow it to be generated from Iconify. Find icon names at https://icones.js.org"
)
end
catch
{:fallback, fallback_icon} when is_binary(fallback_icon) ->
prepare_icon_component(fallback_icon)
other ->
raise other
end
defp prepare_icon_component(icon, _opts) do
icon_error(
icon,
"Expected a binary icon name or an icon component module atom, got `#{inspect(icon)}`"
)
catch
{:fallback, fallback_icon} when is_binary(fallback_icon) ->
prepare_icon_component(fallback_icon)
other ->
raise other
end
defp do_prepare_icon_component(family_name, icon_name, opts) do
icon_name = String.trim_trailing(icon_name, "-icon")
component_path = "#{path()}/#{family_name}"
component_filepath = "#{component_path}/#{icon_name}.ex"
module_name = module_name(family_name, icon_name)
module_atom =
"Elixir.#{module_name}"
|> String.to_atom()
# |> IO.inspect(label: "module_atom")
if not Code.ensure_loaded?(module_atom) do
if dev_env?() do
if not File.exists?(component_filepath) do
component_content =
build_component(
module_name,
svg_for_component(json_path(family_name), icon_name, opts)
)
File.mkdir_p(component_path)
File.write!(component_filepath, component_content)
end
Code.compile_file(component_filepath)
else
icon_error(icon_name, "Icon module not found")
end
end
module_atom
catch
{:fallback, fallback_icon} when is_binary(fallback_icon) ->
prepare_icon_component(fallback_icon)
other ->
raise other
end
def create_component_for_svg(family_name, icon_name, svg_code) do
icon_name = String.trim_trailing(icon_name, "-icon")
component_path = "#{path()}/#{family_name}"
component_filepath = "#{component_path}/#{icon_name}.ex"
module_name = module_name(family_name, icon_name)
module_atom =
"Elixir.#{module_name}"
|> String.to_atom()
# |> IO.inspect(label: "module_atom")
component_content = build_component(module_name, full_svg_for_component(svg_code, icon_name))
File.mkdir_p(component_path)
File.write!(component_filepath, component_content)
Code.compile_file(component_filepath)
module_atom
catch
{:fallback, fallback_icon} when is_binary(fallback_icon) ->
prepare_icon_component(fallback_icon)
other ->
raise other
end
defp prepare_icon_data(icon, opts) do
with [family_name, icon_name] <- family_and_icon(icon) do
icon_name = String.trim_trailing(icon_name, "-icon")
icon_css_name = css_icon_name(family_name, icon_name)
do_prepare_icon_data(family_name, icon_name, icon_css_name, opts)
else
_ ->
icon_error(icon, "Could not process family_and_icon")
end
catch
{:fallback, fallback_icon} when is_binary(fallback_icon) -> nil
other -> raise other
end
defp do_prepare_icon_data(family_name, icon_name, icon_css_name, opts) do
icons_dir = static_path()
css_path = "#{icons_dir}/icons.css"
with {:ok, file} <- file_open(css_path, [:read, :utf8]) do
case extract_from_css_file(css_path, file, icon_css_name) do
nil ->
if dev_env?(), do: do_prepare_icon_css(family_name, icon_name, icon_css_name, opts)
svg_data ->
svg_data
end
end
end
defp prepare_icon_css(icon, opts \\ []) do
with [family_name, icon_name] <- family_and_icon(icon) do
icon_name = String.trim_trailing(icon_name, "-icon")
icon_css_name = css_icon_name(family_name, icon_name)
if dev_env?() do
do_prepare_icon_css(family_name, icon_name, icon_css_name, opts)
end
icon_css_name
else
_ ->
icon_error(icon, "Could not process family_and_icon")
end
catch
{:fallback, fallback_icon} when is_binary(fallback_icon) -> prepare_icon_css(fallback_icon)
other -> raise other
end
defp do_prepare_icon_css(family_name, icon_name, icon_css_name, opts) do
icons_dir = static_path()
css_path = "#{icons_dir}/icons.css"
with {:ok, file} <- file_open(css_path, [:read, :append, :utf8]) do
if !exists_in_css_file?(css_path, file, icon_css_name) do
svg = opts[:svg] || svg_as_is(json_path(family_name), icon_name, opts)
# |> IO.inspect()
data_svg = data_svg(svg)
css = css_with_data_svg(icon_css_name, data_svg)
# |> IO.inspect()
append_css(css_path, file, css)
data_svg
end
end
end
def add_icon_to_css(icon_css_name, svg_code) do
icons_dir = static_path()
css_path = "#{icons_dir}/icons.css"
with {:ok, file} <- file_open(css_path, [:read, :append, :utf8]) do
if !exists_in_css_file?(css_path, file, icon_css_name) do
css = css_svg(icon_css_name, svg_code)
# |> IO.inspect()
append_css(css_path, file, css)
end
end
end
defp file_open(path, args) do
# TODO: put args in key?
key = "iconify_ex_file_#{path}_#{inspect(args)}"
case Process.get(key) do
nil ->
# Logger.debug("open #{path}")
with {:ok, file} <- File.open(path, args) do
Process.put(key, file)
{:ok, file}
end
io_device ->
# Logger.debug("use available #{path}")
{:ok, io_device}
end
end
defp svg_as_is(json_path, icon_name, opts) do
{svg, w, h} = get_svg(json_path, icon_name, opts)
svg_wrap(svg, w, h)
end
defp svg_clean(json_path, icon_name, opts) do
{svg, w, h} = get_svg(json_path, icon_name, opts)
clean_svg(svg, icon_name)
|> svg_wrap(w, h)
end
defp svg_wrap(svg, w, h) do
""
end
defp svg_for_sprite(json_path, icon_name, opts) do
{svg, w, h} = get_svg(json_path, icon_name, opts)
""
end
defp svg_for_component(json_path, icon_name, opts) do
{svg, w, h} = get_svg(json_path, icon_name, opts)
""
end
defp full_svg_for_component(svg_code, icon_name) do
String.replace(
clean_svg(svg_code, icon_name),
"