Browse Source

Support for {% regroup %} tag.

Evan Miller 13 years ago
parent
commit
c624a50107

+ 10 - 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, ifchanged, 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, regroup, spaceless, ssi, templatetag, trans, widthratio, with
 
-_Unsupported tags_: csrf_token, regroup, url
+_Unsupported tags_: csrf_token, 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
 
@@ -158,6 +158,14 @@ file. Useful for frameworks that recompile a template only when the
 template's dependencies change.
 
 
+Differences from standard Django Template Language
+--------------------------------------------------
+
+The "regroup" tag must have an ending "endregroup" tag.
+
+The "ifchanged" tag cannot take arguments.
+
+
 Tests
 -----
 

+ 31 - 14
src/erlydtl_compiler.erl

@@ -574,6 +574,8 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
                 include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, Context, TreeWalkerAcc);
             ({'include_only', {string_literal, _, File}, Args}, TreeWalkerAcc) ->
                 include_ast(unescape_string_literal(File), Args, [], Context, TreeWalkerAcc);
+            ({'regroup', {ListVariable, {identifier, _, Attribute}, {identifier, _, NewVariable}}, Contents}, TreeWalkerAcc) ->
+                regroup_ast(ListVariable, Attribute, NewVariable, Contents, Context, TreeWalkerAcc);
             ({'spaceless', Contents}, TreeWalkerAcc) ->
                 spaceless_ast(Contents, Context, TreeWalkerAcc);
             ({'ssi', Arg}, TreeWalkerAcc) ->
@@ -1009,8 +1011,23 @@ with_ast(ArgList, Contents, Context, TreeWalker) ->
 
     {{erl_syntax:application(
                 erl_syntax:fun_expr([
-            erl_syntax:clause(lists:map(fun({_, Var}) -> Var end, NewScope), none,
-                [InnerAst])]), ArgAstList), merge_info(ArgInfo, InnerInfo)}, TreeWalker2}.
+                        erl_syntax:clause(lists:map(fun({_, Var}) -> Var end, NewScope), none,
+                            [InnerAst])]), ArgAstList), merge_info(ArgInfo, InnerInfo)}, TreeWalker2}.
+
+regroup_ast(ListVariable, AttributeName, LocalVarName, Contents, Context, TreeWalker) ->
+    {{ListAst, ListInfo}, TreeWalker1} = value_ast(ListVariable, false, Context, TreeWalker),
+    NewScope = [{LocalVarName, erl_syntax:variable(lists:concat(["Var_", LocalVarName]))}],
+
+    {{InnerAst, InnerInfo}, TreeWalker2} = body_ast(Contents, 
+        Context#dtl_context{ local_scopes = [NewScope|Context#dtl_context.local_scopes] }, TreeWalker1),
+
+    {{erl_syntax:application(
+                erl_syntax:fun_expr([
+                        erl_syntax:clause([erl_syntax:variable(lists:concat(["Var_", LocalVarName]))], none,
+                            [InnerAst])]), 
+                [erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(regroup),
+                        [ListAst, erl_syntax:atom(AttributeName)])]), merge_info(ListInfo, InnerInfo)},
+        TreeWalker2}.
 
 for_loop_ast(IteratorList, LoopValue, Contents, {EmptyContentsAst, EmptyContentsInfo}, Context, TreeWalker) ->
     Vars = lists:map(fun({identifier, _, Iterator}) -> 
@@ -1063,21 +1080,21 @@ ifchanged_ast(ParseTree, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, Else
             merge_info(IfContentsInfo, ElseContentsInfo)}, TreeWalker}.
 
 cycle_ast(Names, Context, TreeWalker) ->
