defmodule Hyperex do @moduledoc """ A pure-Elixir HTML renderer. """ @void_tags ~w(area base br col embed hr img input link meta param source track wbr) @type tag :: atom @type unescaped_element :: {:dangerously_unescaped, binary, renderable, binary} @type regular_element :: {tag, %{optional(:children) => :void | renderable, optional(any) => any}} @type element :: unescaped_element | regular_element @type renderable :: [element] | element | binary | number | nil @doc """ This function should not be directly used. It has to be public because the `h` macro inserts calls to `merge_props`. """ def merge_props([]), do: %{} def merge_props([a | b]) do Map.merge(Map.new(a), merge_props(b)) end defp preprocess_arg(:void) do [children: :void] end defp preprocess_arg([{:do, {:__block__, _, expr}}]) do [children: expr] end defp preprocess_arg([{:do, expr}]) do [children: expr] end defp preprocess_arg(list) when is_list(list) do Enum.map( list, fn {:do, children} -> {:children, children} {key, value} -> {key, value} end ) end defp preprocess_arg({:%{}, _, props}) when is_list(props) do props end defp preprocess_arg(literal) when is_binary(literal) or is_number(literal) do [children: literal] end defp preprocess_arg({name, meta, args}) when is_atom(name) do {name, meta, args} end defp preprocess_args(args) do quote do merge_props(unquote([[{:children, nil}] | Enum.map(args, &preprocess_arg/1)])) end end defp preprocess([[do: {:__block__, _, block}]]) do block end defp preprocess([tag_expr | args]) do preprocess_elem(tag_expr, preprocess_args(args)) end defp preprocess_elem(tag, props) when is_atom(tag) do preprocess_elem(Atom.to_string(tag), props) end defp preprocess_elem(tag, props) when is_binary(tag) do quote do {unquote(tag), unquote(props)} end end defp preprocess_elem({tag_fun, tag_meta, nil}, props) when is_atom(tag_fun) do {tag_fun, tag_meta, [props]} end defp preprocess_elem({tag_fun = {:., _, [{:__aliases__, _, _}, _]}, tag_meta, []}, props) do {tag_fun, tag_meta, [props]} end @doc """ Generates renderable elements. The first parameter should be the tag name or a function name. Tag names can be atoms or strings. If the first parameter is a function, then it should return renderable elements. The next parameters are what is called “props” in the React world. Each of these parameters must be a keyword list or a map. These maps are merged during rendering (values in the rightmost ones override values in the leftmost ones, see `Map.merge/2`). If the last parameter is not a map or a keyword list, then it is used as the `children` prop. So `h :div, "foo"` is equivalent to `h :div, [children: "foo"]`. Children can be rendered with a `children` prop or an optional `do … end` block. Use `render/1` to convert the returned renderable elements into iodata or strings. ## Example iex> import Hyperex iex> require Hyperex iex> h :html do ...> h :h1 do "Hello" end ...> end {"html", %{children: {"h1", %{children: "Hello"}}}} """ defmacro h(a), do: preprocess([a]) defmacro h(a, b), do: preprocess([a, b]) defmacro h(a, b, c), do: preprocess([a, b, c]) defmacro h(a, b, c, d), do: preprocess([a, b, c, d]) defmacro h(a, b, c, d, e), do: preprocess([a, b, c, d, e]) defmacro h(a, b, c, d, e, f), do: preprocess([a, b, c, d, e, f]) defmacro h(a, b, c, d, e, f, g), do: preprocess([a, b, c, d, e, f, g]) defmacro h(a, b, c, d, e, f, g, h), do: preprocess([a, b, c, d, e, f, g, h]) defp prop_key_to_iodata(key) when is_atom(key) do Atom.to_string(key) end defp prop_key_to_iodata(key) when is_binary(key) do Plug.HTML.html_escape_to_iodata(key) end defp prop_to_iodata(:children, _), do: "" defp prop_to_iodata(_key, nil), do: "" defp prop_to_iodata(_key, false), do: "" defp prop_to_iodata(key, true), do: prop_key_to_iodata(key) defp prop_to_iodata(key, n) when is_number(n) do prop_to_iodata(key, to_string(n)) end defp prop_to_iodata(key, value) when is_binary(value) do ek = prop_key_to_iodata(key) ev = Plug.HTML.html_escape_to_iodata(value) [ek, ?=, ?" | [ev, ?"]] end @spec props_to_iodata([{atom | binary, any}] | %{optional(atom | binary) => any}) :: iodata defp props_to_iodata(props) do props |> Enum.map(fn {key, value} -> prop_to_iodata(key, value) end) |> Enum.intersperse(?\s) end @doc """ Creates HTML iodata from elements. ## Example iex> import Hyperex iex> require Hyperex iex> renderable = h :html do h :h1 do "Hello" end end {"html", %{children: {"h1", %{children: "Hello"}}}} iex> render(renderable) [ 60, "html", 32, [""], 62, [60, "h1", 32, [""], 62, "Hello", "", "h1", 62], "", "html", 62 ] iex> to_string(render(renderable)) "