defmodule Iconify do
use Phoenix.Component
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) do
component(
fun,
assigns,
{__ENV__.module, __ENV__.function, __ENV__.file, __ENV__.line}
)
end
end
def prepare(assigns, mode \\ nil) do
icon = Map.fetch!(assigns, :icon)
# workaround for emojis not playing nice in CSS but also being bulky
mode = if String.contains?(to_string(icon), ["emoji", "noto"]), do: :img, else: mode || mode()
case mode do
:img ->
src = prepare_icon_img(icon)
{:img, &render_svg_with_img/1, assigns |> Enum.into(%{src: src})}
:inline ->
{:inline, &prepare_icon_component(icon).render/1, assigns}
# :css by default
_ ->
icon_name = prepare_icon_css(icon)
{:css, &render_svg_with_css/1, assigns |> Enum.into(%{icon_name: icon_name})}
end
end
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, "")
def 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")
defp prepare_icon_img(icon) 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)
end
"#{static_url()}/#{family_name}/#{icon_name}.svg"
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 do_prepare_icon_img(family_name, icon_name) 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 = svg(json_path, icon_name)
# |> 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 \\ "heroicons-solid:question-mark-circle")
defp prepare_icon_component(icon) when is_binary(icon) do
with [family_name, icon_name] <- family_and_icon(icon) do
do_prepare_icon_component(family_name, icon_name)
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) 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) 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) 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
json_path = json_path(family_name)
component_content =
build_component(module_name, svg_for_component(json_path, icon_name))
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
def list_components do
with {:ok, modules} <-
:application.get_key(
Application.get_env(:iconify_ex, :generated_icon_app, :bonfire),
:modules
) do
modules
|> Enum.filter(&String.starts_with?("#{&1}", "Elixir.Iconify"))
|> Enum.group_by(fn mod ->
String.split("#{mod}", ".", parts: 4)
|> Enum.at(2)
end)
end
end
defp prepare_icon_css(icon) 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)
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) 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
json_path = json_path(family_name)
svg = svg(json_path, icon_name)
# |> IO.inspect()
css = css_svg(icon_css_name, svg)
# |> IO.inspect()
append_css(file, css)
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, clean_svg(svg_code))
# |> IO.inspect()
append_css(file, css)
end
end
end
defp file_open(path, args) do
# TODO: put args in key?
key = "iconify_ex_file_#{path}"
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(json_path, icon_name) do
{svg, w, h} = get_svg(json_path, icon_name)
""
end
defp svg_for_component(json_path, icon_name) do
{svg, w, h} = get_svg(json_path, icon_name)
""
end
defp full_svg_for_component(svg_code, icon_name) do
String.replace(
clean_svg(svg_code, icon_name),
"