Packages
surface
0.1.1
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
import NimbleParsec
@doc """
Parses a surface HTML document.
"""
def parse(content) do
case root(content, context: [macro: nil]) do
{:ok, tree, "", %{macro: nil}, _, _} ->
{:ok, tree}
{:ok, _, rest, context, {line, _}, byte_offset} ->
# Something went wrong then it has to be an error parsing the HTML tag.
# However, because of repeat, the error is discarded, so we call node
# again to get the proper error message.
{:error, message, _rest, _context, {line, _col}, _byte_offset} =
node(rest, context: context, line: line, byte_offset: byte_offset)
{:error, message, line}
{:error, message, _rest, _context, {line, _col}, _byte_offset} ->
{:error, message, line}
end
end
## Common helpers
defparsecp(
:binary,
string("\"")
|> repeat(
choice([
string("\\\""),
utf8_char(not: ?")
])
)
|> string("\"")
)
defparsecp(
:charlist,
string("\'")
|> repeat(
choice([
string("\\'"),
utf8_char(not: ?')
])
)
|> string("\'")
)
defparsecp(
:tuple,
string("{")
|> repeat(
choice([
parsec(:tuple),
parsec(:binary),
parsec(:charlist),
utf8_char(not: ?})
])
)
|> string("}")
)
expression =
ignore(string("{{"))
|> line()
|> repeat(
choice([
parsec(:tuple),
parsec(:binary),
parsec(:charlist),
lookahead_not(string("}}")) |> utf8_char([])
])
)
|> optional(string("}}"))
tag =
ascii_char([?a..?z, ?A..?Z])
|> ascii_string([?a..?z, ?A..?Z, ?0..?9, ?-, ?., ?_], min: 0)
|> reduce({List, :to_string, []})
boolean =
choice([
string("true") |> replace(true),
string("false") |> replace(false)
])
attribute_expr =
expression
|> post_traverse(:attribute_expr)
attribute_value =
ignore(ascii_char([?"]))
|> repeat(
lookahead_not(ignore(ascii_char([?"])))
|> choice([
~S(\") |> string() |> replace(?"),
attribute_expr,
utf8_char([])
])
)
|> ignore(ascii_char([?"]))
|> wrap()
|> post_traverse(:attribute_value)
attr_name = ascii_string([?a..?z, ?0..?9, ?A..?Z, ?-, ?., ?_, ?:, ?@], min: 1)
whitespace = ascii_string([?\s, ?\n], min: 0)
attribute =
whitespace
|> concat(attr_name |> line())
|> concat(whitespace)
|> optional(
choice([
ignore(string("=")) |> concat(whitespace) |> concat(attribute_expr),
ignore(string("=")) |> concat(whitespace) |> concat(attribute_value),
ignore(string("=")) |> concat(whitespace) |> concat(integer(min: 1)),
ignore(string("=")) |> concat(whitespace) |> concat(boolean)
])
)
|> wrap()
comment =
ignore(string("<!--"))
|> repeat(lookahead_not(string("-->")) |> utf8_char([]))
|> ignore(string("-->"))
|> ignore()
## Void element node
void_element =
choice([
string("area"),
string("base"),
string("br"),
string("col"),
string("hr"),
string("img"),
string("input"),
string("link"),
string("meta"),
string("param"),
string("command"),
string("keygen"),
string("source")
])
void_element_node =
ignore(string("<"))
|> concat(void_element)
|> line()
|> concat(repeat(attribute) |> wrap())
|> concat(whitespace)
|> ignore(string(">"))
|> wrap()
|> post_traverse(:void_element_tags)
defp void_element_tags(_rest, [[tag_node, attr_nodes, space]], context, _line, _offset) do
{[tag], {line, _}} = tag_node
attributes = build_attributes(attr_nodes)
{[{tag, attributes, [], %{line: line, space: space}}], context}
end
## Self-closing node
self_closing_node =
ignore(string("<"))
|> concat(tag)
|> line()
|> concat(repeat(attribute) |> wrap())
|> concat(whitespace)
|> ignore(string("/>"))
|> wrap()
|> post_traverse(:self_closing_tags)
defp self_closing_tags(_rest, [[tag_node, attr_nodes, space]], context, _line, _offset) do
{[tag], {line, _}} = tag_node
attributes = build_attributes(attr_nodes)
{[{tag, attributes, [], %{line: line, space: space}}], context}
end
## Regular node
interpolation =
expression
|> post_traverse(:interpolation)
text_with_interpolation = utf8_string([not: ?<, not: ?{], min: 1)
opening_tag =
ignore(string("<"))
|> concat(tag)
|> line()
|> concat(repeat(attribute) |> wrap())
|> concat(whitespace)
|> ignore(string(">"))
|> wrap()
closing_tag = ignore(string("</")) |> concat(tag) |> ignore(string(">"))
regular_node =
opening_tag
|> repeat(
lookahead_not(string("</"))
|> choice([
parsec(:node),
interpolation,
string("{"),
text_with_interpolation
])
)
|> wrap()
|> optional(closing_tag)
|> post_traverse(:match_tags)
defp match_tags(
_rest,
[tag, [[{[tag], {opening_line, _}}, attr_nodes, space] | nodes]],
context,
_line,
_offset
) do
attributes = build_attributes(attr_nodes)
{[{tag, attributes, nodes, %{line: opening_line, space: space}}], context}
end
defp match_tags(_rest, [closing, [[tag_node | _] | _]], _context, _line, _offset) do
{[opening], {_opening_line, _}} = tag_node
{:error, "closing tag #{inspect(closing)} did not match opening tag #{inspect(opening)}"}
end
defp match_tags(rest, [[[tag_node | _] | _]], _context, _line, _offset) do
opening =
case Regex.run(~r/^<(#?[a-zA-Z][a-zA-Z0-9_\-\.]+)/, rest) do
[_, tag] ->
tag
_ ->
{[tag], {_opening_line, _}} = tag_node
tag
end
{:error, "expected closing tag for #{inspect(opening)}"}
end
defp interpolation(_rest, ["}}" | nodes], context, _line, _offset) do
[{[], {opening_line, _}} | rest] = Enum.reverse(nodes)
{[{:interpolation, rest |> IO.iodata_to_binary(), %{line: opening_line}}], context}
end
defp interpolation(_rest, _, _context, _line, _offset),
do: {:error, "expected closing for interpolation"}
defp attribute_expr(_rest, ["}}" | nodes], context, _line, _offset) do
[{[], {opening_line, _}} | rest] = Enum.reverse(nodes)
{[{:attribute_expr, IO.chardata_to_string(rest), %{line: opening_line}}], context}
end
defp attribute_expr(_rest, _, _context, _line, _offset),
do: {:error, "expected closing for attribute expression"}
defp attribute_value(_rest, [nodes], context, _line, _offset) do
value =
case reduce_attribute_value(nodes, {[], []}) do
[node] when not is_tuple(node) ->
node
[] ->
""
nodes ->
nodes
end
{[value], context}
end
defp reduce_attribute_value([], {[], result}) do
Enum.reverse(result)
end
defp reduce_attribute_value([], {chars, result}) do
new_node = chars |> Enum.reverse() |> IO.chardata_to_string()
reduce_attribute_value([], {[], [new_node | result]})
end
defp reduce_attribute_value([{_, _, _} = node | nodes], {[] = chars, result}) do
reduce_attribute_value(nodes, {chars, [node | result]})
end
defp reduce_attribute_value([{_, _, _} = node | nodes], {chars, result}) do
new_node = chars |> Enum.reverse() |> IO.chardata_to_string()
reduce_attribute_value(nodes, {[], [node, new_node | result]})
end
defp reduce_attribute_value([node | nodes], {chars, result}) do
reduce_attribute_value(nodes, {[node | chars], result})
end
defp build_attributes(attr_nodes) do
Enum.map(attr_nodes, fn
# attribute without value (e.g. disabled)
[space1, {[attr], {line, _}}, space2] ->
{attr, true, %{line: line, spaces: [space1, space2]}}
# attribute with value
[space1, {[attr], {line, _}}, space2, space3, value] ->
{attr, value, %{line: line, spaces: [space1, space2, space3]}}
end)
end
## Self-closing macro node
self_closing_macro_node =
ignore(string("<#"))
|> concat(tag)
|> line()
|> concat(repeat(attribute) |> wrap())
|> concat(whitespace)
|> ignore(string("/>"))
|> wrap()
|> post_traverse(:prepend_hashtag)
|> post_traverse(:self_closing_tags)
defp prepend_hashtag(_rest, [[tag_node, attr_nodes, space]], context, _line, _offset) do
{[tag], meta} = tag_node
{[[{["#" <> tag], meta}, attr_nodes, space]], context}
end
## Macro node
text_without_interpolation = utf8_string([not: ?<], min: 1)
opening_macro_tag =
ignore(string("<#"))
|> concat(tag)
|> line()
|> concat(repeat(attribute) |> wrap())
|> concat(whitespace)
|> ignore(string(">"))
|> wrap()
closing_macro_tag = ignore(string("</#")) |> concat(tag) |> ignore(string(">"))
macro_node =
opening_macro_tag
|> post_traverse(:opening_macro_tag)
|> repeat_while(choice([string("<"), text_without_interpolation]), :lookahead_macro_tag)
|> wrap()
|> optional(closing_macro_tag)
|> post_traverse(:closing_macro_tag)
defp opening_macro_tag(_rest, [macro], context, _, _) do
{[], %{context | macro: macro}}
end
defp closing_macro_tag(
_,
[macro, rest],
%{macro: [{[macro], {line, _}}, attr_nodes, space]} = context,
_,
_
) do
tag = "#" <> macro
attributes = build_attributes(attr_nodes)
text = IO.iodata_to_binary(rest)
{[{tag, attributes, [text], %{line: line, space: space}}], %{context | macro: nil}}
end
defp closing_macro_tag(
_rest,
_nodes,
%{macro: [{[macro], {_line, _}}, _attr_nodes, _space]},
_,
_
) do
{:error, "expected closing tag for #{inspect("#" <> macro)}"}
end
defp lookahead_macro_tag(rest, %{macro: macro} = context, _, _) do
[{[macro], {_line, _}}, _attr_nodes, _space] = macro
size = byte_size(macro)
case rest do
<<"</#", macro_match::binary-size(size), ">", _::binary>> when macro_match == macro ->
{:halt, context}
_ ->
{:cont, context}
end
end
defparsecp(
:node,
[
void_element_node,
macro_node,
self_closing_macro_node,
regular_node,
self_closing_node,
comment
]
|> choice()
|> label("opening HTML tag"),
inline: true
)
defparsecp(
:root,
repeat(
choice([
interpolation,
string("{"),
text_with_interpolation,
ascii_string([not: ?<], min: 1),
parsec(:node)
])
)
)
end