diff --git a/README.markdown b/README.markdown index 221004b..ecc900f 100644 --- a/README.markdown +++ b/README.markdown @@ -38,7 +38,7 @@ Options is a proplist possibly containing: defaults to the compiled template's directory. * `custom_tags_dir` - Directory of DTL files (no extension) includable as tags. -E.g. if $custom_tags_dir/foo contains `{{ bar }}`, then `{{ foo bar=100 }}` +E.g. if $custom_tags_dir/foo contains `{{ bar }}`, then `{% foo bar=100 %}` will evaluate to `100`. Get it? * `vars` - Variables (and their values) to evaluate at compile-time rather than diff --git a/examples/docroot/custom_tag b/examples/docroot/custom_tag index bc1760c..7d4a31e 100755 --- a/examples/docroot/custom_tag +++ b/examples/docroot/custom_tag @@ -1,4 +1,3 @@ -{% load flashvideo %} @@ -10,4 +9,4 @@ {% flashvideo dom_id="myvideo" width="800" height="600" static="/static" path_to_video="/myvid.mp4" path_to_preview_image="/mypic.jpg" %} after - \ No newline at end of file + diff --git a/examples/docroot/custom_tag_error b/examples/docroot/custom_tag_error deleted file mode 100755 index 6f4657a..0000000 --- a/examples/docroot/custom_tag_error +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Test variable - - - before - {% flashvideo dom_id="myvideo" width="800" height="600" static="/static" path_to_video="/myvid.mp4" path_to_preview_image="/mypic.jpg" %} - after - - \ No newline at end of file diff --git a/src/erlydtl/erlydtl_compiler.erl b/src/erlydtl/erlydtl_compiler.erl index 57b0d25..7a9ccc7 100755 --- a/src/erlydtl/erlydtl_compiler.erl +++ b/src/erlydtl/erlydtl_compiler.erl @@ -57,12 +57,14 @@ -record(ast_info, { dependencies = [], translatable_strings = [], + custom_tags = [], var_names = [], pre_render_asts = []}). -record(treewalker, { - counter = 0, - custom_tags = []}). +% custom_tags = [], + counter = 0 +}). compile(Binary, Module) when is_binary(Binary) -> compile(Binary, Module, []); @@ -120,19 +122,24 @@ compile(File, Module, Options) -> compile_to_binary(File, DjangoParseTree, Context, CheckSum) -> try body_ast(DjangoParseTree, Context, #treewalker{}) of - {{Ast, Info}, _} -> - case compile:forms(forms(File, Context#dtl_context.module, Ast, Info, CheckSum), - Context#dtl_context.compiler_options) of - {ok, Module1, Bin} -> - code:purge(Module1), - case code:load_binary(Module1, atom_to_list(Module1) ++ ".erl", Bin) of - {module, _} -> {ok, Module1, Bin}; - _ -> {error, lists:concat(["code reload failed: ", Module1])} - end; - error -> - {error, lists:concat(["compilation failed: ", File])}; - OtherError -> - OtherError + {{BodyAst, BodyInfo}, BodyTreeWalker} -> + try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of + {{CustomTagsAst, CustomTagsInfo}, _} -> + case compile:forms(forms(File, Context#dtl_context.module, {BodyAst, BodyInfo}, {CustomTagsAst, CustomTagsInfo}, CheckSum), + Context#dtl_context.compiler_options) of + {ok, Module1, Bin} -> + code:purge(Module1), + case code:load_binary(Module1, atom_to_list(Module1) ++ ".erl", Bin) of + {module, _} -> {ok, Module1, Bin}; + _ -> {error, lists:concat(["code reload failed: ", Module1])} + end; + error -> + {error, lists:concat(["compilation failed: ", File])}; + OtherError -> + OtherError + end + catch + throw:Error -> Error end catch throw:Error -> Error @@ -146,7 +153,7 @@ init_dtl_context(File, Module, Options) -> parse_trail = [File], module = Module, doc_root = proplists:get_value(doc_root, Options, filename:dirname(File)), - custom_tags_dir = proplists:get_value(custom_tags_dir, Options, Ctx#dtl_context.custom_tags_dir), + custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])), vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars), reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader), compiler_options = proplists:get_value(compiler_options, Options, Ctx#dtl_context.compiler_options), @@ -203,7 +210,7 @@ parse(File, Context) -> Result end; _ -> - {error, {File, ["Failed to read file"]}} + {error, {File, [{0, Context#dtl_context.module, "Failed to read file"}]}} end. parse(CheckSum, Data, Context) -> @@ -226,8 +233,38 @@ parse(Data) -> Err -> Err end. + +custom_tags_ast(CustomTags, Context, TreeWalker) -> + {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} = custom_tags_clauses_ast(CustomTags, Context, TreeWalker), + {{erl_syntax:function(erl_syntax:atom(render_tag), CustomTagsClauses), CustomTagsInfo}, TreeWalker1}. + +custom_tags_clauses_ast(CustomTags, Context, TreeWalker) -> + custom_tags_clauses_ast1(CustomTags, [], [], #ast_info{}, Context, TreeWalker). + +custom_tags_clauses_ast1([], _ExcludeTags, ClauseAcc, InfoAcc, _Context, TreeWalker) -> + {{lists:reverse([erl_syntax:clause([erl_syntax:underscore(), erl_syntax:underscore(), erl_syntax:underscore()], none, + [erl_syntax:list([])])|ClauseAcc]), InfoAcc}, TreeWalker}; +custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker) -> + case lists:member(Tag, ExcludeTags) of + true -> + custom_tags_clauses_ast1(CustomTags, ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker); + false -> + CustomTagFile = filename:join([Context#dtl_context.custom_tags_dir, Tag]), + case parse(CustomTagFile, Context) of + {ok, DjangoParseTree, CheckSum} -> + {{BodyAst, BodyAstInfo}, TreeWalker1} = with_dependency({CustomTagFile, CheckSum}, + body_ast(DjangoParseTree, Context, TreeWalker)), + Clause = erl_syntax:clause([erl_syntax:string(Tag), erl_syntax:variable("Variables"), erl_syntax:variable("TranslationFun")], + none, [BodyAst]), + custom_tags_clauses_ast1(CustomTags, [Tag|ExcludeTags], [Clause|ClauseAcc], merge_info(BodyAstInfo, InfoAcc), + Context, TreeWalker1); + Error -> + throw(Error) + end + end. -forms(File, Module, BodyAst, BodyInfo, CheckSum) -> +forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum) -> + MergedInfo = merge_info(BodyInfo, CustomTagsInfo), Render0FunctionAst = erl_syntax:function(erl_syntax:atom(render), [erl_syntax:clause([], none, [erl_syntax:application(none, erl_syntax:atom(render), [erl_syntax:list([])])])]), @@ -258,12 +295,12 @@ forms(File, Module, BodyAst, BodyInfo, CheckSum) -> [erl_syntax:list(lists:map(fun ({XFile, XCheckSum}) -> erl_syntax:tuple([erl_syntax:string(XFile), erl_syntax:string(XCheckSum)]) - end, BodyInfo#ast_info.dependencies))])]), + end, MergedInfo#ast_info.dependencies))])]), TranslatableStringsAst = erl_syntax:function( erl_syntax:atom(translatable_strings), [erl_syntax:clause([], none, [erl_syntax:list(lists:map(fun(String) -> erl_syntax:string(String) end, - BodyInfo#ast_info.translatable_strings))])]), + MergedInfo#ast_info.translatable_strings))])]), BodyAstTmp = erl_syntax:application( erl_syntax:atom(erlydtl_runtime), @@ -286,7 +323,7 @@ forms(File, Module, BodyAst, BodyInfo, CheckSum) -> erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0))])]), [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, Render0FunctionAst, Render1FunctionAst, Render2FunctionAst, - SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst, RenderInternalFunctionAst + SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst, RenderInternalFunctionAst, CustomTagsFunctionAst | BodyInfo#ast_info.pre_render_asts]]. @@ -370,8 +407,6 @@ body_ast(DjangoParseTree, Context, TreeWalker) -> ({'for', {'in', IteratorList, Variable}, Contents, EmptyPartContents}, TreeWalkerAcc) -> {EmptyAstInfo, TreeWalker1} = body_ast(EmptyPartContents, Context, TreeWalkerAcc), for_loop_ast(IteratorList, Variable, Contents, EmptyAstInfo, Context, TreeWalker1); - ({'load', Names}, TreeWalkerAcc) -> - load_ast(Names, Context, TreeWalkerAcc); ({'tag', {'identifier', _, Name}, Args}, TreeWalkerAcc) -> tag_ast(Name, Args, Context, TreeWalkerAcc); ({'call', {'identifier', _, Name}}, TreeWalkerAcc) -> @@ -451,10 +486,11 @@ value_ast(ValueToken, AsString, Context, TreeWalker) -> end. merge_info(Info1, Info2) -> - #ast_info{dependencies = - lists:merge( - lists:sort(Info1#ast_info.dependencies), - lists:sort(Info2#ast_info.dependencies)), + #ast_info{ + dependencies = + lists:merge( + lists:sort(Info1#ast_info.dependencies), + lists:sort(Info2#ast_info.dependencies)), var_names = lists:merge( lists:sort(Info1#ast_info.var_names), @@ -463,6 +499,10 @@ merge_info(Info1, Info2) -> lists:merge( lists:sort(Info1#ast_info.translatable_strings), lists:sort(Info2#ast_info.translatable_strings)), + custom_tags = + lists:merge( + lists:sort(Info1#ast_info.custom_tags), + lists:sort(Info2#ast_info.custom_tags)), pre_render_asts = lists:merge( Info1#ast_info.pre_render_asts, @@ -688,10 +728,6 @@ for_loop_ast(IteratorList, LoopValue, Contents, {EmptyContentsAst, EmptyContents merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo) }, TreeWalker2}. -load_ast(Names, _Context, TreeWalker) -> - CustomTags = lists:merge([X || {identifier, _ , X} <- Names], TreeWalker#treewalker.custom_tags), - {{erl_syntax:list([]), #ast_info{}}, TreeWalker#treewalker{custom_tags = CustomTags}}. - cycle_ast(Names, Context, TreeWalker) -> NamesTuple = lists:map(fun({string_literal, _, Str}) -> erl_syntax:string(unescape_string_literal(Str)); @@ -753,50 +789,16 @@ full_path(File, DocRoot) -> %%------------------------------------------------------------------- tag_ast(Name, Args, Context, TreeWalker) -> - case lists:member(Name, TreeWalker#treewalker.custom_tags) of - true -> - InterpretedArgs = lists:map(fun - ({{identifier, _, Key}, {string_literal, _, Value}}) -> - {Key, erl_syntax:string(unescape_string_literal(Value))}; - ({{identifier, _, Key}, Value}) -> - {Key, format(resolve_variable_ast(Value, Context), Context)} - end, Args), - DefaultFilePath = filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags", Name]), - case Context#dtl_context.custom_tags_dir of - [] -> - case parse(DefaultFilePath, Context) of - {ok, TagParseTree, CheckSum} -> - tag_ast2({DefaultFilePath, CheckSum}, TagParseTree, InterpretedArgs, Context, TreeWalker); - _ -> - Reason = lists:concat(["Loading tag source for '", Name, "' failed: ", - DefaultFilePath]), - throw({error, Reason}) - end; - _ -> - CustomFilePath = filename:join([Context#dtl_context.custom_tags_dir, Name]), - case parse(CustomFilePath, Context) of - {ok, TagParseTree, CheckSum} -> - tag_ast2({CustomFilePath, CheckSum},TagParseTree, InterpretedArgs, Context, TreeWalker); - _ -> - case parse(DefaultFilePath, Context) of - {ok, TagParseTree, CheckSum} -> - tag_ast2({DefaultFilePath, CheckSum}, TagParseTree, InterpretedArgs, Context, TreeWalker); - _ -> - Reason = lists:concat(["Loading tag source for '", Name, "' failed: ", - CustomFilePath, ", ", DefaultFilePath]), - throw({error, Reason}) - end - end - end; - _ -> - throw({error, lists:concat(["Custom tag '", Name, "' not loaded"])}) - end. - -tag_ast2({Source, _} = Dep, TagParseTree, InterpretedArgs, Context, TreeWalker) -> - with_dependency(Dep, body_ast(TagParseTree, Context#dtl_context{ - local_scopes = [ InterpretedArgs | Context#dtl_context.local_scopes ], - parse_trail = [ Source | Context#dtl_context.parse_trail ]}, TreeWalker)). - + {InterpretedArgs, AstInfo} = lists:mapfoldl(fun + ({{identifier, _, Key}, {string_literal, _, Value}}, AstInfoAcc) -> + {erl_syntax:tuple([erl_syntax:string(Key), erl_syntax:string(unescape_string_literal(Value))]), AstInfoAcc}; + ({{identifier, _, Key}, Value}, AstInfoAcc) -> + {AST, AstInfo1} = resolve_variable_ast(Value, Context), + {erl_syntax:tuple([erl_syntax:string(Key), format(AST,Context)]), merge_info(AstInfo1, AstInfoAcc)} + end, #ast_info{}, Args), + RenderAst = erl_syntax:application(none, erl_syntax:atom(render_tag), + [erl_syntax:string(Name), erl_syntax:list(InterpretedArgs), erl_syntax:variable("TranslationFun")]), + {{RenderAst, AstInfo#ast_info{custom_tags = [Name]}}, TreeWalker}. call_ast(Module, TreeWalkerAcc) -> call_ast(Module, erl_syntax:variable("Variables"), #ast_info{}, TreeWalkerAcc). diff --git a/src/erlydtl/erlydtl_parser.yrl b/src/erlydtl/erlydtl_parser.yrl index 2aa6efc..343014c 100755 --- a/src/erlydtl/erlydtl_parser.yrl +++ b/src/erlydtl/erlydtl_parser.yrl @@ -89,9 +89,6 @@ Nonterminals Variable Filter - LoadTag - LoadNames - CustomTag Args @@ -129,7 +126,6 @@ Terminals ifnotequal_keyword in_keyword include_keyword - load_keyword noop_keyword not_keyword now_keyword @@ -163,7 +159,6 @@ Elements -> Elements ValueBraced : '$1' ++ ['$2']. Elements -> Elements ExtendsTag : '$1' ++ ['$2']. Elements -> Elements IncludeTag : '$1' ++ ['$2']. Elements -> Elements NowTag : '$1' ++ ['$2']. -Elements -> Elements LoadTag : '$1' ++ ['$2']. Elements -> Elements CycleTag : '$1' ++ ['$2']. Elements -> Elements BlockBlock : '$1' ++ ['$2']. Elements -> Elements FirstofTag : '$1' ++ ['$2']. @@ -195,10 +190,6 @@ ExtendsTag -> open_tag extends_keyword string_literal close_tag : {extends, '$3' IncludeTag -> open_tag include_keyword string_literal close_tag : {include, '$3'}. NowTag -> open_tag now_keyword string_literal close_tag : {date, now, '$3'}. -LoadTag -> open_tag load_keyword LoadNames close_tag : {load, '$3'}. -LoadNames -> identifier : ['$1']. -LoadNames -> LoadNames identifier : '$1' ++ ['$2']. - BlockBlock -> BlockBraced Elements EndBlockBraced : {block, '$1', '$2'}. BlockBraced -> open_tag block_keyword identifier close_tag : '$3'. EndBlockBraced -> open_tag endblock_keyword close_tag. diff --git a/src/tests/erlydtl_functional_tests.erl b/src/tests/erlydtl_functional_tests.erl index 6c0fa73..7172dd2 100644 --- a/src/tests/erlydtl_functional_tests.erl +++ b/src/tests/erlydtl_functional_tests.erl @@ -44,8 +44,7 @@ test_list() -> "for_tuple", "for_list_preset", "for_preset", "for_records", "for_records_preset", "include", "if", "if_preset", "ifequal", "ifequal_preset", "ifnotequal", "ifnotequal_preset", "now", - "var", "var_preset", "cycle", "custom_tag", - "custom_tag_error", "custom_call", + "var", "var_preset", "cycle", "custom_tag", "custom_call", "include_template", "include_path", "extends_path", "extends_path2", "trans" ]. @@ -73,9 +72,6 @@ setup_compile("ifnotequal_preset") -> setup_compile("var_preset") -> CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}], {ok, CompileVars}; -setup_compile("custom_tag_error") -> - CompileVars = [], - {error, CompileVars}; setup_compile(_) -> {ok, []}. @@ -163,9 +159,6 @@ setup("trans") -> %%-------------------------------------------------------------------- %% Custom tags %%-------------------------------------------------------------------- -setup("custom_tag_error") -> - RenderVars = [], - {skip, RenderVars}; setup("custom_call") -> RenderVars = [{var1, "something"}], {ok, RenderVars}; @@ -265,9 +258,9 @@ test_render(Name, Module) -> _ -> {error, "rendering should have failed :" ++ File} end; - {'EXIT', _} -> + {'EXIT', Reason} -> io:format("~n"), - {error, "failed invoking render method:" ++ Module}; + {error, lists:flatten(io_lib:format("failed invoking render method of ~p ~p", [Module, Reason]))}; Err -> io:format("~n"), case RenderStatus of