Commits

Anonymous committed 350ab0f

Support for "cycle" tag. Patch from Hunter Morris.

  • Participants
  • Parent commits b49b42a

Comments (0)

Files changed (7)

examples/docroot/cycle

+before
+
+<ul>
+{% for i in test %}
+<li>{{ forloop.counter }}. {{ i }} - {% cycle a b c %}</li>
+{% endfor %}
+</ul>
+
+after

src/erlydtl/erlydtl_compiler.erl

             ({'call', {'identifier', _, Name}}, TreeWalkerAcc) ->
             	call_ast(Name, TreeWalkerAcc);
             ({'call', {'identifier', _, Name}, With}, TreeWalkerAcc) ->
-            	call_with_ast(Name, With, Context, TreeWalkerAcc)
+            	call_with_ast(Name, With, Context, TreeWalkerAcc);
+            ({'cycle', Names}, TreeWalkerAcc) ->
+                cycle_ast(Names, Context, TreeWalkerAcc);
+            ({'cycle_compat', Names}, TreeWalkerAcc) ->
+                cycle_compat_ast(Names, Context, TreeWalkerAcc)
         end, TreeWalker, DjangoParseTree),   
     {AstList, {Info, TreeWalker3}} = lists:mapfoldl(
         fun({Ast, Info}, {InfoAcc, TreeWalkerAcc}) -> 
     CustomTags = lists:merge([X || {identifier, _ , X} <- Names], TreeWalker#treewalker.custom_tags),
     {{erl_syntax:list([]), #ast_info{}}, TreeWalker#treewalker{custom_tags = CustomTags}}.  
 
+cycle_ast(Names, Context, TreeWalker) ->
+    NamesTuple = lists:map(fun({string_literal, _, Str}) ->
+                                   erl_syntax:string(unescape_string_literal(Str));
+                              ({variable, _}=Var) ->
+                                   {V, _} = resolve_variable_ast(Var, Context),
+                                   V;
+                              ({number_literal, _, Num}) ->
+                                   format(erl_syntax:integer(Num), Context);
+                              (_) ->
+                                   []
+                           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}.
+
+%% Older Django templates treat cycle with comma-delimited elements as strings
+cycle_compat_ast(Names, _Context, TreeWalker) ->
+    NamesTuple = [erl_syntax:string(X) || {identifier, _, X} <- Names],
+    {{erl_syntax:application(
+        erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('cycle'),
+        [erl_syntax:tuple(NamesTuple), erl_syntax:variable("Counters")]), #ast_info{}}, TreeWalker}.
 
 unescape_string_literal(String) ->
     unescape_string_literal(string:strip(String, both, 34), [], noslash).

src/erlydtl/erlydtl_parser.yrl

     CommentBraced
     EndCommentBraced
 
+    CycleTag
+    CycleNames
+    CycleNamesCompat
+
     ForBlock
     ForBraced
     EndForBraced
     comment_keyword
     colon
     comma
+    cycle_keyword
     dot
     else_keyword
     endautoescape_keyword
 Elements -> Elements IncludeTag : '$1' ++ ['$2'].
 Elements -> Elements NowTag : '$1' ++ ['$2'].
 Elements -> Elements LoadTag : '$1' ++ ['$2'].
+Elements -> Elements CycleTag : '$1' ++ ['$2'].
 Elements -> Elements BlockBlock : '$1' ++ ['$2'].
 Elements -> Elements ForBlock : '$1' ++ ['$2'].
 Elements -> Elements IfBlock : '$1' ++ ['$2'].
 CommentBraced -> open_tag comment_keyword close_tag.
 EndCommentBraced -> open_tag endcomment_keyword close_tag.
 
+CycleTag -> open_tag cycle_keyword CycleNamesCompat close_tag : {cycle_compat, '$3'}.
+CycleTag -> open_tag cycle_keyword CycleNames close_tag : {cycle, '$3'}.
+
+CycleNames -> Value : ['$1'].
+CycleNames -> CycleNames Value : '$1' ++ ['$2'].
+
+CycleNamesCompat -> identifier comma : ['$1'].
+CycleNamesCompat -> CycleNamesCompat identifier comma : '$1' ++ ['$2'].
+CycleNamesCompat -> CycleNamesCompat identifier : '$1' ++ ['$2'].
+
 ForBlock -> ForBraced Elements EndForBraced : {for, '$1', '$2'}.
 ForBraced -> open_tag for_keyword ForExpression close_tag : '$3'.
 EndForBraced -> open_tag endfor_keyword close_tag.

src/erlydtl/erlydtl_runtime.erl

         {revcounter0, RevCounter0 - 1},
         {first, false}, {last, RevCounter0 =:= 1},
         {parentloop, Parent}].
+
+cycle(NamesTuple, Counters) when is_tuple(NamesTuple) ->
+    element(fetch_value(counter0, Counters) rem size(NamesTuple) + 1, NamesTuple).

src/erlydtl/erlydtl_scanner.erl

 scan("\"" ++ T, Scanned, {Row, Column}, {in_identifier, Closer}) ->
     scan(T, [{string_literal, {Row, Column}, "\""} | Scanned], {Row, Column + 1}, {in_double_quote, Closer});
 
+scan("\'" ++ T, Scanned, {Row, Column}, {in_code, Closer}) ->
+    scan(T, [{string_literal, {Row, Column}, "\""} | Scanned], {Row, Column + 1}, {in_single_quote, Closer});
+
+scan("\'" ++ T, Scanned, {Row, Column}, {in_identifier, Closer}) ->
+    scan(T, [{string_literal, {Row, Column}, "\""} | Scanned], {Row, Column + 1}, {in_single_quote, Closer});
+
 scan([$\\ | T], Scanned, {Row, Column}, {in_double_quote, Closer}) ->
     scan(T, append_char(Scanned, $\\), {Row, Column + 1}, {in_double_quote_slash, Closer});
 
 scan([H | T], Scanned, {Row, Column}, {in_double_quote_slash, Closer}) ->
     scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_double_quote, Closer});
 
+scan([$\\ | T], Scanned, {Row, Column}, {in_single_quote, Closer}) ->
+    scan(T, append_char(Scanned, $\\), {Row, Column + 1}, {in_single_quote_slash, Closer});
+
+scan([H | T], Scanned, {Row, Column}, {in_single_quote_slash, Closer}) ->
+    scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_single_quote, Closer});
+
 % end quote
 scan("\"" ++ T, Scanned, {Row, Column}, {in_double_quote, Closer}) ->
     scan(T, append_char(Scanned, 34), {Row, Column + 1}, {in_code, Closer});
 
+% treat single quotes the same as double quotes
+scan("\'" ++ T, Scanned, {Row, Column}, {in_single_quote, Closer}) ->
+    scan(T, append_char(Scanned, 34), {Row, Column + 1}, {in_code, Closer});
+
 scan([H | T], Scanned, {Row, Column}, {in_double_quote, Closer}) ->
     scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_double_quote, Closer});
 
