lib/mix/tasks/heroicons/generate.ex

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

  @shortdoc "To run `mix heroicons.generate`. Converts source SVG files into heex components."
  def run(_) do
    System.cmd("git", ["clone", "https://github.com/tailwindlabs/heroicons.git"])

    Enum.each(
      [
        ["24/solid", "Solid", "solid"],
        ["24/outline", "Outline", "outline"],
        ["20/solid", "Mini.Solid", "mini/solid"]
      ],
      &loop_directory/1
    )

    Mix.Task.run("format")
    System.cmd("rm", ["-rf", "heroicons"])
  end

  defp loop_directory(folder) do
    src_path = "./heroicons/optimized/#{Enum.at(folder, 0)}/"
    namespace = "HeroiconsV2.#{Enum.at(folder, 1)}"

    file_content = """
    defmodule HeroiconsRender.#{namespace} do
      @moduledoc \"\"\"
      Icon name can be the function or passed in as a type eg.

          <HeroiconsRender.HeroiconsV2.Solid.home class="w-6 h-6" />
          <HeroiconsRender.HeroiconsV2.Solid.home title="Optional title for accessibility" class="w-6 h-6" />
          <HeroiconsRender.HeroiconsV2.Solid.render icon="home" class="w-6 h-6" />

          <HeroiconsRender.HeroiconsV2.Mini.Solid.home class="w-5 h-5" />
          <HeroiconsRender.HeroiconsV2.Mini.Solid.home title="Optional title for accessibility" class="w-5 h-5" />
          <HeroiconsRender.HeroiconsV2.Mini.Solid.render icon="home" class="w-5 h-5" />

          <HeroiconsRender.HeroiconsV2.Outline.home class="w-6 h-6" />
          <HeroiconsRender.HeroiconsV2.Outline.home title="Optional title for accessibility" class="w-6 h-6" />
          <HeroiconsRender.HeroiconsV2.Outline.render icon="home" class="w-6 h-6" />
      \"\"\"
      use Phoenix.Component

      alias HeroiconsRender.Svg

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

      def render(%{icon: icon_name} = assigns) do
        icon_name =
          icon_name
          |> String.replace("-", "_")
          |> String.to_existing_atom()

        apply(__MODULE__, icon_name, [assigns])
      end
    """

    functions_content =
      src_path
      |> File.ls!()
      |> Enum.filter(&(Path.extname(&1) == ".svg"))
      |> Enum.sort()
      |> Enum.map(&create_component(src_path, &1, folder))
      |> Enum.join("\n\n")

    file_content =
      file_content <>
        """
          # coveralls-ignore-start
        """ <>
        functions_content <>
        """
          # coveralls-ignore-stop
        end
        """

    dest_dir = Enum.at(folder, 2)
    dest_path = "./lib/heroicons_render/icons/heroicons_v2/#{dest_dir}.ex"

    unless File.exists?(dest_path) do
      case dest_dir do
        "mini/solid" ->
          File.mkdir_p(
            "./lib/heroicons_render/icons/heroicons_v2/#{Enum.at(String.split(dest_dir, "/"), 0)}"
          )

        _rest ->
          File.mkdir_p("./lib/heroicons_render/icons/heroicons_v2/")
      end
    end

    File.write!(dest_path, file_content)
  end

  defp create_component(src_path, filename, type) do
    svg_content =
      File.read!(Path.join(src_path, filename))
      |> String.trim()
      |> String.replace(~r/<svg /, "<svg class={@class} {@rest} ")
      |> String.replace(~r/(\A.*)/, "\\g{1}\n    <Svg.title title={@title} />")
      |> String.replace(~r/<path/, "  <path")

    build_component(filename, svg_content, type)
  end

  defp function_name(current_filename) do
    current_filename
    |> Path.basename(".svg")
    |> String.replace("-", "_")
  end

  defp build_component(filename, svg, type) do
    class = class_for(type)

    """
    def #{function_name(filename)}(assigns) do
      assigns = assigns
        |> assign_new(:title, fn -> nil end)
        |> assign_new(:class, fn -> "#{class}" end)
        |> assign_new(:rest, fn ->
          assigns_to_attributes(assigns, ~w(
            class
          )a)
        end)

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

  defp class_for(["24/outline", _namespace, _dest_dir]), do: "h-6 w-6"
  defp class_for(["24/solid", _namespace, _dest_dir]), do: "h-6 w-6"
  defp class_for(["20/solid", _namespace, _dest_dir]), do: "h-5 w-5"
end