%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2016-2024 Marc Worrell
%% @doc Callback routines for compiled templates.
%% @end
%% 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.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(template_compiler_runtime_internal).
-author('Marc Worrell <marc@worrell.nl>').
-export([
forloop/9,
with_vars/3,
block_call/6,
block_inherit/7,
include/9,
compose/11,
call/4,
print/1,
unique/0
]).
-include_lib("kernel/include/logger.hrl").
%% @doc Runtime implementation of a forloop.
-spec forloop(IsForloopVar :: boolean(), ListExpr :: term(), LoopVars :: [atom()],
LoopBody :: fun(), EmptyPart :: fun(),
Runtime :: atom(), IsContextVars :: boolean(),
Vars :: map(), Context :: term()) -> term().
forloop(IsLoopVar, ListExpr, Idents, BodyFun, EmptyFun, Runtime, IsContextVars, Vars, Context) ->
case forloop_to_list(ListExpr, length(Idents), Runtime, Context) of
[] ->
EmptyFun();
List when IsLoopVar ->
forloop_fold(List, Idents, BodyFun, Runtime, IsContextVars, Vars, Context);
List when not IsLoopVar ->
forloop_map(List, Idents, BodyFun, Runtime, IsContextVars, Vars, Context)
end.
forloop_to_list(undefined, _, _Runtime, _Context) ->
[];
forloop_to_list(V, 1, _Runtime, _Context)
when is_map(V);
is_number(V);
is_atom(V);
is_binary(V) ->
[ V ];
forloop_to_list(ListExpr, _NVars, Runtime, Context) ->
Runtime:to_list(ListExpr, Context).
% For loop with a forloop variable in the body, use a fold with a forloop state
% variable.
forloop_fold(List, Idents, Fun, Runtime, IsContextVars, Vars, Context) ->
Len = length(List),
{Result, _} = lists:foldl(
fun(Val, {Acc, Counter}) ->
Forloop = #{
counter => Counter,
counter0 => Counter-1,
revcounter => Len - Counter + 1,
revcounter0 => Len - Counter,
first => Counter =:= 1,
last => Counter =:= Len,
parentloop => maps:get(forloop, Vars, undefined)
},
Vars1 = assign_vars(Idents, Val, Vars#{forloop => Forloop}),
Context1 = case IsContextVars of
true -> Runtime:set_context_vars(Vars1, Context);
false -> Context
end,
{[Fun(Vars1, Context1) | Acc], Counter+1}
end,
{[], 1},
List),
lists:reverse(Result).
% For loop without any forloop variable, use a direct map
forloop_map(List, Idents, Fun, Runtime, true, Vars, Context) ->
[
begin
Vars1 = assign_vars(Idents, Val, Vars),
Fun(Vars1, Runtime:set_context_vars(Vars1, Context))
end || Val <- List
];
forloop_map(List, Idents, Fun, _Runtime, false, Vars, Context) ->
[ Fun(assign_vars(Idents, Val, Vars), Context) || Val <- List ].
%% @doc Used with forloops, assign variables from an expression value
assign_vars([V], E, Vars) ->
Vars#{V => E};
assign_vars(Vs, L, Vars) when is_list(L) ->
assign_vars_list(Vs, L, Vars);
assign_vars(Vs, T, Vars) when is_tuple(T) ->
assign_vars_tuple(Vs, T, Vars);
assign_vars([V|Vs], E, Vars) ->
assign_vars_list(Vs, [], Vars#{V => E}).
assign_vars_list([], _, Vars) ->
Vars;
assign_vars_list([V|Vs], [], Vars) ->
assign_vars_list(Vs, [], Vars#{ V => undefined });
assign_vars_list([V|Vs], [E|Es], Vars) ->
assign_vars_list(Vs, Es, Vars#{V => E}).
assign_vars_tuple([V1], {E1}, Vars) ->
Vars#{V1 => E1};
assign_vars_tuple([V1,V2], {E1,E2}, Vars) ->
Vars#{V1 => E1, V2 => E2};
assign_vars_tuple([V1,V2,V3], {E1,E2,E3}, Vars) ->
Vars#{V1 => E1, V2 => E2, V3 => E3};
assign_vars_tuple([V1,V2,V3,V4], {E1,E2,E3,E4}, Vars) ->
Vars#{V1 => E1, V2 => E2, V3 => E3, V4 => E4};
assign_vars_tuple(Vs, Es, Vars) ->
assign_vars_list(Vs, tuple_to_list(Es), Vars).
%% @doc Assign variables from a with statement. Care has to be taken for unpacking tuples and lists.
-spec with_vars([atom()], [term()], map()) -> map().
with_vars([_,_|_] = Vs, [E], Vars) ->
assign_vars(Vs, E, Vars);
with_vars(Vs, Es, Vars) ->
assign_vars_list(Vs, Es, Vars).
%% @doc Call the block function, lookup the function in the BlockMap to find
%% the correct module.
-spec block_call({binary(), integer(), integer()}, atom(), map(), map(), atom(), term()) -> term().
block_call(SrcPos, Block, Vars, BlockMap, Runtime, Context) ->
case maps:find(Block, BlockMap) of
{ok, [Module|_]} when is_atom(Module) ->
case Runtime:trace_block(SrcPos, Block, Module, Context) of
ok ->
Module:render_block(Block, Vars, BlockMap, Context);
{ok, Before, After} ->
[
Before,
Module:render_block(Block, Vars, BlockMap, Context),
After
]
end;
{ok, [{Module, RenderFun}|_]} when is_function(RenderFun) ->
case Runtime:trace_block(SrcPos, Block, Module, Context) of
ok ->
RenderFun(Block, Vars, BlockMap, Context);
{ok, Before, After} ->
[
Before,
RenderFun(Block, Vars, BlockMap, Context),
After
]
end;
error ->
% No such block, return empty data.
<<>>
end.
%% @doc Call the block function of the template the current module extends.
-spec block_inherit({binary(), integer(), integer()}, atom(), atom(), map(), map(), atom(), term()) -> term().
block_inherit(SrcPos, Module, Block, Vars, BlockMap, Runtime, Context) ->
case maps:find(Block, BlockMap) of
{ok, Modules} ->
case lists:dropwhile(
fun
({M, _F}) -> M =/= Module;
(M) -> M =/= Module
end, Modules)
of
[ _Module, Next | _ ] when is_atom(Next) ->
case Runtime:trace_block(SrcPos, Block, Next, Context) of
ok ->
Next:render_block(Block, Vars, BlockMap, Context);
{ok, Before, After} ->
[
Before,
Next:render_block(Block, Vars, BlockMap, Context),
After
]
end;
[] ->
<<>>
end;
error ->
% No such block, return empty data.
<<>>
end.
%% @doc Include a template.
-spec include(SrcPos, Method, Template, Args, Runtime, ContextVars, IsContextVars, Vars, Context) -> Output when
SrcPos :: {File::binary(), Line::integer(), Col::integer()},
Method :: normal | optional | all,
Template :: template_compiler:template() | undefined,
Args :: list({atom(),term()}),
Runtime :: atom(),
ContextVars :: list(binary()),
IsContextVars :: boolean(),
Vars :: map(),
Context :: term(),
Output :: template_compiler:render_result().
include(SrcPos, normal, undefined, _Args, _Runtime, _ContextVars, _IsContextVars, _Vars, _Context) ->
{SrcFile, SrcLine, _SrcCol} = SrcPos,
?LOG_ERROR(#{
text => <<"Included template not found">>,
template => undefined,
srcpos => SrcPos,
result => error,
reason => enoent,
at => SrcFile,
line => SrcLine
}),
<<>>;
include(SrcPos, _Method, undefined, _Args, _Runtime, _ContextVars, _IsContextVars, _Vars, _Context) ->
{SrcFile, SrcLine, _SrcCol} = SrcPos,
?LOG_DEBUG(#{
text => <<"Included template not found">>,
template => undefined,
srcpos => SrcPos,
result => error,
reason => enoent,
at => SrcFile,
line => SrcLine
}),
<<>>;
include(SrcPos, Method, Template, Args, Runtime, ContextVars, IsContextVars, Vars, 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,
include_1(SrcPos, Method, Template, Runtime, ContextVars, Vars1, Context1).
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, Vars, Context)
end,
Templates);
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, Vars, Options, Context) of
{ok, Result} ->
Result;
{error, enoent} when Method =:= normal ->
?LOG_ERROR(#{
text => <<"Included template not found">>,
template => Template,
srcpos => SrcPos,
result => error,
reason => enoent,
at => SrcFile,
line => SrcLine
}),
<<>>;
{error, enoent} ->
<<>>;
{error, Err} when is_map(Err) ->
?LOG_ERROR(Err),
<<>>;
{error, Reason} ->
?LOG_ERROR(#{
text => <<"Template render error">>,
template => Template,
srcpos => SrcPos,
result => error,
reason => Reason,
at => SrcFile,
line => SrcLine
}),
<<>>
end.
%% @doc Compose include of a template, with overruling blocks.
-spec compose(SrcPos, Template, Args, Runtime, ContextVars, IsContextVars, Vars, BlockList, BlockModule, 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() ),
BlockModule :: atom(),
BlockFun :: function(), % (_@BlockName@, Vars, Blocks, Context) -> _@BlockAst
Context :: term(),
Output :: template_compiler:render_result().
compose(SrcPos, Template, Args, Runtime, ContextVars, IsContextVars, Vars, BlockList, BlockModule, 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 => [ {BlockModule, 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().
call(Module, Args, Vars, Context) ->
case Module:render(Args, Vars, Context) of
{ok, Result} -> Result;
{error, _} -> <<>>
end.
%% @doc Echo the HTML escape value within <pre> tags.
-spec print(term()) -> iolist().
print(Expr) ->
V = io_lib:format("~p", [Expr]),
[
<<"<pre>">>,
z_html:escape(iolist_to_binary(V)),
<<"</pre>">>
].
%% @doc Make an unique string (about 11 characters). Used for expanding unique args in templates. The string only
%% consists of the characters A-Z and 0-9 and is safe to use as HTML element id.
-spec unique() -> binary().
unique() ->
<<"u", (integer_to_binary(rand_uniform(100000000000000000), 36))/binary>>.
-spec rand_uniform( pos_integer() ) -> non_neg_integer().
rand_uniform(N) ->
rand:uniform(N) - 1.