%%%------------------------------------------------------------------- %%% File: erlydtl_beam_compiler.erl %%% @author Roberto Saccon [http://rsaccon.com] %%% @author Evan Miller %%% @author Andreas Stenius %%% @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 '). %% -------------------------------------------------------------------- %% 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); {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])).