Current section
Files
Jump to
Current section
Files
lib/phiaui_design/codegen/attr_formatter.ex
defmodule PhiaUiDesign.Codegen.AttrFormatter do
@moduledoc """
Formats Elixir values for HEEx template attributes.
Handles the mapping between in-memory attr values and their HEEx text
representation. Phoenix function component attrs use the Elixir atom
name directly (underscores), so `:variant` becomes `variant=...`.
Special rules:
- `nil` values are omitted (returns nil).
- `true` booleans emit the bare attr name: `disabled`.
- `false` booleans are omitted.
- Atoms are wrapped in curlies: `variant={:destructive}`.
- Strings are double-quoted: `label="Save"`.
- Numbers are wrapped in curlies: `count={3}`.
- Lists and maps use `inspect/1` inside curlies.
- The `:class` attr always renders as a plain HTML string attribute.
"""
@doc """
Format a single attr key-value pair for HEEx output.
Returns the formatted attribute string, or `nil` if the attr should be
omitted from the output (nil value, false boolean).
## Examples
iex> AttrFormatter.format(:disabled, true)
"disabled"
iex> AttrFormatter.format(:variant, :destructive)
"variant={:destructive}"
iex> AttrFormatter.format(:label, "Save")
~s(label="Save")
iex> AttrFormatter.format(:hidden, false)
nil
iex> AttrFormatter.format(:tooltip, nil)
nil
"""
@spec format(atom(), term()) :: String.t() | nil
def format(_key, nil), do: nil
def format(_key, false), do: nil
def format(key, true) when is_atom(key), do: to_string(key)
def format(:class, value) when is_binary(value) do
~s(class="#{escape_html(value)}")
end
def format(key, value) when is_atom(key) and is_atom(value) do
"#{attr_name(key)}={:#{value}}"
end
def format(key, value) when is_atom(key) and is_binary(value) do
~s(#{attr_name(key)}="#{escape_html(value)}")
end
def format(key, value) when is_atom(key) and is_integer(value) do
"#{attr_name(key)}={#{value}}"
end
def format(key, value) when is_atom(key) and is_float(value) do
"#{attr_name(key)}={#{value}}"
end
def format(key, value) when is_atom(key) and is_list(value) do
"#{attr_name(key)}={#{inspect(value)}}"
end
def format(key, value) when is_atom(key) and is_map(value) do
"#{attr_name(key)}={#{inspect(value)}}"
end
def format(key, value) when is_atom(key) do
"#{attr_name(key)}={#{inspect(value)}}"
end
@doc """
Format a map of attrs into a single space-separated string suitable for
insertion into an opening HEEx tag.
Keys are sorted alphabetically for deterministic output.
## Examples
iex> AttrFormatter.format_attrs(%{variant: :outline, disabled: true})
"disabled variant={:outline}"
iex> AttrFormatter.format_attrs(%{})
""
"""
@spec format_attrs(map()) :: String.t()
def format_attrs(attrs) when is_map(attrs) do
attrs
|> Enum.sort_by(fn {k, _v} -> to_string(k) end)
|> Enum.map(fn {k, v} -> format(k, v) end)
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")
end
# Convert an atom key to the HEEx attribute name string.
# Phoenix component attrs use the Elixir atom name as-is (underscores).
# This is intentionally simple: `to_string/1` on the atom.
defp attr_name(key), do: to_string(key)
# Escape characters that are meaningful inside HTML attribute values.
defp escape_html(str) do
str
|> String.replace("&", "&")
|> String.replace("<", "<")
|> String.replace(">", ">")
|> String.replace(~s("), """)
end
end