# Covered in Hologram.Template.Parser integration tests
defmodule Hologram.Template.TagAssembler do
alias Hologram.Template.{Helpers, SyntaxError, TokenHTMLEncoder}
# see: https://developer.mozilla.org/en-US/docs/Web/SVG/Element
# DEFER: add the rest of SVG elems, see: https://github.com/segmetric/hologram/issues/21
@svg_tags ["path", "rect"]
# see: https://html.spec.whatwg.org/multipage/syntax.html#void-elements
@void_tags [
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr"
]
# status is one of:
# :text_tag, :start_tag_bracket, :start_tag, :attr_key, :attr_assignment,
# :attr_value_literal, :attr_value_expression, :end_tag_bracket, :end_tag
def assemble(tokens, status, context, tags)
def assemble([], :text_tag, context, tags) do
{tokens, _} = flush_token_buffer(context)
maybe_add_text_tag(tags, tokens)
end
def assemble([], _, context, _) do
raise_error(nil, [], context)
end
def assemble([{:whitespace, _} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([{:whitespace, _} = token | rest], :start_tag, context, tags) do
context = add_prev_token(context, token)
assemble(rest, :start_tag, context, tags)
end
def assemble([{:whitespace, _} = token | rest], :attr_key, context, tags) do
context =
context
|> add_attr(:boolean, context.attr_key, nil)
|> add_prev_token(token)
assemble(rest, :start_tag, context, tags)
end
def assemble([{:whitespace, _} = token | rest], :end_tag, context, tags) do
context = add_prev_token(context, token)
assemble(rest, :end_tag, context, tags)
end
def assemble([{:string, _} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([{:string, str} = token | rest], :start_tag_bracket, context, tags) do
context = context |> reset_tag(str) |> add_prev_token(token)
assemble(rest, :start_tag, context, tags)
end
def assemble([{:string, str} = token | rest], :start_tag, context, tags) do
context = context |> put_attr_key(str) |> add_prev_token(token)
assemble(rest, :attr_key, context, tags)
end
def assemble([{:string, str} = token | rest], :end_tag_bracket, context, tags) do
context = context |> reset_tag(str) |> add_prev_token(token)
assemble(rest, :end_tag, context, tags)
end
def assemble([{:symbol, :"</"} = token | rest], :text_tag, context, tags) do
{tokens, context} = flush_token_buffer(context)
tags = maybe_add_text_tag(tags, tokens)
context = add_prev_token(context, token)
assemble(rest, :end_tag_bracket, context, tags)
end
def assemble([{:symbol, :"/>"} = token | rest], :start_tag, context, tags) do
type = Helpers.tag_type(context.tag_name)
tags =
if type == :component || is_self_closing_tag?(context.tag_name) do
add_self_closing_tag(tags, context)
else
add_start_tag(tags, context)
end
handle_start_tag_end(context, token, rest, tags)
end
def assemble([{:symbol, :<} = token | [{:string, _} | _] = rest], :text_tag, context, tags) do
tags = maybe_add_text_tag(tags, context.token_buffer)
context = context |> reset_token_buffer() |> add_prev_token(token)
assemble(rest, :start_tag_bracket, context, tags)
end
def assemble([{:symbol, :<} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([{:symbol, :>} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([{:symbol, :>} = token | rest], :start_tag, context, tags) do
tags =
if is_self_closing_tag?(context.tag_name) do
add_self_closing_tag(tags, context)
else
add_start_tag(tags, context)
end
handle_start_tag_end(context, token, rest, tags)
end
def assemble([{:symbol, :>} = token | rest], :end_tag, context, tags) do
tags = add_end_tag(tags, context)
context = add_prev_token(context, token)
assemble(rest, :text_tag, context, tags)
end
def assemble([{:symbol, :/} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([{:symbol, :=} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([{:symbol, :=} = token | rest], :attr_key, context, tags) do
context = context |> reset_attr_value() |> add_prev_token(token)
assemble(rest, :attr_assignment, context, tags)
end
def assemble([{:symbol, :"\""} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([{:symbol, :"\""} = token | rest], :attr_assignment, context, tags) do
context = add_prev_token(context, token)
assemble(rest, :attr_value_literal, context, tags)
end
def assemble([{:symbol, :"\""} = token | rest], :attr_value_literal, context, tags) do
handle_attr_value_end(context, :literal, token, rest, tags)
end
def assemble([{:symbol, :"\""} = token | rest], :attr_value_expression, context, tags) do
assemble_attr_value(context, token, rest, tags, :attr_value_expression)
end
def assemble([{:symbol, :"{"} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([{:symbol, :"{"} = token | rest], :attr_assignment, context, tags) do
context = add_prev_token(context, token)
assemble(rest, :attr_value_expression, context, tags)
end
def assemble([{:symbol, :"{"} = token | rest], :attr_value_expression, context, tags) do
context
|> increment_num_open_braces()
|> assemble_attr_value(token, rest, tags, :attr_value_expression)
end
def assemble([{:symbol, :"}"} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble(
[{:symbol, :"}"} = token | rest],
:attr_value_expression,
%{num_open_braces: 0} = context,
tags
) do
handle_attr_value_end(context, :expression, token, rest, tags)
end
def assemble([{:symbol, :"}"} = token | rest], :attr_value_expression, context, tags) do
context
|> decrement_num_open_braces()
|> assemble_attr_value(token, rest, tags, :attr_value_expression)
end
def assemble([{:symbol, :"\\{"} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([{:symbol, :"\\}"} = token | rest], :text_tag, context, tags) do
assemble_text_tag(context, token, rest, tags)
end
def assemble([token | rest], :attr_value_literal, context, tags) do
assemble_attr_value(context, token, rest, tags, :attr_value_literal)
end
def assemble([token | rest], :attr_value_expression, context, tags) do
assemble_attr_value(context, token, rest, tags, :attr_value_expression)
end
def assemble([token | rest], _, context, _) do
raise_error(token, rest, context)
end
defp add_attr(context, type, key, value) do
%{context | attrs: context.attrs ++ [{type, key, value}]}
end
defp add_end_tag(tags, context) do
tags ++ [{:end_tag, context.tag_name}]
end
defp add_prev_token(context, token) do
%{context | prev_tokens: context.prev_tokens ++ [token]}
end
defp add_start_tag(tags, context) do
tags ++ [{:start_tag, {context.tag_name, context.attrs}}]
end
defp add_self_closing_tag(tags, context) do
tags ++ [{:self_closing_tag, {context.tag_name, context.attrs}}]
end
defp assemble_attr_value(context, token, rest, tags, status) do
context = context |> buffer_token(token) |> add_prev_token(token)
assemble(rest, status, context, tags)
end
defp assemble_text_tag(context, token, rest, tags) do
context = context |> buffer_token(token) |> add_prev_token(token)
assemble(rest, :text_tag, context, tags)
end
defp buffer_token(context, token) do
%{context | token_buffer: context.token_buffer ++ [token]}
end
defp decrement_num_open_braces(context) do
%{context | num_open_braces: context.num_open_braces - 1}
end
defp escape_non_printable_chars(str) do
str
|> String.replace("\n", "\\n")
|> String.replace("\r", "\\r")
|> String.replace("\t", "\\t")
end
defp flush_token_buffer(context) do
tokens = context.token_buffer
context = reset_token_buffer(context)
{tokens, context}
end
defp handle_attr_value_end(context, type, token, rest, tags) do
attr_value = TokenHTMLEncoder.encode(context.token_buffer)
context =
context
|> add_attr(type, context.attr_key, attr_value)
|> add_prev_token(token)
assemble(rest, :start_tag, context, tags)
end
defp handle_start_tag_end(context, token, rest, tags) do
context = context |> reset_token_buffer() |> add_prev_token(token)
assemble(rest, :text_tag, context, tags)
end
defp increment_num_open_braces(context) do
%{context | num_open_braces: context.num_open_braces + 1}
end
defp is_self_closing_tag?(tag_name) do
is_void_tag?(tag_name) || is_svg_tag?(tag_name) || tag_name == "slot"
end
defp is_svg_tag?(tag_name) do
tag_name in @svg_tags
end
defp is_void_tag?(tag_name) do
tag_name in @void_tags
end
defp maybe_add_text_tag(tags, tokens) do
if Enum.any?(tokens) do
tags ++ [{:text_tag, TokenHTMLEncoder.encode(tokens)}]
else
tags
end
end
defp put_attr_key(context, key) do
%{context | attr_key: key}
end
defp raise_error(token, rest, %{prev_tokens: prev_tokens}) do
prev_tokens_str = TokenHTMLEncoder.encode(prev_tokens)
prev_tokens_len = String.length(prev_tokens_str)
prev_fragment =
if prev_tokens_len > 20 do
String.slice(prev_tokens_str, -20..-1)
else
prev_tokens_str
end
|> escape_non_printable_chars()
prev_fragment_len = String.length(prev_fragment)
indent = String.duplicate(" ", prev_fragment_len)
current_fragment =
TokenHTMLEncoder.encode(token)
|> escape_non_printable_chars()
next_fragment =
TokenHTMLEncoder.encode(rest)
|> String.slice(0, 20)
|> escape_non_printable_chars()
message = """
#{prev_fragment}#{current_fragment}#{next_fragment}
#{indent}^\
"""
raise SyntaxError, message: message
end
defp reset_attr_value(context) do
%{context | double_quote_opened?: false, num_open_braces: 0, token_buffer: []}
end
defp reset_tag(context, tag_name) do
%{context | attrs: [], tag_name: tag_name}
end
defp reset_token_buffer(context) do
%{context | token_buffer: []}
end
end