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, mode \\ nil) 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 mode || mode(emoji?(icon)) do
:img_url ->
{:img, maybe_prepare_icon_img(icon)}
: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}
:data ->
{:data, prepare_icon_data(icon)}
_ ->
# :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 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
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, "")
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")
def emoji?(icon),
do:
String.starts_with?(to_string(icon), [
"emoji",
"noto",
"openmoji",
"twemoji",
"fluent-emoji",
"fxemoji",
"streamline-emoji"
])
defp prepare_icon_img(icon) do
with img when is_binary(img) <- maybe_prepare_icon_img(icon) 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) 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
_ ->
nil
end
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_data(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)
do_prepare_icon_data(family_name, icon_name, icon_css_name)
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) 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)
svg_data ->
svg_data
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()
data_svg = data_svg(svg)
css = css_with_data_svg(icon_css_name, data_svg)
# |> IO.inspect()
append_css(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, 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}_#{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(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),
"