Packages
surface
0.7.3
0.12.3
0.12.2
0.12.1
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha.0
A component based library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
lib/surface/compiler/parser.ex
defmodule Surface.Compiler.Parser do
@moduledoc false
alias Surface.Compiler.Tokenizer
alias Surface.Compiler.ParseError
alias Surface.Compiler.Helpers
alias Surface.IOHelper
@type state :: %{
token_stack:
list({Tokenizer.block_open() | Tokenizer.tag_open(), Surface.Compiler.NodeTranslator.context()}),
translator: module(),
caller: Macro.Env.t(),
checks: keyword(boolean()),
warnings: keyword(boolean())
}
@blocks [
"if",
"unless",
"for",
"case"
]
@sub_blocks [
"else",
"elseif",
"match"
]
@sub_blocks_valid_parents %{
"else" => ["if", "for"],
"elseif" => ["if"],
"match" => ["case"]
}
def parse!(code, opts \\ []) do
code
|> Tokenizer.tokenize!(opts)
|> handle_token(opts)
end
defp handle_token(tokens, opts) do
state = %{
translator: opts[:translator] || Surface.Compiler.ParseTreeTranslator,
token_stack: [],
caller: opts[:caller] || __ENV__,
checks: opts[:checks] || [],
warnings: opts[:warnings] || []
}
handle_token(tokens, [[]], state.translator.handle_init(state))
end
defp handle_token([], [buffer], _state) do
Enum.reverse(buffer)
end
defp handle_token([], _buffers, %{token_stack: [{{_, _name, _attrs, meta} = node, _ctx} | _]}) do
message = "end of file reached without closing #{node_type(node)} for #{format_node(node)}"
raise parse_error(message, meta)
end
defp handle_token([{:text, text} | rest], buffers, state) do
{node, state} = state.translator.handle_text(text, state)
buffers = push_node_to_current_buffer(node, buffers)
handle_token(rest, buffers, state)
end
defp handle_token([{:comment, comment, meta} | rest], buffers, state) do
{node, state} = state.translator.handle_comment(comment, meta, state)
buffers = push_node_to_current_buffer(node, buffers)
handle_token(rest, buffers, state)
end
defp handle_token([{:expr, expr, meta} | rest], buffers, state) do
{node, state} = state.translator.handle_expression(expr, meta, state)
buffers = push_node_to_current_buffer(node, buffers)
handle_token(rest, buffers, state)
end
defp handle_token([{:tagged_expr, marker, {:expr, value, meta}, _marker_meta} | rest], buffers, state) do
{node, state} = state.translator.handle_tagged_expression(marker, value, meta, state)
buffers = push_node_to_current_buffer(node, buffers)
handle_token(rest, buffers, state)
end
defp handle_token([{:tag_open, name, attrs, %{void_tag?: true} = meta} | rest], buffers, state) do
context = state.translator.context_for_node(name, meta, state)
{node, state} =
state.translator.handle_node(name, translate_attrs(state, context, attrs), [], meta, state, context)
buffers = push_node_to_current_buffer(node, buffers)
handle_token(rest, buffers, state)
end
defp handle_token([{:tag_open, name, attrs, %{self_close: true} = meta} | rest], buffers, state) do
context = state.translator.context_for_node(name, meta, state)
{node, state} =
state.translator.handle_node(name, translate_attrs(state, context, attrs), [], meta, state, context)
buffers = push_node_to_current_buffer(node, buffers)
handle_token(rest, buffers, state)
end
defp handle_token([{:tag_open, name, _attrs, meta} = token | rest], buffers, state) do
context = state.translator.context_for_node(name, meta, state)
state = push_tag(state, token, context)
# create a new buffer for the node
buffers = [[] | buffers]
handle_token(rest, buffers, state)
end
defp handle_token([{:tag_close, name, _meta} = token | rest], buffers, state) do
{{:tag_open, _name, attrs, meta}, context, state} = pop_matching_tag(state, token)
# pop the current buffer and use it as children for the node
[buffer | buffers] = buffers
translated_attrs = translate_attrs(state, context, attrs)
{node, state} =
state.translator.handle_node(name, translated_attrs, Enum.reverse(buffer), meta, state, context)
buffers = push_node_to_current_buffer(node, buffers)
handle_token(rest, buffers, state)
end
defp handle_token([{:block_open, name, expr, meta} = token | rest], buffers, state) when name in @sub_blocks do
{buffers, state} = close_sub_block(token, buffers, state)
context = state.translator.context_for_subblock(name, parent_context(state.token_stack), meta, state)
# push the current sub-block token to state
state = push_tag(state, {:block_open, name, expr, meta}, context)
# create a new buffer for the current sub-block
buffers = [[] | buffers]
handle_token(rest, buffers, state)
end
defp handle_token([{:block_open, name, _expr, meta} = token | rest], buffers, state) when name in @blocks do
context = state.translator.context_for_block(name, meta, state)
state = push_tag(state, token, context)
# create a new buffer for the node
buffers = [[] | buffers]
handle_token(rest, buffers, state)
end
defp handle_token([{:block_open, name, _expr, meta} | _], _buffers, _state) do
blocks = Helpers.list_to_string("block is", "blocks are", @blocks ++ @sub_blocks)
raise parse_error("unknown `{##{name}}` block. Available #{blocks}", meta)
end
defp handle_token(
[{:block_close, _name, _meta} = token | _] = tokens,
buffers,
%{token_stack: [{{:block_open, name, _, _}, _} | _]} = state
)
when name in @sub_blocks do
{buffers, state} = close_sub_block(token, buffers, state)
handle_token(tokens, buffers, state)
end
defp handle_token([{:block_close, name, _meta} = token | rest], buffers, state) when name in @blocks do
{{:block_open, name, expr, meta}, context, state} = pop_matching_tag(state, token)
meta = Map.put_new(meta, :has_sub_blocks?, false)
# pop the current buffer and use it as children for the node
[buffer | buffers] = buffers
expression = state.translator.handle_block_expression(name, expr, state, context)
{node, state} =
state.translator.handle_block(
name,
expression,
Enum.reverse(buffer),
meta,
state,
context
)
buffers = push_node_to_current_buffer(node, buffers)
handle_token(rest, buffers, state)
end
defp handle_token([{:block_close, name, meta} | _], _buffers, _state) do
blocks = Helpers.list_to_string("block is", "blocks are", @blocks)
raise parse_error("unknown `{/#{name}}` block. Available #{blocks}", meta)
end
# If there's a previous sub-block defined. Close it.
defp close_sub_block(
_token,
buffers,
%{token_stack: [{{:block_open, name, expr, meta}, context} | tokens]} = state
)
when name in @sub_blocks do
# pop the current buffer and use it as children for the sub-block node
[buffer | buffers] = buffers
expression = state.translator.handle_block_expression(name, expr, state, context)
{node, state} = state.translator.handle_subblock(name, expression, Enum.reverse(buffer), meta, state, context)
state = %{state | token_stack: tokens}
buffers = push_node_to_current_buffer(node, buffers)
{buffers, state}
end
# If there's no previous sub-block defined. Create a :default sub-block,
# move the buffer there and close it.
defp close_sub_block(
token,
buffers,
%{token_stack: [{{:block_open, name, expr, meta}, ctx} | tokens]} = state
) do
validate_sub_block!(token, name)
# pop the current buffer and use it as children for the :default sub-block node
[buffer | buffers] = buffers
context = state.translator.context_for_subblock(:default, meta, state, ctx)
expression = state.translator.handle_block_expression(:default, nil, state, context)
{node, state} =
state.translator.handle_subblock(:default, expression, Enum.reverse(buffer), meta, state, context)
# create a new buffer for the parent node to replace the one that was popped
buffers = [[] | buffers]
buffers = push_node_to_current_buffer(node, buffers)
# push back the parent token to state
meta = Map.put(meta, :has_sub_blocks?, true)
state = %{state | token_stack: [{{:block_open, name, expr, meta}, ctx} | tokens]}
{buffers, state}
end
# If there's no parent node
defp close_sub_block({:block_open, name, _expr, meta}, _buffers, _state) do
message = message_for_invalid_sub_block_parent(name)
raise parse_error(message, meta)
end
defp validate_sub_block!({:block_open, name, _expr, meta}, parent_name) do
if parent_name not in @sub_blocks_valid_parents[name] do
message = message_for_invalid_sub_block_parent(name)
raise parse_error(message, meta)
end
end
defp message_for_invalid_sub_block_parent(name) do
valid_parents = @sub_blocks_valid_parents[name]
valid_parents_tokens = Enum.map(valid_parents, &"{##{&1}}")
"no valid parent node defined for {##{name}}. " <>
Helpers.list_to_string(
"The {##{name}} construct can only be used inside a",
"Possible parents are",
valid_parents_tokens
)
end
defp push_node_to_current_buffer(:ignore, buffers) do
buffers
end
defp push_node_to_current_buffer(node, buffers) do
[buffer | buffers] = buffers
buffer = [node | buffer]
[buffer | buffers]
end
defp translate_attrs(state, context, attrs),
do: Enum.map(attrs, &translate_attr(state, context, &1))
defp translate_attr(state, context, {name, {:string, value, %{delimiter: ?"}}, meta}) do
state.translator.handle_attribute(name, value, meta, state, context)
end
defp translate_attr(state, context, {name, {:string, value, %{delimiter: ?'}}, meta}) do
state.translator.handle_attribute(name, value, meta, state, context)
end
defp translate_attr(state, context, {name, {:string, value, %{delimiter: nil}}, meta}) do
IOHelper.warn(
"""
Using unquoted attribute values is not recommended as they will always be converted to strings.
For instance, `selected=true` and `tabindex=3` will be translated to `selected="true"` and `tabindex="3"` respectively.
Hint: if you want to pass a literal boolean or integer, replace `#{name}=#{value}` with `#{name}={#{value}}`
""",
state.caller,
meta.file,
meta.line
)
state.translator.handle_attribute(name, value, meta, state, context)
end
defp translate_attr(state, context, {:root, {:expr, _value, expr_meta} = expr, _attr_meta}) do
state.translator.handle_attribute(:root, expr, expr_meta, state, context)
end
defp translate_attr(
state,
context,
{:root, {:tagged_expr, _marker, _expr, marker_meta} = expr, _attr_meta}
) do
state.translator.handle_attribute(:root, expr, marker_meta, state, context)
end
defp translate_attr(state, context, {name, {:expr, _value, _expr_meta} = expr, attr_meta}) do
state.translator.handle_attribute(name, expr, attr_meta, state, context)
end
defp translate_attr(
state,
context,
{name, {:tagged_expr, _marker, _expr, marker_meta} = expr, _attr_meta}
) do
state.translator.handle_attribute(name, expr, marker_meta, state, context)
end
defp translate_attr(state, context, {name, nil, meta}) do
state.translator.handle_attribute(name, true, meta, state, context)
end
defp push_tag(state, {:tag_open, _tag, _attrs, %{void_tag?: true}}, _context) do
state
end
defp push_tag(state, token, context) do
%{state | token_stack: [{token, context} | state.token_stack]}
end
defp pop_matching_tag(
%{token_stack: [{{:tag_open, tag_name, _, _} = token, context} | tokens]} = state,
{:tag_close, tag_name, _}
) do
{token, context, %{state | token_stack: tokens}}
end
defp pop_matching_tag(
%{token_stack: [{{:block_open, name, _, _} = token, context} | tokens]} = state,
{:block_close, name, _}
) do
{token, context, %{state | token_stack: tokens}}
end
defp pop_matching_tag(%{token_stack: [{{_, _, _, meta} = token_open, _ctx} | _]}, token_close) do
{_, _, meta_close} = token_close
message = """
expected closing #{node_type(token_open)} for #{format_node(token_open)} defined on line #{meta.line}, \
got #{format_node(token_close)}\
"""
raise parse_error(message, meta_close)
end
defp parent_context([{_tag, context} | _]), do: context
defp parent_context([]), do: nil
def parse_error(message, meta) do
%ParseError{
message: message,
file: meta.file,
line: meta.line,
column: meta.column
}
end
defp node_type({:tag_open, _, _, _}), do: "tag"
defp node_type({:tag_close, _, _}), do: "tag"
defp node_type({:block_open, _, _, _}), do: "block"
defp node_type({:block_close, _, _}), do: "block"
defp format_node({:tag_open, name, _attrs, _meta}), do: "<#{name}>"
defp format_node({:tag_close, name, _meta}), do: "</#{name}>"
defp format_node({:block_open, name, _attrs, _meta}), do: "{##{name}}"
defp format_node({:block_close, name, _meta}), do: "{/#{name}}"
end