-    NamesTuple = lists:map(fun
-            ({string_literal, _, Str}) ->
+    {NamesTuple, VarNames} = lists:mapfoldl(fun
+            ({string_literal, _, Str}, VarNamesAcc) ->
                 {{S, _}, _} = string_ast(unescape_string_literal(Str), Context, TreeWalker),
-                S;
-            ({variable, _}=Var) ->
-                {V, _} = resolve_variable_ast(Var, Context),
-                V;
-            ({number_literal, _, Num}) ->
-                format(erl_syntax:integer(Num), Context, TreeWalker);
-            (_) ->
-                []
-        end, Names),
+                {S, VarNamesAcc};
+            ({variable, _}=Var, VarNamesAcc) ->
+                {V, VarName} = resolve_variable_ast(Var, Context),
+                {V, [VarName|VarNamesAcc]};
+            ({number_literal, _, Num}, VarNamesAcc) ->
+                {format(erl_syntax:integer(Num), Context, TreeWalker), VarNamesAcc};
+            (_, VarNamesAcc) ->
+                {[], VarNamesAcc}
+        end, [], Names),
     {{erl_syntax:application(
         erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('cycle'),
-        [erl_syntax:tuple(NamesTuple), erl_syntax:variable("Counters")]), #ast_info{}}, TreeWalker}.
+        [erl_syntax:tuple(NamesTuple), erl_syntax:variable("Counters")]), #ast_info{ var_names = VarNames }}, TreeWalker}.
 
 %% Older Django templates treat cycle with comma-delimited elements as strings
 cycle_compat_ast(Names, Context, TreeWalker) ->

+ 13 - 0
src/erlydtl_parser.yrl

@@ -100,6 +100,10 @@ Nonterminals
     CustomTag
     Args
 
+    RegroupBlock
+    RegroupBraced
+    EndRegroupBraced
+
     SpacelessBlock
 
     SSITag
@@ -123,9 +127,11 @@ Nonterminals
 
 Terminals
     and_keyword
+    as_keyword
     autoescape_keyword
     block_keyword
     blocktrans_keyword
+    by_keyword
     call_keyword
     close_tag
     close_var
@@ -143,6 +149,7 @@ Terminals
     endifchanged_keyword
     endifequal_keyword
     endifnotequal_keyword
+    endregroup_keyword
     endspaceless_keyword
     endwith_keyword
     extends_keyword
@@ -165,6 +172,7 @@ Terminals
     open_tag
     open_var
     parsed_keyword
+    regroup_keyword
     spaceless_keyword
     ssi_keyword
     string_literal
@@ -217,6 +225,7 @@ 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 SpacelessBlock : '$1' ++ ['$2'].
 Elements -> Elements SSITag : '$1' ++ ['$2'].
 Elements -> Elements TemplatetagTag : '$1' ++ ['$2'].
@@ -325,6 +334,10 @@ 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 identifier as_keyword identifier close_tag : {'$3', '$5', '$7'}.
+EndRegroupBraced -> open_tag endregroup_keyword close_tag.
+
 SpacelessBlock -> open_tag spaceless_keyword close_tag Elements open_tag endspaceless_keyword close_tag : {spaceless, '$4'}.
 
 SSITag -> open_tag ssi_keyword Value close_tag : {ssi, '$3'}.

+ 17 - 0
src/erlydtl_runtime.erl

@@ -52,6 +52,23 @@ fetch_value(Key, Data) ->
             Val
     end.
 
+regroup(List, Attribute) ->
+    regroup(List, Attribute, []).
+
+regroup([], _, []) ->
+    [];
+regroup([], _, [[{grouper, LastGrouper}, {list, LastList}]|Acc]) ->
+    lists:reverse([[{grouper, LastGrouper}, {list, lists:reverse(LastList)}]|Acc]);
+regroup([Item|Rest], Attribute, []) ->
+    regroup(Rest, Attribute, [[{grouper, find_value(Attribute, Item)}, {list, [Item]}]]);
+regroup([Item|Rest], Attribute, [[{grouper, PrevGrouper}, {list, PrevList}]|Acc]) ->
+    case find_value(Attribute, Item) of
+        Value when Value =:= PrevGrouper ->
+            regroup(Rest, Attribute, [[{grouper, PrevGrouper}, {list, [Item|PrevList]}]|Acc]);
+        Value ->
+            regroup(Rest, Attribute, [[{grouper, Value}, {list, [Item]}], [{grouper, PrevGrouper}, {list, lists:reverse(PrevList)}]|Acc])
+    end.
+
 translate(_, none, Default) ->
     Default;
 translate(String, TranslationFun, Default) when is_function(TranslationFun) ->

+ 1 - 1
src/erlydtl_scanner.erl

@@ -88,7 +88,7 @@ scan([], Scanned, _, in_text) ->
 
                             "now", 
 
-                            %TODO "regroup", 
+                            "regroup", "endregroup", "as", "by",
                             
                             "spaceless", "endspaceless", 
                             

+ 4 - 0
src/erlydtl_unparser.erl

@@ -90,6 +90,10 @@ unparse([{'include_only', Value, []}|Rest], Acc) ->
     unparse(Rest, [["{% include ", unparse_value(Value), " only %}"]|Acc]);
 unparse([{'include_only', Value, Args}|Rest], Acc) ->
     unparse(Rest, [["{% include ", unparse_value(Value), " with ", unparse_args(Args), " only %}"]|Acc]);
+unparse([{'regroup', {Variable, Identifier1, Identifier2}, Contents}|Rest], Acc) ->
+    unparse(Rest, [["{% regroup ", unparse_value(Variable), " by ", unparse_identifier(Identifier1), " as ", unparse_identifier(Identifier2), " %}",
+                unparse(Contents),
+                "{% endregroup %}"]|Acc]);
 unparse([{'spaceless', Contents}|Rest], Acc) ->
     unparse(Rest, [["{% spaceless %}", unparse(Contents), "{% endspaceless %}"]|Acc]);
 unparse([{'ssi', Arg}|Rest], Acc) ->

+ 13 - 0
tests/src/erlydtl_unittests.erl

@@ -916,6 +916,19 @@ tests() ->
                 [],
                 <<"baz">>}
         ]},
+    {"regroup", [
+            {"Ordered", <<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}{% endregroup %}">>, 
+                [{people, [[{first_name, "George"}, {gender, "Male"}], [{first_name, "Bill"}, {gender, "Male"}],
+                            [{first_name, "Margaret"}, {gender, "Female"}], [{first_name, "Condi"}, {gender, "Female"}]]}],
+                <<"Male\nGeorge\nBill\nFemale\nMargaret\nCondi\n">>},
+            {"Unordered", <<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}{% endregroup %}">>, 
+                [{people, [[{first_name, "George"}, {gender, "Male"}], 
+                            [{first_name, "Margaret"}, {gender, "Female"}], 
+                            [{first_name, "Condi"}, {gender, "Female"}],
+                            [{first_name, "Bill"}, {gender, "Male"}]
+                        ]}],
+                <<"Male\nGeorge\nFemale\nMargaret\nCondi\nMale\nBill\n">>}
+        ]},
     {"spaceless", [
             {"Beginning", <<"{% spaceless %}    <b>foo</b>{% endspaceless %}">>, [], <<"<b>foo</b>">>},
             {"Middle", <<"{% spaceless %}<b>foo</b>  <b>bar</b>{% endspaceless %}">>, [], <<"<b>foo</b><b>bar</b>">>},