lib/svg.ex

defmodule Heroicons do
  @moduledoc "Defines a convenience macro `icon/1`."

  @doc """
  Use this macro in Phoenix HEEX templates to generate
  Heroicons.

  The name of the icon can be defined using the `name` attribute or (deprecated)
  `type` attribute.

  The style of the icon is defined by the `solid`, `outline`, or `mini` boolean
  attribute or a (deprecated) `style` attribute.

  ## Attributes

  - `name`    - icon name. Valid names are found at [this url](https://heroicons.com).
  - `solid`   - use solid fill 24x24 icon.
  - `outline` - use outline fill 24x24 icon (this is the default).
  - `mini`    - use mini size (20x20) for elements like buttons, form elements.
  - `title`   - when defined, include the title value in the SVG.
  - `class`   - add class attribute to the SVG.
  - `rest`    - other attributes to add to the SVG.

  ## Example

    ```
    ~H"""
    <Heroicons.icon name="academic-cap" solid class="h-4 w-4" title="Cap"/>
    <Heroicons.icon type="academic_cap" style="solid" class="h-4 w-4" title="Cap"/>
    \"""
    ```
  """
  defmacro icon(assigns) do
    quote do
      {name, assigns} =
        case Map.fetch(unquote(assigns), :name) do
          {:ok, name} -> {name, Map.delete(unquote(assigns), :name)}
          :error      -> Map.pop!(unquote(assigns), :type)
        end
      {style, assigns} = Map.pop(assigns, :style)
      icon = Heroicons.to_atom(name)
      {mod, assigns}   =
        case style do
          nil ->
            cond do
              assigns[:solid]   -> {Heroicons.Solid,   Map.delete(assigns, :solid)}
              assigns[:outline] -> {Heroicons.Outline, Map.delete(assigns, :outline)}
              assigns[:mini]    -> {Heroicons.Mini,    Map.delete(assigns, :mini)}
              true              ->
                raise ArgumentError, message: "Missing style|solid|outline|mini attribute"
            end
          "solid"   -> {Heroicons.Solid,   assigns}
          "outline" -> {Heroicons.Outline, assigns}
          "mini"    -> {Heroicons.Mini,    assigns}
        end

      Code.loaded?(mod) || ({:module, ^mod} = :code.load_file(mod))

      :erlang.function_exported(mod, icon, 1) ||
        raise ArgumentError, message:
          "Invalid Heroicon type '#{name}': no function #{inspect(mod)}.#{icon}/1"

      apply(mod, icon, [assigns])
    end
  end

  @doc false
  def to_atom(s) when is_binary(s), do: s |> String.replace("-", "_") |> String.to_existing_atom()
  def to_atom(s) when is_atom(s),   do: s
end

defmodule Heroicons.Helpers.Svg do
  @moduledoc false
  # SVG helper component
  use Phoenix.Component

  attr :title, :string, default: nil
  attr :class, :string, default: nil
  attr :rest,  :global, doc: "HTML attributes for the svg container", include: ~w(fill stroke stroke-width)
  attr :paths, :string, required: true

  @doc false
  def icon(assigns) do
    assigns =
      case assigns[:class] do
        nil -> assigns
        val -> put_in(assigns, [:rest, :class], val)
      end
    ~H"""
    <svg {@rest} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
      <%= if not is_nil(@title) do %>
        <title><%= @title %></title>
      <% end %>
      <%= Phoenix.HTML.raw @paths %>
    </svg>
    """
  end
end