+scan([H | T], Scanned, {Row, Column}, {in_single_quote, Closer}) ->
+    scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_single_quote, Closer});
+
 
 scan("," ++ T, Scanned, {Row, Column}, {_, Closer}) ->
     scan(T, [{comma, {Row, Column}, ","} | Scanned], {Row, Column + 1}, {in_code, Closer});

src/tests/erlydtl_functional_tests.erl

 %%% @author    Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
 %%% @author    Evan Miller <emmiller@gmail.com>
 %%% @copyright 2008 Roberto Saccon, Evan Miller
-%%% @doc       ErlyDTS test suite
+%%% @doc       ErlyDTL test suite
 %%% @end
 %%%
 %%% The MIT License
 setup("var_error") ->
     CompileVars = [],
     RenderVars = [{var1, "foostring1"}],   
-    {ok, CompileVars, error, RenderVars}; 
+    {ok, CompileVars, error, RenderVars};
+setup("cycle") ->
+    CompileVars = [],
+    RenderVars = [{test, [integer_to_list(X) || X <- lists:seq(1, 20)]},
+                  {a, "Apple"}, {b, "Banana"}, {c, "Cherry"}],
+    {ok, CompileVars, ok, RenderVars};
 %%--------------------------------------------------------------------       
 %% Custom tags
 %%--------------------------------------------------------------------

src/tests/erlydtl_unittests.erl

                 {"Newlines are escaped",
                     <<"{{ \"foo\\n\" }}">>, [], <<"foo\n">>}
             ]},
+        {"cycle", [
+                {"Cycling through quoted strings",
+                    <<"{% for i in test %}{% cycle 'a' 'b' %}{{ i }},{% endfor %}">>,
+                    [{test, ["0", "1", "2", "3", "4"]}], <<"a0,b1,a2,b3,a4,">>},
+                {"Cycling through normal variables",
+                    <<"{% for i in test %}{% cycle aye bee %}{{ i }},{% endfor %}">>,
+                    [{test, ["0", "1", "2", "3", "4"]}, {aye, "a"}, {bee, "b"}],
+                    <<"a0,b1,a2,b3,a4,">>}
+            ]},
         {"number literal", [
                 {"Render integer",
                     <<"{{ 5 }}">>, [], <<"5">>}