lib/mix/tasks/heroicons/generate.ex

defmodule Mix.Tasks.Heroicons.Generate do
  use Mix.Task

  @shortdoc "Convert source SVG files into Phoenix LiveView components"
  def run(_) do
    Enum.each(["outline", "solid"], &loop_directory/1)

    Mix.Task.run("format")
  end

  defp loop_directory(folder) do
    src_path = "./priv/heroicons/src/#{folder}/"
    dest_path = "./lib/heroicons/#{String.downcase(folder)}.ex"
    module_name = "Heroicons.#{String.capitalize(folder)}"

    File.rm(dest_path)

    content =
      src_path
      |> File.ls!()
      |> Enum.filter(&(Path.extname(&1) == ".svg"))
      |> Enum.map(&build_component(folder, src_path, &1))
      |> build_module(module_name)

    File.write!(dest_path, content)
  end

  defp build_component(folder, src_path, filename) do
    svg_filepath = Path.join(src_path, filename)
    docs = "#{folder}/#{filename}"

    svg_content = File.read!(svg_filepath) |> String.trim()
    [{_, attributes, children} | _] = Floki.parse_document!(svg_content)
    attributes = Map.new(attributes)

    assigns = %{
      class: Map.get(attributes, "class", ""),
      fill: Map.get(attributes, "fill", "none"),
      stroke: Map.get(attributes, "stroke", "none")
    }

    attributes =
      attributes
      |> Map.delete("class")
      |> Map.delete("fill")
      |> Map.delete("stroke")
      |> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
      |> Enum.join(" ")

    svg_content = """
    <svg class={@class} fill={@fill} stroke={@stroke} #{attributes}>
      #{Floki.raw_html(children)}
    </svg>
    """

    filename |> function_name() |> build_function(docs, svg_content, assigns)
  end

  defp function_name(current_filename) do
    current_filename
    |> Path.basename(".svg")
    |> String.split("-")
    |> Enum.join("_")
  end

  defp build_module(functions, module_name) do
    """
    defmodule #{module_name} do
      @moduledoc \"\"\"
      Icon name can be the function or passed in as a icon eg.
      <#{module_name}.search class="w-6 h-6" />
      <#{module_name}.render icon={:search} class="w-6 h-6" />

      <#{module_name}.search class="w-6 h-6" />
      <#{module_name}.render icon={:search} class="w-6 h-6" />
      \"\"\"

      use Phoenix.Component

      def render(%{icon: icon} = assigns) when is_bitstring(icon) do
        # load the function names into the atom table
        __MODULE__.module_info(:functions) |> Keyword.keys()
        icon_atom = icon |> String.replace("-", "_") |> String.downcase() |> String.to_existing_atom()

        apply(__MODULE__, icon_atom, [assigns])
      end

      def render(%{icon: icon} = assigns), do: apply(__MODULE__, icon, [assigns])

      #{Enum.join(functions, "\n")}
    end
    """
  end

  defp build_function(function_name, docs, svg, assigns) do
    """
    @doc "#{docs}"
    def #{function_name}(assigns) do
      assigns =
        assigns
        |> assign_new(:class, fn -> \"#{Map.get(assigns, :class)}\" end)
        |> assign_new(:fill, fn -> \"#{Map.get(assigns, :fill)}\" end)
        |> assign_new(:stroke, fn -> \"#{Map.get(assigns, :stroke)}\" end)

      ~H\"\"\"
      #{svg}
      \"\"\"
    end
    """
  end
end