From aca2e53efa90648137d8652f8a315306d4957ef6 Mon Sep 17 00:00:00 2001 From: Marc Worrell Date: Fri, 28 Jun 2024 17:48:45 +0200 Subject: [PATCH] Add compose block --- README.md | 37 +++++++++ rebar.lock | 12 +-- src/template_compiler.erl | 25 ++++-- src/template_compiler_element.erl | 92 ++++++++++++++++++++++ src/template_compiler_parser.yrl | 20 ++++- src/template_compiler_runtime_internal.erl | 85 ++++++++++++++++++-- src/template_compiler_scanner.erl | 1 + test/template_compiler_include_SUITE.erl | 6 ++ test/test-data/compose.tpl | 1 + test/test-data/compose_b.tpl | 1 + 10 files changed, 258 insertions(+), 22 deletions(-) create mode 100644 test/test-data/compose.tpl create mode 100644 test/test-data/compose_b.tpl diff --git a/README.md b/README.md index 2922df6..2db5f80 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,43 @@ Then `a.tpl` renders like: this is hello the base world template ``` +#### Template compose + +This includes a template, but also defines extra blocks for the template +to overrule the blocks in the template. + +It is like a nameless `{% overrules %}` template, directly defined in the +template text at the spot of the include. + + +```django +{% compose "a.tpl" what="moon" %} +{% block a %}{{ what }}{% endblock %} +{% endcompose %} +``` + +And a.tpl is like: + +```django +Hello {% block a %}world{% endblock %}, and bye. +``` + +Then the above renders: + +``` +Hello moon, and bye. +``` + +There is also a `catcompose` to use a `catinclude`: + +```django +{% catcompose "a.tpl" id what="moon" %} +{% block a %}{{ what }}{% endblock %} +{% endcompose %} + +``` + + #### If tag Conditionally show or hide parts of a template: diff --git a/rebar.lock b/rebar.lock index dd94b2a..92e1597 100644 --- a/rebar.lock +++ b/rebar.lock @@ -3,20 +3,20 @@ {<<"qdate_localtime">>,{pkg,<<"qdate_localtime">>,<<"1.2.0">>},0}, {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},2}, {<<"tls_certificate_check">>, - {pkg,<<"tls_certificate_check">>,<<"1.20.0">>}, + {pkg,<<"tls_certificate_check">>,<<"1.21.0">>}, 1}, - {<<"zotonic_stdlib">>,{pkg,<<"zotonic_stdlib">>,<<"1.15.0">>},0}]}. + {<<"zotonic_stdlib">>,{pkg,<<"zotonic_stdlib">>,<<"1.20.0">>},0}]}. [ {pkg_hash,[ {<<"cowlib">>, <<"A9FA9A625F1D2025FE6B462CB865881329B5CAFF8F1854D1CBC9F9533F00E1E1">>}, {<<"qdate_localtime">>, <<"644ADE4C7F7EAC765E2048DFA714D78EA86BAF5255FE46279B2EAC5729760A07">>}, {<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>}, - {<<"tls_certificate_check">>, <<"1AC0C53F95E201FEB8D398EF9D764AE74175231289D89F166BA88A7F50CD8E73">>}, - {<<"zotonic_stdlib">>, <<"0876BB82B9CFDE331DC758C8A7818181BB5654F137723DF137DE53C25AD3DD1D">>}]}, + {<<"tls_certificate_check">>, <<"042AB2C0C860652BC5CF69C94E3A31F96676D14682E22EC7813BD173CEFF1788">>}, + {<<"zotonic_stdlib">>, <<"51A484CF4B692042A5A251510010E00FF419CFF6C85E094DD3E00E283817B53D">>}]}, {pkg_hash_ext,[ {<<"cowlib">>, <<"163B73F6367A7341B33C794C4E88E7DBFE6498AC42DCD69EF44C5BC5507C8DB0">>}, {<<"qdate_localtime">>, <<"98A538A5B6046B8652DFC5630B030D0414A1B31D0130C81FA6B88B5C1E625109">>}, {<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>}, - {<<"tls_certificate_check">>, <<"AB57B74B1A63DC5775650699A3EC032EC0065005EFF1F020818742B7312A8426">>}, - {<<"zotonic_stdlib">>, <<"1864A96F2B5AE278DC52607F89CC90BF492713E7C3AC8EADDC1BA863E06E5921">>}]} + {<<"tls_certificate_check">>, <<"6CEE6CFFC35A390840D48D463541D50746A7B0E421ACAADB833CFC7961E490E7">>}, + {<<"zotonic_stdlib">>, <<"C8651604BB9165EC4A6F9EAB1FB1A4D5BE5174C2E3D02815F76EB87CBBC495F2">>}]} ]. diff --git a/src/template_compiler.erl b/src/template_compiler.erl index 1658275..19e965b 100644 --- a/src/template_compiler.erl +++ b/src/template_compiler.erl @@ -1,9 +1,9 @@ %% @author Marc Worrell -%% @copyright 2016-2023 Marc Worrell +%% @copyright 2016-2024 Marc Worrell %% @doc Main template compiler entry points. %% @end -%% Copyright 2016-2023 Marc Worrell +%% Copyright 2016-2024 Marc Worrell %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ -export([ render/4, + render/5, render_block/5, lookup/3, flush/0, @@ -31,7 +32,8 @@ compile_binary/4, get_option/2, is_template_module/1, - translations/1 + translations/1, + compile_blocks/2 ]). -include_lib("syntax_tools/include/merl.hrl"). @@ -92,12 +94,19 @@ %% returns the rendering result. -spec render(Template :: template(), Vars :: map() | list(), Options :: options(), Context :: term()) -> {ok, render_result()} | {error, term()}. -render(Template, Vars, Options, Context) when is_list(Vars) -> - render(Template, props_to_map(Vars, #{}), Options, Context); -render(Template0, Vars, Options, Context) when is_map(Vars) -> +render(Template0, Vars, Options, Context) -> + render(Template0, #{}, Vars, Options, Context). + +%% @doc Render a template. This looks up the templates needed, ensures compilation and +%% returns the rendering result. Start with a block-map to find some predefined blocks. +-spec render(Template :: template(), BlockMap :: map(), Vars :: map() | list(), Options :: options(), Context :: term()) -> + {ok, render_result()} | {error, term()}. +render(Template0, BlockMap0, Vars, Options, Context) when is_list(Vars) -> + render(Template0, BlockMap0, props_to_map(Vars, #{}), Options, Context); +render(Template0, BlockMap0, Vars, Options, Context) when is_map(Vars) -> Template = normalize_template(Template0), Runtime = proplists:get_value(runtime, Options, template_compiler_runtime), - case block_lookup(Runtime:map_template(Template, Vars, Context), #{}, [], [], Options, Vars, Runtime, Context) of + case block_lookup(Runtime:map_template(Template, Vars, Context), BlockMap0, [], [], Options, Vars, Runtime, Context) of {ok, BaseModule, ExtendsStack, BlockMap, OptDebugWrap} -> % Start with the render function of the "base" template % Optionally add the unique prefix for this rendering. @@ -468,7 +477,7 @@ split_loc({Filename, Line}) -> line => Line }. --spec compile_blocks([block_element()], #cs{}) -> {#ws{}, [{atom(), erl_syntax:syntaxTree()}]}. +-spec compile_blocks([block_element()], #cs{}) -> {#ws{}, [{atom(), erl_syntax:syntaxTree(), #ws{}}]}. compile_blocks(Blocks, CState) -> Ws = #ws{}, lists:foldl( diff --git a/src/template_compiler_element.erl b/src/template_compiler_element.erl index 5ba185e..a5790c6 100644 --- a/src/template_compiler_element.erl +++ b/src/template_compiler_element.erl @@ -213,6 +213,15 @@ compile({'call_with', {identifier, SrcPos, Name}, Expr}, CState, Ws) -> {context, erl_syntax:variable(CState#cs.context_var)} ]), {Ws1, Ast}; +compile({'compose', {TagPos, Template, Args}, Blocks}, CState, Ws) -> + {Ws1, ArgsList} = with_args(Args, CState, Ws, false), + IsContextVar = is_context_vars_arg(Args, CState), + compose(TagPos, Template, ArgsList, IsContextVar, Blocks, CState, Ws1); +compile({'catcompose', {TagPos, Template, IdExpr, Args}, Blocks}, CState, Ws) -> + {Ws1, ArgsList} = with_args(Args, CState, Ws, false), + {Ws2, IdAst} = template_compiler_expr:compile(IdExpr, CState, Ws1), + IsContextVar = is_context_vars_arg(Args, CState), + catcompose(TagPos, Template, IdAst, ArgsList, IsContextVar, Blocks, CState, Ws2); compile({custom_tag, {identifier, SrcPos, Name}, Args}, #cs{runtime=Runtime} = CState, Ws) -> {Ws1, ArgsList} = with_args(Args, CState, Ws, true), TagName = template_compiler_utils:to_atom(Name), @@ -722,6 +731,89 @@ maybe_add_include({string_literal, SrcPos, Text}, Method, IsCatinclude, Ws) -> maybe_add_include(_Token, _Method, _IsCatinclude, Ws) -> Ws. + +compose({_, SrcPos, _}, Template, ArgsList, IsContextVars, Blocks, #cs{runtime=Runtime} = CState, Ws) -> + {Ws1, TemplateAst} = template_compiler_expr:compile(Template, CState, Ws), + ArgsListAst = erl_syntax:list([ erl_syntax:tuple([A,B]) || {A,B} <- ArgsList ]), + {_BlocksWs, BlocksAsts} = template_compiler:compile_blocks(Blocks, CState), + BlockClauses = [ + ?Q("(_@BlockName@, Vars, Blocks, Context) -> _@BlockAst") + || {BlockName, BlockAst, _BlockWs} <- BlocksAsts + ] ++ [ + ?Q("(_BlockName, _Vars, _Blocks, _Context) -> <<>>") + ], + BlockFunAst = erl_syntax:fun_expr(BlockClauses), + BlockListAst = erl_syntax:abstract([ BlockName || {BlockName, _, _} <- BlocksAsts ]), + Ast = ?Q([ + "template_compiler_runtime_internal:compose(" + "_@srcpos," + "_@template," + "_@args," + "_@runtime," + "_@context_vars," + "_@is_context_vars," + "_@vars," + "_@block_list,", + "_@block_fun,", + "_@context)" + ], + [ + {srcpos, erl_syntax:abstract(SrcPos)}, + {template, TemplateAst}, + {args, ArgsListAst}, + {vars, erl_syntax:variable(CState#cs.vars_var)}, + {runtime, erl_syntax:atom(Runtime)}, + {context, erl_syntax:variable(CState#cs.context_var)}, + {context_vars, erl_syntax:abstract(CState#cs.context_vars)}, + {block_list, BlockListAst}, + {block_fun, BlockFunAst}, + {is_context_vars, erl_syntax:abstract(IsContextVars)} + ]), + Ws2 = maybe_add_include(Template, undefined, false, Ws1), + {Ws2, Ast}. + +catcompose({_, SrcPos, _}, Template, IdAst, ArgsList, IsContextVars, Blocks, #cs{runtime=Runtime} = CState, Ws) -> + {Ws1, TemplateAst} = template_compiler_expr:compile(Template, CState, Ws), + ArgsList1 = [ {erl_syntax:atom('$cat'), IdAst} | ArgsList ], + ArgsListAst = erl_syntax:list([ erl_syntax:tuple([A,B]) || {A,B} <- ArgsList1 ]), + {_BlocksWs, BlocksAsts} = template_compiler:compile_blocks(Blocks, CState), + BlockClauses = [ + ?Q("(_@BlockName@, Vars, Blocks, Context) -> _@BlockAst") + || {BlockName, BlockAst, _BlockWs} <- BlocksAsts + ] ++ [ + ?Q("(_BlockName, _Vars, _Blocks, _Context) -> <<>>") + ], + BlockFunAst = erl_syntax:fun_expr(BlockClauses), + BlockListAst = erl_syntax:abstract([ BlockName || {BlockName, _, _} <- BlocksAsts ]), + Ast = ?Q([ + "template_compiler_runtime_internal:compose(" + "_@srcpos," + "{cat, _@template}," + "_@args," + "_@runtime," + "_@context_vars," + "_@is_context_vars," + "_@vars," + "_@block_list,", + "_@block_fun,", + "_@context)" + ], + [ + {srcpos, erl_syntax:abstract(SrcPos)}, + {template, TemplateAst}, + {args, ArgsListAst}, + {vars, erl_syntax:variable(CState#cs.vars_var)}, + {runtime, erl_syntax:atom(Runtime)}, + {context, erl_syntax:variable(CState#cs.context_var)}, + {context_vars, erl_syntax:abstract(CState#cs.context_vars)}, + {block_list, BlockListAst}, + {block_fun, BlockFunAst}, + {is_context_vars, erl_syntax:abstract(IsContextVars)} + ]), + Ws2 = maybe_add_include(Template, undefined, false, Ws1), + {Ws2, Ast}. + + expr_list(ExprList, CState, Ws) -> lists:foldr( fun(E, {WsAcc, ExprAcc}) -> diff --git a/src/template_compiler_parser.yrl b/src/template_compiler_parser.yrl index 1a7754c..cf1cb33 100644 --- a/src/template_compiler_parser.yrl +++ b/src/template_compiler_parser.yrl @@ -55,6 +55,12 @@ Nonterminals CatIncludeTag NowTag + ComposeBlock + ComposeBraced + EndComposeBraced + CatComposeBlock + CatComposeBraced + BlockBlock BlockBraced EndBlockBraced @@ -172,10 +178,12 @@ Terminals block_keyword cache_keyword call_keyword + catcompose_keyword catinclude_keyword close_tag close_var comment_keyword + compose_keyword colon colons comma @@ -188,6 +196,7 @@ Terminals endblock_keyword endcache_keyword endcomment_keyword + endcompose_keyword endfilter_keyword endfor_keyword endif_keyword @@ -264,7 +273,7 @@ Left 500 '*' '/' '%'. Unary 600 Uminus Unot. %% Expected shift/reduce conflicts -Expect 5. +Expect 7. Template -> ExtendsTag BlockElements : {extends, '$1', '$2'}. Template -> OverrulesTag BlockElements : {overrules, '$2'}. @@ -291,6 +300,8 @@ Elements -> Elements WithBlock : '$1' ++ ['$2']. Elements -> Elements CacheBlock : '$1' ++ ['$2']. Elements -> Elements ScriptBlock : '$1' ++ ['$2']. Elements -> Elements CommentBlock : '$1'. +Elements -> Elements ComposeBlock : '$1' ++ ['$2']. +Elements -> Elements CatComposeBlock : '$1' ++ ['$2']. % Tags Elements -> Elements TransTag : '$1' ++ ['$2']. Elements -> Elements TransExtTag : '$1' ++ ['$2']. @@ -345,6 +356,13 @@ LoadTag -> open_tag load_keyword LoadNames close_tag : {load, '$3'}. LoadNames -> identifier : ['$1']. LoadNames -> LoadNames identifier : '$1' ++ ['$2']. +ComposeBlock -> ComposeBraced BlockElements EndComposeBraced : {compose, '$1', '$2'}. +ComposeBraced -> open_tag compose_keyword E OptWith WithArgs close_tag : {'$1', '$3', '$5'}. +EndComposeBraced -> open_tag endcompose_keyword close_tag. + +CatComposeBlock -> CatComposeBraced BlockElements EndComposeBraced : {catcompose, '$1', '$2'}. +CatComposeBraced -> open_tag catcompose_keyword E E OptWith WithArgs close_tag : {'$1', '$3', '$4', '$6'}. + BlockBlock -> BlockBraced Elements EndBlockBraced : {block, '$1', '$2'}. BlockBraced -> open_tag block_keyword identifier close_tag : '$3'. EndBlockBraced -> open_tag endblock_keyword close_tag. diff --git a/src/template_compiler_runtime_internal.erl b/src/template_compiler_runtime_internal.erl index 2afdb2d..14d4673 100644 --- a/src/template_compiler_runtime_internal.erl +++ b/src/template_compiler_runtime_internal.erl @@ -1,9 +1,9 @@ %% @author Marc Worrell -%% @copyright 2016-2023 Marc Worrell +%% @copyright 2016-2024 Marc Worrell %% @doc Callback routines for compiled templates. %% @end -%% Copyright 2016-2023 Marc Worrell +%% Copyright 2016-2024 Marc Worrell %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ block_call/6, block_inherit/7, include/9, + compose/10, call/4, print/1, unique/0 @@ -152,6 +153,8 @@ block_call(SrcPos, Block, Vars, BlockMap, Runtime, Context) -> After ] end; + {ok, [RenderFun|_]} when is_function(RenderFun) -> + RenderFun(Block, Vars, BlockMap, Context); error -> % No such block, return empty data. <<>> @@ -208,21 +211,21 @@ include(SrcPos, Method, Template, Args, Runtime, ContextVars, IsContextVars, Var end, include_1(SrcPos, Method, Template, Runtime, ContextVars, Vars1, Context1). -include_1(SrcPos, all, Template, Runtime, ContextVars, Vars1, Context) -> - Templates = Runtime:map_template_all(Template, Vars1, Context), +include_1(SrcPos, all, Template, Runtime, ContextVars, Vars, Context) -> + Templates = Runtime:map_template_all(Template, Vars, Context), lists:map( fun(Tpl) -> - include_1(SrcPos, optional, Tpl, Runtime, ContextVars, Vars1, Context) + include_1(SrcPos, optional, Tpl, Runtime, ContextVars, Vars, Context) end, Templates); -include_1(SrcPos, Method, Template, Runtime, ContextVars, Vars1, Context) -> +include_1(SrcPos, Method, Template, Runtime, ContextVars, Vars, Context) -> {SrcFile, SrcLine, _SrcCol} = SrcPos, Options = [ {runtime, Runtime}, {trace_position, SrcPos}, {context_vars, ContextVars} ], - case template_compiler:render(Template, Vars1, Options, Context) of + case template_compiler:render(Template, Vars, Options, Context) of {ok, Result} -> Result; {error, enoent} when Method =:= normal -> @@ -254,6 +257,74 @@ include_1(SrcPos, Method, Template, Runtime, ContextVars, Vars1, Context) -> <<>> end. +%% @doc Compose include of a template, with overruling blocks. +-spec compose(SrcPos, Template, Args, Runtime, ContextVars, IsContextVars, Vars, BlockList, BlockFun, Context) -> Output when + SrcPos :: {File::binary(), Line::integer(), Col::integer()}, + Template :: template_compiler:template(), + Args :: list({atom(),term()}), + Runtime :: atom(), + ContextVars :: list(binary()), + IsContextVars :: boolean(), + Vars :: map(), + BlockList :: list( atom() ), + BlockFun :: function(), % (_@BlockName@, Vars, Blocks, Context) -> _@BlockAst + Context :: term(), + Output :: template_compiler:render_result(). +compose(SrcPos, Template, Args, Runtime, ContextVars, IsContextVars, Vars, BlockList, BlockFun, Context) -> + Vars1 = lists:foldl( + fun + ({'$cat', [Cat|_] = E}, Acc) when is_atom(Cat); is_binary(Cat); is_list(Cat) -> + Acc#{ '$cat' => E }; + ({'$cat', E}, Acc) -> + Acc#{ + 'id' => E, + '$cat' => E + }; + ({V,E}, Acc) -> + Acc#{ V => E } + end, + Vars, + Args), + Context1 = case IsContextVars of + true -> Runtime:set_context_vars(Args, Context); + false -> Context + end, + BlockMap = lists:foldl(fun(Block, Acc) -> Acc#{ Block => [ BlockFun ] } end, #{}, BlockList), + {SrcFile, SrcLine, _SrcCol} = SrcPos, + Options = [ + {runtime, Runtime}, + {trace_position, SrcPos}, + {context_vars, ContextVars} + ], + case template_compiler:render(Template, BlockMap, Vars1, Options, Context1) of + {ok, Result} -> + Result; + {error, enoent} -> + ?LOG_ERROR(#{ + text => <<"Compose template not found">>, + template => Template, + srcpos => SrcPos, + result => error, + reason => enoent, + at => SrcFile, + line => SrcLine + }), + <<>>; + {error, Err} when is_map(Err) -> + ?LOG_ERROR(Err), + <<>>; + {error, Reason} -> + ?LOG_ERROR(#{ + text => <<"Compose render error">>, + template => Template, + srcpos => SrcPos, + result => error, + reason => Reason, + at => SrcFile, + line => SrcLine + }), + <<>> + end. %% @doc Call a module's render function. -spec call(Module::atom(), Args::map(), Vars::map(), Context::term()) -> template_compiler:render_result(). diff --git a/src/template_compiler_scanner.erl b/src/template_compiler_scanner.erl index cf953e2..3270941 100644 --- a/src/template_compiler_scanner.erl +++ b/src/template_compiler_scanner.erl @@ -83,6 +83,7 @@ identifier_to_keyword({identifier, Pos, String}, {PrevToken, Acc}) Keywords = [ <<"for">>, <<"empty">>, <<"endfor">>, <<"in">>, <<"include">>, <<"catinclude">>, <<"block">>, <<"endblock">>, <<"extends">>, <<"overrules">>, + <<"compose">>, <<"catcompose">>, <<"endcompose">>, <<"inherit">>, <<"autoescape">>, <<"endautoescape">>, <<"if">>, <<"else">>, <<"elif">>, <<"elseif">>, <<"endif">>, <<"not">>, <<"or">>, <<"and">>, <<"xor">>, <<"comment">>, <<"endcomment">>, <<"cycle">>, <<"firstof">>, <<"ifchanged">>, diff --git a/test/template_compiler_include_SUITE.erl b/test/template_compiler_include_SUITE.erl index 5e2870d..6e7f505 100644 --- a/test/template_compiler_include_SUITE.erl +++ b/test/template_compiler_include_SUITE.erl @@ -22,6 +22,7 @@ groups() -> [include_test ,include_dynamic_test ,include_args_test + ,compose_test ]}]. init_per_suite(Config) -> @@ -72,6 +73,11 @@ include_args_test(_Config) -> <<"a3:2:truec">> = iolist_to_binary(Bin1), ok. +compose_test(_Config) -> + {ok, Bin1} = template_compiler:render("compose.tpl", #{}, [], undefined), + <<"AxB1yC">> = iolist_to_binary(Bin1), + ok. + test_data_dir(Config) -> filename:join([ filename:dirname(filename:dirname(?config(data_dir, Config))), diff --git a/test/test-data/compose.tpl b/test/test-data/compose.tpl new file mode 100644 index 0000000..5fd0e23 --- /dev/null +++ b/test/test-data/compose.tpl @@ -0,0 +1 @@ +A{% compose "compose_b.tpl" v=1 %}{% block a %}B{{ v }}{% endblock %}{% endcompose %}C \ No newline at end of file diff --git a/test/test-data/compose_b.tpl b/test/test-data/compose_b.tpl new file mode 100644 index 0000000..1030691 --- /dev/null +++ b/test/test-data/compose_b.tpl @@ -0,0 +1 @@ +x{% block a %}XY{% endblock %}y \ No newline at end of file