Skip to content

Commit

Permalink
Compile custom tags at most once per template
Browse files Browse the repository at this point in the history
  • Loading branch information
evanmiller committed Jan 25, 2011
1 parent 978c9f5 commit 6c59be1
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 109 deletions.
2 changes: 1 addition & 1 deletion README.markdown
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions 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>
Expand All @@ -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>
12 changes: 0 additions & 12 deletions examples/docroot/custom_tag_error

This file was deleted.

152 changes: 77 additions & 75 deletions src/erlydtl/erlydtl_compiler.erl
Expand Up @@ -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, []);
Expand Down Expand Up @@ -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
Expand All @@ -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),
Expand Down Expand Up @@ -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) ->
Expand All @@ -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([])])])]),
Expand Down Expand Up @@ -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),
Expand All @@ -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]].


Expand Down Expand Up @@ -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) ->
Expand Down Expand Up @@ -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),
Expand All @@ -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,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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).
Expand Down
9 changes: 0 additions & 9 deletions src/erlydtl/erlydtl_parser.yrl
Expand Up @@ -89,9 +89,6 @@ Nonterminals
Variable
Filter

LoadTag
LoadNames

CustomTag
Args

Expand Down Expand Up @@ -129,7 +126,6 @@ Terminals
ifnotequal_keyword
in_keyword
include_keyword
load_keyword
noop_keyword
not_keyword
now_keyword
Expand Down Expand Up @@ -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'].
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 3 additions & 10 deletions src/tests/erlydtl_functional_tests.erl
Expand Up @@ -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" ].

Expand Down Expand Up @@ -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, []}.

Expand Down Expand Up @@ -163,9 +159,6 @@ setup("trans") ->
%%--------------------------------------------------------------------
%% Custom tags
%%--------------------------------------------------------------------
setup("custom_tag_error") ->
RenderVars = [],
{skip, RenderVars};
setup("custom_call") ->
RenderVars = [{var1, "something"}],
{ok, RenderVars};
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 6c59be1

Please sign in to comment.