Skip to content

Commit

Permalink
Support for {% ifchanged %} tag
Browse files Browse the repository at this point in the history
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
Evan Miller committed Feb 19, 2012
1 parent dc377d7 commit 1b16fa0
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 23 deletions.
4 changes: 2 additions & 2 deletions README.markdown
Expand Up @@ -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

Expand Down
52 changes: 33 additions & 19 deletions src/erlydtl_compiler.erl
Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/erlydtl_parser.yrl
Expand Up @@ -83,6 +83,10 @@ Nonterminals
ElseBraced
EndIfBraced

IfChangedBlock
IfChangedBraced
EndIfChangedBraced

IfEqualBlock
IfEqualBraced
IfEqualExpression
Expand Down Expand Up @@ -136,6 +140,7 @@ Terminals
endfilter_keyword
endfor_keyword
endif_keyword
endifchanged_keyword
endifequal_keyword
endifnotequal_keyword
endspaceless_keyword
Expand All @@ -146,6 +151,7 @@ Terminals
for_keyword
identifier
if_keyword
ifchanged_keyword
ifequal_keyword
ifnotequal_keyword
in_keyword
Expand Down Expand Up @@ -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'].
Expand Down Expand Up @@ -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'].
Expand Down
31 changes: 31 additions & 0 deletions src/erlydtl_runtime.erl
Expand Up @@ -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) ->
Expand Down Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion src/erlydtl_scanner.erl
Expand Up @@ -78,7 +78,7 @@ scan([], Scanned, _, in_text) ->

"if", "else", "endif", "not", "or", "and",

%TODO "ifchanged",
"ifchanged", "endifchanged",

"ifequal", "endifequal",

Expand Down
10 changes: 10 additions & 0 deletions src/erlydtl_unparser.erl
Expand Up @@ -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),
Expand Down
8 changes: 7 additions & 1 deletion tests/src/erlydtl_unittests.erl
Expand Up @@ -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",
Expand Down

0 comments on commit 1b16fa0

Please sign in to comment.