lib/sneeze.ex

defmodule Sneeze do
  alias Sneeze.Internal

  @doc ~s"""
  Render a data-structure, containing 'elements' to html.
  An element is either:
  - [tag, attribute_map | body]
  - [tag, attribute_map]
  - [tag | body]
  - [tag]
  - [:@__raw_html, html_string]
  - [:script, attribute_map, script_text]
  - [:script, script_text]
  - [:style, attribute_map, style_text]
  - [:style, style_text]
  - A bare, stringable value (such as a string or number)
  - A list of elements

  The content of `:__@raw_html`, `:style` and `:script` elements will not be escaped.
  All other elements content will be html-escaped.

  Examples:
  ```
  render([:p, %{class: "outlined"}, "hello"])
  render([:br])
  render([[:span, "one"], [:span, %{class: "highlight"}, "two"]])
  render([:ul, %{id: "some-list"},
          [:li, [:a, %{href: "/"},      "Home"]],
          [:li, [:a, %{href: "/about"}, "About"]]])
  render([:__@raw_html, "<!DOCTYPE html>"])
  render([:script, "console.log(42 < 9);"])
  ```
  """
  def render(data) do
    IO.iodata_to_binary(_render(data))
  end

  @doc ~s"""
  Render a data-structure, containing 'elements' to an iodata list.
  This can be more performant than `render/1` if you are passing the result to a function that takes iodata.

  Example:
  ```
  render_iodata([:a, %{href: "example.com"}, "hello"])
  # becomes...
  [
    ["<", "a", [[" ", "href", "=\"", "example.com", "\""]], ">"],
    [["hello"]],
    ["</", "a", ">"]
  ]
  ```
  """
  def render_iodata(data) do
    _render(data)
  end

  defp _render(data) do
    case data do
      [] ->
        []

      # list with tag
      [tag] when is_atom(tag) ->
        if Internal.is_void_tag?(tag) do
          [Internal.render_void_tag(tag)]
        else
          [Internal.render_tag(tag)]
        end

      # script tags
      [:script, attributes, script_body] when is_map(attributes) ->
        [
          Internal.render_opening_tag(:script, attributes),
          script_body,
          Internal.render_closing_tag(:script)
        ]

      [:script, script_body] ->
        [Internal.render_opening_tag(:script), script_body, Internal.render_closing_tag(:script)]

      # style tags
      [:style, attributes, style_body] when is_map(attributes) ->
        [
          Internal.render_opening_tag(:style, attributes),
          style_body,
          Internal.render_closing_tag(:style)
        ]

      [:style, style_body] ->
        [Internal.render_opening_tag(:style), style_body, Internal.render_closing_tag(:style)]

      # list with tag and attribute map
      [tag, attributes] when is_atom(tag) and is_map(attributes) ->
        if Internal.is_void_tag?(tag) do
          [Internal.render_void_tag(tag, attributes)]
        else
          [Internal.render_tag(tag, attributes)]
        end

      [:__@raw_html, html_string] ->
        [html_string]

      # list with tag, attribute map and child nodes
      [tag, attributes | body] when is_map(attributes) ->
        if Internal.is_void_tag?(tag) do
          [
            Internal.render_void_tag(tag, attributes),
            # not actually body, next elements
            _render(body)
          ]
        else
          [
            Internal.render_opening_tag(tag, attributes),
            _render_body(body),
            Internal.render_closing_tag(tag)
          ]
        end

      # list with tag and child nodes
      [tag | body] when is_atom(tag) ->
        if Internal.is_void_tag?(tag) do
          [
            Internal.render_void_tag(tag),
            # not actually body, next elements
            _render_body(body)
          ]
        else
          [Internal.render_opening_tag(tag), _render_body(body), Internal.render_closing_tag(tag)]
        end

      # list with list of child nodes
      [node] when is_list(node) ->
        [_render(node)]

      # list with sub-list as first element
      [node | rest] when is_list(node) ->
        [_render(node), _render(rest)]

      # list with single, stringible member
      [something] ->
        [to_string(something) |> HtmlEntities.encode()]

      # any non-list node
      bare_node ->
        [to_string(bare_node) |> HtmlEntities.encode()]
    end
  end

  defp _render_body(body_elements) do
    body_elements
    |> Enum.map(&_render/1)
  end
end