%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2016-2023 Marc Worrell
%% @doc Simple runtime for the compiled templates. Needs to be
%% copied and adapted for different environments.
%% @end
%% Copyright 2016-2023 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).
-author('Marc Worrell <marc@worrell.nl>').
-export([
map_template/3,
map_template_all/3,
is_modified/3,
compile_map_nested_value/3,
find_nested_value/3,
find_nested_value/4,
find_value/4,
get_context_name/1,
set_context_vars/2,
get_translations/2,
lookup_translation/3,
model_call/4,
custom_tag/4,
builtin_tag/5,
cache_tag/6,
javascript_tag/3,
spaceless_tag/3,
to_bool/2,
to_list/2,
to_simple_value/2,
to_render_result/3,
escape/2,
trace_compile/4,
trace_render/3,
trace_block/4
]).
-callback map_template(template_compiler:template(), map(), term()) ->
{ok, template_compiler:template_file()} | {error, enoent|term()}.
-callback map_template_all(template_compiler:template(), map(), term()) -> [template_compiler:template_file()].
-callback is_modified(file:filename_all(), calendar:datetime(), term()) -> boolean().
-callback compile_map_nested_value(Tokens :: list(), ContextVar::string(), Context :: term()) -> NewTokens :: list().
-callback find_nested_value(Keys :: list(), TplVars :: term(), Context :: term()) -> term().
-callback find_nested_value(BaseValue :: term(), Keys :: list(), TplVars :: term(), Context :: term()) -> term().
-callback find_value(Key :: term(), Vars :: term(), TplVars :: map(), Context :: term()) -> term().
-callback get_context_name(Context::term()) -> atom().
-callback set_context_vars(map()|list(), Context::term()) -> Context::term().
-callback get_translations(Text :: binary(), Context :: term()) -> binary() | {trans, [{atom(), binary()}]}.
-callback lookup_translation({trans, list({atom(), binary()})}, TplVars :: map(), Context :: term()) -> binary().
-callback model_call(Model::atom(), Path::list(), Payload::term(), Context::term()) -> template_compiler:model_return().
-callback custom_tag(Module::atom(), Args::list(), TplVars::map(), Context::term()) -> template_compiler:render_result().
-callback builtin_tag(template_compiler:builtin_tag(), term(), Args::list(), TplVars::map(), Context::term()) -> template_compiler:render_result().
-callback cache_tag(Seconds::integer(), Name::binary(), Args::list(), function(), TplVars::map(), Context::term()) -> template_compiler:render_result().
-callback javascript_tag(template_compiler:render_result(), map(), term()) -> template_compiler:render_result().
-callback spaceless_tag(template_compiler:render_result(), map(), term()) -> template_compiler:render_result().
-callback to_bool(Value :: term(), Context :: term()) -> boolean().
-callback to_list(Value :: term(), Context :: term()) -> list().
-callback to_simple_value(Value :: term(), Context :: term()) -> term().
-callback to_render_result(Value :: term(), TplVars :: map(), Context :: term()) -> template_compiler:render_result().
-callback escape(iodata() | undefined, Context :: term()) -> iodata().
-callback trace_compile(atom(), binary(), template_compiler:options(), term()) -> ok.
-callback trace_render(binary(), template_compiler:options(), term()) -> ok | {ok, iodata(), iodata()}.
-callback trace_block({binary(),integer(),integer()}, atom(), atom(), term()) -> ok | {ok, iodata(), iodata()}.
-include_lib("kernel/include/logger.hrl").
-include("template_compiler.hrl").
%% @doc Dynamic mapping of a template to a template name, context sensitive on the template vars.
-spec map_template(template_compiler:template(), map(), Context::term()) ->
{ok, template_compiler:template_file()} | {error, enoent|term()}.
map_template(#template_file{} = TplFile, _Vars, _Context) ->
{ok, TplFile};
map_template({cat, Template}, Vars, Context) ->
map_template(Template, Vars, Context);
map_template({cat, Template, _}, Vars, Context) ->
map_template(Template, Vars, Context);
map_template(Template, _Vars, _Context) ->
case application:get_env(template_compiler, template_dir) of
{ok, {App, SubDir}} when is_atom(App) ->
case code:priv_dir(App) of
{error, _} = Error ->
Error;
PrivDir ->
{ok, #template_file{
template=Template,
filename=filename:join([PrivDir, SubDir, Template])
}}
end;
{ok, Dir} when is_list(Dir); is_binary(Dir) ->
{ok, #template_file{
template=Template,
filename=filename:join([Dir, Template])
}};
undefined ->
{error, enoent}
end.
%% @doc Dynamically find all templates matching the template
-spec map_template_all(template_compiler:template(), map(), Context::term()) -> [template_compiler:template_file()].
map_template_all(Template, Vars, Context) ->
case map_template(Template, Vars, Context) of
{ok, Tpl} -> [Tpl];
{error, _} -> []
end.
%% @doc Check if a file has been modified
-spec is_modified(file:filename_all(), calendar:datetime(), term()) -> boolean().
is_modified(Filename, Mtime, _Context) ->
template_compiler_utils:file_mtime(Filename) /= Mtime.
%% @doc Compile time mapping of nested value lookup
-spec compile_map_nested_value(Tokens :: list(), _ContextVar::string(), Context :: term()) -> NewTokens :: list().
compile_map_nested_value(Ts, _ContextVar, _Context) ->
Ts.
%% @doc Find a list of values at once, easier and more efficient than a nested find_value/4
%% Add pattern matching here for nested lookups.
find_nested_value([K|Ks], TplVars, Context) ->
find_nested_value(find_value(K, TplVars, TplVars, Context), Ks, TplVars, Context).
find_nested_value(undefined, _Ks, _TplVars, _Context) ->
undefined;
find_nested_value(V, [], _TplVars, _Context) ->
V;
find_nested_value(V, [K|Ks], TplVars, Context) ->
find_nested_value(find_value(K, V, TplVars, Context), Ks, TplVars, Context).
%% @doc Find the value of key in some structure.
-spec find_value(Key :: term(), Vars :: term(), TplVars :: map(), Context :: term()) -> term().
find_value(undefined, _, _TplVars, _Context) ->
undefined;
find_value(_, undefined, _TplVars, _Context) ->
undefined;
find_value(Name, Vars, _TplVars, _Context) when is_map(Vars) ->
case maps:find(Name, Vars) of
{ok, V} -> V;
error when is_atom(Name) ->
% Maybe keys are binary
maps:get(atom_to_binary(Name, utf8), Vars, undefined);
error when is_binary(Name) ->
% Maybe keys are atoms
try
Name1 = binary_to_existing_atom(Name, utf8),
maps:get(Name1, Vars, undefined)
catch _:_ -> undefined
end;
error ->
undefined
end;
find_value(Name, [ V | _ ] = Vars, _TplVars, _Context) when is_binary(Name), is_atom(V) ->
try
Atom = binary_to_existing_atom(Name, utf8),
proplists:get_value(Atom, Vars)
catch _:_ -> undefined
end;
find_value(Key, [{B,_}|_] = L, _TplVars, _Context) when is_list(B) ->
proplists:get_value(z_convert:to_list(Key), L);
find_value(Key, [{B,_}|_] = L, _TplVars, _Context) when is_binary(B) ->
proplists:get_value(z_convert:to_binary(Key), L);
find_value(Name, Vars, _TplVars, _Context) when is_atom(Name), is_list(Vars) ->
proplists:get_value(Name, Vars);
find_value(Name, [{A,_}|_] = L, _TplVars, _Context) when is_atom(A), is_binary(Name) ->
try
Name1 = binary_to_existing_atom(Name, utf8),
proplists:get_value(Name1, L)
catch _:_ -> undefined
end;
find_value(Nr, Vars, _TplVars, _Context) when is_integer(Nr), is_list(Vars) ->
try lists:nth(Nr, Vars)
catch _:_ -> undefined
end;
find_value(SomeKey, [ {_, _} | _ ] = List, _TplVars, _Context) ->
proplists:get_value(SomeKey, List);
find_value(IsoAtom, {trans, Tr}, _TplVars, _Context) when is_atom(IsoAtom) ->
proplists:get_value(IsoAtom, Tr, <<>>);
find_value(Iso, {trans, Tr}, _TplVars, _Context) when is_binary(Iso) ->
try
IsoAtom = binary_to_existing_atom(Iso, utf8),
proplists:get_value(IsoAtom, Tr, <<>>)
catch _:_ -> <<>>
end;
find_value(Key, {obj, Props}, _TplVars, _Context) when is_list(Props) ->
proplists:get_value(z_convert:to_list(Key), Props);
find_value(Key, {obj, Props}, _TplVars, _Context) when is_list(Props) ->
proplists:get_value(z_convert:to_list(Key), Props);
find_value(Key, {struct, Props}, _TplVars, _Context) when is_list(Props) ->
case proplists:get_value(z_convert:to_binary(Key), Props) of
null -> undefined;
V -> V
end;
find_value(Key, Tuple, _TplVars, _Context) when is_tuple(Tuple) ->
case element(1, Tuple) of
dict ->
find_value_dict(Key, Tuple);
_ when is_integer(Key) ->
try element(Key, Tuple)
catch _:_ -> undefined
end;
_ when is_binary(Key) ->
try
Key1 = binary_to_integer(Key),
element(Key1, Tuple)
catch _:_ -> undefined
end;
_ ->
undefined
end;
find_value(Key, F, _TplVars, _Context) when is_function(F, 1) ->
F(Key);
find_value(Key, F, _TplVars, Context) when is_function(F, 2) ->
F(Key, Context);
find_value(Key, F, TplVars, Context) when is_function(F, 3) ->
F(Key, TplVars, Context);
find_value(_Key, _Vars, _TplVars, _Context) ->
undefined.
-dialyzer({nowarn_function, find_value_dict/2}).
-spec find_value_dict( term(), tuple() ) -> term().
find_value_dict( Key, Dict ) ->
try
case dict:find(Key, Dict) of
{ok, Val} -> Val;
_ -> undefined
end
catch _:_ -> undefined
end.
%% @doc Set the context name for this context, used for flush or recompile all templates
%% belonging to a certain context (like a single site).
-spec get_context_name( term() ) -> atom().
get_context_name(Context) when is_map(Context) ->
maps:get(context_name, Context, undefined);
get_context_name(Context) when is_list(Context) ->
proplists:get_value(context_name, Context, undefined);
get_context_name(Context) when is_atom(Context) ->
Context;
get_context_name(_) ->
undefined.
%% @doc Set any contextual arguments from the map or argument list. User for sudo/anondo and language settings
-spec set_context_vars(map()|list(), term()) -> term().
set_context_vars(Args, Context) when is_map(Args); is_list(Args) ->
Context.
%% @doc Fetch the translations for the given text.
-spec get_translations(binary(), term()) -> binary() | {trans, [{atom(), binary()}]}.
get_translations(Text, _Context) ->
{trans, [{en, Text}]}.
%% @doc Find the best fitting translation.
-spec lookup_translation({trans, list({atom(), binary()})}, TplVars :: map(), Context :: term()) -> binary().
lookup_translation({trans, Tr}, TplVars, _Context) when is_map(TplVars) ->
Lang = maps:get(z_language, TplVars, en),
case lists:keyfind(Lang, 1, Tr) of
{Lang, Text} ->
Text;
false when Lang =/= en ->
case lists:keyfind(en, 1, Tr) of
{Lang, Text} -> Text;
false -> <<>>
end;
false ->
<<>>
end.
%% @doc A model call with optional payload. Compiled from m.model.path!payload
-spec model_call(Model::atom(), Path::list(), Payload::term(), Context::term()) -> template_compiler:model_return().
model_call(Model, Path, Payload, _Context) ->
{ok, {io_lib:format("model:~p ~p :: ~p", [ Model, Path, Payload ]), []}}.
%% @doc Render a custom tag (Zotonic scomp) - this can be changed to more complex runtime lookups.
-spec custom_tag(Tag::atom(), Args::list(), Vars::map(), Context::term()) -> template_compiler:render_result().
custom_tag(Tag, Args, Vars, Context) ->
Tag:render(Args, Vars, Context).
%% @doc Render image/image_url/image_data_url/media/url/lib/lib_url tag.
%% The Expr is the media item or dispatch rule.
-spec builtin_tag(template_compiler:builtin_tag(), Expr::term(), Args::list(), Vars::map(), Context::term()) ->
template_compiler:render_result().
builtin_tag(_Tag, _Expr, _Args, _Vars, _Context) ->
<<>>.
%% @doc Render a block, cache the result for some time. Caching should be implemented by the runtime.
-spec cache_tag(Seconds::integer(), Name::binary(), Args::list(), function(), TplVars::map(), Context::term()) -> template_compiler:render_result().
cache_tag(_Seconds, _Name, Args, Fun, TplVars, Context) ->
FunVars = lists:foldl(
fun({K,V}, Acc) ->
Acc#{K => V}
end,
TplVars,
Args),
Fun(FunVars, Context).
%% @doc Render a script block, for Zotonic this is added to the scripts in the Context
-spec javascript_tag(template_compiler:render_result(), map(), term()) -> template_compiler:render_result().
javascript_tag(_Javascript, _TplVars, _Context) ->
<<>>.
%% @doc Remove spaces between HTML tags
-spec spaceless_tag(template_compiler:render_result(), map(), term()) -> template_compiler:render_result().
spaceless_tag(Value, _TplVars, _Context) ->
Contents1 = re:replace(iolist_to_binary(Value), "^[ \t\n\f\r]+<", "<"),
Contents2 = re:replace(Contents1, ">[ \t\n\f\r]+$", ">"),
re:replace(Contents2, ">[ \t\n\f\r]+<", "><", [global]).
%% @doc Convert a value to a boolean.
-spec to_bool(Value :: term(), Context :: term()) -> boolean().
to_bool(Value, _Context) ->
z_convert:to_bool_strict(Value).
%% @doc Convert a value to a list.
-spec to_list(Value :: term(), Context :: term()) -> list().
to_list(undefined, _Context) ->
[];
to_list(<<>>, _Context) ->
[];
to_list(Map, _Context) when is_map(Map) ->
maps:to_list(Map);
to_list({trans, Tr}, _Context) when is_list(Tr) ->
Tr;
to_list(Tuple, _Context) when is_tuple(Tuple) ->
tuple_to_list(Tuple);
to_list(B, _Context) when is_binary(B) ->
[B];
to_list(Value, _Context) ->
z_convert:to_list(Value).
%% @doc Convert a value to a more simpler value like binary, list, boolean.
-spec to_simple_value(Value :: term(), Context :: term()) -> term().
to_simple_value({trans, _} = Tr, Context) -> z_convert:to_binary(Tr, Context);
to_simple_value(T, _Context) -> T.
%% @doc Convert a value to an render_result, used for converting values in {{ ... }} expressions.
-spec to_render_result(Value::term(), TplVars::map(), Context::term()) -> template_compiler:render_result().
to_render_result(undefined, _TplVars, _Context) -> <<>>;
to_render_result(B, _TplVars, _Context) when is_binary(B) ->
B;
to_render_result(A, _TplVars, _Context) when is_atom(A) ->
atom_to_binary(A, 'utf8');
to_render_result(N, _TplVars, _Context) when is_integer(N) ->
integer_to_binary(N);
to_render_result(F, _TplVars, _Context) when is_float(F) ->
io_lib:format("~p", [F]);
to_render_result({{Y,M,D},{H,I,S}} = Date, TplVars, _Context)
when is_integer(Y), is_integer(M), is_integer(D),
is_integer(H), is_integer(I), is_integer(S) ->
Options = [
{tz, maps:get(tz, TplVars, "GMT")}
],
z_dateformat:format(Date, "Y-m-d H:i:s", Options);
to_render_result(T, _TplVars, _Context) when is_tuple(T) ->
io_lib:format("~p", [T]);
to_render_result(L, TplVars, Context) when is_list(L) ->
try
unicode:characters_to_binary(L)
catch
error:badarg ->
[ to_render_result(C, TplVars, Context) || C <- L ]
end.
%% @doc HTML escape a value
-spec escape(Value :: iodata() | undefined, Context :: term()) -> iodata().
escape(undefined, _Context) ->
<<>>;
escape(Value, _Context) ->
z_html:escape(iolist_to_binary(Value)).
%% @doc Called when compiling a module
-spec trace_compile(atom(), binary(), template_compiler:options(), term()) -> ok.
trace_compile(_Module, Filename, Options, _Context) ->
case proplists:get_value(trace_position, Options) of
{File, Line, _Col} ->
?LOG_DEBUG(#{
text => <<"Compiling template">>,
filename => Filename,
at => File,
line => Line
});
undefined ->
?LOG_DEBUG(#{
text => <<"Compiling template">>,
filename => Filename
})
end,
ok.
%% @doc Called when a template is rendered (could be from an include) - the return is
%% kept in a trace for displaying template extends recursion information.
-spec trace_render(binary(), template_compiler:options(), term()) -> ok | {ok, iodata(), iodata()}.
trace_render(Filename, Options, _Context) ->
case proplists:get_value(trace_position, Options) of
{File, Line, _Col} ->
?LOG_DEBUG(#{
text => <<"Template include">>,
filename => Filename,
at => File,
line => Line
});
undefined ->
?LOG_DEBUG(#{
text => <<"Template render">>,
filename => Filename
})
end,
ok.
%% @doc Called when a block function is called
-spec trace_block({binary(), integer(), integer()}, atom(), atom(), term()) -> ok | {ok, iodata(), iodata()}.
trace_block(_SrcPos, _Name, _Module, _Context) ->
ok.