Current section

Files

Jump to
glemplate src glemplate@parser.erl
Raw

src/glemplate@parser.erl

-module(glemplate@parser).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/glemplate/parser.gleam").
-export([parse/2, parse_to_template/3, new/0]).
-export_type([parse_error/0, parser/0, token/0, lexer_mode/0, incomplete_tag/0, state/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" The parser transforms a string input into a template AST.\n"
"\n"
" ## Template language description\n"
"\n"
" The template language uses `<%` and `%>` as the tag delimiters. For\n"
" outputting, the start delimiter `<%=` is used.\n"
"\n"
" Comments can be inserted with `<%!-- Comment --%>`, they are not emitted to\n"
" the AST.\n"
"\n"
" Assigns can be referenced using their name (key in the assigns map). After\n"
" an assign name, the contents of the assign can be accessed using field\n"
" based access: `foo.wibble` accesses field `wibble` in assign `foo`.\n"
"\n"
" The accesses can be chained, e.g. `foo.wibble.wobble`. These access methods\n"
" work in any place where an assign reference is expected.\n"
"\n"
" Any other content in the template is emitted as text nodes.\n"
"\n"
" ### Tags\n"
"\n"
" #### Output\n"
"\n"
" ```text\n"
" <%= foo %>\n"
" ```\n"
"\n"
" Emits an output node of the assign `foo`. The contents are encoded using\n"
" the encoder given in the render call.\n"
"\n"
" #### Raw output\n"
"\n"
" ```text\n"
" <%= raw foo %>\n"
" ```\n"
"\n"
" Emits an output node of the assign `foo`. The contents are not encoded.\n"
"\n"
" #### If\n"
"\n"
" ```text\n"
" <% if foo %>\n"
" ...\n"
" <% else %>\n"
" ...\n"
" <% end %>\n"
" ```\n"
"\n"
" Emits an if node. If the assign `foo` is truthy (not `False`, 0, or empty),\n"
" the first nodes are rendered, otherwise the nodes in the `else` block. The\n"
" `else` block is optional.\n"
"\n"
" #### For\n"
"\n"
" ```text\n"
" <% for bar in foo %>\n"
" ...\n"
" <% end %>\n"
" ```\n"
"\n"
" Emits a for node. Each item of `foo` is looped through, making it available\n"
" as the assign `bar` inside the block. The contents of the block are\n"
" rendered for each item.\n"
"\n"
" #### Render\n"
"\n"
" ```text\n"
" <% render tpl.txt.glemp %>\n"
" <% render tpl.txt.glemp, name: user.name, level: user.level %>\n"
" ```\n"
"\n"
" Emits a render node. This will render another template in this place. The\n"
" listed assigns will be the assigns given to the template. The name of the\n"
" assign in the child template is on the left, the value is on the right.\n"
).
-type parse_error() :: {lexer_error, binary()} | {parser_error, list(binary())}.
-opaque parser() :: {parser,
nibble@lexer:lexer(token(), lexer_mode()),
nibble:parser(state(), token(), nil)}.
-type token() :: {text, binary()} |
tag_start |
output_tag_start |
right_angle |
percent |
'end' |
'if' |
'else' |
for |
in |
raw |
render |
colon |
comma |
period |
{variable, binary()}.
-type lexer_mode() :: tag | comment_tag | content.
-type incomplete_tag() :: {if_tag,
glemplate@ast:var(),
gleam@option:option(list(glemplate@ast:node_()))} |
{iter_tag, glemplate@ast:var(), binary()}.
-type state() :: {state,
list(glemplate@ast:node_()),
gleam@string_tree:string_tree(),
list(glemplate@ast:node_()),
gleam@option:option(incomplete_tag()),
gleam@option:option(state())}.
-file("src/glemplate/parser.gleam", 136).
-spec lex(binary(), nibble@lexer:lexer(LYH, lexer_mode())) -> {ok,
list(nibble@lexer:token(LYH))} |
{error, parse_error()}.
lex(Input, Lexer) ->
_pipe = nibble@lexer:run_advanced(Input, content, Lexer),
gleam@result:map_error(
_pipe,
fun(Error) ->
{lexer_error,
<<<<<<<<<<"No match found in ("/utf8,
(erlang:integer_to_binary(
erlang:element(2, Error)
))/binary>>/binary,
":"/utf8>>/binary,
(erlang:integer_to_binary(erlang:element(3, Error)))/binary>>/binary,
"), last lexeme: "/utf8>>/binary,
(erlang:element(4, Error))/binary>>}
end
).
-file("src/glemplate/parser.gleam", 150).
-spec gen_lexer() -> nibble@lexer:lexer(token(), lexer_mode()).
gen_lexer() ->
Right_angle = nibble@lexer:token(<<">"/utf8>>, right_angle),
Percent = nibble@lexer:token(<<"%"/utf8>>, percent),
End = nibble@lexer:keyword(<<"end"/utf8>>, <<"\\W"/utf8>>, 'end'),
If_ = nibble@lexer:keyword(<<"if"/utf8>>, <<"\\W"/utf8>>, 'if'),
Else_ = nibble@lexer:keyword(<<"else"/utf8>>, <<"\\W"/utf8>>, 'else'),
For = nibble@lexer:keyword(<<"for"/utf8>>, <<"\\W"/utf8>>, for),
In = nibble@lexer:keyword(<<"in"/utf8>>, <<"\\W"/utf8>>, in),
Raw = nibble@lexer:keyword(<<"raw"/utf8>>, <<"\\W"/utf8>>, raw),
Render = nibble@lexer:keyword(<<"render"/utf8>>, <<"\\W"/utf8>>, render),
Colon = nibble@lexer:token(<<":"/utf8>>, colon),
Comma = nibble@lexer:token(<<","/utf8>>, comma),
Period = nibble@lexer:token(<<"."/utf8>>, period),
Variable = nibble@lexer:variable(
gleam@set:from_list(
[<<"end"/utf8>>,
<<"if"/utf8>>,
<<"else"/utf8>>,
<<"for"/utf8>>,
<<"in"/utf8>>,
<<"raw"/utf8>>,
<<"render"/utf8>>]
),
fun(Field@0) -> {variable, Field@0} end
),
Whitespace = nibble@lexer:whitespace(nil),
Text = nibble@lexer:keep(fun(Lexeme, Lookahead) -> case Lookahead of
<<"<"/utf8>> ->
{ok, {text, Lexeme}};
<<""/utf8>> ->
{ok, {text, Lexeme}};
_ ->
{error, nil}
end end),
Tag_start = begin
nibble@lexer:custom(
fun(Mode, Lexeme@1, Lookahead@1) -> case {Lexeme@1, Lookahead@1} of
{<<""/utf8>>, <<"<"/utf8>>} ->
no_match;
{<<"<"/utf8>>, <<"%"/utf8>>} ->
no_match;
{<<"<%"/utf8>>, <<"!"/utf8>>} ->
skip;
{<<"<%"/utf8>>, <<"%"/utf8>>} ->
skip;
{<<"<%"/utf8>>, <<"="/utf8>>} ->
skip;
{<<"<%"/utf8>>, _} ->
{keep, tag_start, tag};
{<<"<%%"/utf8>>, _} ->
{keep, {text, <<"<%%"/utf8>>}, Mode};
{<<"<%="/utf8>>, _} ->
{keep, output_tag_start, tag};
{<<"<%!"/utf8>>, <<"-"/utf8>>} ->
skip;
{<<"<%!-"/utf8>>, <<"-"/utf8>>} ->
skip;
{<<"<%!--"/utf8>>, _} ->
{drop, comment_tag};
{_, _} ->
no_match
end end
)
end,
Comment_content = begin
nibble@lexer:custom(
fun(Mode@1, Lexeme@2, Lookahead@2) ->
case {Lexeme@2, Lookahead@2} of
{<<""/utf8>>, <<"-"/utf8>>} ->
skip;
{<<"-"/utf8>>, <<"-"/utf8>>} ->
skip;
{<<"--"/utf8>>, <<"%"/utf8>>} ->
skip;
{<<"--%"/utf8>>, <<">"/utf8>>} ->
skip;
{<<"--%>"/utf8>>, _} ->
{drop, content};
{_, _} ->
{drop, Mode@1}
end
end
)
end,
In_tag = [Percent,
End,
If_,
Else_,
For,
In,
Raw,
Render,
Colon,
Comma,
Period,
Variable],
nibble@lexer:advanced(fun(Mode@2) -> case Mode@2 of
comment_tag ->
[Comment_content];
tag ->
[begin
_pipe = Right_angle,
nibble@lexer:into(_pipe, fun(_) -> content end)
end,
begin
_pipe@1 = Whitespace,
nibble@lexer:ignore(_pipe@1)
end |
In_tag];
content ->
[Tag_start, Text]
end end).
-file("src/glemplate/parser.gleam", 279).
-spec do_parse(
list(nibble@lexer:token(MCJ)),
nibble:parser(state(), MCJ, any())
) -> {ok, list(glemplate@ast:node_())} | {error, parse_error()}.
do_parse(Tokens, Parser) ->
_pipe = nibble:run(Tokens, Parser),
_pipe@1 = gleam@result:map(
_pipe,
fun(State) -> erlang:element(2, State) end
),
gleam@result:map_error(
_pipe@1,
fun(Deadends) ->
{parser_error,
gleam@list:map(
Deadends,
fun(Deadend) ->
<<<<<<<<<<<<<<<<<<"Dead end between ("/utf8,
(erlang:integer_to_binary(
erlang:element(
2,
erlang:element(
2,
Deadend
)
)
))/binary>>/binary,
":"/utf8>>/binary,
(erlang:integer_to_binary(
erlang:element(
3,
erlang:element(
2,
Deadend
)
)
))/binary>>/binary,
") and ("/utf8>>/binary,
(erlang:integer_to_binary(
erlang:element(
4,
erlang:element(2, Deadend)
)
))/binary>>/binary,
":"/utf8>>/binary,
(erlang:integer_to_binary(
erlang:element(
5,
erlang:element(2, Deadend)
)
))/binary>>/binary,
"): "/utf8>>/binary,
(gleam@string:inspect(erlang:element(3, Deadend)))/binary>>
end
)}
end
).
-file("src/glemplate/parser.gleam", 274).
?DOC(" Parse the tokens into a node list.\n").
-spec parse(binary(), parser()) -> {ok, list(glemplate@ast:node_())} |
{error, parse_error()}.
parse(Input, Parser) ->
gleam@result:'try'(
lex(Input, erlang:element(2, Parser)),
fun(Lexed) -> do_parse(Lexed, erlang:element(3, Parser)) end
).
-file("src/glemplate/parser.gleam", 267).
?DOC(" Parse the input into a template with the given name.\n").
-spec parse_to_template(binary(), binary(), parser()) -> {ok,
glemplate@ast:template()} |
{error, parse_error()}.
parse_to_template(Input, Name, Parser) ->
gleam@result:'try'(
parse(Input, Parser),
fun(Parsed) -> {ok, {template, Name, Parsed}} end
).
-file("src/glemplate/parser.gleam", 300).
-spec empty_state() -> state().
empty_state() ->
{state, [], gleam@string_tree:new(), [], none, none}.
-file("src/glemplate/parser.gleam", 459).
-spec end_tag(state()) -> nibble:parser(state(), token(), any()).
end_tag(State) ->
nibble:do(
nibble:token('end'),
fun(_) ->
Outer_state = gleam@option:lazy_unwrap(
erlang:element(6, State),
fun() -> empty_state() end
),
case erlang:element(5, State) of
{some, {if_tag, Var, If_true}} ->
{If_true@1, State@1} = case If_true of
{some, If_true_nodes} ->
{If_true_nodes, State};
none ->
{lists:reverse(erlang:element(4, State)),
{state,
erlang:element(2, State),
erlang:element(3, State),
[],
erlang:element(5, State),
erlang:element(6, State)}}
end,
If_false = lists:reverse(erlang:element(4, State@1)),
nibble:succeed(
{state,
erlang:element(2, Outer_state),
erlang:element(3, Outer_state),
[{dynamic, {'if', Var, If_true@1, If_false}} |
erlang:element(4, Outer_state)],
erlang:element(5, Outer_state),
erlang:element(6, Outer_state)}
);
{some, {iter_tag, Over, Binding}} ->
nibble:succeed(
{state,
erlang:element(2, Outer_state),
erlang:element(3, Outer_state),
[{dynamic,
{iter,
Over,
Binding,
lists:reverse(erlang:element(4, State))}} |
erlang:element(4, Outer_state)],
erlang:element(5, Outer_state),
erlang:element(6, Outer_state)}
);
_ ->
nibble:fail(
<<"Expected `if` or `for` tag for `end` tag"/utf8>>
)
end
end
).
-file("src/glemplate/parser.gleam", 504).
-spec else_tag(state()) -> nibble:parser(state(), token(), any()).
else_tag(State) ->
nibble:do(nibble:token('else'), fun(_) -> case erlang:element(5, State) of
{some, {if_tag, Var, none}} ->
nibble:succeed(
{state,
erlang:element(2, State),
erlang:element(3, State),
[],
{some,
{if_tag,
Var,
{some,
lists:reverse(erlang:element(4, State))}}},
erlang:element(6, State)}
);
_ ->
nibble:fail(<<"Expected `if` to match `else` tag"/utf8>>)
end end).
-file("src/glemplate/parser.gleam", 521).
-spec stringify_current_text(state()) -> state().
stringify_current_text(State) ->
{state,
erlang:element(2, State),
gleam@string_tree:new(),
[{text, unicode:characters_to_binary(erlang:element(3, State))} |
erlang:element(4, State)],
erlang:element(5, State),
erlang:element(6, State)}.
-file("src/glemplate/parser.gleam", 532).
-spec variable_name() -> nibble:parser(binary(), token(), any()).
variable_name() ->
nibble:take_map(<<"Variable name"/utf8>>, fun(Token) -> case Token of
{variable, Var} ->
{some, Var};
_ ->
none
end end).
-file("src/glemplate/parser.gleam", 540).
-spec accessors() -> nibble:parser(list(binary()), token(), any()).
accessors() ->
nibble:do(
nibble:sequence(variable_name(), nibble:token(period)),
fun(Accessors) -> nibble:return(Accessors) end
).
-file("src/glemplate/parser.gleam", 548).
-spec variable() -> nibble:parser(glemplate@ast:var(), token(), any()).
variable() ->
nibble:do(
variable_name(),
fun(Name) ->
nibble:do(
nibble:one_of(
[begin
nibble:do(
nibble:token(period),
fun(_) ->
nibble:do(
accessors(),
fun(Accs) -> nibble:return(Accs) end
)
end
)
end,
nibble:return([])]
),
fun(Accessors) ->
nibble:return(
gleam@list:fold(
Accessors,
{assign, Name},
fun(Acc, Accessor) ->
{field_access, Acc, Accessor}
end
)
)
end
)
end
).
-file("src/glemplate/parser.gleam", 356).
-spec output_tag(state()) -> nibble:parser(state(), token(), any()).
output_tag(State) ->
nibble:do(
nibble:one_of(
[begin
nibble:do(
nibble:token(raw),
fun(_) ->
nibble:do(
variable(),
fun(Var) ->
nibble:succeed(
{state,
erlang:element(2, State),
erlang:element(3, State),
[{dynamic, {raw_output, Var}} |
erlang:element(4, State)],
erlang:element(5, State),
erlang:element(6, State)}
)
end
)
end
)
end,
begin
nibble:do(
variable(),
fun(Var@1) ->
nibble:succeed(
{state,
erlang:element(2, State),
erlang:element(3, State),
[{dynamic, {output, Var@1}} |
erlang:element(4, State)],
erlang:element(5, State),
erlang:element(6, State)}
)
end
)
end]
),
fun(Out) ->
nibble:do(
nibble:token(percent),
fun(_) ->
nibble:do(
nibble:token(right_angle),
fun(_) -> nibble:return(Out) end
)
end
)
end
).
-file("src/glemplate/parser.gleam", 410).
-spec if_tag(state()) -> nibble:parser(state(), token(), any()).
if_tag(State) ->
nibble:do(
nibble:token('if'),
fun(_) ->
nibble:do(
variable(),
fun(Var) ->
nibble:return(
begin
_record = empty_state(),
{state,
erlang:element(2, _record),
erlang:element(3, _record),
erlang:element(4, _record),
{some, {if_tag, Var, none}},
{some, State}}
end
)
end
)
end
).
-file("src/glemplate/parser.gleam", 423).
-spec for_tag(state()) -> nibble:parser(state(), token(), any()).
for_tag(State) ->
nibble:do(
nibble:token(for),
fun(_) ->
nibble:do(
variable_name(),
fun(Binding) ->
nibble:do(
nibble:token(in),
fun(_) ->
nibble:do(
variable(),
fun(Over) ->
nibble:return(
begin
_record = empty_state(),
{state,
erlang:element(2, _record),
erlang:element(3, _record),
erlang:element(4, _record),
{some,
{iter_tag, Over, Binding}},
{some, State}}
end
)
end
)
end
)
end
)
end
).
-file("src/glemplate/parser.gleam", 584).
-spec render_binding() -> nibble:parser({glemplate@ast:var(), binary()}, token(), any()).
render_binding() ->
nibble:do(
variable_name(),
fun(To) ->
nibble:do(
nibble:token(colon),
fun(_) ->
nibble:do(
variable(),
fun(From) -> nibble:return({From, To}) end
)
end
)
end
).
-file("src/glemplate/parser.gleam", 592).
-spec stringify_token(token()) -> binary().
stringify_token(Token) ->
case Token of
{text, Text} ->
Text;
tag_start ->
<<"<%"/utf8>>;
output_tag_start ->
<<"<%="/utf8>>;
right_angle ->
<<">"/utf8>>;
percent ->
<<"%"/utf8>>;
'end' ->
<<"end"/utf8>>;
'if' ->
<<"if"/utf8>>;
'else' ->
<<"else"/utf8>>;
for ->
<<"for"/utf8>>;
in ->
<<"in"/utf8>>;
raw ->
<<"raw"/utf8>>;
render ->
<<"render"/utf8>>;
colon ->
<<":"/utf8>>;
comma ->
<<","/utf8>>;
period ->
<<"."/utf8>>;
{variable, Var_name} ->
Var_name
end.
-file("src/glemplate/parser.gleam", 568).
-spec filename() -> nibble:parser(binary(), token(), any()).
filename() ->
nibble:do(
nibble:take_while1(
<<"a filename without spaces"/utf8>>,
fun(Token) -> case Token of
comma ->
false;
percent ->
false;
_ ->
true
end end
),
fun(File_name_tokens) ->
_pipe = gleam@list:fold(
File_name_tokens,
gleam@string_tree:new(),
fun(Acc, Token@1) ->
gleam@string_tree:append(Acc, stringify_token(Token@1))
end
),
_pipe@1 = unicode:characters_to_binary(_pipe),
nibble:return(_pipe@1)
end
).
-file("src/glemplate/parser.gleam", 438).
-spec render_tag(state()) -> nibble:parser(state(), token(), any()).
render_tag(State) ->
nibble:do(
nibble:token(render),
fun(_) ->
nibble:do(
filename(),
fun(Filename) ->
nibble:do(
nibble:optional(nibble:token(comma)),
fun(Maybe_comma) -> nibble:do(case Maybe_comma of
{some, _} ->
nibble:sequence(
render_binding(),
nibble:token(comma)
);
none ->
nibble:return([])
end, fun(Bindings) ->
nibble:return(
{state,
erlang:element(2, State),
erlang:element(3, State),
[{dynamic,
{render, Filename, Bindings}} |
erlang:element(4, State)],
erlang:element(5, State),
erlang:element(6, State)}
)
end) end
)
end
)
end
).
-file("src/glemplate/parser.gleam", 393).
-spec other_tag(state()) -> nibble:parser(state(), token(), any()).
other_tag(State) ->
nibble:do(
nibble:one_of(
[if_tag(State),
for_tag(State),
render_tag(State),
else_tag(State),
end_tag(State)]
),
fun(Tag) ->
nibble:do(
nibble:token(percent),
fun(_) ->
nibble:do(
nibble:token(right_angle),
fun(_) -> nibble:return(Tag) end
)
end
)
end
).
-file("src/glemplate/parser.gleam", 341).
-spec tag_parser(state()) -> nibble:parser(state(), token(), any()).
tag_parser(State) ->
With_updated_text = stringify_current_text(State),
nibble:one_of(
[begin
nibble:do(
nibble:token(tag_start),
fun(_) -> other_tag(With_updated_text) end
)
end,
begin
nibble:do(
nibble:token(output_tag_start),
fun(_) -> output_tag(With_updated_text) end
)
end]
).
-file("src/glemplate/parser.gleam", 310).
-spec base_parser(state()) -> nibble:parser(state(), token(), any()).
base_parser(Old_state) ->
_pipe@5 = nibble:loop(
Old_state,
fun(State) ->
nibble:one_of(
[begin
_pipe = tag_parser(State),
nibble:map(
_pipe,
fun(Field@0) -> {continue, Field@0} end
)
end,
begin
_pipe@1 = nibble:any(),
_pipe@2 = nibble:map(
_pipe@1,
fun(Token) ->
{state,
erlang:element(2, State),
gleam@string_tree:append(
erlang:element(3, State),
stringify_token(Token)
),
erlang:element(4, State),
erlang:element(5, State),
erlang:element(6, State)}
end
),
nibble:map(
_pipe@2,
fun(Field@0) -> {continue, Field@0} end
)
end,
begin
_pipe@3 = nibble:eof(),
_pipe@4 = nibble:replace(
_pipe@3,
stringify_current_text(State)
),
nibble:map(
_pipe@4,
fun(Field@0) -> {break, Field@0} end
)
end]
)
end
),
nibble:map(
_pipe@5,
fun(State@1) ->
{state,
[{nodes, erlang:element(2, State@1)} |
lists:reverse(erlang:element(4, State@1))],
gleam@string_tree:new(),
[],
erlang:element(5, State@1),
erlang:element(6, State@1)}
end
).
-file("src/glemplate/parser.gleam", 103).
?DOC(
" Create a new parser.\n"
"\n"
" This same parser should be used for multiple operations to avoid creating\n"
" the parser over and over again.\n"
).
-spec new() -> parser().
new() ->
{parser, gen_lexer(), base_parser(empty_state())}.