Packages
surface
0.5.2
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/tokenizer.ex
defmodule Surface.Compiler.Tokenizer do
@moduledoc false
@space_chars '\n\r\t\f\s'
@name_stop_chars @space_chars ++ '>/='
@unquoted_value_invalid_chars '"\'=<`'
@unquoted_value_stop_chars @space_chars ++ '>'
@block_name_stop_chars @space_chars ++ '}'
@markers ["=", "...", "~", "$", "^"]
@ignored_body_tags ["style", "script"]
@void_elements [
"area",
"base",
"br",
"col",
"hr",
"img",
"input",
"link",
"meta",
"param",
"command",
"keygen",
"source"
]
@type metadata :: %{
line: integer(),
column: integer(),
line_end: integer(),
column_end: integer(),
file: binary()
}
@type text :: {:text, value :: binary()}
@type comment_metadata :: metadata() | %{visibility: :public | :private}
@type comment :: {:comment, value :: binary(), comment_metadata()}
@type block_metadata :: metadata()
@type block_name :: binary() | :default
@type block_open ::
{:block_open, block_name, expression :: nil | binary(), block_metadata()}
@type block_close :: {:block_close, block_name, metadata()}
@type expression_metadata :: {:expr, value :: binary(), metadata()}
@type expression :: {:expr, value :: binary(), metadata()}
@type attribute_value :: {:string, value :: binary() | nil, metadata()} | expression()
@type attribute_name :: binary() | :root
@type attribute_metadata :: metadata()
@type attribute :: {attribute_name(), attribute_value(), metadata()}
@type tag_metadata ::
metadata()
| %{
void_tag?: boolean(),
macro?: boolean(),
ignored_body?: boolean(),
self_close: boolean(),
node_line_end: integer(),
node_column_end: integer()
}
@type tag_name :: binary()
@type tag_open :: {:tag_open, name :: binary(), list(attribute()), tag_metadata()}
@type tag_close :: {:tag_close, name :: binary(), metadata()}
@type token :: text() | comment() | block_open() | block_close() | tag_open() | tag_close()
alias Surface.Compiler.ParseError
@spec tokenize!(binary(), keyword()) :: list(token())
def tokenize!(text, opts \\ []) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
indentation = Keyword.get(opts, :indentation, 0)
# TODO: Fix for the converter. Remove `embedded_expr?` eventually after 0.5.0 is released
state = %{file: file, column_offset: indentation + 1, braces: [], embedded_expr?: false}
handle_text(text, line, column, [], [], state)
end
## handle_text
defp handle_text("\r\n" <> rest, line, _column, buffer, acc, state) do
handle_text(rest, line + 1, state.column_offset, ["\r\n" | buffer], acc, state)
end
defp handle_text("\n" <> rest, line, _column, buffer, acc, state) do
handle_text(rest, line + 1, state.column_offset, ["\n" | buffer], acc, state)
end
defp handle_text("<!doctype" <> rest, line, column, buffer, acc, state) do
handle_doctype(rest, line, column + 9, ["<!doctype" | buffer], acc, state)
end
defp handle_text("<!DOCTYPE" <> rest, line, column, buffer, acc, state) do
handle_doctype(rest, line, column + 9, ["<!DOCTYPE" | buffer], acc, state)
end
defp handle_text("<!--" <> rest, line, column, buffer, acc, state) do
acc = text_to_acc(buffer, acc)
{new_rest, new_line, new_column, new_buffer} = handle_comment(rest, line, column + 4, ["<!--"], state)
comment = buffer_to_string(new_buffer)
meta = %{
line: line,
column: column,
new_line: new_line,
new_column: new_column,
file: state.file,
visibility: :public
}
handle_text(new_rest, new_line, new_column, [], [{:comment, comment, meta} | acc], state)
end
defp handle_text("{!--" <> rest, line, column, buffer, acc, state) do
acc = text_to_acc(buffer, acc)
{new_rest, new_line, new_column, new_buffer} = handle_private_comment(rest, line, column + 4, ["{!--"], state)
comment = buffer_to_string(new_buffer)
meta = %{
line: line,
column: column,
new_line: new_line,
new_column: new_column,
file: state.file,
visibility: :private
}
handle_text(new_rest, new_line, new_column, [], [{:comment, comment, meta} | acc], state)
end
defp handle_text("{#" <> rest, line, column, buffer, acc, state) do
handle_block_open(rest, line, column + 2, text_to_acc(buffer, acc), state)
end
defp handle_text("{/" <> rest, line, column, buffer, acc, state) do
handle_block_close(rest, line, column + 2, text_to_acc(buffer, acc), state)
end
defp handle_text("{" <> rest, line, column, buffer, acc, state) do
{expr, new_line, new_column, rest} = handle_expression(rest, line, column + 1, state)
acc = [expr | text_to_acc(buffer, acc)]
handle_text(rest, new_line, new_column, [], acc, state)
end
defp handle_text("</" <> rest, line, column, buffer, acc, state) do
handle_tag_close(rest, line, column + 2, text_to_acc(buffer, acc), state)
end
defp handle_text("<" <> rest, line, column, buffer, acc, state) do
handle_tag_open(rest, line, column + 1, text_to_acc(buffer, acc), state)
end
defp handle_text(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do
handle_text(rest, line, column + 1, [<<c::utf8>> | buffer], acc, state)
end
defp handle_text(<<>>, _line, _column, buffer, acc, _state) do
ok(text_to_acc(buffer, acc))
end
## handle_block_open
defp handle_block_open(text, line, column, acc, state) do
case handle_block_name(text, column, []) do
{:ok, name, new_column, rest} ->
meta = %{line: line, column: column, line_end: line, column_end: new_column, file: state.file}
{new_rest, new_line, new_column} = ignore_spaces(rest, line, new_column, state)
acc = [{:block_open, name, nil, meta} | acc]
handle_block_open_expr(new_rest, new_line, new_column, acc, state)
{:error, message} ->
raise parse_error(message, line, column, state)
end
end
## handle_block_close
defp handle_block_close(text, line, column, acc, state) do
case handle_block_name(text, column, []) do
{:ok, name, new_column, rest} ->
meta = %{line: line, column: column, line_end: line, column_end: new_column, file: state.file}
acc = [{:block_close, name, meta} | acc]
handle_block_close_end(rest, line, new_column, acc, state)
{:error, message} ->
raise parse_error(message, line, column, state)
end
end
defp handle_block_close_end("}" <> rest, line, column, acc, state) do
handle_text(rest, line, column + 1, [], acc, state)
end
defp handle_block_close_end(_text, line, column, _acc, state) do
raise parse_error("expected closing `}`", line, column, state)
end
## handle_block_name
defp handle_block_name(<<c::utf8, _rest::binary>>, _column, []) when c in @block_name_stop_chars do
{:error, "expected block name"}
end
defp handle_block_name(<<c::utf8, _rest::binary>> = text, column, buffer) when c in @block_name_stop_chars do
{:ok, buffer_to_string(buffer), column, text}
end
defp handle_block_name(<<c::utf8, rest::binary>>, column, buffer) do
handle_block_name(rest, column + 1, [<<c::utf8>> | buffer])
end
## handle_block_open_expr
defp handle_block_open_expr(text, line, column, acc, state) do
case handle_expression_value(text, line, column, state) do
{:ok, {:expr, value, expr_meta}, new_line, new_column, rest, state} ->
expr = if value == "", do: nil, else: {:expr, value, expr_meta}
[{:block_open, name, nil, meta} | acc] = acc
acc = [{:block_open, name, expr, meta} | acc]
handle_text(rest, new_line, new_column, [], acc, state)
{:error, :expected_closing_brace, error_line, error_column} ->
[{:block_open, name, nil, %{line: line, column: column}} | _] = acc
message = """
expected closing `}` for opening block expression `{##{name}` begining at \
line: #{line}, column: #{column - 1}\
"""
raise parse_error(message, error_line, error_column, state)
{:error, type, line, column} ->
raise parse_error("parser error: #{inspect(type)}", line, column, state)
end
end
## handle_doctype
defp handle_doctype(<<?>, rest::binary>>, line, column, buffer, acc, state) do
handle_text(rest, line, column + 1, [?> | buffer], acc, state)
end
defp handle_doctype("\r\n" <> rest, line, _column, buffer, acc, state) do
handle_doctype(rest, line + 1, state.column_offset, ["\r\n" | buffer], acc, state)
end
defp handle_doctype("\n" <> rest, line, _column, buffer, acc, state) do
handle_doctype(rest, line + 1, state.column_offset, ["\n" | buffer], acc, state)
end
defp handle_doctype(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do
handle_doctype(rest, line, column + 1, [<<c::utf8>> | buffer], acc, state)
end
## handle_comment
defp handle_comment("\r\n" <> rest, line, _column, buffer, state) do
handle_comment(rest, line + 1, state.column_offset, ["\r\n" | buffer], state)
end
defp handle_comment("\n" <> rest, line, _column, buffer, state) do
handle_comment(rest, line + 1, state.column_offset, ["\n" | buffer], state)
end
defp handle_comment("-->" <> rest, line, column, buffer, _state) do
{rest, line, column + 3, ["-->" | buffer]}
end
defp handle_comment(<<c::utf8, rest::binary>>, line, column, buffer, state) do
handle_comment(rest, line, column + 1, [<<c::utf8>> | buffer], state)
end
defp handle_comment(<<>>, line, column, _buffer, state) do
raise parse_error("expected closing `-->` for comment", line, column, state)
end
## handle_private_comment
defp handle_private_comment("\r\n" <> rest, line, _column, buffer, state) do
handle_private_comment(rest, line + 1, state.column_offset, ["\r\n" | buffer], state)
end
defp handle_private_comment("\n" <> rest, line, _column, buffer, state) do
handle_private_comment(rest, line + 1, state.column_offset, ["\n" | buffer], state)
end
defp handle_private_comment("--}" <> rest, line, column, buffer, _state) do
{rest, line, column + 3, ["--}" | buffer]}
end
defp handle_private_comment(<<c::utf8, rest::binary>>, line, column, buffer, state) do
handle_private_comment(rest, line, column + 1, [<<c::utf8>> | buffer], state)
end
defp handle_private_comment(<<>>, line, column, _buffer, state) do
raise parse_error("expected closing `--}` for comment", line, column, state)
end
## handle_macro_body
defp handle_macro_body("\r\n" <> rest, line, _column, buffer, acc, state) do
handle_macro_body(rest, line + 1, state.column_offset, ["\r\n" | buffer], acc, state)
end
defp handle_macro_body("\n" <> rest, line, _column, buffer, acc, state) do
handle_macro_body(rest, line + 1, state.column_offset, ["\n" | buffer], acc, state)
end
defp handle_macro_body("</#" <> text, line, column, buffer, acc, state) do
handle_maybe_macro_close_tag("#" <> text, line, column + 2, buffer, acc, state)
end
defp handle_macro_body(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do
handle_macro_body(rest, line, column + 1, [<<c::utf8>> | buffer], acc, state)
end
defp handle_macro_body(<<>>, _line, _column, buffer, acc, _state) do
ok(text_to_acc(buffer, acc))
end
## handle_maybe_macro_close_tag
defp handle_maybe_macro_close_tag(text, line, column, buffer, [{:tag_open, tag_name, _, _} | _] = acc, state) do
case handle_tag_name(text, column, []) do
{:ok, name, new_column, rest} when name == tag_name ->
meta = %{line: line, column: column, line_end: line, column_end: new_column, file: state.file}
acc = text_to_acc(buffer, acc)
acc = [{:tag_close, name, meta} | acc]
handle_tag_close_end(rest, line, new_column, acc, state)
_ ->
handle_macro_body(text, line, column, ["</" | buffer], acc, state)
end
end
## handle_ignored_body
defp handle_ignored_body("\r\n" <> rest, line, _column, buffer, acc, state) do
handle_ignored_body(rest, line + 1, state.column_offset, ["\r\n" | buffer], acc, state)
end
defp handle_ignored_body("\n" <> rest, line, _column, buffer, acc, state) do
handle_ignored_body(rest, line + 1, state.column_offset, ["\n" | buffer], acc, state)
end
defp handle_ignored_body("</" <> rest, line, column, buffer, acc, state) do
acc = text_to_acc(buffer, acc)
handle_tag_close(rest, line, column + 2, acc, state)
end
defp handle_ignored_body(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do
handle_ignored_body(rest, line, column + 1, [<<c::utf8>> | buffer], acc, state)
end
defp handle_ignored_body(<<>>, _line, _column, buffer, acc, _state) do
ok(text_to_acc(buffer, acc))
end
## handle_tag_open
defp handle_tag_open(text, line, column, acc, state) do
case handle_tag_name(text, column, []) do
{:ok, name, new_column, rest} ->
meta = %{
line: line,
column: column,
line_end: line,
column_end: new_column,
file: state.file,
self_close: false,
void_tag?: name in @void_elements,
macro?: macro_tag?(name),
ignored_body?: name in @ignored_body_tags
}
acc = [{:tag_open, name, [], meta} | acc]
handle_maybe_tag_open_end(rest, line, new_column, acc, state)
{:error, message} ->
raise parse_error(message, line, column, state)
end
end
## handle_tag_close
defp handle_tag_close(text, line, column, acc, state) do
case handle_tag_name(text, column, []) do
{:ok, name, new_column, rest} ->
meta = %{line: line, column: column, line_end: line, column_end: new_column, file: state.file}
acc = [{:tag_close, name, meta} | acc]
handle_tag_close_end(rest, line, new_column, acc, state)
{:error, message} ->
raise parse_error(message, line, column, state)
end
end
defp handle_tag_close_end(">" <> rest, line, column, acc, state) do
handle_text(rest, line, column + 1, [], acc, state)
end
defp handle_tag_close_end(_text, line, column, _acc, state) do
raise parse_error("expected closing `>`", line, column, state)
end
## handle_tag_name
defp handle_tag_name(<<c::utf8, _rest::binary>>, _column, _buffer = []) when c in @name_stop_chars do
{:error, "expected tag name"}
end
defp handle_tag_name(<<c::utf8, _rest::binary>> = text, column, buffer) when c in @name_stop_chars do
{:ok, buffer_to_string(buffer), column, text}
end
defp handle_tag_name(<<c::utf8, rest::binary>>, column, buffer) do
handle_tag_name(rest, column + 1, [<<c::utf8>> | buffer])
end
## handle_maybe_tag_open_end
defp handle_maybe_tag_open_end("\r\n" <> rest, line, _column, acc, state) do
handle_maybe_tag_open_end(rest, line + 1, state.column_offset, acc, state)
end
defp handle_maybe_tag_open_end("\n" <> rest, line, _column, acc, state) do
handle_maybe_tag_open_end(rest, line + 1, state.column_offset, acc, state)
end
defp handle_maybe_tag_open_end(<<c::utf8, rest::binary>>, line, column, acc, state) when c in @space_chars do
handle_maybe_tag_open_end(rest, line, column + 1, acc, state)
end
defp handle_maybe_tag_open_end("/>" <> rest, line, column, acc, state) do
acc =
acc
|> reverse_attrs()
|> update_meta(self_close: true, node_line_end: line, node_column_end: column)
handle_text(rest, line, column + 2, [], acc, state)
end
defp handle_maybe_tag_open_end(
">" <> rest,
line,
column,
[{:tag_open, _name, _, %{macro?: true}} | _] = acc,
state
) do
acc =
acc
|> reverse_attrs()
|> update_meta(node_line_end: line, node_column_end: column)
handle_macro_body(rest, line, column + 1, [], acc, state)
end
defp handle_maybe_tag_open_end(
">" <> rest,
line,
column,
[{:tag_open, _name, _, %{ignored_body?: true}} | _] = acc,
state
) do
acc =
acc
|> reverse_attrs()
|> update_meta(node_line_end: line, node_column_end: column)
handle_ignored_body(rest, line, column + 1, [], acc, state)
end
defp handle_maybe_tag_open_end(">" <> rest, line, column, acc, state) do
acc =
acc
|> reverse_attrs()
|> update_meta(node_line_end: line, node_column_end: column)
handle_text(rest, line, column + 1, [], acc, state)
end
defp handle_maybe_tag_open_end("{" <> rest, line, column, acc, state) do
{expr, new_line, new_column, rest} = handle_expression(rest, line, column + 1, state)
meta = %{line: line, column: column, line_end: new_line, column_end: new_column, file: state.file}
acc = put_attr(acc, :root, expr, meta)
handle_maybe_tag_open_end(rest, new_line, new_column, acc, state)
end
defp handle_maybe_tag_open_end(<<>>, line, column, _acc, state) do
raise parse_error("expected closing `>` or `/>`", line, column, state)
end
defp handle_maybe_tag_open_end(text, line, column, acc, state) do
handle_attribute(text, line, column, acc, state)
end
## handle_attribute
defp handle_attribute(text, line, column, acc, state) do
case handle_attr_name(text, column, []) do
{:ok, name, new_column, rest} ->
meta = %{line: line, column: column, line_end: line, column_end: new_column, file: state.file}
acc = put_attr(acc, name, nil, meta)
handle_maybe_attr_value(rest, line, new_column, acc, state)
{:error, message} ->
raise parse_error(message, line, column, state)
end
end
## handle_attr_name
defp handle_attr_name(<<c::utf8, _rest::binary>>, _column, []) when c in @name_stop_chars do
{:error, "expected attribute name"}
end
defp handle_attr_name(<<c::utf8, _rest::binary>> = text, column, buffer) when c in @name_stop_chars do
{:ok, buffer_to_string(buffer), column, text}
end
defp handle_attr_name(<<c::utf8, rest::binary>>, column, buffer) do
handle_attr_name(rest, column + 1, [<<c::utf8>> | buffer])
end
## handle_maybe_attr_value
defp handle_maybe_attr_value("\r\n" <> rest, line, _column, acc, state) do
handle_maybe_attr_value(rest, line + 1, state.column_offset, acc, state)
end
defp handle_maybe_attr_value("\n" <> rest, line, _column, acc, state) do
handle_maybe_attr_value(rest, line + 1, state.column_offset, acc, state)
end
defp handle_maybe_attr_value(<<c::utf8, rest::binary>>, line, column, acc, state) when c in @space_chars do
handle_maybe_attr_value(rest, line, column + 1, acc, state)
end
defp handle_maybe_attr_value("=" <> rest, line, column, acc, state) do
handle_attr_value_begin(rest, line, column + 1, acc, state)
end
defp handle_maybe_attr_value(text, line, column, acc, state) do
handle_maybe_tag_open_end(text, line, column, acc, state)
end
## handle_attr_value_begin
defp handle_attr_value_begin("\r\n" <> rest, line, _column, acc, state) do
handle_attr_value_begin(rest, line + 1, state.column_offset, acc, state)
end
defp handle_attr_value_begin("\n" <> rest, line, _column, acc, state) do
handle_attr_value_begin(rest, line + 1, state.column_offset, acc, state)
end
defp handle_attr_value_begin(<<c::utf8, rest::binary>>, line, column, acc, state) when c in @space_chars do
handle_attr_value_begin(rest, line, column + 1, acc, state)
end
defp handle_attr_value_begin("\"" <> rest, line, column, acc, state) do
acc = put_attr_value(acc, {:string, nil, %{line: line, column: column + 1, delimiter: ?"}})
handle_attr_value_double_quote(rest, line, column + 1, [], acc, state)
end
defp handle_attr_value_begin("'" <> rest, line, column, acc, state) do
acc = put_attr_value(acc, {:string, nil, %{line: line, column: column + 1, delimiter: ?'}})
handle_attr_value_single_quote(rest, line, column + 1, [], acc, state)
end
defp handle_attr_value_begin("{" <> rest, line, column, acc, state) do
{expr, new_line, new_column, rest} = handle_expression(rest, line, column + 1, state)
acc = put_attr_value(acc, expr)
handle_maybe_tag_open_end(rest, new_line, new_column, acc, state)
end
defp handle_attr_value_begin(<<c::utf8, _::binary>> = text, line, column, acc, state)
when c not in @name_stop_chars do
acc = put_attr_value(acc, {:string, nil, %{line: line, column: column, delimiter: nil}})
handle_attr_value_unquoted(text, line, column, [], acc, state)
end
defp handle_attr_value_begin(_text, line, column, _acc, state) do
raise parse_error("expected attribute value or expression after `=`", line, column, state)
end
## handle_attr_value_double_quote
defp handle_attr_value_double_quote("\r\n" <> rest, line, _column, buffer, acc, state) do
column = state.column_offset
handle_attr_value_double_quote(rest, line + 1, column, ["\r\n" | buffer], acc, state)
end
defp handle_attr_value_double_quote("\n" <> rest, line, _column, buffer, acc, state) do
column = state.column_offset
handle_attr_value_double_quote(rest, line + 1, column, ["\n" | buffer], acc, state)
end
# TODO: Fix for the converter. Remove it eventually after 0.5.0 is released
defp handle_attr_value_double_quote("{{" <> rest, line, column, buffer, acc, state) do
state = %{state | embedded_expr?: true}
handle_attr_value_double_quote(rest, line, column + 2, ["{{" | buffer], acc, state)
end
defp handle_attr_value_double_quote("}}" <> rest, line, column, buffer, acc, state) do
state = %{state | embedded_expr?: false}
handle_attr_value_double_quote(rest, line, column + 2, ["{{" | buffer], acc, state)
end
defp handle_attr_value_double_quote("\"" <> rest, line, column, buffer, acc, %{embedded_expr?: true} = state) do
handle_attr_value_double_quote(rest, line, column + 1, ["\"" | buffer], acc, state)
end
###
defp handle_attr_value_double_quote("\"" <> rest, line, column, buffer, acc, state) do
value = buffer_to_string(buffer)
acc =
update_attr_value(acc, fn {type, _old_value, meta} ->
{type, value, Map.merge(meta, %{line_end: line, column_end: column})}
end)
handle_maybe_tag_open_end(rest, line, column + 1, acc, state)
end
defp handle_attr_value_double_quote(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do
handle_attr_value_double_quote(rest, line, column + 1, [<<c::utf8>> | buffer], acc, state)
end
defp handle_attr_value_double_quote(<<>>, line, column, _buffer, _acc, state) do
raise parse_error("expected closing `\"` for attribute value", line, column, state)
end
## handle_attr_value_single_quote
defp handle_attr_value_single_quote("\r\n" <> rest, line, _column, buffer, acc, state) do
column = state.column_offset
handle_attr_value_single_quote(rest, line + 1, column, ["\r\n" | buffer], acc, state)
end
defp handle_attr_value_single_quote("\n" <> rest, line, _column, buffer, acc, state) do
column = state.column_offset
handle_attr_value_single_quote(rest, line + 1, column, ["\n" | buffer], acc, state)
end
defp handle_attr_value_single_quote("'" <> rest, line, column, buffer, acc, state) do
value = buffer_to_string(buffer)
acc =
update_attr_value(acc, fn {type, _old_value, meta} ->
{type, value, Map.merge(meta, %{line_end: line, column_end: column})}
end)
handle_maybe_tag_open_end(rest, line, column + 1, acc, state)
end
defp handle_attr_value_single_quote(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do
handle_attr_value_single_quote(rest, line, column + 1, [<<c::utf8>> | buffer], acc, state)
end
defp handle_attr_value_single_quote(<<>>, line, column, _buffer, _acc, state) do
raise parse_error("expected closing `'` for attribute value", line, column, state)
end
## handle_attr_value_unquoted
defp handle_attr_value_unquoted(<<c::utf8, _::binary>> = text, line, column, buffer, acc, state)
when c in @unquoted_value_stop_chars do
value = buffer_to_string(buffer)
acc =
update_attr_value(acc, fn {type, _old_value, meta} ->
{type, value, Map.merge(meta, %{line_end: line, column_end: column})}
end)
handle_maybe_tag_open_end(text, line, column, acc, state)
end
defp handle_attr_value_unquoted(<<c::utf8, _::binary>>, line, column, _buffer, _acc, state)
when c in @unquoted_value_invalid_chars do
message = """
unexpected character `#{<<c::utf8>>}`. \
Unquoted attribute values cannot contain `\"`, `'`, `=` nor `<`
"""
raise parse_error(message, line, column, state)
end
defp handle_attr_value_unquoted(<<c::utf8, rest::binary>>, line, column, buffer, acc, state) do
handle_attr_value_unquoted(rest, line, column + 1, [<<c::utf8>> | buffer], acc, state)
end
## handle_expression
# handle tagged expressions
for marker <- @markers do
defp handle_expression(unquote(marker) <> rest, line, column, state) do
marker = unquote(marker)
marker_column_end = column + String.length(marker)
meta = %{line: line, column: column, line_end: line, column_end: marker_column_end, file: state.file}
{rest, line_after_spaces, column_after_spaces} = ignore_spaces(rest, line, marker_column_end, state)
case handle_expression_value(rest, line_after_spaces, column_after_spaces, state) do
{:ok, {:expr, value, expr_meta}, new_line, new_column, rest, _state} ->
expr = if value == "", do: nil, else: {:expr, value, expr_meta}
{{:tagged_expr, marker, expr, meta}, new_line, new_column, rest}
{:error, :expected_closing_brace, error_line, error_column} ->
message = """
expected closing `}` for tagged expression `{#{marker}` begining at \
line: #{line}, column: #{column}\
"""
raise parse_error(message, error_line, error_column, state)
{:error, type, line, column} ->
raise parse_error("parser error: #{inspect(type)}", line, column, state)
end
end
end
# handle normal expression
defp handle_expression(text, line, column, state) do
case handle_expression_value(text, line, column, state) do
{:ok, expr, new_line, new_column, rest, _state} ->
{expr, new_line, new_column, rest}
{:error, :expected_closing_brace, error_line, error_column} ->
message = """
expected closing `}` for expression begining at line: #{line}, column: #{column}\
"""
raise parse_error(message, error_line, error_column, state)
{:error, type, line, column} ->
raise parse_error("parser error: #{inspect(type)}", line, column, state)
end
end
## handle_expression_value
defp handle_expression_value(text, line, column, state) do
case handle_expression_value_end(text, line, column, [], state) do
{:ok, value, new_line, new_column, rest, state} ->
meta = %{line: line, column: column, line_end: new_line, column_end: new_column - 1, file: state.file}
{:ok, {:expr, value, meta}, new_line, new_column, rest, state}
error ->
error
end
end
defp handle_expression_value_end("\r\n" <> rest, line, _column, buffer, state) do
handle_expression_value_end(rest, line + 1, state.column_offset, ["\r\n" | buffer], state)
end
defp handle_expression_value_end("\n" <> rest, line, _column, buffer, state) do
handle_expression_value_end(rest, line + 1, state.column_offset, ["\n" | buffer], state)
end
defp handle_expression_value_end("}" <> rest, line, column, buffer, %{braces: []} = state) do
value = buffer_to_string(buffer)
{:ok, value, line, column + 1, rest, state}
end
defp handle_expression_value_end(~S(\}) <> rest, line, column, buffer, state) do
handle_expression_value_end(rest, line, column + 2, [~S(\}) | buffer], state)
end
defp handle_expression_value_end(~S(\{) <> rest, line, column, buffer, state) do
handle_expression_value_end(rest, line, column + 2, [~S(\{) | buffer], state)
end
defp handle_expression_value_end("}" <> rest, line, column, buffer, state) do
{_pos, state} = pop_brace(state)
handle_expression_value_end(rest, line, column + 1, ["}" | buffer], state)
end
defp handle_expression_value_end("{" <> rest, line, column, buffer, state) do
state = push_brace(state, {line, column})
handle_expression_value_end(rest, line, column + 1, ["{" | buffer], state)
end
defp handle_expression_value_end(<<c::utf8, rest::binary>>, line, column, buffer, state) do
handle_expression_value_end(rest, line, column + 1, [<<c::utf8>> | buffer], state)
end
defp handle_expression_value_end(<<>>, line, column, _buffer, _state) do
{:error, :expected_closing_brace, line, column}
end
## ignore_spaces
defp ignore_spaces("\r\n" <> rest, line, _column, state) do
ignore_spaces(rest, line + 1, state.column_offset, state)
end
defp ignore_spaces("\n" <> rest, line, _column, state) do
ignore_spaces(rest, line + 1, state.column_offset, state)
end
defp ignore_spaces(<<c::utf8, rest::binary>>, line, column, state) when c in @space_chars do
ignore_spaces(rest, line, column + 1, state)
end
defp ignore_spaces(text, line, column, _state) do
{text, line, column}
end
## helpers
defp ok(acc), do: Enum.reverse(acc)
defp buffer_to_string(buffer) do
IO.iodata_to_binary(Enum.reverse(buffer))
end
defp text_to_acc([], acc), do: acc
defp text_to_acc(buffer, acc), do: [{:text, buffer_to_string(buffer)} | acc]
defp put_attr([{:tag_open, name, attrs, meta} | acc], attr, value, attr_meta) do
attrs = [{attr, value, attr_meta} | attrs]
[{:tag_open, name, attrs, meta} | acc]
end
defp put_attr_value([{:tag_open, name, [{attr, _value, attr_meta} | attrs], meta} | acc], value) do
attrs = [{attr, value, attr_meta} | attrs]
[{:tag_open, name, attrs, meta} | acc]
end
defp update_attr_value([{:tag_open, name, [{attr, value, attr_meta} | attrs], meta} | acc], fun) do
attrs = [{attr, fun.(value), attr_meta} | attrs]
[{:tag_open, name, attrs, meta} | acc]
end
defp update_meta([{:tag_open, name, attrs, meta} | acc], values) do
meta = Map.merge(meta, Map.new(values))
[{:tag_open, name, attrs, meta} | acc]
end
defp reverse_attrs([{:tag_open, name, attrs, meta} | acc]) do
attrs = Enum.reverse(attrs)
[{:tag_open, name, attrs, meta} | acc]
end
defp push_brace(state, pos) do
%{state | braces: [pos | state.braces]}
end
defp pop_brace(%{braces: [pos | braces]} = state) do
{pos, %{state | braces: braces}}
end
defp macro_tag?(<<"#", first, _rest::binary>>) when first in ?A..?Z, do: true
defp macro_tag?(_name), do: false
defp parse_error(message, line, column, state) do
%ParseError{message: message, file: state.file, line: line, column: column}
end
end