defmodule Xain do require Logger import Xain.Helpers import Phoenix.HTML, except: [raw: 1] defdelegate raw(value), to: Phoenix.HTML defmacro __using__(opts) do except = Keyword.get opts, :except, [] quote do import unquote(__MODULE__), except: unquote(except) import Kernel, except: [div: 2] end end def quote_symbol, do: Application.get_env(:xain, :quote, "'") @defaults [ input: [type: :text], form: [method: :post], table: [border: 0, cellspacing: 0, cellpadding: 0] ] @auto_build_elements [ :a, :abbr, :address, :area, :article, :aside, :audio, :b, :base, :bdo, :blockquote, :body, :br, :button, :canvas, :caption, :cite, :code, :col, :colgroup, :command, :datalist, :dd, :del, :details, :dfn, :div, :dl, :dt, :em, :embed, :fieldset, :figcaption, :figure, :footer, :form, :h1, :h2, :h3, :h4, :h5, :h6, :head, :header, :hgroup, :hr, :html, :i, :iframe, :img, :input, :ins, :keygen, :kbd, :label, :legend, :li, :link, :map, :mark, :menu, :menuitem, :meta, :meter, :nav, :noscript, :object, :ol, :optgroup, :option, :output, :param, :pre, :progress, :q, :s, :samp, :script, :section, :select, :small, :source, :span, :strong, :style, :sub, :summary, :sup, :svg, :table, :tbody, :td, :textarea, :tfoot, :th, :thead, :time, :title, :tr, :track, :ul, :var, :video, :wbr ] @self_closing_elements [ :area, :base, :br, :col, :embed, :hr, :img, :input, :keygen, :link, :menuitem, :meta, :param, :source, :track, :wbr ] @html5_elements [ :p ] ++ @auto_build_elements for tag <- @html5_elements -- @self_closing_elements do defmacro unquote(tag)(contents \\ nil, attrs \\ nil, inner \\ nil) do tag = unquote(tag) quote location: :keep, do: tag(unquote(tag), unquote(contents), unquote(attrs), unquote(inner), false) end end for tag <- @self_closing_elements do defmacro unquote(tag)(contents \\ nil, attrs \\ nil, inner \\ nil) do tag = unquote(tag) quote location: :keep, do: tag(unquote(tag), unquote(contents), unquote(attrs), unquote(inner), true) end end defmacro tag(name, inline_content \\ nil, attrs \\ nil, inner_block \\ nil, sc \\ false) do {inline_content, attrs, inner_content} = prepare_args(inline_content, attrs, inner_block) inner_content = join_lines(inner_content) quote bind_quoted: [name: name, inline_content: inline_content, attrs: attrs, inner_content: inner_content, sc: sc], location: :keep do Xain.build_tag(name, inline_content, attrs, inner_content, sc) end end defp prepare_args([do: inner_content], nil, nil), do: {nil, [], inner_content} defp prepare_args(content, [do: inner_content], nil), do: {content, [], inner_content} defp prepare_args(content, attrs, [do: inner_content]), do: {content, attrs, inner_content} defp prepare_args(content, attrs, nil), do: {content, attrs || [], []} defp join_lines(ast) do case ast do {:__block__, trace, inner_list} -> {:__block__, trace, handle_inner_list(inner_list)} _ -> ast end end def join(list) do list |> Enum.map(&item_to_string/1) |> Enum.filter(&(&1)) |> Enum.reverse |> Enum.join |> raw end defp item_to_string({:safe, _} = item), do: safe_to_string(item) defp item_to_string(item) when is_list(item) do item |> Enum.map(&item_to_string/1) |> Enum.filter(&(&1)) |> Enum.join |> raw |> item_to_string end defp item_to_string(item) when is_binary(item) do item |> html_escape() |> item_to_string end defp item_to_string(_) do nil |> html_escape() |> item_to_string end defp handle_inner_list(list, acc \\ []) defp handle_inner_list([], acc) do quoted_join = {{:., [], [{:__aliases__, [alias: false], [:Xain]}, :join]}, [], [{:xain_buffer, [], Elixir}]} acc = [quoted_join | acc] acc = Enum.reverse(acc) [{:=, [], [{:xain_buffer, [], Elixir}, []]} | acc] end defp handle_inner_list([line|tail], acc) do case line do {:=, _, _} -> handle_inner_list(tail, [line | acc]) _ -> line = {:=, [], [{:xain_buffer, [], Elixir}, [{:|, [], [line, {:xain_buffer, [], Elixir}]}]]} handle_inner_list(tail, [line | acc]) end end def build_tag(name, attrs, _, inner, sc) when is_list(attrs) do build_tag(name, "", attrs, inner, sc) end def build_tag(name, inline_content, attrs, inner, sc) do inline_content |> ensure_valid_contents(name) |> merge_attrs(attrs, name) |> merge_content(inner) |> wrap_in_tags(name, sc) |> raw end defp merge_attrs(content, attrs, tag_name) do attrs = attrs |> set_defaults(tag_name) {content, attrs} = id_and_class_shortcuts(content, attrs) attrs_html = for {key, val} <- attrs, into: "" do safe_key = key |> html_escape |> safe_to_string safe_val = val |> html_escape |> safe_to_string " #{safe_key}=#{quote_symbol()}#{safe_val}#{quote_symbol()}" end {content, attrs_html} end defp merge_content({inline_content, attrs_html}, {:safe, _} = inner) do {safe_to_string(html_escape(inline_content)) <> safe_to_string(html_escape(inner)), attrs_html} end defp merge_content({inline_content, attrs_html}, inner) when is_list(inner) do inner = inner |> List.flatten |> Enum.reject(&is_nil(&1)) |> Enum.map(&safe_to_string(&1)) |> Enum.join() |> raw() merge_content({inline_content, attrs_html}, inner) end defp merge_content({inline_content, attrs_html}, "") do merge_content({inline_content, attrs_html}, html_escape(nil)) end defp merge_content({inline_content, attrs_html}, inner) when is_binary(inner) or is_nil(inner) do merge_content({inline_content, attrs_html}, html_escape(inner)) end defp merge_content({inline_content, attrs_html}, _) do merge_content({inline_content, attrs_html}, html_escape(nil)) end defp wrap_in_tags({_content, attrs_html}, name, true) do safe_name = name |> html_escape |> safe_to_string "<#{safe_name}#{attrs_html}/>" end defp wrap_in_tags({content, attrs_html}, name, false) do safe_name = name |> html_escape |> safe_to_string "<#{safe_name}#{attrs_html}>#{content}" end defmacro markup(opts \\ [], block) defmacro markup(:nested, do: block) do quote location: :keep do markup([safe: true], do: unquote(block)) end end defmacro markup(opts, do: block) do block = join_lines(block) quote location: :keep do require Logger import Kernel, except: [div: 2] import unquote(__MODULE__) opts = unquote(opts) result = try do Phoenix.HTML.html_escape(unquote(block)) rescue exception -> Logger.error inspect(exception) Logger.error inspect(System.stacktrace) reraise exception, System.stacktrace end end end defmacro text(string) do quote do: to_string(unquote(string)) end defp get_defaults(name) do Keyword.get(@defaults, name, []) end defp set_defaults(attrs, name) do Keyword.merge(get_defaults(name), attrs) end end