Permalink
Browse files

Support for {% ifchanged %} tag

The implementation is a bit messy as it uses the process dictionary to
check for previous values in a loop. But I could not think of a clean
functional way to support this tag without rewriting the entire
compiler.
  • Loading branch information...
1 parent dc377d7 commit 1b16fa011b220ded9d5f729a2c1fdb082d89f24f Evan Miller committed Feb 19, 2012
View
@@ -3,9 +3,9 @@ ErlyDTL
ErlyDTL compiles Django Template Language to Erlang bytecode.
-*Supported tags*: autoescape, block, blocktrans, comment, cycle, extends, filter, firstof, for, if, ifequal, ifnotequal, include, now, spaceless, ssi, templatetag, trans, widthratio, with
+*Supported tags*: autoescape, block, blocktrans, comment, cycle, extends, filter, firstof, for, if, ifchanged, ifequal, ifnotequal, include, now, spaceless, ssi, templatetag, trans, widthratio, with
-_Unsupported tags_: csrf_token, ifchanged, regroup, url
+_Unsupported tags_: csrf_token, regroup, url
*Supported filters*: add, addslashes, capfirst, center, cut, date, default, default_if_none, dictsort, dictsortreversed, divisibleby, escape, escapejs, filesizeformat, first, fix_ampersands, floatformat, force_escape, format_integer, format_number, get_digit, iriencode, join, last, length, length_is, linebreaks, linebreaksbr, linenumbers, ljust, lower, make_list, phonenumeric, pluralize, pprint, random, random_num, random_range, removetags, rjust, safe, safeseq, slice, slugify, stringformat, striptags, time, timesince, timeuntil, title, truncatechars, truncatewords, truncatewords_html, unordered_list, upper, urlencode, urlize, urlizetrunc, wordcount, wordwrap, yesno
View
@@ -542,6 +542,14 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
{IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
{ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+ ({'ifchanged', Contents}, TreeWalkerAcc) ->
+ {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
+ {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
+ ifchanged_ast(Contents, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+ ({'ifchangedelse', IfContents, ElseContents}, TreeWalkerAcc) ->
+ {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
+ {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
+ ifchanged_ast(IfContents, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
({'ifelse', Expression, IfContents, ElseContents}, TreeWalkerAcc) ->
{IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
{ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
@@ -1026,27 +1034,33 @@ for_loop_ast(IteratorList, LoopValue, Contents, {EmptyContentsAst, EmptyContents
erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst, Value])
end,
{{erl_syntax:case_expr(
- erl_syntax:application(
- erl_syntax:atom('lists'), erl_syntax:atom('mapfoldl'),
- [erl_syntax:fun_expr([
- erl_syntax:clause([erl_syntax:tuple(Vars), erl_syntax:variable("Counters")], none,
- [erl_syntax:tuple([InnerAst, CounterAst])]),
- erl_syntax:clause(case Vars of [H] -> [H, erl_syntax:variable("Counters")];
- _ -> [erl_syntax:list(Vars), erl_syntax:variable("Counters")] end, none,
- [erl_syntax:tuple([InnerAst, CounterAst])])
- ]),
- CounterVars0, LoopValueAst]),
- [erl_syntax:clause(
- [erl_syntax:tuple([erl_syntax:underscore(),
+ erl_syntax:application(
+ erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('forloop'),
+ [erl_syntax:fun_expr([
+ erl_syntax:clause([erl_syntax:tuple(Vars), erl_syntax:variable("Counters")], none,
+ [erl_syntax:tuple([InnerAst, CounterAst])]),
+ erl_syntax:clause(case Vars of [H] -> [H, erl_syntax:variable("Counters")];
+ _ -> [erl_syntax:list(Vars), erl_syntax:variable("Counters")] end, none,
+ [erl_syntax:tuple([InnerAst, CounterAst])])
+ ]),
+ CounterVars0, LoopValueAst]),
+ [erl_syntax:clause(
+ [erl_syntax:tuple([erl_syntax:underscore(),
erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(counter), erl_syntax:integer(1)])],
erl_syntax:underscore())])],
- none, [EmptyContentsAst]),
- erl_syntax:clause(
- [erl_syntax:tuple([erl_syntax:variable("L"), erl_syntax:underscore()])],
- none, [erl_syntax:variable("L")])]
- ),
- merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)
- }, TreeWalker2}.
+ none, [EmptyContentsAst]),
+ erl_syntax:clause(
+ [erl_syntax:tuple([erl_syntax:variable("L"), erl_syntax:underscore()])],
+ none, [erl_syntax:variable("L")])]
+ ),
+ merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)
+ }, TreeWalker2}.
+
+ifchanged_ast(ParseTree, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, _Context, TreeWalker) ->
+ SourceText = lists:flatten(erlydtl_unparser:unparse(ParseTree)),
+ {{erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(ifchanged),
+ [erl_syntax:string(SourceText), IfContentsAst, ElseContentsAst]),
+ merge_info(IfContentsInfo, ElseContentsInfo)}, TreeWalker}.
cycle_ast(Names, Context, TreeWalker) ->
NamesTuple = lists:map(fun
View
@@ -83,6 +83,10 @@ Nonterminals
ElseBraced
EndIfBraced
+ IfChangedBlock
+ IfChangedBraced
+ EndIfChangedBraced
+
IfEqualBlock
IfEqualBraced
IfEqualExpression
@@ -136,6 +140,7 @@ Terminals
endfilter_keyword
endfor_keyword
endif_keyword
+ endifchanged_keyword
endifequal_keyword
endifnotequal_keyword
endspaceless_keyword
@@ -146,6 +151,7 @@ Terminals
for_keyword
identifier
if_keyword
+ ifchanged_keyword
ifequal_keyword
ifnotequal_keyword
in_keyword
@@ -208,6 +214,7 @@ Elements -> Elements ForBlock : '$1' ++ ['$2'].
Elements -> Elements IfBlock : '$1' ++ ['$2'].
Elements -> Elements IfEqualBlock : '$1' ++ ['$2'].
Elements -> Elements IfNotEqualBlock : '$1' ++ ['$2'].
+Elements -> Elements IfChangedBlock : '$1' ++ ['$2'].
Elements -> Elements IncludeTag : '$1' ++ ['$2'].
Elements -> Elements NowTag : '$1' ++ ['$2'].
Elements -> Elements SpacelessBlock : '$1' ++ ['$2'].
@@ -301,6 +308,11 @@ Unot -> not_keyword IfExpression : {expr, "not", '$2'}.
ElseBraced -> open_tag else_keyword close_tag.
EndIfBraced -> open_tag endif_keyword close_tag.
+IfChangedBlock -> IfChangedBraced Elements ElseBraced Elements EndIfChangedBraced : {ifchangedelse, '$2', '$4'}.
+IfChangedBlock -> IfChangedBraced Elements EndIfChangedBraced : {ifchanged, '$2'}.
+IfChangedBraced -> open_tag ifchanged_keyword close_tag.
+EndIfChangedBraced -> open_tag endifchanged_keyword close_tag.
+
IfEqualBlock -> IfEqualBraced Elements ElseBraced Elements EndIfEqualBraced : {ifequalelse, '$1', '$2', '$4'}.
IfEqualBlock -> IfEqualBraced Elements EndIfEqualBraced : {ifequal, '$1', '$2'}.
IfEqualBraced -> open_tag ifequal_keyword IfEqualExpression Value close_tag : ['$3', '$4'].
View
@@ -2,6 +2,8 @@
-compile(export_all).
+-define(IFCHANGED_CONTEXT_VARIABLE, erlydtl_ifchanged_context).
+
find_value(_, undefined) ->
undefined;
find_value(Key, Fun) when is_function(Fun, 1) ->
@@ -192,6 +194,35 @@ increment_counter_stats([{counter, Counter}, {counter0, Counter0}, {revcounter,
{first, false}, {last, RevCounter0 =:= 1},
{parentloop, Parent}].
+forloop(Fun, Acc0, Values) ->
+ push_ifchanged_context(),
+ Result = lists:mapfoldl(Fun, Acc0, Values),
+ pop_ifchanged_context(),
+ Result.
+
+push_ifchanged_context() ->
+ IfChangedContextStack = case get(?IFCHANGED_CONTEXT_VARIABLE) of
+ undefined -> [];
+ Stack -> Stack
+ end,
+ put(?IFCHANGED_CONTEXT_VARIABLE, [[]|IfChangedContextStack]).
+
+pop_ifchanged_context() ->
+ [_|Rest] = get(?IFCHANGED_CONTEXT_VARIABLE),
+ put(?IFCHANGED_CONTEXT_VARIABLE, Rest).
+
+ifchanged(SourceText, EvaluatedText, AlternativeText) ->
+ [IfChangedContext|Rest] = get(?IFCHANGED_CONTEXT_VARIABLE),
+ PreviousText = proplists:get_value(SourceText, IfChangedContext),
+ if
+ PreviousText =:= EvaluatedText ->
+ AlternativeText;
+ true ->
+ NewContext = [{SourceText, EvaluatedText}|proplists:delete(SourceText, IfChangedContext)],
+ put(?IFCHANGED_CONTEXT_VARIABLE, [NewContext|Rest]),
+ EvaluatedText
+ end.
+
cycle(NamesTuple, Counters) when is_tuple(NamesTuple) ->
element(fetch_value(counter0, Counters) rem size(NamesTuple) + 1, NamesTuple).
View
@@ -78,7 +78,7 @@ scan([], Scanned, _, in_text) ->
"if", "else", "endif", "not", "or", "and",
- %TODO "ifchanged",
+ "ifchanged", "endifchanged",
"ifequal", "endifequal",
View
@@ -46,6 +46,16 @@ unparse([{'if', Expression, Contents}|Rest], Acc) ->
unparse(Rest, [["{% if ", unparse_expression(Expression), " %}",
unparse(Contents),
"{% endif %}"]|Acc]);
+unparse([{'ifchanged', IfContents}|Rest], Acc) ->
+ unparse(Rest, [["{% ifchanged %}",
+ unparse(IfContents),
+ "{% endif %}"]|Acc]);
+unparse([{'ifchangedelse', IfContents, ElseContents}|Rest], Acc) ->
+ unparse(Rest, [["{% ifchanged %}",
+ unparse(IfContents),
+ "{% else %}",
+ unparse(ElseContents),
+ "{% endif %}"]|Acc]);
unparse([{'ifelse', Expression, IfContents, ElseContents}|Rest], Acc) ->
unparse(Rest, [["{% if ", unparse_expression(Expression), " %}",
unparse(IfContents),
@@ -237,7 +237,13 @@ tests() ->
<<"Al\nAlbert\nJo\nJoseph\n">>},
{"Access parent loop counters",
<<"{% 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">>}
+ [{'list', [["One", "two"], ["One", "two"]]}], <<"(0, 0)\n(0, 1)\n(1, 0)\n(1, 1)\n">>},
+ {"If changed",
+ <<"{% for x in list %}{% ifchanged %}{{ x }}\n{% endifchanged %}{% endfor %}">>,
+ [{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nthree\n">>},
+ {"If changed/else",
+ <<"{% for x in list %}{% ifchanged %}{{ x }}\n{% else %}foo\n{% endifchanged %}{% endfor %}">>,
+ [{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nfoo\nthree\nfoo\nfoo\n">>}
]},
{"for/empty", [
{"Simple loop",

0 comments on commit 1b16fa0

Please sign in to comment.