defmodule Pile do @moduledoc ~S""" This library allows representing HTML in Elixir. It uses tuples to represent elements, and maps to represent element attributes: iex> {:html, ...> {:head, ...> {:title, "Hello World"}, ...> {:link, %{rel: "stylesheet", href: "..."}}, ...> } ...> } ...> |> Pile.to_html() ...> "Hello World" See `Pile.to_html/2` for more details about the syntax """ @default_options [pretty: false, doctype: false, iodata: false] @spec to_html(input :: keyword(), options :: keyword()) :: String.t() @doc ~S""" Converts a tuple (or a list of tuples) into an HTML string ## Options: - `pretty`: Passing `true` causes the HTML output to be pretty-printed. Defaults to `false`. - `doctype`: Prepend the `` to the resulting string. Defaults to `false`. - `iodata`: Return the HTML as iodata. Defaults to `false`. ## Syntax: A tuple begining with an atom represents an HTML element: iex> {:div} |> Pile.to_html() "
" Elements can have children: iex> {:div, {:p}, {:p}} |> Pile.to_html() "

" Lists are automatically flattened, which is particularly useful for iteration & composition: iex> {:div, ...> {:p}, ...> 1..2 |> Enum.map(fn _ -> {:span} end) ...> } |> Pile.to_html() "

" Strings are HTML-escaped, then rendered as text: iex> {:div, "hello"} |> Pile.to_html() "
hello
" iex> {:div, ""} |> Pile.to_html() "
<span>
" To bypass HTML escaping, using the special `_rawtext` element: iex> {:div, {:_rawtext, ""}} |> Pile.to_html() "
" Elements may have attributes. These are represented as a map, and if present must come right after the element name: iex> {:div, %{class: "container"}} |> Pile.to_html() "
" iex> {:div, %{class: "container"}, "foo", {:p}} |> Pile.to_html() "
foo

" An attribute with a boolean value is treated as an [HTML boolean attribute](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML) iex> {:div, %{readonly: true}} |> Pile.to_html() "
" iex> {:div, %{readonly: false}} |> Pile.to_html() "
" """ def to_html(input, opts \\ @default_options) when is_tuple(input) or is_list(input) do opts = Keyword.validate!(opts, @default_options) input = if is_tuple(input), do: [input], else: input normalized = input |> Enum.map(&Pile.Normalize.run/1) html = normalized |> Enum.map(fn h -> Pile.Visitor.traverse(h, Pile.Visitor.Serializer, opts) end) html = if opts[:doctype] do ["" | html] else html end if opts[:iodata] do html else IO.iodata_to_binary(html) end end end