Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Compile custom tags at most once per template

  • Loading branch information...
commit 6c59be1320510eaecaa086ef27f7b42851f7d444 1 parent 978c9f5
@evanmiller evanmiller authored
View
2  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 `<b>{{ bar }}</b>`, then `{{ foo bar=100 }}`
+E.g. if $custom_tags_dir/foo contains `<b>{{ bar }}</b>`, then `{% foo bar=100 %}`
will evaluate to `<b>100</b>`. Get it?
* `vars` - Variables (and their values) to evaluate at compile-time rather than
View
3  examples/docroot/custom_tag
@@ -1,4 +1,3 @@
-{% load flashvideo %}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
@@ -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
</body>
-</html>
+</html>
View
12 examples/docroot/custom_tag_error
@@ -1,12 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>Test variable</title>
- </head>
- <body>
- before
- {% flashvideo dom_id="myvideo" width="800" height="600" static="/static" path_to_video="/myvid.mp4" path_to_preview_image="/mypic.jpg" %}
- after
- </body>
-</html>
View
152 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).
View
9 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.
View
13 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
Please sign in to comment.
Something went wrong with that request. Please try again.