Permalink
Browse files

Make endregroup tag optional (fixes #101)

Django does not have endregroup at all.
We keep it as optional for backward compatibility with ErlyDTL.

The dtl context has been moved into the treewalker record while
traversing the template AST, to allow it to change on any node.

Local scopes can now begin at any point in a block, and all
nested scopes are automatically closed when the block scope ends.
  • Loading branch information...
1 parent daa5f62 commit 1ecf7475d012487d1fd4bf57bf2edda1b9c74160 @kaos kaos committed Feb 26, 2014
View
@@ -243,8 +243,9 @@ Differences from standard Django Template Language
tag is not implemented. This should be
[addressed](https://github.com/erlydtl/erlydtl/issues/115) in a
future release.
-* `regroup` requires a closing `endregroup` tag. See
- [Issue #101](https://github.com/erlydtl/erlydtl/issues/101).
+* For an up-to-date list, see all
+ [issues](https://github.com/erlydtl/erlydtl/issues) marked
+ `dtl_compat`.
Tests
@@ -254,7 +255,7 @@ From a Unix shell, run:
make test
-Note that the tests will create some output in tests/output.
+Note that the tests will create some output in tests/output in case of regressions.
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/erlydtl/erlydtl/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
View
@@ -45,7 +45,8 @@
-record(treewalker, {
counter = 0,
safe = false,
- extension = undefined
+ extension = undefined,
+ context
}).
-record(scanner_state, {

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -55,7 +55,15 @@
merge_info/2,
print/3,
to_string/2,
- unescape_string_literal/1
+ unescape_string_literal/1,
+ reset_parse_trail/2,
+ resolve_variable/2,
+ resolve_variable/3,
+ push_scope/2,
+ restore_scope/2,
+ begin_scope/1,
+ begin_scope/3,
+ end_scope/4
]).
-include("erlydtl_ext.hrl").
@@ -66,7 +74,7 @@
%% --------------------------------------------------------------------
init_treewalker(Context) ->
- TreeWalker = #treewalker{},
+ TreeWalker = #treewalker{ context=Context },
case call_extension(Context, init_treewalker, [TreeWalker]) of
{ok, TW} when is_record(TW, treewalker) -> TW;
undefined -> TreeWalker
@@ -89,6 +97,8 @@ full_path(File, DocRoot) ->
print(Fmt, Args, #dtl_context{ verbose = true }) -> io:format(Fmt, Args);
print(_Fmt, _Args, _Context) -> ok.
+get_current_file(#treewalker{ context=Context }) ->
+ get_current_file(Context);
get_current_file(#dtl_context{ parse_trail=[File|_] }) -> File;
get_current_file(#dtl_context{ doc_root=Root }) -> Root.
@@ -126,6 +136,8 @@ add_warnings(Warnings, Context) ->
fun (W, C) -> add_warning(?MODULE, W, C) end,
Context, Warnings).
+call_extension(#treewalker{ context=Context }, Fun, Args) ->
+ call_extension(Context, Fun, Args);
call_extension(#dtl_context{ extension_module=undefined }, _Fun, _Args) ->
undefined;
call_extension(#dtl_context{ extension_module=Mod }, Fun, Args)
@@ -178,7 +190,39 @@ merge_info(Info1, Info2) ->
Info1#ast_info.pre_render_asts,
Info2#ast_info.pre_render_asts)}.
-
+resolve_variable(VarName, TreeWalker) ->
+ resolve_variable(VarName, undefined, TreeWalker).
+
+resolve_variable(VarName, Default, #treewalker{ context=Context }) ->
+ resolve_variable1(Context#dtl_context.local_scopes, VarName, Default).
+
+push_scope(Scope, #treewalker{ context=Context }=TreeWalker) ->
+ TreeWalker#treewalker{ context=push_scope(Scope, Context) };
+push_scope(Scope, #dtl_context{ local_scopes=Scopes }=Context) ->
+ Context#dtl_context{ local_scopes=[Scope|Scopes] }.
+
+restore_scope(Target, #treewalker{ context=Context }=TreeWalker) ->
+ TreeWalker#treewalker{ context=restore_scope(Target, Context) };
+restore_scope(#treewalker{ context=Target }, Context) ->
+ restore_scope(Target, Context);
+restore_scope(#dtl_context{ local_scopes=Scopes }, Context) ->
+ Context#dtl_context{ local_scopes=Scopes }.
+
+begin_scope(TreeWalker) -> begin_scope([], [], TreeWalker).
+
+begin_scope(Scope, Values, TreeWalker) ->
+ Id = make_ref(),
+ {Id, push_scope({Id, Scope, Values}, TreeWalker)}.
+
+end_scope(Fun, Id, AstList, TreeWalker) ->
+ close_scope(Fun, Id, AstList, TreeWalker).
+
+reset_parse_trail(ParseTrail, #treewalker{ context=Context }=TreeWalker) ->
+ TreeWalker#treewalker{ context=reset_parse_trail(ParseTrail, Context) };
+reset_parse_trail(ParseTrail, Context) ->
+ Context#dtl_context{ parse_trail=ParseTrail }.
+
+
format_error(Other) ->
io_lib:format("## Error description for ~p not implemented.", [Other]).
@@ -252,3 +296,54 @@ pos_info(Line) when is_integer(Line) ->
io_lib:format("~b: ", [Line]);
pos_info({Line, Col}) when is_integer(Line), is_integer(Col) ->
io_lib:format("~b:~b ", [Line, Col]).
+
+resolve_variable1([], _VarName, Default) -> Default;
+resolve_variable1([Scope|Scopes], VarName, Default) ->
+ case proplists:get_value(VarName, get_scope(Scope), Default) of
+ Default ->
+ resolve_variable1(Scopes, VarName, Default);
+ Value -> Value
+ end.
+
+get_scope({_Id, Scope, _Values}) -> Scope;
+get_scope(Scope) -> Scope.
+
+close_scope(Fun, Id, AstList, TreeWalker) ->
+ case merge_scopes(Id, TreeWalker) of
+ {[], TreeWalker1} -> {AstList, TreeWalker1};
+ {Values, TreeWalker1} ->
+ {lists:foldl(
+ fun ({ScopeId, ScopeValues}, AstAcc) ->
+ {Pre, Target, Post} = split_ast(ScopeId, AstAcc),
+ Pre ++ Fun(ScopeValues ++ Target) ++ Post
+ end,
+ AstList, Values),
+ TreeWalker1}
+ end.
+
+merge_scopes(Id, #treewalker{ context=Context }=TreeWalker) ->
+ {Values, Scopes} = merge_scopes(Id, Context#dtl_context.local_scopes, []),
+ {Values, TreeWalker#treewalker{ context=Context#dtl_context{ local_scopes = Scopes } }}.
+
+merge_scopes(Id, [{Id, _Scope, []}|Scopes], Acc) -> {Acc, Scopes};
+merge_scopes(Id, [{_ScopeId, _Scope, []}|Scopes], Acc) ->
+ merge_scopes(Id, Scopes, Acc);
+merge_scopes(Id, [{ScopeId, _Scope, Values}|Scopes], Acc) ->
+ merge_scopes(Id, Scopes, [{ScopeId, Values}|Acc]);
+merge_scopes(Id, [_PlainScope|Scopes], Acc) ->
+ merge_scopes(Id, Scopes, Acc).
+
+
+split_ast(Id, AstList) ->
+ split_ast(Id, AstList, []).
+
+split_ast(_Split, [], {Pre, Acc}) ->
+ {Pre, lists:reverse(Acc), []};
+split_ast(Split, [Split|Rest], {Pre, Acc}) ->
+ {Pre, lists:reverse(Acc), Rest};
+split_ast(Split, [Split|Rest], Acc) ->
+ split_ast(end_scope, Rest, {lists:reverse(Acc), []});
+split_ast(Split, [Ast|Rest], {Pre, Acc}) ->
+ split_ast(Split, Rest, {Pre, [Ast|Acc]});
+split_ast(Split, [Ast|Rest], Acc) ->
+ split_ast(Split, Rest, [Ast|Acc]).
View
@@ -104,9 +104,8 @@ Nonterminals
CustomArgs
Args
- RegroupBlock
- RegroupBraced
- EndRegroupBraced
+ RegroupTag
+ EndRegroupTag
SpacelessBlock
@@ -232,7 +231,8 @@ Elements -> Elements IfNotEqualBlock : '$1' ++ ['$2'].
Elements -> Elements IfChangedBlock : '$1' ++ ['$2'].
Elements -> Elements IncludeTag : '$1' ++ ['$2'].
Elements -> Elements NowTag : '$1' ++ ['$2'].
-Elements -> Elements RegroupBlock : '$1' ++ ['$2'].
+Elements -> Elements RegroupTag : '$1' ++ ['$2'].
+Elements -> Elements EndRegroupTag : '$1' ++ ['$2'].
Elements -> Elements SpacelessBlock : '$1' ++ ['$2'].
Elements -> Elements SSITag : '$1' ++ ['$2'].
Elements -> Elements TemplatetagTag : '$1' ++ ['$2'].
@@ -359,9 +359,8 @@ IfNotEqualBraced -> open_tag ifnotequal_keyword IfNotEqualExpression Value close
IfNotEqualExpression -> Value : '$1'.
EndIfNotEqualBraced -> open_tag endifnotequal_keyword close_tag.
-RegroupBlock -> RegroupBraced Elements EndRegroupBraced : {regroup, '$1', '$2'}.
-RegroupBraced -> open_tag regroup_keyword Value by_keyword Value as_keyword identifier close_tag : {'$3', '$5', '$7'}.
-EndRegroupBraced -> open_tag endregroup_keyword close_tag.
+RegroupTag -> open_tag regroup_keyword Value by_keyword Value as_keyword identifier close_tag : {regroup, {'$3', '$5', '$7'}}.
+EndRegroupTag -> open_tag endregroup_keyword close_tag : end_regroup.
SpacelessBlock -> open_tag spaceless_keyword close_tag Elements open_tag endspaceless_keyword close_tag : {spaceless, '$4'}.
@@ -1,6 +1,6 @@
-module(erlydtl_extension_test).
--export([scan/1, parse/1, compile_ast/3]).
+-export([scan/1, parse/1, compile_ast/2]).
-include("erlydtl_ext.hrl").
%% look for a foo identifer followed by a #
@@ -22,9 +22,9 @@ parse(State) ->
erlydtl_extension_testparser:resume(State).
%% {{ varA or varB }} is equivalent to {% if varA %}{{ varA }}{% else %}{{ varB }}{% endif %}
-compile_ast({value_or, {Value1, Value2}}, Context, TreeWalker) ->
- {{V1_Ast, V1_Info}, TW1} = erlydtl_beam_compiler:value_ast(Value1, false, false, Context, TreeWalker),
- {{V2_Ast, V2_Info}, TW2} = erlydtl_beam_compiler:value_ast(Value2, false, false, Context, TW1),
+compile_ast({value_or, {Value1, Value2}}, TreeWalker) ->
+ {{V1_Ast, V1_Info}, TW1} = erlydtl_beam_compiler:value_ast(Value1, false, false, TreeWalker),
+ {{V2_Ast, V2_Info}, TW2} = erlydtl_beam_compiler:value_ast(Value2, false, false, TW1),
{{erl_syntax:case_expr(V1_Ast,
[erl_syntax:clause([erl_syntax:atom(undefined)], none, [V2_Ast]),
erl_syntax:clause([erl_syntax:underscore()], none, [V1_Ast])
@@ -239,6 +239,7 @@ fold_tests() ->
Res = case catch test_compile_render(Name) of
ok -> {AccCount + 1, AccErrs};
{'EXIT', Reason} ->
+ io:format("crash"),
{AccCount + 1, [{Name, crash,
io_lib:format("~p", [Reason])}
| AccErrs]};
@@ -284,7 +284,7 @@ tests() ->
<<"{% for outer in list %}{% for inner in outer %}({{ forloop.parentloop.counter0 }}, {{ forloop.counter0 }})\n{% endfor %}{% endfor %}">>,
[{'list', [["One", "two"], ["One", "two"]]}], [], [], <<"(0, 0)\n(0, 1)\n(1, 0)\n(1, 1)\n">>,
%% the warnings we get from the erlang compiler still needs some care..
- [error_info([{0, erl_lint, {unused_var, 'Var_inner/1_1:31'}}, no_out_dir])]},
+ [error_info([{0, erl_lint, {unused_var, 'Var_inner/3_1:31'}}, no_out_dir])]},
{"If changed",
<<"{% for x in list %}{% ifchanged %}{{ x }}\n{% endifchanged %}{% endfor %}">>,
[{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nthree\n">>},
@@ -1389,10 +1389,10 @@ run_tests() ->
0 ->
io:format("~nAll unit tests PASS~nTotal~s (msec)~n~n", [format_times(Times)]);
Length ->
- io:format("~n### FAILED groups: ~b ####~n", [Length]),
+ io:format("~n~n### FAILED groups: ~b ####~n", [Length]),
[begin
- io:format(" Group: ~s (~b failures)~n", [Group, length(Failed)]),
- [io:format(" Test: ~s~n~s~n", [Name, Error])
+ io:format("~n Group: ~s (~b failures)~n", [Group, length(Failed)]),
+ [io:format("~n Test: ~s~n ~s~n", [Name, Error])
|| {Name, Error} <- lists:reverse(Failed)]
end || {Group, Failed} <- lists:reverse(Failures)],
throw(failed)
@@ -1419,7 +1419,7 @@ format_times(Ts, Count) ->
format_error(Name, Class, Error) ->
io:format("!"),
- {Name, io_lib:format("~s:~p~n ~p", [Class, Error, erlang:get_stacktrace()])}.
+ {Name, io_lib:format("~s:~p~n ~p", [Class, Error, erlang:get_stacktrace()])}.
compile_test(DTL, Opts) ->
Options = [force_recompile,

0 comments on commit 1ecf747

Please sign in to comment.