-module(glemplate@parser). -compile(no_auto_import). -export([parse_to_template/2, parse/1]). -export_type([incomplete_tag/0, incomplete_access/0, state/0]). -type incomplete_tag() :: {'if', glemplate@ast:var(), gleam@option:option(list(glemplate@ast:node_()))} | {iter, glemplate@ast:var(), binary()}. -type incomplete_access() :: {field_access, binary()} | {index_access, integer()}. -type state() :: {state, list(glemplate@ast:node_()), gleam@string_builder:string_builder(), list(glemplate@ast:node_()), gleam@option:option(incomplete_tag()), gleam@option:option(state())}. -spec parse_to_template(binary(), binary()) -> {ok, glemplate@ast:template()} | {error, list(nibble:dead_end(any()))}. parse_to_template(Template, Name) -> case parse(Template) of {error, _try} -> {error, _try}; {ok, Parsed} -> {ok, {template, Name, Parsed}} end. -spec parse(binary()) -> {ok, list(glemplate@ast:node_())} | {error, list(nibble:dead_end(any()))}. parse(Template) -> _pipe = nibble:run(Template, base_parser(empty_state())), gleam@result:map(_pipe, fun(State) -> erlang:element(2, State) end). -spec empty_state() -> state(). empty_state() -> {state, [], gleam@string_builder:new(), [], none, none}. -spec base_parser(state()) -> nibble:parser(state(), 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(C) -> erlang:setelement( 3, State, gleam@string_builder:append( erlang:element(3, State), C ) ) 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) -> erlang:setelement( 3, erlang:setelement( 4, erlang:setelement( 2, State@1, [{nodes, erlang:element(2, State@1)} | gleam@list:reverse(erlang:element(4, State@1))] ), [] ), gleam@string_builder:new() ) end ). -spec tag_parser(state()) -> nibble:parser(state(), any()). tag_parser(State) -> nibble:then( nibble:string(<<"<%"/utf8>>), fun(_) -> State@1 = stringify_current_text(State), nibble:one_of( [escaped_tag(State@1), comment_tag(State@1), output_tag(State@1), other_tag(State@1)] ) end ). -spec escaped_tag(state()) -> nibble:parser(state(), any()). escaped_tag(State) -> _pipe = nibble:string(<<"%"/utf8>>), nibble:replace( _pipe, erlang:setelement( 4, State, [{text, <<"<%"/utf8>>} | erlang:element(4, State)] ) ). -spec comment_tag(state()) -> nibble:parser(state(), any()). comment_tag(State) -> nibble:then( nibble:string(<<"!--"/utf8>>), fun(_) -> _pipe@2 = nibble:loop( nil, fun(_) -> nibble:one_of( [begin _pipe = nibble:string(<<"--"/utf8, "%>"/utf8>>), nibble:map( _pipe, fun(Field@0) -> {break, Field@0} end ) end, begin _pipe@1 = nibble:any(), nibble:replace(_pipe@1, {continue, nil}) end] ) end ), nibble:replace(_pipe@2, State) end ). -spec output_tag(state()) -> nibble:parser(state(), any()). output_tag(State) -> nibble:then( nibble:string(<<"="/utf8>>), fun(_) -> _pipe = nibble:one_of( [begin nibble:then( nibble:whitespace(), fun(_) -> nibble:then( nibble:string(<<"raw"/utf8>>), fun(_) -> nibble:then( nibble:whitespace(), fun(_) -> nibble:then( variable(), fun(Var) -> nibble:succeed( erlang:setelement( 4, State, [{dynamic, {raw_output, Var}} | erlang:element( 4, State )] ) ) end ) end ) end ) end ) end, begin nibble:then( nibble:whitespace(), fun(_) -> nibble:then( variable(), fun(Var@1) -> nibble:succeed( erlang:setelement( 4, State, [{dynamic, {output, Var@1}} | erlang:element(4, State)] ) ) end ) end ) end] ), _pipe@1 = nibble:drop(_pipe, nibble:whitespace()), nibble:drop(_pipe@1, nibble:string(<<"%>"/utf8>>)) end ). -spec other_tag(state()) -> nibble:parser(state(), any()). other_tag(State) -> nibble:then( nibble:whitespace(), fun(_) -> _pipe = nibble:one_of( [if_tag(State), for_tag(State), render_tag(State), else_tag(State), end_tag(State)] ), _pipe@1 = nibble:drop(_pipe, nibble:whitespace()), nibble:drop(_pipe@1, nibble:string(<<"%>"/utf8>>)) end ). -spec if_tag(state()) -> nibble:parser(state(), any()). if_tag(State) -> nibble:then( nibble:string(<<"if"/utf8>>), fun(_) -> nibble:then( nibble:whitespace(), fun(_) -> nibble:then( variable(), fun(Var) -> nibble:succeed( erlang:setelement( 6, erlang:setelement( 5, empty_state(), {some, {'if', Var, none}} ), {some, State} ) ) end ) end ) end ). -spec for_tag(state()) -> nibble:parser(state(), any()). for_tag(State) -> nibble:then( nibble:string(<<"for"/utf8>>), fun(_) -> nibble:then( nibble:whitespace(), fun(_) -> nibble:then( variable_name(), fun(Binding) -> nibble:then( nibble:whitespace(), fun(_) -> nibble:then( nibble:string(<<"in"/utf8>>), fun(_) -> nibble:then( nibble:whitespace(), fun(_) -> nibble:then( variable(), fun(Over) -> nibble:succeed( erlang:setelement( 6, erlang:setelement( 5, empty_state( ), {some, {iter, Over, Binding}} ), {some, State} ) ) end ) end ) end ) end ) end ) end ) end ). -spec render_tag(state()) -> nibble:parser(state(), any()). render_tag(State) -> nibble:then( nibble:string(<<"render"/utf8>>), fun(_) -> nibble:then( nibble:whitespace(), fun(_) -> nibble:then( filename(), fun(Filename) -> nibble:then( nibble:whitespace(), fun(_) -> _pipe = nibble:many( render_binding(), render_separator() ), nibble:map( _pipe, fun(Bindings) -> erlang:setelement( 4, State, [{dynamic, {render, Filename, Bindings}} | erlang:element(4, State)] ) end ) end ) end ) end ) end ). -spec end_tag(state()) -> nibble:parser(state(), any()). end_tag(State) -> nibble:then( nibble:string(<<"end"/utf8>>), fun(_) -> Outer_state = gleam@option:lazy_unwrap( erlang:element(6, State), fun() -> empty_state() end ), case erlang:element(5, State) of {some, {'if', Var, If_true}} -> {If_true@1, State@1} = case If_true of {some, If_true_nodes} -> {If_true_nodes, State}; none -> {gleam@list:reverse(erlang:element(4, State)), erlang:setelement(4, State, [])} end, If_false = gleam@list:reverse(erlang:element(4, State@1)), nibble:succeed( erlang:setelement( 4, Outer_state, [{dynamic, {'if', Var, If_true@1, If_false}} | erlang:element(4, Outer_state)] ) ); {some, {iter, Over, Binding}} -> nibble:succeed( erlang:setelement( 4, Outer_state, [{dynamic, {iter, Over, Binding, gleam@list:reverse( erlang:element(4, State) )}} | erlang:element(4, Outer_state)] ) ); _ -> nibble:fail( <<"Expected `if` or `for` tag for `end` tag"/utf8>> ) end end ). -spec else_tag(state()) -> nibble:parser(state(), any()). else_tag(State) -> nibble:then( nibble:string(<<"else"/utf8>>), fun(_) -> case erlang:element(5, State) of {some, {'if', Var, none}} -> nibble:succeed( erlang:setelement( 4, erlang:setelement( 5, State, {some, {'if', Var, {some, gleam@list:reverse( erlang:element(4, State) )}}} ), [] ) ); _ -> nibble:fail(<<"Expected `if` to match `else` tag"/utf8>>) end end ). -spec stringify_current_text(state()) -> state(). stringify_current_text(State) -> erlang:setelement( 3, erlang:setelement( 4, State, [{text, gleam@string_builder:to_string(erlang:element(3, State))} | erlang:element(4, State)] ), gleam@string_builder:new() ). -spec variable_name() -> nibble:parser(binary(), any()). variable_name() -> nibble:take_if_and_while( fun(Grapheme) -> nibble@predicates:is_alphanum(Grapheme) orelse (Grapheme =:= <<"_"/utf8>>) end, <<"a variable name"/utf8>> ). -spec field_access() -> nibble:parser(binary(), any()). field_access() -> nibble:take_if_and_while( fun(Grapheme) -> nibble@predicates:is_alphanum(Grapheme) orelse (Grapheme =:= <<"_"/utf8>>) end, <<"a field accessor"/utf8>> ). -spec index_access() -> nibble:parser(binary(), any()). index_access() -> nibble:take_if_and_while( fun nibble@predicates:is_digit/1, <<"an index accessor"/utf8>> ). -spec accessors() -> nibble:parser(list(incomplete_access()), any()). accessors() -> nibble:map( nibble:many( nibble:one_of([field_access(), index_access()]), nibble:string(<<"."/utf8>>) ), fun(Accessors) -> gleam@list:map( Accessors, fun(Accessor) -> case gleam@int:parse(Accessor) of {ok, Data} -> {index_access, Data}; {error, _} -> {field_access, Accessor} end end ) end ). -spec variable() -> nibble:parser(glemplate@ast:var(), any()). variable() -> nibble:then( variable_name(), fun(Name) -> nibble:then( nibble:one_of( [begin nibble:then( nibble:string(<<"."/utf8>>), fun(_) -> nibble:then( accessors(), fun(Accs) -> nibble:succeed(Accs) end ) end ) end, nibble:succeed([])] ), fun(Accessors) -> nibble:succeed( gleam@list:fold( Accessors, {assign, Name}, fun(Acc, Accessor) -> case Accessor of {field_access, Field} -> {field_access, Acc, Field}; {index_access, Index} -> {index_access, Acc, Index} end end ) ) end ) end ). -spec filename() -> nibble:parser(binary(), any()). filename() -> nibble:take_if_and_while( fun(Grapheme) -> ((nibble@predicates:is_alphanum(Grapheme) orelse (Grapheme =:= <<"_"/utf8>>)) orelse (Grapheme =:= <<"-"/utf8>>)) orelse (Grapheme =:= <<"."/utf8>>) end, <<"a filename without spaces"/utf8>> ). -spec render_binding() -> nibble:parser({glemplate@ast:var(), binary()}, any()). render_binding() -> nibble:then( variable_name(), fun(To) -> nibble:then( nibble:string(<<":"/utf8>>), fun(_) -> nibble:then( nibble:whitespace(), fun(_) -> nibble:then( variable(), fun(From) -> nibble:succeed({From, To}) end ) end ) end ) end ). -spec render_separator() -> nibble:parser(nil, any()). render_separator() -> nibble:then( nibble:string(<<","/utf8>>), fun(_) -> nibble:then( nibble:whitespace(), fun(_) -> nibble:succeed(nil) end ) end ).