Current section
Files
Jump to
Current section
Files
lib/hologram/template/parser.ex
defmodule Hologram.Template.Parser do
@moduledoc false
if Application.compile_env(:hologram, :debug_parser) do
use Interceptor.Annotated,
config: %{
{Hologram.Template.Parser, :parse_tokens, 3} => [
on_success: {Hologram.Template.Parser, :debug, 3},
on_error: {Hologram.Template.Parser, :debug, 3}
]
}
end
alias Hologram.Commons.StringUtils
alias Hologram.Template.Helpers
alias Hologram.Template.Parser
alias Hologram.Template.Tokenizer
alias Hologram.TemplateSyntaxError
@default_error_details StringUtils.normalize_newlines("""
Reason:
Unknown reason.
Hint:
Please report that you received this message here: https://github.com/bartblast/hologram/issues
and include a markup snippet that will allow us to reproduce the issue.
""")
@unescaped_lt_char_details StringUtils.normalize_newlines("""
Reason:
Unescaped '<' character inside text node.
Hint:
To escape use HTML entity: '<'.
""")
@type parsed_tag ::
{:block_end
| :block_start
| :doctype
| :end_tag
| :expression
| :public_comment_end
| :public_comment_start
| :self_closing_tag
| :start_tag
| :text, any}
@type status ::
:attribute_assignment
| :attribute_name
| :block_end
| :block_start
| :doctype
| :end_tag
| :end_tag_name
| :expression
| :text
| :start_tag
| :start_tag_name
defmodule Context do
@moduledoc false
defstruct attribute_name: nil,
attribute_value: [],
attributes: [],
block_name: nil,
delimiter_stack: [],
node_type: :text,
prev_status: nil,
processed_tags: [],
processed_tokens: [],
raw?: false,
script?: false,
tag_name: nil,
token_buffer: []
@type attribute_value_part :: list({:expression | :text, String.t()})
@type delimiter ::
:backtick
| :curly_bracket
| :double_quote
| :elixir_interpolation
| :javascript_interpolation
| :single_quote
@type t :: %__MODULE__{
attribute_name: String.t() | nil,
attribute_value: list(attribute_value_part),
attributes: list({String.t(), list(attribute_value_part())}),
block_name: String.t() | nil,
delimiter_stack: list(delimiter),
node_type: :attribute | :block | :public_comment | :tag | :text,
prev_status: Parser.status() | nil,
processed_tags: [],
processed_tokens: list(Tokenizer.token()),
raw?: boolean,
script?: boolean,
tag_name: String.t() | nil,
token_buffer: list(Tokenizer.token())
}
end
@doc """
Parses markup into tags.
## Examples
iex> markup = "<div id=\"test\"></div>"
iex> parse_markup(markup)
[start_tag: {"div", [{"id", [text: "test"]}]}, end_tag: "div"]
"""
@spec parse_markup(String.t()) :: list(parsed_tag)
def parse_markup(markup) do
markup
|> Tokenizer.tokenize()
|> parse_tokens()
end
@doc """
Parses tokens into tags.
## Examples
iex> tokens = [
...> symbol: "<",
...> string: "div",
...> whitespace: " ",
...> string: "id",
...> symbol: "=",
...> symbol: "\"",
...> string: "test",
...> symbol: "\"",
...> symbol: ">",
...> symbol: "</",
...> string: "div",
...> symbol: ">"
...> ]
iex> parse_tokens(tokens)
[start_tag: {"div", [{"id", [text: "test"]}]}, end_tag: "div"]
"""
@intercept true
@spec parse_tokens(Context.t(), status, list(Tokenizer.token())) :: list(parsed_tag)
def parse_tokens(context \\ %Context{}, status \\ :text, tokens)
# Note: try to keep the pattern matching order from Hologram.Template.Tokenizer where possible.
# --- ATTRIBUTE ASSIGNMENT ---
def parse_tokens(context, :attribute_assignment, [{:symbol, "\""} = token | rest]) do
context
|> add_processed_token(token)
|> set_prev_status(:attribute_assignment)
|> parse_tokens(:text, rest)
end
def parse_tokens(%{raw?: true} = context, :attribute_assignment, [{:symbol, "{"} = token | rest]) do
tag_type = Helpers.tag_type(context.tag_name)
node_name = if tag_type == :element, do: "attribute", else: "property"
details =
StringUtils.normalize_newlines("""
Reason:
Expression #{node_name} value inside raw block detected.
Hint:
Either wrap the #{node_name} value with double quotes or remove the parent raw block".
""")
raise_error(details, context, :attribute_assignment, token, rest)
end
def parse_tokens(context, :attribute_assignment, [{:symbol, "{"} = token | rest]) do
context
|> buffer_token(token)
|> add_processed_token(token)
|> set_prev_status(:attribute_assignment)
|> push_delimiter_stack(:expression)
|> parse_tokens(:expression, rest)
end
# --- ATTRIBUTE NAME ---
def parse_tokens(context, :attribute_name, [{:whitespace, _value} = token | rest]) do
context
|> set_attribute_name()
|> flush_attribute()
|> add_processed_token(token)
|> set_prev_status(:attribute_name)
|> parse_tokens(:start_tag, rest)
end
def parse_tokens(context, :attribute_name, [{:symbol, "="} = token | rest]) do
context
|> set_attribute_name()
|> reset_token_buffer()
|> add_processed_token(token)
|> set_prev_status(:attribute_name)
|> set_node_type(:attribute)
|> parse_tokens(:attribute_assignment, rest)
end
def parse_tokens(context, :attribute_name, [{:symbol, ">"} = token | rest]) do
context
|> set_attribute_name()
|> flush_attribute()
|> parse_start_tag_end(token, rest, false)
end
def parse_tokens(context, :attribute_name, [{:symbol, "-"} = token | rest]) do
parse_attribute_name(context, token, rest)
end
def parse_tokens(context, :attribute_name, [{:string, _str} = token | rest]) do
parse_attribute_name(context, token, rest)
end
# --- DOCTYPE ---
def parse_tokens(context, :doctype, []) do
details =
StringUtils.normalize_newlines("""
Reason:
Unclosed DOCTYPE declaration.
Hint:
Close the DOCTYPE declaration with '>' character.
""")
raise_error(details, context, :doctype, nil, [])
end
def parse_tokens(context, :doctype, [{:symbol, ">"} = token | rest]) do
context
|> add_doctype_declaration()
|> reset_token_buffer()
|> add_processed_token(token)
|> parse_tokens(:text, rest)
end
def parse_tokens(context, :doctype, [token | rest]) do
context
|> buffer_token(token)
|> add_processed_token(token)
|> parse_tokens(:doctype, rest)
end
# --- END TAG NAME ---
def parse_tokens(context, :end_tag_name, [{:string, tag_name} = token | rest]) do
context
|> set_tag_name(tag_name)
|> maybe_disable_script_mode(tag_name)
|> add_processed_token(token)
|> set_prev_status(:end_tag_name)
|> parse_tokens(:end_tag, rest)
end
# --- END TAG ---
def parse_tokens(context, :end_tag, [{:whitespace, _value} = token | rest]) do
context
|> add_processed_token(token)
|> parse_tokens(:end_tag, rest)
end
def parse_tokens(context, :end_tag, [{:symbol, ">"} = token | rest]) do
context
|> add_end_tag()
|> add_processed_token(token)
|> set_prev_status(:end_tag)
|> set_node_type(:text)
|> parse_tokens(:text, rest)
end
# --- EXPRESSION ---
def parse_tokens(%{delimiter_stack: [delimiter | _tail]} = context, :expression, [
{:symbol, "\#{"} = token | rest
])
when delimiter in [:double_quote, :single_quote] do
context
|> push_delimiter_stack(:elixir_interpolation)
|> parse_expression(token, rest)
end
def parse_tokens(%{delimiter_stack: [:double_quote | _tail]} = context, :expression, [
{:symbol, "\""} = token | rest
]) do
context
|> pop_delimiter_stack()
|> parse_expression(token, rest)
end
def parse_tokens(%{delimiter_stack: [:single_quote | _tail]} = context, :expression, [
{:symbol, "\""} = token | rest
]) do
parse_expression(context, token, rest)
end
def parse_tokens(context, :expression, [{:symbol, "\""} = token | rest]) do
context
|> push_delimiter_stack(:double_quote)
|> parse_expression(token, rest)
end
def parse_tokens(%{delimiter_stack: [:double_quote | _tail]} = context, :expression, [
{:symbol, "'"} = token | rest
]) do
parse_expression(context, token, rest)
end
def parse_tokens(%{delimiter_stack: [:single_quote | _tail]} = context, :expression, [
{:symbol, "'"} = token | rest
]) do
context
|> pop_delimiter_stack()
|> parse_expression(token, rest)
end
def parse_tokens(context, :expression, [{:symbol, "'"} = token | rest]) do
context
|> push_delimiter_stack(:single_quote)
|> parse_expression(token, rest)
end
def parse_tokens(%{delimiter_stack: [delimiter | _tail]} = context, :expression, [
{:symbol, "{"} = token | rest
])
when delimiter in [:double_quote, :single_quote] do
parse_expression(context, token, rest)
end
def parse_tokens(context, :expression, [{:symbol, "{"} = token | rest]) do
context
|> push_delimiter_stack(:curly_bracket)
|> parse_expression(token, rest)
end
def parse_tokens(
%{
delimiter_stack: [:expression],
node_type: :attribute,
prev_status: :attribute_assignment
} = context,
:expression,
[{:symbol, "}"} = token | rest]
) do
context
|> buffer_token(token)
|> add_processed_token(token)
|> add_attribute_value_part(:expression)
|> flush_attribute()
|> set_prev_status(:expression)
|> pop_delimiter_stack()
|> parse_tokens(:start_tag, rest)
end
def parse_tokens(
%{delimiter_stack: [:expression], node_type: :attribute, prev_status: :text} = context,
:expression,
[{:symbol, "}"} = token | rest]
) do
context
|> buffer_token(token)
|> add_processed_token(token)
|> add_attribute_value_part(:expression)
|> reset_token_buffer()
|> set_prev_status(:expression)
|> pop_delimiter_stack()
|> parse_tokens(:text, rest)
end
def parse_tokens(
%{delimiter_stack: [:expression | _tail], node_type: :block} = context,
:expression,
[
{:symbol, "}"} = token | rest
]
) do
context
|> buffer_token(token)
|> add_block_start_tag()
|> add_processed_token(token)
|> pop_delimiter_stack()
|> reset_token_buffer()
|> set_prev_status(:expression)
|> set_node_type(:text)
|> parse_tokens(:text, rest)
end
def parse_tokens(
%{delimiter_stack: [:expression | _tail], node_type: node_type} = context,
:expression,
[
{:symbol, "}"} = token | rest
]
)
when node_type == :text or node_type == :public_comment do
context
|> buffer_token(token)
|> add_expression_tag()
|> pop_delimiter_stack()
|> add_processed_token(token)
|> reset_token_buffer()
|> set_prev_status(:expression)
|> parse_tokens(:text, rest)
end
def parse_tokens(%{delimiter_stack: [:curly_bracket | _tail]} = context, :expression, [
{:symbol, "}"} = token | rest
]) do
context
|> pop_delimiter_stack()
|> parse_expression(token, rest)
end
def parse_tokens(%{delimiter_stack: [:elixir_interpolation | _tail]} = context, :expression, [
{:symbol, "}"} = token | rest
]) do
context
|> pop_delimiter_stack()
|> parse_expression(token, rest)
end
def parse_tokens(context, :expression, [token | rest]) do
parse_expression(context, token, rest)
end
# --- START TAG NAME ---
def parse_tokens(context, :start_tag_name, [{:string, tag_name} = token | rest]) do
context
|> reset_attributes()
|> set_tag_name(tag_name)
|> maybe_enable_script_mode(tag_name)
|> add_processed_token(token)
|> set_prev_status(:start_tag_name)
|> parse_tokens(:start_tag, rest)
end
# --- START TAG ---
def parse_tokens(context, :start_tag, []) do
details =
StringUtils.normalize_newlines("""
Reason:
Unclosed start tag.
Hint:
Close the start tag with '>' character.
""")
raise_error(details, context, :start_tag, nil, [])
end
def parse_tokens(context, :start_tag, [{:whitespace, _value} = token | rest]) do
context
|> add_processed_token(token)
|> parse_tokens(:start_tag, rest)
end
def parse_tokens(context, :start_tag, [{:symbol, "="} = token | rest]) do
details =
StringUtils.normalize_newlines("""
Reason:
Missing attribute name.
Hint:
Specify the attribute name before the '=' character.
""")
raise_error(details, context, :start_tag, token, rest)
end
def parse_tokens(context, :start_tag, [{:symbol, "/>"} = token | rest]) do
parse_start_tag_end(context, token, rest, true)
end
def parse_tokens(context, :start_tag, [{:symbol, ">"} = token | rest]) do
parse_start_tag_end(context, token, rest, false)
end
def parse_tokens(context, :start_tag, [{:string, _str} = token | rest]) do
context
|> reset_attribute_value()
|> reset_token_buffer()
|> set_prev_status(:start_tag)
|> parse_attribute_name(token, rest)
end
def parse_tokens(context, :start_tag, [{:symbol, "$"} = token | rest]) do
context
|> reset_attribute_value()
|> reset_token_buffer()
|> set_prev_status(:start_tag)
|> parse_attribute_name(token, rest)
end
# --- TEXT ---
def parse_tokens(%{node_type: :public_comment} = context, :text, []) do
details =
StringUtils.normalize_newlines("""
Reason:
Unclosed public comment.
Hint:
Close the public comment with '-->' marker.
""")
raise_error(details, context, :text, nil, [])
end
def parse_tokens(context, :text, []) do
context
|> maybe_add_text_tag()
|> Map.fetch!(:processed_tags)
|> Enum.reverse()
end
def parse_tokens(%{node_type: :attribute} = context, :text, [{:symbol, "\""} = token | rest]) do
context
|> add_attribute_value_part(:text)
|> flush_attribute()
|> add_processed_token(token)
|> set_prev_status(:text)
|> set_node_type(:tag)
|> parse_tokens(:start_tag, rest)
end
def parse_tokens(%{script?: true, delimiter_stack: []} = context, :text, [
{:symbol, "\""} = token | rest
]) do
context
|> push_delimiter_stack(:double_quote)
|> parse_text(token, rest)
end
def parse_tokens(%{script?: true, delimiter_stack: [:double_quote | _tail]} = context, :text, [
{:symbol, "\""} = token | rest
]) do
context
|> pop_delimiter_stack()
|> parse_text(token, rest)
end
def parse_tokens(%{script?: true, delimiter_stack: []} = context, :text, [
{:symbol, "'"} = token | rest
]) do
context
|> push_delimiter_stack(:single_quote)
|> parse_text(token, rest)
end
def parse_tokens(%{script?: true, delimiter_stack: [:single_quote | _tail]} = context, :text, [
{:symbol, "'"} = token | rest
]) do
context
|> pop_delimiter_stack()
|> parse_text(token, rest)
end
def parse_tokens(%{script?: true, delimiter_stack: []} = context, :text, [
{:symbol, "`"} = token | rest
]) do
context
|> push_delimiter_stack(:backtick)
|> parse_text(token, rest)
end
def parse_tokens(%{script?: true, delimiter_stack: [:backtick | _tail]} = context, :text, [
{:symbol, "`"} = token | rest
]) do
context
|> pop_delimiter_stack()
|> parse_text(token, rest)
end
def parse_tokens(context, :text, [{:symbol, "\\{"} = token | rest]) do
context
|> buffer_token({:symbol, "{"})
|> add_processed_token(token)
|> parse_tokens(:text, rest)
end
def parse_tokens(%{node_type: :text, script?: false} = context, :text, [
{:symbol, "<!--"} = token | rest
]) do
context
|> maybe_add_text_tag()
|> reset_token_buffer()
|> add_public_comment_start_tag()
|> add_processed_token(token)
|> set_node_type(:public_comment)
|> parse_tokens(:text, rest)
end
def parse_tokens(%{node_type: :public_comment} = context, :text, [
{:symbol, "<!--"} | [{:symbol, ">"} | rest]
]) do
parse_tokens(context, :text, [{:symbol, "<!"} | [{:symbol, "-->"} | rest]])
end
def parse_tokens(%{node_type: :public_comment} = context, :text, [
{:symbol, "<!--"} = token | rest
]) do
parse_text(context, token, rest)
end
def parse_tokens(%{node_type: :text, script?: false} = context, :text, [
{:symbol, "<!"} = token_1 | rest_1
]) do
case rest_1 do
[{:string, str} = token_2 | rest_2] ->
if String.downcase(str) == "doctype" do
context
|> maybe_add_text_tag()
|> reset_token_buffer()
|> add_processed_token(token_1)
|> add_processed_token(token_2)
|> parse_tokens(:doctype, rest_2)
else
raise_error(@unescaped_lt_char_details, context, :text, token_1, rest_1)
end
_fallback ->
raise_error(@unescaped_lt_char_details, context, :text, token_1, rest_1)
end
end
def parse_tokens(%{node_type: :public_comment} = context, :text, [
{:symbol, "-->"} = token | rest
]) do
context
|> maybe_add_text_tag()
|> reset_token_buffer()
|> add_public_comment_end_tag()
|> add_processed_token(token)
|> set_node_type(:text)
|> parse_tokens(:text, rest)
end
def parse_tokens(%{raw?: false} = context, :text, [{:symbol, "{%raw}"} = token | rest]) do
context
|> add_processed_token(token)
|> enable_raw_mode()
|> parse_tokens(:text, rest)
end
def parse_tokens(%{raw?: true} = context, :text, [{:symbol, "{/raw}"} = token | rest]) do
context
|> add_processed_token(token)
|> disable_raw_mode()
|> parse_tokens(:text, rest)
end
def parse_tokens(%{raw?: false} = context, :text, [{:symbol, "{%else}"} = token | rest]) do
context
|> maybe_add_text_tag()
|> add_processed_token(token)
|> add_processed_tag({:block_start, "else"})
|> reset_token_buffer()
|> parse_tokens(:text, rest)
end
def parse_tokens(%{raw?: false} = context, :text, [{:symbol, "{%for"} = token | rest]) do
parse_block_start(context, "for", token, rest)
end
def parse_tokens(%{raw?: false} = context, :text, [{:symbol, "{/for}"} = token | rest]) do
parse_block_end(context, "for", token, rest)
end
def parse_tokens(%{raw?: false} = context, :text, [{:symbol, "{%if"} = token | rest]) do
parse_block_start(context, "if", token, rest)
end
def parse_tokens(%{raw?: false} = context, :text, [{:symbol, "{/if}"} = token | rest]) do
parse_block_end(context, "if", token, rest)
end
def parse_tokens(%{raw?: false} = context, :text, [{:symbol, "\#{"} | rest]) do
context
|> buffer_token({:symbol, "#"})
|> maybe_add_text_tag()
|> add_processed_token({:symbol, "#"})
|> reset_token_buffer()
|> set_prev_status(:text)
|> push_delimiter_stack(:expression)
|> parse_expression({:symbol, "{"}, rest)
end
def parse_tokens(%{raw?: false, script?: false} = context, :text, [{:symbol, "${"} | rest]) do
context
|> buffer_token({:symbol, "$"})
|> maybe_add_text_tag()
|> add_processed_token({:symbol, "$"})
|> reset_token_buffer()
|> set_prev_status(:text)
|> push_delimiter_stack(:expression)
|> parse_expression({:symbol, "{"}, rest)
end
def parse_tokens(%{raw?: false, node_type: :attribute} = context, :text, [
{:symbol, "{"} = token | rest
]) do
context
|> add_attribute_value_part(:text)
|> reset_token_buffer()
|> set_prev_status(:text)
|> push_delimiter_stack(:expression)
|> parse_expression(token, rest)
end
def parse_tokens(%{raw?: false} = context, :text, [{:symbol, "{"} = token | rest]) do
context
|> maybe_add_text_tag()
|> reset_token_buffer()
|> set_prev_status(:text)
|> push_delimiter_stack(:expression)
|> parse_expression(token, rest)
end
def parse_tokens(context, :text, [{:symbol, "\\}"} = token | rest]) do
context
|> buffer_token({:symbol, "}"})
|> add_processed_token(token)
|> parse_tokens(:text, rest)
end
def parse_tokens(%{script?: true, delimiter_stack: [delimiter | _tail]} = context, :text, [
{:symbol, "</"} = token | rest
])
when delimiter in [:backtick, :double_quote, :single_quote] do
parse_text(context, token, rest)
end
def parse_tokens(%{node_type: :text} = context, :text, [{:symbol, "</"} = token | rest]) do
context
|> maybe_add_text_tag()
|> reset_token_buffer()
|> add_processed_token(token)
|> set_prev_status(:text)
|> parse_tokens(:end_tag_name, rest)
end
def parse_tokens(%{node_type: :text, script?: false} = context, :text, [
{:symbol, "<"} = token | [{:string, _value} | _tokens] = rest
]) do
context
|> maybe_add_text_tag()
|> reset_token_buffer()
|> add_processed_token(token)
|> set_prev_status(:text)
|> set_node_type(:tag)
|> parse_tokens(:start_tag_name, rest)
end
def parse_tokens(%{node_type: :text, script?: false} = context, :text, [
{:symbol, "<"} = token | rest
]) do
raise_error(@unescaped_lt_char_details, context, :text, token, rest)
end
def parse_tokens(%{node_type: :text, script?: false} = context, :text, [
{:symbol, ">"} = token | rest
]) do
details =
StringUtils.normalize_newlines("""
Reason:
Unescaped '>' character inside text node.
Hint:
To escape use HTML entity: '>'.
""")
raise_error(details, context, :text, token, rest)
end
def parse_tokens(context, :text, [token | rest]) do
parse_text(context, token, rest)
end
# These two cases shouldn't happen once we've got template syntax errors covered.
def parse_tokens(context, type, [token | rest]) do
raise_error(@default_error_details, context, type, token, rest)
end
def parse_tokens(context, type, []) do
raise_error(@default_error_details, context, type, nil, [])
end
@doc """
Prints debug info for intercepted parse/3 calls.
"""
@spec debug({module, atom, list}, list | %{__struct__: FunctionClauseError}, integer) :: :ok
def debug({_module, _function, [context, status, tokens] = _args}, result, _start_timestamp) do
# credo:disable-for-lines:13 /Credo.Check.Refactor.IoPuts|Credo.Check.Warning.IoInspect/
IO.puts("\nPARSER..................................\n")
IO.puts("context")
IO.inspect(context)
IO.puts("")
IO.puts("status")
IO.inspect(status)
IO.puts("")
IO.puts("tokens")
IO.inspect(tokens)
IO.puts("")
IO.puts("result")
IO.inspect(result)
IO.puts("\n........................................\n")
end
defp add_attribute_value_part(context, type) do
part = {type, encode_tokens(context.token_buffer)}
%{context | attribute_value: [part | context.attribute_value]}
end
defp add_block_start_tag(context) do
expression = encode_tokens(context.token_buffer)
new_tag = {:block_start, {context.block_name, expression}}
add_processed_tag(context, new_tag)
end
defp add_block_end_tag(context, block_name) do
new_tag = {:block_end, block_name}
add_processed_tag(context, new_tag)
end
defp add_doctype_declaration(%{token_buffer: token_buffer} = context) do
content =
token_buffer
|> encode_tokens()
|> String.trim()
new_tag = {:doctype, content}
add_processed_tag(context, new_tag)
end
defp add_end_tag(context) do
new_tag = {:end_tag, context.tag_name}
add_processed_tag(context, new_tag)
end
defp add_expression_tag(%{token_buffer: token_buffer} = context) do
new_tag = {:expression, encode_tokens(token_buffer)}
add_processed_tag(context, new_tag)
end
defp add_processed_tag(%{processed_tags: processed_tags} = context, tag) do
%{context | processed_tags: [tag | processed_tags]}
end
defp add_processed_token(%{processed_tokens: processed_tokens} = context, token) do
%{context | processed_tokens: [token | processed_tokens]}
end
defp add_public_comment_end_tag(context) do
add_processed_tag(context, :public_comment_end)
end
defp add_public_comment_start_tag(context) do
add_processed_tag(context, :public_comment_start)
end
defp add_self_closing_tag(context) do
attributes = Enum.reverse(context.attributes)
new_tag = {:self_closing_tag, {context.tag_name, attributes}}
add_processed_tag(context, new_tag)
end
defp add_start_tag(context) do
attributes = Enum.reverse(context.attributes)
new_tag = {:start_tag, {context.tag_name, attributes}}
add_processed_tag(context, new_tag)
end
defp buffer_token(%{token_buffer: token_buffer} = context, token) do
%{context | token_buffer: [token | token_buffer]}
end
defp disable_raw_mode(context) do
%{context | raw?: false}
end
defp disable_script_mode(context) do
%{context | script?: false}
end
defp enable_raw_mode(context) do
%{context | raw?: true}
end
defp enable_script_mode(context) do
%{context | script?: true}
end
defp encode_tokens(tokens) do
tokens
|> Enum.reverse()
|> join_tokens()
end
defp escape_non_printable_chars(str) do
str
|> String.replace("\n", "\\n")
|> String.replace("\r", "\\r")
|> String.replace("\t", "\\t")
end
defp flush_attribute(context) do
new_attr = {context.attribute_name, Enum.reverse(context.attribute_value)}
%{
context
| attribute_name: nil,
attribute_value: [],
attributes: [new_attr | context.attributes]
}
end
defp join_tokens(tokens) do
Enum.map_join(tokens, "", fn {_type, value} -> value end)
end
defp maybe_add_text_tag(%{token_buffer: token_buffer} = context) do
if Enum.any?(token_buffer) do
text_tag = {:text, encode_tokens(token_buffer)}
add_processed_tag(context, text_tag)
else
context
end
end
defp maybe_disable_script_mode(context, "script") do
disable_script_mode(context)
end
defp maybe_disable_script_mode(context, _tag_name), do: context
defp maybe_enable_script_mode(context, "script") do
enable_script_mode(context)
end
defp maybe_enable_script_mode(context, _tag_name), do: context
defp parse_attribute_name(context, token, rest) do
context
|> buffer_token(token)
|> add_processed_token(token)
|> parse_tokens(:attribute_name, rest)
end
defp parse_block_end(context, block_name, token, rest) do
context
|> maybe_add_text_tag()
|> add_processed_token(token)
|> set_prev_status(:text)
|> add_block_end_tag(block_name)
|> set_prev_status(:block_end)
|> reset_token_buffer()
|> parse_tokens(:text, rest)
end
defp parse_block_start(context, block_name, token, rest) do
context
|> maybe_add_text_tag()
|> add_processed_token(token)
|> push_delimiter_stack(:expression)
|> set_node_type(:block)
|> set_block_name(block_name)
|> reset_token_buffer()
|> buffer_token({:symbol, "{"})
|> set_prev_status(:block_start)
|> parse_tokens(:expression, rest)
end
defp parse_expression(context, token, rest) do
context
|> buffer_token(token)
|> add_processed_token(token)
|> parse_tokens(:expression, rest)
end
defp parse_start_tag_end(context, token, rest, self_closing?) do
add_tag_fun =
if self_closing? || Helpers.void_element?(context.tag_name) do
&add_self_closing_tag/1
else
&add_start_tag/1
end
context
|> add_tag_fun.()
|> reset_token_buffer()
|> add_processed_token(token)
|> set_prev_status(:start_tag)
|> set_node_type(:text)
|> parse_tokens(:text, rest)
end
defp parse_text(context, token, rest) do
context
|> buffer_token(token)
|> add_processed_token(token)
|> parse_tokens(:text, rest)
end
defp pop_delimiter_stack(%{delimiter_stack: [_head | tail]} = context) do
%{context | delimiter_stack: tail}
end
defp push_delimiter_stack(context, delimiter) do
%{context | delimiter_stack: [delimiter | context.delimiter_stack]}
end
defp raise_error(details, %{processed_tokens: processed_tokens} = context, status, token, rest) do
encoded_tokens = encode_tokens(processed_tokens)
encoded_tokens_len = String.length(encoded_tokens)
prev_fragment =
if encoded_tokens_len > 20 do
String.slice(encoded_tokens, -20..-1)
else
encoded_tokens
end
escaped_prev_fragment = escape_non_printable_chars(prev_fragment)
prev_fragment_len = String.length(escaped_prev_fragment)
indent = String.duplicate(" ", prev_fragment_len)
current_fragment =
token
|> List.wrap()
|> encode_tokens()
|> escape_non_printable_chars()
next_fragment =
rest
|> join_tokens()
|> String.slice(0, 20)
|> escape_non_printable_chars()
message =
StringUtils.normalize_newlines("""
#{details}
#{escaped_prev_fragment}#{current_fragment}#{next_fragment}
#{indent}^
status = #{inspect(status)}
token = #{inspect(token)}
context = #{inspect(context)}
""")
raise TemplateSyntaxError, message: message
end
defp reset_attribute_value(context) do
%{context | attribute_value: []}
end
defp reset_attributes(context) do
%{context | attributes: []}
end
defp reset_token_buffer(context) do
%{context | token_buffer: []}
end
defp set_attribute_name(context) do
%{context | attribute_name: encode_tokens(context.token_buffer)}
end
defp set_block_name(context, name) do
%{context | block_name: name}
end
defp set_node_type(context, type) do
%{context | node_type: type}
end
defp set_prev_status(context, status) do
%{context | prev_status: status}
end
defp set_tag_name(context, name) do
%{context | tag_name: name}
end
end