Browse Source

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.
Evan Miller 13 years ago
parent
commit
1b16fa011b

+ 2 - 2
README.markdown

@@ -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
 

+ 33 - 19
src/erlydtl_compiler.erl

@@ -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

+ 12 - 0
src/erlydtl_parser.yrl

@@ -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'].

+ 31 - 0
src/erlydtl_runtime.erl

@@ -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).
 

+ 1 - 1
src/erlydtl_scanner.erl

@@ -78,7 +78,7 @@ scan([], Scanned, _, in_text) ->
 
                             "if", "else", "endif", "not", "or", "and", 
 
-                            %TODO "ifchanged", 
+                            "ifchanged", "endifchanged",
                             
                             "ifequal", "endifequal", 
 

+ 10 - 0
src/erlydtl_unparser.erl

@@ -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),

+ 7 - 1
tests/src/erlydtl_unittests.erl

@@ -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",