Permalink
Fetching contributors…
Cannot retrieve contributors at this time
1703 lines (1543 sloc) 77.6 KB
%%%-------------------------------------------------------------------
%%% File: erlydtl_beam_compiler.erl
%%% @author Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
%%% @author Evan Miller <emmiller@gmail.com>
%%% @author Andreas Stenius <kaos@astekk.se>
%%% @copyright 2008 Roberto Saccon, Evan Miller
%%% @copyright 2014 Andreas Stenius
%%% @doc
%%% ErlyDTL template compiler for beam targets.
%%% @end
%%%
%%% The MIT License
%%%
%%% Copyright (c) 2007 Roberto Saccon, Evan Miller
%%% Copyright (c) 2014 Andreas Stenius
%%%
%%% Permission is hereby granted, free of charge, to any person obtaining a copy
%%% of this software and associated documentation files (the "Software"), to deal
%%% in the Software without restriction, including without limitation the rights
%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
%%% copies of the Software, and to permit persons to whom the Software is
%%% furnished to do so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included in
%%% all copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
%%% THE SOFTWARE.
%%%
%%% @since 2007-12-16 by Roberto Saccon, Evan Miller
%%% @since 2014 by Andreas Stenius
%%%-------------------------------------------------------------------
-module(erlydtl_beam_compiler).
-author('rsaccon@gmail.com').
-author('emmiller@gmail.com').
-author('Andreas Stenius <kaos@astekk.se>').
%% --------------------------------------------------------------------
%% Definitions
%% --------------------------------------------------------------------
-export([compile/3, compile_dir/2, format_error/1]).
%% internal use
-export([
is_up_to_date/2,
format/1,
value_ast/4,
interpret_args/2
]).
-import(erlydtl_compiler, [parse_file/2, do_parse_template/2]).
-import(erlydtl_compiler_utils,
[unescape_string_literal/1, full_path/2, push_scope/2,
restore_scope/2, begin_scope/1, begin_scope/2, end_scope/4,
empty_scope/0, get_current_file/1, add_errors/2,
add_warnings/2, merge_info/2, call_extension/3,
init_treewalker/1, resolve_variable/2, resolve_variable/3,
reset_block_dict/2, reset_parse_trail/2, load_library/3,
load_library/4, shorten_filename/2, push_auto_escape/2,
pop_auto_escape/1, token_pos/1, is_stripped_token_empty/1]).
-ifdef(MERL_DEP).
-include_lib("merl/include/merl.hrl").
-else.
-include_lib("syntax_tools/include/merl.hrl").
-endif.
-include("erlydtl_ext.hrl").
%% --------------------------------------------------------------------
%% API
%% --------------------------------------------------------------------
compile(DjangoParseTree, CheckSum, Context) ->
compile_to_binary(DjangoParseTree, CheckSum, Context).
compile_dir(Dir, Context) ->
do_compile_dir(Dir, Context).
format_error(no_out_dir) ->
"Compiled template not saved (need out_dir option)";
format_error(unexpected_extends_tag) ->
"The extends tag must be at the very top of the template";
format_error(circular_include) ->
"Circular file inclusion!";
format_error({write_file, Error}) ->
io_lib:format(
"Failed to write file: ~s",
[file:format_error(Error)]);
format_error(compile_beam) ->
"Failed to compile template to BEAM code";
format_error({unknown_filter, Name, Arity}) ->
io_lib:format("Unknown filter '~s' (arity ~b)", [Name, Arity]);
format_error({filter_args, Name, {Mod, Fun}, Arity}) ->
io_lib:format("Wrong number of arguments to filter '~s' (~s:~s): ~b", [Name, Mod, Fun, Arity]);
format_error({unknown_tag, Name}) ->
io_lib:format("Unknown tag '~s'", [Name]);
format_error({missing_tag, Name, {Mod, Fun}}) ->
io_lib:format("Custom tag '~s' not exported (~s:~s)", [Name, Mod, Fun]);
format_error({bad_tag, Name, {Mod, Fun}, Arity}) ->
io_lib:format("Invalid tag '~s' (~s:~s/~b)", [Name, Mod, Fun, Arity]);
format_error({load_code, Error}) ->
io_lib:format("Failed to load BEAM code: ~p", [Error]);
format_error({reserved_variable, ReservedName}) ->
io_lib:format("Variable '~s' is reserved for internal use.", [ReservedName]);
format_error({translation_fun, Fun}) ->
io_lib:format("Invalid translation function: ~s~n",
[if is_function(Fun) ->
Info = erlang:fun_info(Fun),
io_lib:format("~s:~s/~b", [proplists:get_value(K, Info) || K <- [module, name, arity]]);
true -> io_lib:format("~p", [Fun])
end]);
format_error(non_block_tag) ->
"Non-block tag in extends-template.";
format_error(Error) ->
erlydtl_compiler:format_error(Error).
%%====================================================================
%% Internal functions
%%====================================================================
do_compile_dir(Dir, Context) ->
%% Find all files in Dir (recursively), matching the regex (no
%% files ending in "~").
Files = filelib:fold_files(Dir, ".+[^~]$", true, fun(F1,Acc1) -> [F1 | Acc1] end, []),
{ParserResults,
#dtl_context{ errors=#error_info{ list=ParserErrors } }=Context1}
= lists:foldl(
fun (File, {ResultAcc, Ctx}) ->
case filename:basename(File) of
"."++_ ->
{ResultAcc, Ctx};
_ ->
FilePath = filename:absname(File),
case filelib:is_dir(FilePath) of
true ->
{ResultAcc, Ctx};
false ->
case parse_file(FilePath, Ctx) of
up_to_date -> {ResultAcc, Ctx};
{ok, DjangoParseTree, CheckSum} ->
{[{FilePath, DjangoParseTree, CheckSum}|ResultAcc], Ctx};
{error, Reason} -> {ResultAcc, ?ERR(Reason, Ctx)}
end
end
end
end,
{[], Context},
Files),
if length(ParserErrors) == 0 ->
compile_multiple_to_binary(Dir, ParserResults, Context1);
true -> Context1
end.
compile_multiple_to_binary(Dir, ParserResults, Context) ->
MatchAst = options_match_ast(Context),
{Functions,
{AstInfo,
#treewalker{
context=#dtl_context{
errors=#error_info{ list=Errors }
}=Context1 } }
} = lists:mapfoldl(
fun ({File, DjangoParseTree, CheckSum},
{AstInfo, #treewalker{ context=Ctx }=TreeWalker}) ->
try
FilePath = full_path(File, Ctx#dtl_context.doc_root),
{{BodyAst, BodyInfo}, TreeWalker1} = with_dependency(
{FilePath, CheckSum},
body_ast(DjangoParseTree, TreeWalker)),
FunctionName = filename:rootname(filename:basename(File)),
FunctionDefs = ?Q(["'@func'(Variables) -> _@func(Variables, []).",
"'@func'(_Variables, RenderOptions) ->",
" _@MatchAst, _@body."],
[{func, erl_syntax:atom(FunctionName)},
{body, stringify(BodyAst, Ctx)}]),
{{FunctionName, FunctionDefs}, {merge_info(AstInfo, BodyInfo), TreeWalker1}}
catch
throw:Error ->
{error, {AstInfo, TreeWalker#treewalker{ context=?ERR(Error, Ctx) }}}
end
end,
{#ast_info{}, init_treewalker(Context)},
ParserResults),
if length(Errors) == 0 ->
Forms = custom_forms(Dir, Context1#dtl_context.module, Functions, AstInfo),
compile_forms(Forms, Context1);
true ->
Context1
end.
compile_to_binary(DjangoParseTree, CheckSum, Context) ->
try body_ast(DjangoParseTree, init_treewalker(Context)) of
{{BodyAst, BodyInfo}, BodyTreeWalker} ->
try custom_tags_ast(BodyInfo#ast_info.custom_tags, BodyTreeWalker) of
{CustomTags,
#treewalker{
context=#dtl_context{
errors=#error_info{ list=Errors }
} }=CustomTagsTreeWalker}
when length(Errors) == 0 ->
Forms = forms(
{BodyAst, BodyInfo},
CustomTags, CheckSum,
CustomTagsTreeWalker),
compile_forms(Forms, CustomTagsTreeWalker#treewalker.context);
{_, #treewalker{ context=Context1 }} ->
Context1
catch
throw:Error -> ?ERR(Error, BodyTreeWalker#treewalker.context)
end
catch
throw:Error -> ?ERR(Error, Context)
end.
compile_forms(Forms, Context) ->
maybe_debug_template(Forms, Context),
Options = Context#dtl_context.compiler_options,
case compile:forms(Forms, [nowarn_shadow_vars|Options]) of
Compiled when element(1, Compiled) =:= ok ->
[ok, Module, Bin|Info] = tuple_to_list(Compiled),
lists:foldl(
fun (F, C) -> F(Module, Bin, C) end,
Context#dtl_context{ bin=Bin },
[fun maybe_write/3,
fun maybe_load/3,
fun (_, _, C) ->
case Info of
[Ws] when length(Ws) > 0 ->
add_warnings(Ws, C);
_ -> C
end
end
]);
error ->
?ERR(compile_beam, Context);
{error, Es, Ws} ->
add_warnings(Ws, add_errors(Es, Context))
end.
maybe_write(Module, Bin, Context) ->
case proplists:get_value(out_dir, Context#dtl_context.all_options) of
false -> Context;
undefined ->
?WARN(no_out_dir, Context);
OutDir ->
BeamFile = filename:join([OutDir, [Module, ".beam"]]),
?LOG_INFO("Template module: ~w -> ~s\n", [Module, BeamFile], Context),
case file:write_file(BeamFile, Bin) of
ok -> Context;
{error, Reason} ->
?ERR({write_file, Reason}, Context)
end
end.
maybe_load(Module, Bin, Context) ->
case proplists:get_bool(no_load, Context#dtl_context.all_options) of
true -> Context;
false -> load_code(Module, Bin, Context)
end.
load_code(Module, Bin, Context) ->
code:purge(Module),
case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of
{module, Module} -> Context;
Error -> ?WARN({load_code, Error}, Context)
end.
maybe_debug_template(Forms, Context) ->
case proplists:get_bool(debug_compiler, Context#dtl_context.all_options) of
false -> nop;
true ->
Options = Context#dtl_context.compiler_options,
?LOG_DEBUG("Compiler options: ~p~n", [Options], Context),
try
Source = erl_prettypr:format(
erl_syntax:form_list(Forms),
[{ribbon, 100}, {paper, 200}]),
SourceFile = lists:concat(
[proplists:get_value(source, Options),".erl"]),
File = case proplists:get_value(
debug_root,
Context#dtl_context.all_options) of
false -> undefined;
undefined -> SourceFile;
Dir ->
Abs = filename:absname(
shorten_filename(
SourceFile,
Context#dtl_context.doc_root),
Dir),
case filelib:ensure_dir(Abs) of
ok -> Abs;
{error, Reason} ->
io:format(
"Failed to ensure directories for file '~s': ~p~n",
[Abs, Reason]),
undefined
end
end,
if File =/= undefined ->
io:format("Saving template source to: ~s.. ~p~n",
[File, file:write_file(File, Source)]);
true -> ok
end
catch
error:Err ->
io:format("Pretty printing failed: ~p~n"
"Context: ~n~p~n"
"Forms: ~n~p~n",
[Err, Context, Forms])
end
end.
is_up_to_date(CheckSum, Context) ->
Module = Context#dtl_context.module,
{M, F} = Context#dtl_context.reader,
ReaderOptions = Context#dtl_context.reader_options,
case catch Module:source() of
{_, CheckSum} ->
case catch Module:dependencies() of
L when is_list(L) ->
RecompileList = lists:foldl(
fun ({XFile, XCheckSum}, Acc) ->
case catch erlydtl_runtime:read_file_internal(M, F, XFile, ReaderOptions) of
{ok, Data} ->
case binary_to_list(erlang:md5(Data)) of
XCheckSum ->
Acc;
_ ->
[recompile | Acc]
end;
_ ->
[recompile | Acc]
end
end, [], L),
case RecompileList of
[] -> true;
_ -> false
end;
_ ->
false
end;
_ ->
false
end.
%%====================================================================
%% AST functions
%%====================================================================
custom_tags_ast(CustomTags, TreeWalker) ->
%% avoid adding the render_tag/3 fun if it isn't used,
%% since we can't add a -compile({nowarn_unused_function, render_tag/3}).
%% attribute due to a bug in syntax_tools.
case custom_tags_clauses_ast(CustomTags, TreeWalker) of
skip ->
{{erl_syntax:comment(
["%% render_tag/3 is not used in this template."]),
#ast_info{}},
TreeWalker};
{{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} ->
{{erl_syntax:function(
erl_syntax:atom(render_tag),
CustomTagsClauses),
CustomTagsInfo},
TreeWalker1}
end.
custom_tags_clauses_ast([], _TreeWalker) -> skip;
custom_tags_clauses_ast(CustomTags, TreeWalker) ->
custom_tags_clauses_ast1(CustomTags, [], [], #ast_info{}, TreeWalker).
custom_tags_clauses_ast1([], _ExcludeTags, ClauseAcc, InfoAcc, TreeWalker) ->
{{DefaultAst, DefaultInfo}, TreeWalker1} =
case call_extension(TreeWalker, custom_tag_ast, [TreeWalker]) of
undefined ->
{{?Q("(_TagName, _, _) -> []"), InfoAcc}, TreeWalker};
{{ExtAst, ExtInfo}, ExtTreeWalker} ->
Clause = ?Q("(TagName, _Variables, RenderOptions) -> _@tag",
[{tag, options_match_ast(ExtTreeWalker) ++ [ExtAst]}]),
{{Clause, merge_info(ExtInfo, InfoAcc)}, ExtTreeWalker}
end,
{{lists:reverse([DefaultAst|ClauseAcc]), DefaultInfo}, TreeWalker1};
custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, TreeWalker) ->
case lists:member(Tag, ExcludeTags) of
true ->
custom_tags_clauses_ast1(CustomTags, ExcludeTags, ClauseAcc, InfoAcc, TreeWalker);
false ->
Context = TreeWalker#treewalker.context,
CustomTagFile = full_path(Tag, Context#dtl_context.custom_tags_dir),
case filelib:is_file(CustomTagFile) of
true ->
case parse_file(CustomTagFile, Context) of
{ok, DjangoParseTree, CheckSum} ->
{{BodyAst, BodyAstInfo}, TreeWalker1} = with_dependency(
{CustomTagFile, CheckSum},
body_ast(DjangoParseTree, TreeWalker)),
MatchAst = options_match_ast(TreeWalker1),
Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@MatchAst, _@BodyAst"),
custom_tags_clauses_ast1(
CustomTags, [Tag|ExcludeTags], [Clause|ClauseAcc],
merge_info(BodyAstInfo, InfoAcc), TreeWalker1);
{error, Reason} ->
empty_ast(?ERR(Reason, TreeWalker))
end;
false ->
case call_extension(TreeWalker, custom_tag_ast, [Tag, TreeWalker]) of
undefined ->
custom_tags_clauses_ast1(
CustomTags, [Tag | ExcludeTags],
ClauseAcc, InfoAcc,
?WARN({unknown_tag, Tag}, TreeWalker)
);
{{Ast, Info}, TW} ->
Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@match, _@Ast",
[{match, options_match_ast(TW)}]),
custom_tags_clauses_ast1(
CustomTags, [Tag | ExcludeTags], [Clause|ClauseAcc],
merge_info(Info, InfoAcc), TW)
end
end
end.
custom_forms(Dir, Module, Functions, AstInfo) ->
Dependencies = AstInfo#ast_info.dependencies,
TranslatableStrings = AstInfo#ast_info.translatable_strings,
TranslatedBlocks = AstInfo#ast_info.translated_blocks,
Variables = lists:usort(AstInfo#ast_info.var_names),
DefaultVariables = lists:usort(AstInfo#ast_info.def_names),
Constants = lists:usort(AstInfo#ast_info.const_names),
erl_syntax:revert_forms(
lists:flatten(
?Q(["-module('@Module@').",
"-export([source_dir/0, dependencies/0, translatable_strings/0,",
" translated_blocks/0, variables/0, default_variables/0,",
" constants/0, render/1, render/2, render/3]).",
"-export(['@__export_functions'/0]).",
"source_dir() -> _@Dir@.",
"dependencies() -> _@Dependencies@.",
"variables() -> _@Variables@.",
"default_variables() -> _@DefaultVariables@.",
"constants() -> _@Constants@.",
"translatable_strings() -> _@TranslatableStrings@.",
"translated_blocks() -> _@TranslatedBlocks@.",
"render(Tag) -> render(Tag, [], []).",
"render(Tag, Vars) -> render(Tag, Vars, []).",
"render(Tag, Vars, Opts) ->",
" try '@Module@':Tag(Vars, Opts) of",
" Val -> {ok, Val}",
" catch",
" Err -> {error, Err}",
" end.",
"'@_functions'() -> _."
],
[{export_functions,
erl_syntax:list(
[erl_syntax:arity_qualifier(erl_syntax:atom(FName), erl_syntax:integer(Arity))
|| {FName, _} <- Functions, Arity <- [1, 2]])},
{functions, [Ast || {_, Ast} <- Functions]}
]))
).
stringify(BodyAst, #dtl_context{ binary_strings=BinaryStrings }) ->
[?Q("erlydtl_runtime:stringify_final(_@BodyAst, '@BinaryStrings@')")].
forms({BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum,
#treewalker{
context=#dtl_context{
module=Module,
parse_trail=[File|_]
}=Context
}=TreeWalker) ->
MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
Dependencies = MergedInfo#ast_info.dependencies,
TranslatableStrings = MergedInfo#ast_info.translatable_strings,
TranslatedBlocks = MergedInfo#ast_info.translated_blocks,
Variables = lists:usort(MergedInfo#ast_info.var_names),
DefaultVariables = lists:usort(MergedInfo#ast_info.def_names),
Constants = lists:usort(MergedInfo#ast_info.const_names),
FinalBodyAst = options_match_ast(TreeWalker) ++ stringify(BodyAst, Context),
erl_syntax:revert_forms(
?Q(["-module('@Module@').",
"-export([render/0, render/1, render/2, source/0, dependencies/0,",
" translatable_strings/0, translated_blocks/0, variables/0,",
" default_variables/0, constants/0]).",
"source() -> {_@File@, _@CheckSum@}.",
"dependencies() -> _@Dependencies@.",
"variables() -> _@Variables@.",
"default_variables() -> _@DefaultVariables@.",
"constants() -> _@Constants@.",
"translatable_strings() -> _@TranslatableStrings@.",
"translated_blocks() -> _@TranslatedBlocks@.",
"'@_CustomTagsFunctionAst'() -> _.",
"render() -> render([], []).",
"render(Variables) -> render(Variables, []).",
"render(Variables, RenderOptions) ->",
" try render_internal(Variables, RenderOptions) of",
" Val -> {ok, Val}",
" catch",
" Err -> {error, Err}",
" end.",
"render_internal(_Variables, RenderOptions) -> _@FinalBodyAst."
])).
options_match_ast(#treewalker{ context=Context }=TreeWalker) ->
options_match_ast(Context, TreeWalker);
options_match_ast(Context) ->
options_match_ast(Context, undefined).
options_match_ast(Context, TreeWalker) ->
[
?Q(["_TranslationFun = erlydtl_runtime:init_translation(",
" proplists:get_value(translation_fun, RenderOptions, none))"]),
?Q("_CurrentLocale = proplists:get_value(locale, RenderOptions, default)"),
?Q("_RecordInfo = _@info", [{info, merl:term(Context#dtl_context.record_info)}])
| case call_extension(Context, setup_render_ast, [Context, TreeWalker]) of
undefined -> [];
Ast when is_list(Ast) -> Ast
end].
%% child templates should only consist of blocks at the top level
body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], #treewalker{ context=Context }=TreeWalker) ->
ThisFile = get_current_file(Context),
File = full_path(unescape_string_literal(String), Context#dtl_context.doc_root),
case lists:member(File, Context#dtl_context.parse_trail) of
true ->
empty_ast(?ERR(circular_include, TreeWalker));
_ ->
case parse_file(File, Context) of
{ok, ParentParseTree, CheckSum} ->
{BlockDict, Context1} = lists:foldl(
fun ({block, {identifier, Pos, Name}, Contents}, {Dict, Ctx}) ->
{dict:store(Name, [{ThisFile, Pos, Contents}], Dict), Ctx};
(Token, {Dict, Ctx}) ->
case proplists:get_bool(non_block_tag, Ctx#dtl_context.checks) of
true ->
case is_stripped_token_empty(Token) of
false ->
{Dict, ?WARN({token_pos(Token), non_block_tag}, Ctx)};
true ->
{Dict, Ctx}
end;
false ->
{Dict, Ctx}
end
end,
{dict:new(), Context},
ThisParseTree),
{Info, TreeWalker1} = with_dependency(
{File, CheckSum},
body_ast(
ParentParseTree,
TreeWalker#treewalker{
context=Context1#dtl_context{
block_dict = dict:merge(
fun(_Key, ParentVal, ChildVal) ->
ChildVal ++ ParentVal
end,
BlockDict, Context#dtl_context.block_dict),
parse_trail = [File | Context1#dtl_context.parse_trail]
}
})),
{Info, reset_parse_trail(
Context1#dtl_context.parse_trail,
reset_block_dict(
Context1#dtl_context.block_dict,
TreeWalker1))};
{error, Reason} ->
empty_ast(?ERR(Reason, TreeWalker))
end
end;
body_ast(DjangoParseTree, TreeWalker) ->
ScopeFun = fun ([ScopeVars|ScopeBody]) -> [?Q("(fun() -> _@ScopeVars, [_@ScopeBody] end)()")] end,
body_ast(DjangoParseTree, empty_scope(), ScopeFun, TreeWalker).
body_ast(DjangoParseTree, BodyScope, ScopeFun, TreeWalker) ->
{ScopeId, TreeWalkerScope} = begin_scope(BodyScope, TreeWalker),
BodyFun =
fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
{Info, BodyTW} = body_ast(Contents, push_auto_escape(OnOrOff, TW)),
{Info, pop_auto_escape(BodyTW)};
({'block', {identifier, _Pos, Name}, Contents}, #treewalker{ context=Context }=TW) ->
ContentsAst = body_ast(Contents, TW),
case dict:find(Name, Context#dtl_context.block_dict) of
{ok, ChildBlocks} ->
lists:foldr(
fun ({ChildFile, ChildPos, ChildBlock}, {{SuperAst, SuperInfo}, AccTW}) ->
BlockScope = create_scope(
[{block, ?Q("fun (super) -> _@SuperAst; (_) -> [] end"), safe}],
ChildPos, ChildFile, AccTW),
{{BlockAst, BlockInfo}, BlockTW} = body_ast(ChildBlock, BlockScope, ScopeFun, AccTW),
{{BlockAst, merge_info(SuperInfo, BlockInfo)}, BlockTW}
end,
ContentsAst, ChildBlocks);
_ ->
ContentsAst
end;
({'blocktrans', Args, Contents, PluralContents}, TW) ->
blocktrans_ast(Args, Contents, PluralContents, TW);
({'call', {identifier, _, Name}}, TW) ->
call_ast(Name, TW);
({'call', {identifier, _, Name}, With}, TW) ->
call_with_ast(Name, With, TW);
({'comment', _Contents}, TW) ->
empty_ast(TW);
({'comment_tag', _, _}, TW) ->
empty_ast(TW);
({'cycle', Names, AsVar}, TW) ->
cycle_ast(Names, AsVar, TW);
({'cycle_compat', Names}, TW) ->
cycle_compat_ast(Names, TW);
({'date', 'now', {string_literal, _Pos, FormatString}}, TW) ->
now_ast(FormatString, TW);
({'filter', FilterList, Contents}, TW) ->
filter_tag_ast(FilterList, Contents, TW);
({'firstof', Vars}, TW) ->
firstof_ast(Vars, TW);
({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TW) ->
{EmptyAstInfo, TW1} = empty_ast(TW),
for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TW) ->
{EmptyAstInfo, TW1} = body_ast(EmptyPartContents, TW),
for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
({'if', Expression, Contents, Elif}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElifAstInfo, TW2} = body_ast(Elif, TW1),
ifelse_ast(Expression, IfAstInfo, ElifAstInfo, TW2);
({'if', Expression, Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
({'ifchanged', '$undefined', Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, TW2);
({'ifchanged', Values, Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
({'ifchangedelse', '$undefined', IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, TW2);
({'ifchangedelse', Values, IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
({'ifelse', Expression, IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
({'ifequal', [Arg1, Arg2], Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents,TW1),
ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
({'ifnotequal', [Arg1, Arg2], Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
({'include', {string_literal, _, File}, Args}, #treewalker{ context=Context }=TW) ->
include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, TW);
({'include_only', {string_literal, _, File}, Args}, TW) ->
{Info, IncTW} = include_ast(unescape_string_literal(File), Args, [], TW),
{Info, restore_scope(TW, IncTW)};
({'load_libs', Libs}, TW) ->
load_libs_ast(Libs, TW);
({'load_from_lib', What, Lib}, TW) ->
load_from_lib_ast(What, Lib, TW);
({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}}, TW) ->
regroup_ast(ListVariable, Grouper, NewVariable, TW);
('end_regroup', TW) ->
{{end_scope, #ast_info{}}, TW};
({'spaceless', Contents}, TW) ->
spaceless_ast(Contents, TW);
({'ssi', Arg}, TW) ->
ssi_ast(Arg, TW);
({'ssi_parsed', {string_literal, _, FileName}}, #treewalker{ context=Context }=TW) ->
include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, TW);
({'string', _Pos, String}, TW) ->
string_ast(String, TW);
({'tag', Name, Args}, TW) ->
tag_ast(Name, Args, TW);
({'tag', Name, Args, {identifier, _, NewTagVar}}, TW) ->
tag_ast(Name, Args, NewTagVar, TW);
({'templatetag', {_, _, TagName}}, TW) ->
templatetag_ast(TagName, TW);
({'trans', Value}, TW) ->
translated_ast(Value, TW);
({'trans', Value, Context}, TW) ->
translated_ast(Value, Context, TW);
({'widthratio', Numerator, Denominator, Scale}, TW) ->
widthratio_ast(Numerator, Denominator, Scale, TW);
({'with', Args, Contents}, TW) ->
with_ast(Args, Contents, TW);
({'scope_as', {identifier, _, Name}, Contents}, TW) ->
scope_as(Name, Contents, TW);
({'extension', Tag}, TW) ->
extension_ast(Tag, TW);
({'extends', _}, TW) ->
empty_ast(?ERR(unexpected_extends_tag, TW));
({'language', Locale, Contents}, TW) ->
{{LocaleAst, LocaleInfo}, LocaleTW} = value_ast(Locale, true, false, TW),
LanguageScopeFun = fun ([ScopeVars|ScopeBody]) -> [?Q("(fun(_CurrentLocale) -> _@ScopeVars, [_@ScopeBody] end)(_@LocaleAst)")] end,
{{BodyAst, BodyInfo}, BodyTW} = body_ast(Contents, {[], [?Q("")]}, LanguageScopeFun, LocaleTW),
{{BodyAst, merge_info(BodyInfo, LocaleInfo)}, BodyTW};
(ValueToken, TW) ->
format(value_ast(ValueToken, true, true, TW))
end,
{AstInfoList, TreeWalker1} = lists:mapfoldl(BodyFun, TreeWalkerScope, DjangoParseTree),
{AstList, Info} =
lists:mapfoldl(
fun ({Ast, Info}, InfoAcc) ->
{Ast, merge_info(Info, InfoAcc)}
end, #ast_info{}, AstInfoList),
{Ast, TreeWalker2} = end_scope(ScopeFun, ScopeId, AstList, TreeWalker1),
{{erl_syntax:list(Ast), Info}, TreeWalker2}.
value_ast(ValueToken, AsString, EmptyIfUndefined, TreeWalker) ->
case ValueToken of
{'expr', Operator, Value} ->
{{ValueAst,InfoValue}, TreeWalker1} = value_ast(Value, false, EmptyIfUndefined, TreeWalker),
Op = list_to_atom(Operator),
Ast = ?Q("erlydtl_runtime:_@Op@(_@ValueAst)"),
{{Ast, InfoValue}, TreeWalker1};
{'expr', Operator, Value1, Value2} ->
{{Value1Ast,InfoValue1}, TreeWalker1} = value_ast(Value1, false, EmptyIfUndefined, TreeWalker),
{{Value2Ast,InfoValue2}, TreeWalker2} = value_ast(Value2, false, EmptyIfUndefined, TreeWalker1),
Op = list_to_atom(Operator),
Ast = ?Q("erlydtl_runtime:_@Op@(_@Value1Ast, _@Value2Ast)"),
{{Ast, merge_info(InfoValue1,InfoValue2)}, TreeWalker2};
{'string_literal', _Pos, String} ->
string_ast(unescape_string_literal(String), TreeWalker);
{'number_literal', _Pos, Number} ->
case AsString of
true -> string_ast(Number, TreeWalker);
false -> {{erl_syntax:integer(Number), #ast_info{}}, TreeWalker}
end;
{'apply_filter', Variable, Filter} ->
filter_ast(Variable, Filter, TreeWalker);
{'attribute', _} = Variable ->
resolve_variable_ast(Variable, EmptyIfUndefined, TreeWalker);
{'variable', _} = Variable ->
resolve_variable_ast(Variable, EmptyIfUndefined, TreeWalker);
{'trans', Value} ->
translated_ast(Value, TreeWalker);
{extension, Tag} ->
extension_ast(Tag, TreeWalker)
end.
extension_ast(Tag, TreeWalker) ->
case call_extension(TreeWalker, compile_ast, [Tag, TreeWalker]) of
undefined ->
empty_ast(?WARN({unknown_extension, Tag}, TreeWalker));
Result ->
Result
end.
with_dependencies([], Ast) -> Ast;
with_dependencies([Dependency | Rest], Ast) ->
with_dependencies(Rest, with_dependency(Dependency, Ast)).
with_dependency(FilePath, {{Ast, Info}, TreeWalker}) ->
Dependencies = [FilePath | Info#ast_info.dependencies],
{{Ast, Info#ast_info{ dependencies = Dependencies }}, TreeWalker}.
empty_ast(TreeWalker) ->
{{erl_syntax:list([]), #ast_info{}}, TreeWalker}.
%%% Note: Context here refers to the translation context, not the #dtl_context{} record
blocktrans_ast(Args, Contents, PluralContents, TreeWalker) ->
%% get args, count and context
ArgList = [{Name, Value}
|| {{identifier, _, Name}, Value}
<- proplists:get_value(args, Args, [])],
Count = proplists:get_value(count, Args),
Context = case proplists:get_value(context, Args) of
undefined -> undefined;
{string_literal, _, S} ->
unescape_string_literal(S)
end,
Trimmed = proplists:get_value(trimmed, Args),
%% add new scope using 'with' values
{NewScope, {ArgInfo, TreeWalker1}} =
lists:mapfoldl(
fun ({LocalVarName, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
{{Ast, Info}, TW} = value_ast(Value, false, false, TreeWalkerAcc),
{{LocalVarName, Ast}, {merge_info(AstInfoAcc, Info), TW}}
end,
{#ast_info{}, TreeWalker},
case Count of
{{identifier, _, Name}, Value} ->
[{Name, Value}|ArgList];
_ ->
ArgList
end),
TreeWalker2 = push_scope(NewScope, TreeWalker1),
%% key for translation lookup
SourceText = erlydtl_unparser:unparse(Contents, Trimmed),
{{DefaultAst, AstInfo}, TreeWalker3} = body_ast(Contents, TreeWalker2),
MergedInfo = merge_info(AstInfo, ArgInfo),
#dtl_context{
trans_fun = TFun,
trans_locales = TLocales, auto_escape = AutoEscape } = TreeWalker3#treewalker.context,
if TFun =:= none; PluralContents =/= undefined ->
%% translate in runtime
{FinalAst, FinalTW} = blocktrans_runtime_ast(
{DefaultAst, MergedInfo}, SourceText, Contents, Context, AutoEscape,
plural_contents(PluralContents, Count, TreeWalker3), Trimmed),
{FinalAst, restore_scope(TreeWalker1, FinalTW)};
is_function(TFun, 2) ->
%% translate in compile-time
{FinalAstInfo, FinalTreeWalker, Clauses} =
lists:foldr(
fun (Locale, {AstInfoAcc, TreeWalkerAcc, ClauseAcc}) ->
case TFun(SourceText, phrase_locale(Locale, Context)) of
default ->
{AstInfoAcc, TreeWalkerAcc, ClauseAcc};
Body ->
{ok, DjangoParseTree} = do_parse_template(Body, TreeWalkerAcc#treewalker.context),
{{BodyAst, BodyInfo}, BodyTreeWalker} = body_ast(DjangoParseTree, TreeWalkerAcc),
{merge_info(BodyInfo, AstInfoAcc), BodyTreeWalker,
[?Q("_@Locale@ -> _@BodyAst")|ClauseAcc]}
end
end,
{MergedInfo, TreeWalker3, []}, TLocales),
FinalAst = ?Q("case _CurrentLocale of _@_Clauses -> _; _ -> _@DefaultAst end"),
{{FinalAst, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }},
restore_scope(TreeWalker1, FinalTreeWalker)};
true ->
empty_ast(?ERR({translation_fun, TFun}, TreeWalker3))
end.
blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, AutoEscape, {Plural, TreeWalker}, Trimmed) ->
%% Contents is flat - only strings and '{{var}}' allowed.
%% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
{variable, {identifier, _, B}}) ->
A =< B
end, [Var || {variable, _}=Var
<- Contents ++ maybe_plural_contents(Plural)]),
VarBuilder = fun({variable, {identifier, _, Name}}=Var, TW) ->
{{VarAst, _VarInfo}, VarTW} = resolve_variable_ast(Var, false, TW),
{?Q("{_@name, _@VarAst}", [{name, merl:term(atom_to_list(Name))}]), VarTW}
end,
{VarAsts, TreeWalker1} = lists:mapfoldl(VarBuilder, TreeWalker, USortedVariables),
VarListAst = erl_syntax:list(VarAsts),
BlockTransAst = ?Q(["begin",
" case erlydtl_runtime:translate_block(",
" _@phrase, _@locale, _@auto_escape, ",
" _@VarListAst, _TranslationFun) of",
" default -> _@DefaultAst;",
" Text -> Text",
" end",
"end"],
[{phrase, phrase_ast(SourceText, Plural, Trimmed)},
{auto_escape, autoescape_ast(AutoEscape)},
{locale, phrase_locale_ast(Context)}]),
{{BlockTransAst, merge_count_info(Info, Plural)}, TreeWalker1}.
maybe_plural_contents(undefined) -> [];
maybe_plural_contents({Contents, _}) -> Contents.
merge_count_info(Info, undefined) -> Info;
merge_count_info(Info, {_Contents, {_CountAst, CountInfo}}) ->
merge_info(Info, CountInfo).
plural_contents(undefined, _, TreeWalker) -> {undefined, TreeWalker};
plural_contents(Contents, {_CountVarName, Value}, TreeWalker) ->
{CountAst, TW} = value_ast(Value, false, false, TreeWalker),
{{Contents, CountAst}, TW}.
phrase_ast(Text, undefined, _) -> merl:term(Text);
phrase_ast(Text, {Contents, {CountAst, _CountInfo}}, Trimmed) ->
erl_syntax:tuple(
[merl:term(Text),
erl_syntax:tuple(
[merl:term(erlydtl_unparser:unparse(Contents, Trimmed)),
CountAst])
]).
autoescape_ast([]) -> autoescape_ast([on]);
autoescape_ast([V | _]) -> erl_syntax:atom(V == on).
phrase_locale_ast(undefined) -> merl:var('_CurrentLocale');
phrase_locale_ast(Context) -> erl_syntax:tuple([merl:var('_CurrentLocale'), merl:term(Context)]).
phrase_locale(Locale, undefined) -> Locale;
phrase_locale(Locale, Context) -> {Locale, Context}.
translated_ast(Text, TreeWalker) ->
translated_ast(Text, undefined, TreeWalker).
translated_ast({noop, Value}, _, TreeWalker) ->
value_ast(Value, true, true, TreeWalker);
translated_ast(Text, {string_literal, _, Context}, TreeWalker) ->
translated_ast(Text, unescape_string_literal(Context), TreeWalker);
translated_ast({string_literal, _, String}, Context, TreeWalker) ->
Text = unescape_string_literal(String),
case call_extension(TreeWalker, translate_ast, [Text, Context, TreeWalker]) of
undefined ->
case TreeWalker#treewalker.context#dtl_context.trans_fun of
none -> runtime_trans_ast(Text, Context, TreeWalker);
Fun when is_function(Fun, 2) ->
compiletime_trans_ast(Fun, Text, Context, TreeWalker);
Fun when is_function(Fun, 1) ->
compiletime_trans_ast(fun (T, _) -> Fun(T) end,
Text, Context, TreeWalker);
Fun ->
empty_ast(?ERR({translation_fun, Fun}, TreeWalker))
end;
TranslatedAst ->
TranslatedAst
end;
translated_ast(Value, Context, TreeWalker) ->
runtime_trans_ast(value_ast(Value, true, false, TreeWalker), Context).
runtime_trans_ast(Text, Context, TreeWalker) ->
Info = #ast_info{ translatable_strings = [Text] },
runtime_trans_ast({{merl:term(Text), Info}, TreeWalker}, Context).
runtime_trans_ast({{ValueAst, AstInfo}, TreeWalker}, undefined) ->
{{?Q("erlydtl_runtime:translate(_@ValueAst, _CurrentLocale, _TranslationFun)"),
AstInfo}, TreeWalker};
runtime_trans_ast({{ValueAst, AstInfo}, TreeWalker}, Context) ->
{{?Q("erlydtl_runtime:translate(_@ValueAst, {_CurrentLocale, _@Context@}, _TranslationFun)"),
AstInfo}, TreeWalker}.
compiletime_trans_ast(TFun, Text, LContext,
#treewalker{
context=#dtl_context{
trans_locales=TLocales
}=Context
}=TreeWalker) ->
ClAst = lists:foldl(
fun(Locale, ClausesAcc) ->
[?Q("_@Locale@ -> _@translated",
[{translated, case TFun(Text, phrase_locale(Locale, LContext)) of
default -> string_ast(Text, Context);
Translated -> string_ast(Translated, Context)
end}])
|ClausesAcc]
end,
[], TLocales),
{{?Q(["case _CurrentLocale of",
" _@_ClAst -> _;",
" _ -> _@string",
"end"],
[{string, string_ast(Text, Context)}]),
#ast_info{ translatable_strings = [Text] }},
TreeWalker}.
%%% end of context being translation context
%% Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility.
templatetag_ast("openblock", TreeWalker) ->
string_ast("{%", TreeWalker);
templatetag_ast("closeblock", TreeWalker) ->
string_ast("%}", TreeWalker);
templatetag_ast("openvariable", TreeWalker) ->
string_ast("{{", TreeWalker);
templatetag_ast("closevariable", TreeWalker) ->
string_ast("}}", TreeWalker);
templatetag_ast("openbrace", TreeWalker) ->
string_ast("{", TreeWalker);
templatetag_ast("closebrace", TreeWalker) ->
string_ast("}", TreeWalker);
templatetag_ast("opencomment", TreeWalker) ->
string_ast("{#", TreeWalker);
templatetag_ast("closecomment", TreeWalker) ->
string_ast("#}", TreeWalker).
widthratio_ast(Numerator, Denominator, Scale, TreeWalker) ->
{{NumAst, NumInfo}, TreeWalker1} = value_ast(Numerator, false, true, TreeWalker),
{{DenAst, DenInfo}, TreeWalker2} = value_ast(Denominator, false, true, TreeWalker1),
{{ScaleAst, ScaleInfo}, TreeWalker3} = value_ast(Scale, false, true, TreeWalker2),
{{format_number_ast(?Q("erlydtl_runtime:widthratio(_@NumAst, _@DenAst, _@ScaleAst)")),
merge_info(ScaleInfo, merge_info(NumInfo, DenInfo))},
TreeWalker3}.
string_ast(Arg, #treewalker{ context=Context }=TreeWalker) ->
{{string_ast(Arg, Context), #ast_info{}}, TreeWalker};
string_ast(Arg, Context) ->
merl:term(erlydtl_compiler_utils:to_string(Arg, Context)).
include_ast(File, ArgList, Scopes, #treewalker{ context=Context }=TreeWalker) ->
FilePath = full_path(File, Context#dtl_context.doc_root),
?LOG_TRACE("include file: ~s~n", [FilePath], Context),
case parse_file(FilePath, Context) of
{ok, InclusionParseTree, CheckSum} ->
{NewScope, {ArgInfo, TreeWalker1}}
= lists:mapfoldl(
fun ({{identifier, _, LocalVarName}, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
{{Ast, Info}, TW} = value_ast(Value, false, false, TreeWalkerAcc),
{{LocalVarName, Ast}, {merge_info(AstInfoAcc, Info), TW}}
end, {#ast_info{}, TreeWalker}, ArgList),
C = TreeWalker1#treewalker.context,
{{BodyAst, BodyInfo}, TreeWalker2} = with_dependency(
{FilePath, CheckSum},
body_ast(
InclusionParseTree,
TreeWalker1#treewalker{
context=C#dtl_context{
parse_trail = [FilePath | C#dtl_context.parse_trail],
local_scopes = [NewScope|Scopes]
}
})),
{{BodyAst, merge_info(BodyInfo, ArgInfo)},
reset_parse_trail(C#dtl_context.parse_trail, TreeWalker2)};
{error, Reason} ->
empty_ast(?ERR(Reason, TreeWalker))
end.
%% include at run-time
ssi_ast(FileName, #treewalker{
context=#dtl_context{
reader = {Mod, Fun},
reader_options = ReaderOptions,
doc_root = Dir
}
}=TreeWalker) ->
{{FileAst, Info}, TreeWalker1} = value_ast(FileName, true, true, TreeWalker),
{{?Q("erlydtl_runtime:read_file(_@Mod@, _@Fun@, _@Dir@, _@FileAst, _@ReaderOptions@)"), Info}, TreeWalker1}.
filter_tag_ast(FilterList, Contents, TreeWalker) ->
{{InnerAst, Info}, TreeWalker1} = body_ast(Contents, push_auto_escape(did, TreeWalker)),
{{FilteredAst, FilteredInfo}, TreeWalker2} =
lists:foldl(
fun ({{identifier, _, Name}, []}, {{AstAcc, InfoAcc}, TreeWalkerAcc})
when Name =:= 'escape'; Name =:= 'safe'; Name =:= 'safeseq' ->
{{AstAcc, InfoAcc}, TreeWalkerAcc#treewalker{ safe = true }};
(Filter, {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
{{Ast, AstInfo}, TW} = filter_ast1(Filter, AstAcc, TreeWalkerAcc),
{{Ast, merge_info(InfoAcc, AstInfo)}, TW}
end,
{{?Q("erlang:iolist_to_binary(_@InnerAst)"), Info},
pop_auto_escape(TreeWalker1)},
FilterList),
EscapedAst = case search_for_escape_filter(
lists:reverse(FilterList),
TreeWalker2#treewalker.context) of
on -> ?Q("erlydtl_filters:force_escape(_@FilteredAst)");
_ -> FilteredAst
end,
{{EscapedAst, FilteredInfo}, TreeWalker2}.
search_for_escape_filter(FilterList, #dtl_context{auto_escape = [on|_]}) ->
search_for_safe_filter(FilterList);
search_for_escape_filter(_, #dtl_context{auto_escape = [did|_]}) -> off;
search_for_escape_filter([{{identifier, _, 'escape'}, []}|Rest], _Context) ->
search_for_safe_filter(Rest);
search_for_escape_filter([_|Rest], Context) ->
search_for_escape_filter(Rest, Context);
search_for_escape_filter([], _Context) -> off.
search_for_safe_filter([{{identifier, _, Name}, []}|_])
when Name =:= 'safe'; Name =:= 'safeseq' -> off;
search_for_safe_filter([_|Rest]) -> search_for_safe_filter(Rest);
search_for_safe_filter([]) -> on.
filter_ast(Variable, Filter, TreeWalker) ->
%% the escape filter is special; it is always applied last, so we have to go digging for it
%% AutoEscape = 'did' means we (will have) decided whether to escape the current variable,
%% so don't do any more escaping
{{UnescapedAst, Info}, TreeWalker1} =
filter_ast_noescape(
Variable, Filter,
push_auto_escape(did, TreeWalker)),
{EscapedAst, TreeWalker2} =
case search_for_escape_filter(Variable, Filter, TreeWalker#treewalker.context) of
on -> {?Q("erlydtl_filters:force_escape(_@UnescapedAst)"),
TreeWalker1#treewalker{ safe = true }};
_ -> {UnescapedAst, TreeWalker1}
end,
{{EscapedAst, Info}, pop_auto_escape(TreeWalker2)}.
filter_ast_noescape(Variable, {{identifier, _, Name}, []}, TreeWalker)
when Name =:= 'escape'; Name =:= 'safe'; Name =:= 'safeseq' ->
value_ast(Variable, true, false, TreeWalker#treewalker{safe = true});
filter_ast_noescape(Variable, Filter, TreeWalker) ->
{{ValueAst, Info1}, TreeWalker2} = value_ast(Variable, true, false, TreeWalker),
{{VarValue, Info2}, TreeWalker3} = filter_ast1(Filter, ValueAst, TreeWalker2),
{{VarValue, merge_info(Info1, Info2)}, TreeWalker3}.
filter_ast1({{identifier, Pos, Name}, Args}, ValueAst, TreeWalker) ->
{{ArgsAst, ArgsInfo}, TreeWalker1} =
lists:foldr(
fun (Arg, {{AccAst, AccInfo}, AccTreeWalker}) ->
{{ArgAst, ArgInfo}, ArgTreeWalker} = value_ast(Arg, false, false, AccTreeWalker),
{{[ArgAst|AccAst], merge_info(ArgInfo, AccInfo)}, ArgTreeWalker}
end,
{{[], #ast_info{}}, TreeWalker},
Args),
case filter_ast2(Name, [ValueAst|ArgsAst], TreeWalker1#treewalker.context) of
{ok, FilterAst} ->
{{FilterAst, ArgsInfo}, TreeWalker1};
Error ->
empty_ast(?WARN({Pos, Error}, TreeWalker1))
end.
% special case for date, which reqires localisation
% may be replaced later with a query to a list
% of functions which require translation
filter_ast2('date' = Name, Args, #dtl_context{ filters = Filters } = Ctx) ->
case proplists:get_value(Name, Filters) of
{Mod, Fun} ->
case erlang:function_exported(Mod, Fun, length(Args) + 2) of
true -> {ok, ?Q("'@Mod@':'@Fun@'(_@Args, _TranslationFun, _CurrentLocale )")};
false -> filter_ast3(Name, Args, Ctx) % redefined 'date'?
end;
% should never happen
undefined -> {unknown_filter, Name, length(Args)}
end;
filter_ast2(Name, Args, Ctx) ->
filter_ast3(Name, Args, Ctx).
filter_ast3(Name, Args, #dtl_context{ filters = Filters }) ->
case proplists:get_value(Name, Filters) of
{Mod, Fun}=Filter ->
case erlang:function_exported(Mod, Fun, length(Args)) of
true -> {ok, ?Q("'@Mod@':'@Fun@'(_@Args)")};
false -> {filter_args, Name, Filter, length(Args)}
end;
undefined ->
{unknown_filter, Name, length(Args)}
end.
search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = [on|_]}) ->
search_for_safe_filter(Variable, Filter);
search_for_escape_filter(_, _, #dtl_context{auto_escape = [did|_]}) ->
off;
search_for_escape_filter(Variable, {{identifier, _, 'escape'}, []} = Filter, _Context) ->
search_for_safe_filter(Variable, Filter);
search_for_escape_filter({apply_filter, Variable, Filter}, _, Context) ->
search_for_escape_filter(Variable, Filter, Context);
search_for_escape_filter(_Variable, _Filter, _Context) ->
off.
search_for_safe_filter(_, {{identifier, _, 'safe'}, []}) ->
off;
search_for_safe_filter(_, {{identifier, _, 'safeseq'}, []}) ->
off;
search_for_safe_filter({apply_filter, Variable, Filter}, _) ->
search_for_safe_filter(Variable, Filter);
search_for_safe_filter(_Variable, _Filter) ->
on.
finder_function(true) -> {erlydtl_runtime, fetch_value};
finder_function(false) -> {erlydtl_runtime, find_value}.
finder_function(EmptyIfUndefined, TreeWalker) ->
case call_extension(TreeWalker, finder_function, [EmptyIfUndefined]) of
undefined -> finder_function(EmptyIfUndefined);
Result -> Result
end.
resolve_variable_ast({extension, Tag}, _, TreeWalker) ->
extension_ast(Tag, TreeWalker);
resolve_variable_ast(VarTuple, EmptyIfUndefined, TreeWalker)
when is_boolean(EmptyIfUndefined) ->
resolve_variable_ast(VarTuple, finder_function(EmptyIfUndefined, TreeWalker), TreeWalker);
resolve_variable_ast(VarTuple, FinderFunction, TreeWalker) ->
resolve_variable_ast1(VarTuple, FinderFunction, TreeWalker).
resolve_variable_ast1({attribute, {{_, Pos, Attr}, Variable}}, {Runtime, Finder}=FinderFunction, TreeWalker) ->
{{VarAst, VarInfo}, TreeWalker1} = resolve_variable_ast(Variable, FinderFunction, TreeWalker),
#treewalker{
context=#dtl_context{
lists_0_based = Lists0Based,
tuples_0_based = Tuples0Based
}
} = TreeWalker,
FileName = get_current_file(TreeWalker1),
{{?Q(["'@Runtime@':'@Finder@'(",
" _@Attr@, _@VarAst,",
" [{lists_0_based, _@Lists0Based@},",
" {tuples_0_based, _@Tuples0Based@},",
" {render_options, RenderOptions},",
" {record_info, _RecordInfo},",
" {filename, _@FileName@},",
" {pos, _@Pos@}",
" ])"]),
VarInfo},
TreeWalker1};
resolve_variable_ast1({variable, {identifier, Pos, VarName}}, {Runtime, Finder}, TreeWalker) ->
{Source, Value, Filters} = resolve_variable(VarName, TreeWalker),
Ast = case {Source, Value} of
{_, undefined} ->
FileName = get_current_file(TreeWalker),
{?Q(["'@Runtime@':'@Finder@'(",
" _@VarName@, _Variables,",
" [{filename, _@FileName@},",
" {pos, _@Pos@},",
" {record_info, _RecordInfo},",
" {render_options, RenderOptions}])"
]),
#ast_info{ var_names=[VarName] }};
{default_vars, Val} ->
FileName = get_current_file(TreeWalker),
{?Q(["'@Runtime@':fetch_value(",
" _@VarName@, _Variables,",
" [{filename, _@FileName@},",
" {pos, _@Pos@},",
" {record_info, _RecordInfo},",
" {render_options, RenderOptions}],",
" _@val)"
],
[{val, merl:term(erlydtl_filters:format_number(Val))}]),
#ast_info{ var_names=[VarName], def_names=[VarName] }};
{constant, Val} ->
{merl:term(erlydtl_filters:format_number(Val)),
#ast_info{ const_names=[VarName] }};
{scope, Val} ->
{Val, #ast_info{}}
end,
lists:foldr(
fun ({escape, []}, {{AccAst, AccInfo}, TW}) ->
{{?Q("erlydtl_filters:force_escape(_@AccAst)"), AccInfo}, TW#treewalker{ safe = true }};
({Safe, []}, {Acc, TW}) when Safe == safe; Safe == safeseq ->
{Acc, TW#treewalker{ safe = true }};
({Filter, Args}, {{AccAst, AccInfo}, TW})
when is_atom(Filter), is_list(Args) ->
case filter_ast2(Filter, [AccAst|Args], TW#treewalker.context) of
{ok, FilteredAst} ->
{{FilteredAst, AccInfo}, TW};
Error ->
empty_ast(?WARN({Pos, Error}, TW))
end
end,
{Ast, TreeWalker},
Filters
).
resolve_reserved_variable(ReservedName, TreeWalker) ->
resolve_reserved_variable(ReservedName, merl:term(undefined), TreeWalker).
resolve_reserved_variable(ReservedName, Default, TreeWalker) ->
case resolve_variable(ReservedName, Default, TreeWalker) of
{Src, Value, []} when Src =:= scope; Value =:= Default ->
{Value, TreeWalker};
_ ->
{Default, ?ERR({reserved_variable, ReservedName}, TreeWalker)}
end.
format({{Ast, Info}, TreeWalker}) ->
auto_escape({{format_number_ast(Ast), Info}, TreeWalker}).
format_number_ast(Ast) ->
?Q("erlydtl_filters:format_number(_@Ast)").
auto_escape({AstInfo, #treewalker{ safe = true }=TW}) ->
{AstInfo, TW#treewalker{ safe = false }};
auto_escape({{Value, Info}, #treewalker{ context=#dtl_context{auto_escape=[on|_]} }=TW}) ->
{{?Q("erlydtl_filters:force_escape(_@Value)"), Info}, TW};
auto_escape(Value) -> Value.
firstof_ast(Vars, TreeWalker) ->
body_ast(
[lists:foldr(
fun ({L, _, _}=Var, [])
when L=:=string_literal;L=:=number_literal ->
Var;
({L, _, _}, _)
when L=:=string_literal;L=:=number_literal ->
erlang:error(errbadliteral);
(Var, []) ->
{'ifelse', Var, [Var], []};
(Var, Acc) ->
{'ifelse', Var, [Var], [Acc]}
end,
[], Vars)
],
TreeWalker).
ifelse_ast(Expression, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
Info = merge_info(IfContentsInfo, ElseContentsInfo),
{{Ast, ExpressionInfo}, TreeWalker1} = value_ast(Expression, false, false, TreeWalker),
{{?Q(["case erlydtl_runtime:is_true(_@Ast) of",
" true -> _@IfContentsAst;",
" _ -> _@ElseContentsAst",
"end"]),
merge_info(ExpressionInfo, Info)},
TreeWalker1}.
with_ast(ArgList, Contents, TreeWalker) ->
{ArgAstList, {ArgInfo, TreeWalker1}} =
lists:mapfoldl(
fun ({{identifier, _, _LocalVarName}, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
{{Ast, Info}, TW} = value_ast(Value, false, false, TreeWalkerAcc),
{Ast, {merge_info(AstInfoAcc, Info), TW}}
end,
{#ast_info{}, TreeWalker},
ArgList),
NewScope = lists:map(
fun ({{identifier, _, LocalVarName}, _Value}) ->
{LocalVarName, varname_ast(LocalVarName)}
end,
ArgList),
{{InnerAst, InnerInfo}, TreeWalker2} =
body_ast(
Contents,
push_scope(NewScope, TreeWalker1)),
{{?Q("fun (_@args) -> _@InnerAst end (_@ArgAstList)",
[{args, element(2, lists:unzip(NewScope))}]),
merge_info(ArgInfo, InnerInfo)},
restore_scope(TreeWalker1, TreeWalker2)}.
scope_as(VarName, Contents, TreeWalker) ->
{{ContentsAst, ContentsInfo}, TreeWalker1} = body_ast(Contents, TreeWalker),
VarAst = varname_ast(VarName),
{Id, TreeWalker2} = begin_scope(
{[{VarName, VarAst}],
[?Q("_@VarAst = _@ContentsAst")]},
TreeWalker1),
{{Id, ContentsInfo}, TreeWalker2}.
regroup_ast(ListVariable, GrouperVariable, LocalVarName, TreeWalker) ->
{{ListAst, ListInfo}, TreeWalker1} = value_ast(ListVariable, false, true, TreeWalker),
LocalVarAst = varname_ast(LocalVarName),
{Id, TreeWalker2} = begin_scope(
{[{LocalVarName, LocalVarAst}],
[?Q(["_@LocalVarAst = erlydtl_runtime:regroup(",
" _@ListAst, _@regroup,",
" [{record_info, _RecordInfo}]",
")"],
[{regroup, regroup_filter(GrouperVariable, [])}])
]},
TreeWalker1),
{{Id, ListInfo}, TreeWalker2}.
regroup_filter({attribute,{{identifier,_,Ident},Next}},Acc) ->
regroup_filter(Next,[erl_syntax:atom(Ident)|Acc]);
regroup_filter({variable,{identifier,_,Var}},Acc) ->
erl_syntax:list([erl_syntax:atom(Var)|Acc]).
to_list_ast(Value, IsReversed) ->
?Q("erlydtl_runtime:to_list(_@Value, _@IsReversed)").
to_list_ast(Value, IsReversed, TreeWalker) ->
case call_extension(TreeWalker, to_list_ast, [Value, IsReversed, TreeWalker]) of
undefined -> to_list_ast(Value, IsReversed);
Result -> Result
end.
for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
{EmptyContentsAst, EmptyContentsInfo},
#treewalker{ context=Context }=TreeWalker) ->
%% create unique namespace for this instance
Level = length(Context#dtl_context.local_scopes),
{Row, Col} = element(2, hd(IteratorList)),
ForId = lists:concat(["/", Level, "_", Row, ":", Col]),
Counters = merl:var(lists:concat(["Counters", ForId])),
Vars = merl:var(lists:concat(["Vars", ForId])),
%% setup
VarScope = lists:map(
fun({identifier, {R, C}, Iterator}) ->
{Iterator, varname_ast(lists:concat([
Iterator,"/", Level, "_", R, ":", C]))}
end, IteratorList),
{Iterators, IteratorVars} = lists:unzip(VarScope),
IteratorCount = length(IteratorVars),
{{LoopBodyAst, Info}, TreeWalker1} =
body_ast(
Contents,
push_scope([{'forloop', Counters} | VarScope],
TreeWalker)),
{{LoopValueAst, LoopValueInfo}, TreeWalker2} =
value_ast(LoopValue, false, true, restore_scope(TreeWalker, TreeWalker1)),
LoopValueAst0 = to_list_ast(LoopValueAst, merl:term(IsReversed), TreeWalker2),
{ParentLoop, TreeWalker3} = resolve_reserved_variable('forloop', TreeWalker2),
{{?Q(["erlydtl_runtime:forloop(",
" fun (_@Vars, _@Counters) ->",
" {_@IteratorVars} = if is_tuple(_@Vars), size(_@Vars) == _@IteratorCount@ -> _@Vars;",
" _@___ifclauses -> _",
" end,",
" {_@LoopBodyAst, erlydtl_runtime:increment_counter_stats(_@Counters)}",
" end,",
" _@LoopValueAst0, _@ParentLoop, _@EmptyContentsAst)"],
[{ifclauses, if IteratorCount > 1 ->
?Q(["() when is_list(_@Vars), length(_@Vars) == _@IteratorCount@ ->",
" list_to_tuple(_@Vars);",
"() when true -> throw({for_loop, _@Iterators@, _@Vars})"]);
true ->
?Q("() when true -> {_@Vars}")
end}]),
merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)},
TreeWalker3}.
ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
Info = merge_info(IfContentsInfo, ElseContentsInfo),
ValueAstFun = fun (Expr, {LTreeWalker, LInfo, Acc}) ->
{{EAst, EInfo}, ETw} = value_ast(Expr, false, true, LTreeWalker),
{ETw, merge_info(LInfo, EInfo),
[?Q("{_@hash, _@EAst}",
[{hash, merl:term(erlang:phash2(Expr))}])
|Acc]}
end,
{TreeWalker1, MergedInfo, Changed} = lists:foldl(ValueAstFun, {TreeWalker, Info, []}, Values),
{{?Q(["case erlydtl_runtime:ifchanged([_@Changed]) of",
" true -> _@IfContentsAst;",
" _ -> _@ElseContentsAst",
"end"]),
MergedInfo},
TreeWalker1}.
ifchanged_contents_ast(Contents, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
{{?Q(["case erlydtl_runtime:ifchanged([{_@hash, _@IfContentsAst}]) of",
" true -> _@IfContentsAst;",
" _ -> _@ElseContentsAst",
"end"],
[{hash, merl:term(erlang:phash2(Contents))}]),
merge_info(IfContentsInfo, ElseContentsInfo)},
TreeWalker}.
cycle_ast(Names, undefined, #treewalker{ context=Context }=TreeWalker) ->
{NamesTuple, VarNames}
= lists:mapfoldl(
fun ({string_literal, _, Str}, VarNamesAcc) ->
S = string_ast(unescape_string_literal(Str), Context),
{S, VarNamesAcc};
({variable, _}=Var, VarNamesAcc) ->
{{V, #ast_info{ var_names=[VarName] }}, _} = resolve_variable_ast(Var, true, TreeWalker),
{V, [VarName|VarNamesAcc]};
({number_literal, _, Num}, VarNamesAcc) ->
{format_number_ast(erl_syntax:integer(Num)), VarNamesAcc};
(_, VarNamesAcc) ->
{[], VarNamesAcc}
end, [], Names),
{ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
{{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
#ast_info{ var_names = VarNames }},
TreeWalker1};
cycle_ast(Names, [{identifier, _, VarName}|Opts], TreeWalker) ->
{{VarAst, AstInfo}, TW1} = cycle_ast(Names, undefined, TreeWalker),
VarNameAst = varname_ast(VarName),
{Scope, TW2} = begin_scope(
{[{VarName, VarNameAst}],
[?Q("_@VarNameAst = _@VarAst")
| case Opts of
[silent] -> [];
[] -> [VarAst]
end
]},
TW1),
{{Scope, AstInfo}, TW2}.
%% Older Django templates treat cycle with comma-delimited elements as strings
cycle_compat_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
NamesTuple = lists:map(
fun ({identifier, _, X}) ->
string_ast(X, Context)
end, Names),
{ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
{{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
#ast_info{}},
TreeWalker1}.
now_ast(FormatString, TreeWalker) ->
%% Note: we can't use unescape_string_literal here
%% because we want to allow escaping in the format string.
%% We only want to remove the surrounding escapes,
%% i.e. \"foo\" becomes "foo"
UnescapeOuter = string:strip(FormatString, both, 34),
{{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, TreeWalker),
{{?Q("erlydtl_dateformat:format(_@StringAst, _TranslationFun, _CurrentLocale)"), Info}, TreeWalker1}.
spaceless_ast(Contents, TreeWalker) ->
{{Ast, Info}, TreeWalker1} = body_ast(Contents, TreeWalker),
{{?Q("erlydtl_runtime:spaceless(_@Ast)"), Info}, TreeWalker1}.
load_libs_ast(Libs, TreeWalker) ->
TreeWalker1 = lists:foldl(
fun ({identifier, Pos, Lib}, TW) ->
load_library(Pos, Lib, TW)
end,
TreeWalker, Libs),
empty_ast(TreeWalker1).
load_from_lib_ast(What, {identifier, Pos, Lib}, TreeWalker) ->
Names = lists:foldl(
fun ({identifier, _, Name}, Acc) -> [Name|Acc] end,
[], What),
empty_ast(load_library(Pos, Lib, Names, TreeWalker)).
%%-------------------------------------------------------------------
%% Custom tags
%%-------------------------------------------------------------------
interpret_value({trans, StringLiteral}, TreeWalker) ->
translated_ast(StringLiteral, TreeWalker);
interpret_value(Value, TreeWalker) ->
value_ast(Value, false, false, TreeWalker).
interpret_args(Args, TreeWalker) ->
lists:foldr(
fun ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
{{Ast0, AstInfo0}, TreeWalker0} = interpret_value(Value, TreeWalkerAcc),
{{[?Q("{_@Key@, _@Ast0}")|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0};
(Value, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
{{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, TreeWalkerAcc),
{{[Ast0|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0}
end, {{[], #ast_info{}}, TreeWalker}, Args).
tag_ast(Name, Args, TreeWalker) ->
{{InterpretedArgs, AstInfo1}, TreeWalker1} = interpret_args(Args, TreeWalker),
{{RenderAst, RenderInfo}, TreeWalker2} = custom_tags_modules_ast(Name, InterpretedArgs, TreeWalker1),
{{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker2}.
custom_tags_modules_ast({identifier, Pos, Name}, InterpretedArgs,
#treewalker{
context=#dtl_context{
tags = Tags,
module = Module,
is_compiling_dir=IsCompilingDir
}
}=TreeWalker) ->
case proplists:get_value(Name, Tags) of
{Mod, Fun}=Tag ->
case lists:max([-1] ++ [I || {N,I} <- Mod:module_info(exports), N =:= Fun]) of
2 ->
{{?Q("'@Mod@':'@Fun@'([_@InterpretedArgs], RenderOptions)"),
#ast_info{}}, TreeWalker};
1 ->
{{?Q("'@Mod@':'@Fun@'([_@InterpretedArgs])"),
#ast_info{}}, TreeWalker};
-1 ->
empty_ast(?WARN({Pos, {missing_tag, Name, Tag}}, TreeWalker));
I ->
empty_ast(?WARN({Pos, {bad_tag, Name, Tag, I}}, TreeWalker))
end;
undefined ->
if IsCompilingDir =/= false ->
{{?Q("'@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)"),
#ast_info{ custom_tags = [Name] }}, TreeWalker};
true ->
{{?Q("render_tag(_@Name@, [_@InterpretedArgs], RenderOptions)"),
#ast_info{ custom_tags = [Name] }}, TreeWalker}
end
end.
tag_ast(Name, Args, NewTagVar, TreeWalker) ->
{{InterpretedArgs, AstInfo1}, TreeWalker1} = interpret_args(Args, TreeWalker),
{{RenderAst, RenderInfo}, TreeWalker2} = custom_tags_modules_ast(Name, InterpretedArgs, NewTagVar, TreeWalker1),
{{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker2}.
custom_tags_modules_ast({identifier, Pos, Name}, InterpretedArgs, NewTagVar,
#treewalker{
context=#dtl_context{
tags = Tags,
module = Module,
is_compiling_dir=IsCompilingDir
}
}=TreeWalker) ->
LocalVarAst = varname_ast(NewTagVar),
case proplists:get_value(Name, Tags) of
{Mod, Fun}=Tag ->
case lists:max([-1] ++ [I || {N,I} <- Mod:module_info(exports), N =:= Fun]) of
2 ->
{Id, TreeWalker1} = begin_scope(
{[{NewTagVar, LocalVarAst}],
[?Q("_@LocalVarAst = '@Mod@':'@Fun@'([_@InterpretedArgs], RenderOptions)")]},
TreeWalker
),
{{Id, #ast_info{}}, TreeWalker1};
1 ->
{Id, TreeWalker1} = begin_scope(
{[{NewTagVar, LocalVarAst}],
[?Q("_@LocalVarAst = '@Mod@':'@Fun@'([_@InterpretedArgs])")]},
TreeWalker
),
{{Id, #ast_info{}}, TreeWalker1};
-1 ->
empty_ast(?WARN({Pos, {missing_tag, Name, Tag}}, TreeWalker));
I ->
empty_ast(?WARN({Pos, {bad_tag, Name, Tag, I}}, TreeWalker))
end;
undefined ->
if IsCompilingDir =/= false ->
{Id, TreeWalker1} = begin_scope(
{[{NewTagVar, LocalVarAst}],
[?Q("_@LocalVarAst = '@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)")]},
TreeWalker
),
{{Id, #ast_info{ custom_tags = [Name]}}, TreeWalker1};
true ->
{Id, TreeWalker1} = begin_scope(
{[{NewTagVar, LocalVarAst}],
[?Q("_@LocalVarAst = '@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)")]},
TreeWalker
),
{{Id, #ast_info{ custom_tags = [Name]}}, TreeWalker1}
end
end.
call_ast(Module, TreeWalker) ->
call_ast(Module, merl:var("_Variables"), #ast_info{}, TreeWalker).
call_with_ast(Module, Variable, TreeWalker) ->
{{VarAst, VarInfo}, TreeWalker2} = resolve_variable_ast(Variable, false, TreeWalker),
call_ast(Module, VarAst, VarInfo, TreeWalker2).
call_ast(Module, Variable, AstInfo, TreeWalker) ->
Ast = ?Q(["case '@Module@':render(_@Variable, RenderOptions) of",
" {ok, Rendered} -> Rendered;",
" {error, Reason} -> io_lib:format(\"error: ~p\", [Reason])",
"end"]),
with_dependencies(Module:dependencies(), {{Ast, AstInfo}, TreeWalker}).
create_scope(Vars, VarScope) ->
{Scope, Values} =
lists:foldl(
fun (Var, {VarAcc, ValueAcc}) ->
{Name, Value, Filters} =
case Var of
{N, V} -> {N, V, []};
{_, _, _} -> Var
end,
NameAst = varname_ast(lists:concat(["_", Name, VarScope])),
{[{Name, NameAst, Filters}|VarAcc],
[?Q("_@NameAst = _@Value")|ValueAcc]
}
end,
empty_scope(),
Vars),
{Scope, [Values]}.
create_scope(Vars, {Row, Col}, FileName, #treewalker{ context=Context }) ->
Level = length(Context#dtl_context.local_scopes),
create_scope(Vars, lists:concat(["::", FileName, "[", Level, ",", Row, ":", Col, "]"])).
varname_ast([$_|VarName]) ->
merl:var(lists:concat(["_Var__", VarName]));
varname_ast(VarName) ->
merl:var(lists:concat(["Var_", VarName])).