defmodule Slime.Parser.Transform do @moduledoc """ PEG parser callbacks module. Define transformations from parsed iolist to ast. See https://github.com/seancribbs/neotoma/wiki#working-with-the-ast """ import Slime.Parser.Preprocessor, only: [indent_size: 1] alias Slime.Parser.{AttributesKeyword, EmbeddedEngine, TextBlock} alias Slime.Parser.Nodes.{DoctypeNode, EExNode, HTMLCommentNode, HTMLNode, InlineHTMLNode, VerbatimTextNode} alias Slime.TemplateSyntaxError @merge_attrs %{"class" => " "} @default_tag "div" @sort_attrs true @shortcut %{ "." => %{attr: "class"}, "#" => %{attr: "id"} } @type ast :: term @type index :: {{:line, non_neg_integer}, {:column, non_neg_integer}} @spec transform(atom, iolist, index) :: ast def transform(:document, input, _index) do case input do [_blank_lines, [], tags | _] -> tags [_blank_lines, doctype, [""] | _] -> [doctype] [_blank_lines, doctype, tags | _] -> [doctype | tags] end end def transform(:doctype, input, _index) do %DoctypeNode{name: to_string(input[:name])} end def transform(:tag, [tag, _], _index), do: tag def transform(:tag_item, [_, tag], _index), do: tag def transform(:tags, input, _index) do Enum.flat_map(input, fn [tag, crlfs] -> [tag | newlines(crlfs)] end) end def transform(:nested_tags, [crlfs, _, children, _], _index) do newlines(crlfs) ++ children end def transform(:slime_tag, [tag, spaces, _, content], _index) do {name, shorthand_attrs} = tag {attrs, children, is_closed} = content merge_attrs = Application.get_env(:jgy_slime, :merge_attrs, @merge_attrs) attributes = shorthand_attrs |> Enum.concat(attrs) |> AttributesKeyword.merge(merge_attrs) attributes = if Application.get_env(:jgy_slime, :sort_attrs, @sort_attrs) do Enum.sort_by(attributes, fn {key, _value} -> key end) else attributes end %HTMLNode{name: name, attributes: attributes, spaces: spaces, closed: is_closed, children: children} end def transform(:tag_attributes_and_content, input, _index) do case input do [attrs, _, {children, is_closed}] -> {attrs, children, is_closed} [_, {children, is_closed}] -> {[], children, is_closed} end end def transform(:tag_content, input, _index) do case input do "/" -> {[], true} "" -> {[], false} [] -> {[], false} other when is_list(other) -> {other, false} _ -> {[input], false} end end def transform(:inline_tag, [_, _, tag], _index), do: tag def transform(:inline_text, [_, text], _index) do %VerbatimTextNode{ content: TextBlock.render_without_indentation(text) } end def transform(:text_item, input, _index) do case input do {:dynamic, {:safe, expression}} -> {:safe_eex, expression} {:dynamic, expression} -> {:eex, expression} {:static, text} -> to_string(text) end end def transform(:interpolation, [_, expression, _], _index) do to_string(expression) end def transform(:safe_interpolation, [_, expression, _], _index) do to_string(expression) end def transform(:html_comment, input, _index) do indent = indent_size(input[:indent]) decl_indent = indent + String.length(input[:type]) %HTMLCommentNode{ content: TextBlock.render_content(input[:content], decl_indent) } end def transform(:code_comment, _input, _index), do: "" def transform(:verbatim_text, input, _index) do indent = indent_size(input[:indent]) decl_indent = indent + String.length(input[:type]) content = TextBlock.render_content(input[:content], decl_indent) content = if input[:type] == "'", do: content ++ [" "], else: content %VerbatimTextNode{content: content} end def transform(:text_block, input, _index) do case input do [line, []] -> [line] [line, nested_lines] -> [line | nested_lines[:lines]] end end def transform(:text_block_nested_lines, input, _index) do case input do [line, []] -> [line] [line, nested] -> [ line | Enum.flat_map(nested, fn [_crlf, nested_line] -> case nested_line do {:lines, lines} -> lines [_indent, {:lines, lines}, _dedent] -> lines end end) ] end end def transform(:embedded_engine, [engine, _, content], index) do case EmbeddedEngine.parse(engine, content[:lines]) do {:ok, {tag, content}} -> %HTMLNode{name: tag, attributes: content[:attributes] || [], children: content[:children]} {:ok, content} -> content {:error, message} -> {{:line, line_number}, {:column, column}} = index raise TemplateSyntaxError, message: message, line: "", line_number: line_number, column: column end end def transform(:embedded_engine_lines, [first_line, rest], _index) do [first_line | Enum.map(rest, fn [_, lines] -> lines end)] end def transform(:indented_text_line, [space, content], _index) do {indent_size(space), content} end def transform(:inline_html, [_, content, children], _index) do %InlineHTMLNode{content: content, children: children} end def transform(:code, input, _index) do {output, safe, spaces} = case input[:output] do "-" -> {false, false, %{}} [_, safe, spaces] -> {true, safe == "=", spaces} end %EExNode{ content: input[:code], output: output, spaces: spaces, children: input[:children] ++ input[:optional_else], safe?: safe } end def transform(:code_else_condition, input, _index) do [%EExNode{content: "else", children: input[:children]}] end def transform(:code_lines, input, _index) do case input do [code_line, crlf, line] -> code_line <> crlf <> line line -> line end end def transform(:code_line, input, _index), do: to_string(input) def transform(:code_line_with_break, input, _index), do: to_string(input) def transform(:dynamic_content, [_, safe, _, content], _index) do %EExNode{content: to_string(content), output: true, safe?: safe == "="} end def transform(:tag_spaces, input, _index) do leading = input[:leading] trailing = input[:trailing] case {leading, trailing} do {"<", ">"} -> %{leading: true, trailing: true} {"<", _} -> %{leading: true} {_, ">"} -> %{trailing: true} _ -> %{} end end def transform(:tag_shortcut, input, _index) do {tag, attrs} = case input do {:tag, value} -> {value, []} {:attrs, value} -> {Application.get_env(:jgy_slime, :default_tag, @default_tag), value} list -> {list[:tag], list[:attrs]} end {tag_name, initial_attrs} = expand_tag_shortcut(tag) {tag_name, Enum.concat(initial_attrs, attrs)} end def transform(:shortcuts, input, _index) do Enum.concat([input[:head] | input[:tail]]) end def transform(:shortcut, input, _index) do {nil, attrs} = expand_attr_shortcut(input[:type], input[:value]) attrs end def transform(:wrapped_attributes, [_o, attrs, _c], _index), do: attrs def transform(:wrapped_attributes, indented, _index), do: Enum.at(indented, 3) def transform(:wrapped_attribute, [_space, attribute], _index) do case attribute do {:attribute, attr} -> attr {:attribute_name, name} -> {name, true} end end def transform(:plain_attributes, input, _index) do head = input[:head] tail = Enum.map(input[:tail] || [[]], &List.last/1) [head | tail] end def transform(:attribute, [name, _, safe, value], _index) do value = if safe == "=" do case value do {:eex, content} -> {:safe_eex, content} _ -> {:safe_eex, ~s["#{value}"]} end else value end {name, value} end def transform(:attribute_value, input, _index) do case input do {:simple, [_, content, _]} -> to_string(content) {:dynamic, content} -> {:eex, to_string(content)} end end def transform(:tag_name, input, _index), do: to_string(input) def transform(:shortcut_value, input, _index), do: to_string(input) def transform(:attribute_name, input, _index), do: to_string(input) def transform(:crlf, input, _index), do: to_string(input) def transform(_symdol, input, _index), do: input def expand_tag_shortcut(tag) do shortcut = Application.get_env(:jgy_slime, :shortcut, @shortcut) case Map.fetch(shortcut, tag) do :error -> {tag, []} {:ok, spec} -> expand_shortcut(spec, tag) end end defp expand_attr_shortcut(type, value) do shortcut = Application.get_env(:jgy_slime, :shortcut, @shortcut) spec = Map.fetch!(shortcut, type) expand_shortcut(spec, value) end def newlines(crlfs) do if Application.get_env(:jgy_slime, :keep_lines) do Enum.map(crlfs, fn _ -> %VerbatimTextNode{content: ["\n"]} end) else [] end end def expand_shortcut(spec, value) do attrs = case spec[:attr] do nil -> [] attr_names -> attr_names |> List.wrap() |> Enum.map(&{&1, value}) end final_attrs = Enum.concat(attrs, Map.get(spec, :additional_attrs, [])) {spec[:tag], final_attrs} end end