freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [7/9] incubator-freemarker git commit: Added the continue directive, which can be used inside a list to skip to the next iteration (similarly as in Java).
Date Sun, 17 Sep 2017 10:56:45 GMT
Added the continue directive, which can be used inside a list to skip to the next iteration
(similarly as in Java).


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/da570caa
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/da570caa
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/da570caa

Branch: refs/heads/2.3
Commit: da570caa85c9b17ece822fe72fcce22502f57b54
Parents: 3dfa8ce
Author: ddekany <ddekany@apache.org>
Authored: Sun Sep 17 12:46:02 2017 +0200
Committer: ddekany <ddekany@apache.org>
Committed: Sun Sep 17 12:46:02 2017 +0200

----------------------------------------------------------------------
 .../java/freemarker/core/BreakInstruction.java  |   8 +-
 .../core/BreakOrContinueException.java          |  12 ++
 .../freemarker/core/ContinueInstruction.java    |  64 ++++++++++
 .../java/freemarker/core/IteratorBlock.java     | 102 ++++++++--------
 src/main/java/freemarker/core/SwitchBlock.java  |   2 +-
 src/main/java/freemarker/core/_CoreAPI.java     |   1 +
 src/main/javacc/FTL.jj                          |  41 +++++++
 src/manual/en_US/book.xml                       | 116 ++++++++++++++++---
 .../core/BreakAndContinuePlacementTest.java     |  75 ++++++++++++
 .../freemarker/core/BreakPlacementTest.java     |  68 -----------
 .../freemarker/core/ListBreakContinueTest.java  |  93 +++++++++++++++
 11 files changed, 443 insertions(+), 139 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/BreakInstruction.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/BreakInstruction.java b/src/main/java/freemarker/core/BreakInstruction.java
index b41f05b..e18f6d0 100644
--- a/src/main/java/freemarker/core/BreakInstruction.java
+++ b/src/main/java/freemarker/core/BreakInstruction.java
@@ -26,7 +26,7 @@ final class BreakInstruction extends TemplateElement {
 
     @Override
     TemplateElement[] accept(Environment env) {
-        throw Break.INSTANCE;
+        throw BreakOrContinueException.BREAK_INSTANCE;
     }
 
     @Override
@@ -54,12 +54,6 @@ final class BreakInstruction extends TemplateElement {
         throw new IndexOutOfBoundsException();
     }
     
-    static class Break extends RuntimeException {
-        static final Break INSTANCE = new Break();
-        private Break() {
-        }
-    }
-
     @Override
     boolean isNestedBlockRepeater() {
         return false;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/BreakOrContinueException.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/BreakOrContinueException.java b/src/main/java/freemarker/core/BreakOrContinueException.java
new file mode 100644
index 0000000..86bfbd8
--- /dev/null
+++ b/src/main/java/freemarker/core/BreakOrContinueException.java
@@ -0,0 +1,12 @@
+package freemarker.core;
+
+/**
+ * Used for implementing #break and #continue. 
+ */
+class BreakOrContinueException extends RuntimeException {
+    
+    static final BreakOrContinueException BREAK_INSTANCE = new BreakOrContinueException();
+    static final BreakOrContinueException CONTINUE_INSTANCE = new BreakOrContinueException();
+    
+    private BreakOrContinueException() { }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/ContinueInstruction.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/ContinueInstruction.java b/src/main/java/freemarker/core/ContinueInstruction.java
new file mode 100644
index 0000000..21ff84f
--- /dev/null
+++ b/src/main/java/freemarker/core/ContinueInstruction.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+/**
+ * Represents a &lt;break&gt; instruction to break out of a loop.
+ */
+final class ContinueInstruction extends TemplateElement {
+
+    @Override
+    TemplateElement[] accept(Environment env) {
+        throw BreakOrContinueException.CONTINUE_INSTANCE;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        return canonical ? "<" + getNodeTypeSymbol() + "/>" : getNodeTypeSymbol();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "#continue";
+    }
+
+    @Override
+    int getParameterCount() {
+        return 0;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        throw new IndexOutOfBoundsException();
+    }
+    
+    @Override
+    boolean isNestedBlockRepeater() {
+        return false;
+    }
+    
+}
+
+

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/IteratorBlock.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/IteratorBlock.java b/src/main/java/freemarker/core/IteratorBlock.java
index 0d9fcec..83c0b22 100644
--- a/src/main/java/freemarker/core/IteratorBlock.java
+++ b/src/main/java/freemarker/core/IteratorBlock.java
@@ -284,16 +284,18 @@ final class IteratorBlock extends TemplateElement {
                 listNotEmpty = iterModel.hasNext();
                 if (listNotEmpty) {
                     if (loopVarName != null) {
-                        try {
-                            do {
+                            listLoop: do {
                                 loopVar = iterModel.next();
                                 hasNext = iterModel.hasNext();
-                                env.visit(childBuffer);
+                                try {
+                                    env.visit(childBuffer);
+                                } catch (BreakOrContinueException br) {
+                                    if (br == BreakOrContinueException.BREAK_INSTANCE) {
+                                        break listLoop;
+                                    }
+                                }
                                 index++;
                             } while (hasNext);
-                        } catch (BreakInstruction.Break br) {
-                            // Silently exit loop
-                        }
                         openedIterator = null;
                     } else {
                         // We must reuse this later, because TemplateCollectionModel-s that
wrap an Iterator only
@@ -308,15 +310,17 @@ final class IteratorBlock extends TemplateElement {
                 listNotEmpty = size != 0;
                 if (listNotEmpty) {
                     if (loopVarName != null) {
-                        try {
-                            for (index = 0; index < size; index++) {
+                            listLoop: for (index = 0; index < size; index++) {
                                 loopVar = seqModel.get(index);
                                 hasNext = (size > index + 1);
-                                env.visit(childBuffer);
+                                try {
+                                    env.visit(childBuffer);
+                                } catch (BreakOrContinueException br) {
+                                    if (br == BreakOrContinueException.BREAK_INSTANCE) {
+                                        break listLoop;
+                                    }
+                                }
                             }
-                        } catch (BreakInstruction.Break br) {
-                            // Silently exit loop
-                        }
                     } else {
                         env.visit(childBuffer);
                     }
@@ -329,7 +333,7 @@ final class IteratorBlock extends TemplateElement {
                 }
                 try {
                     env.visit(childBuffer);
-                } catch (BreakInstruction.Break br) {
+                } catch (BreakOrContinueException br) {
                     // Silently exit "loop"
                 }
             } else if (listedValue instanceof TemplateHashModelEx
@@ -359,18 +363,20 @@ final class IteratorBlock extends TemplateElement {
                     hashNotEmpty = kvpIter.hasNext();
                     if (hashNotEmpty) {
                         if (loopVarName != null) {
-                            try {
-                                do {
-                                    KeyValuePair kvp = kvpIter.next();
-                                    loopVar = kvp.getKey();
-                                    loopVar2 = kvp.getValue();
-                                    hasNext = kvpIter.hasNext();
+                            listLoop: do {
+                                KeyValuePair kvp = kvpIter.next();
+                                loopVar = kvp.getKey();
+                                loopVar2 = kvp.getValue();
+                                hasNext = kvpIter.hasNext();
+                                try {
                                     env.visit(childBuffer);
-                                    index++;
-                                } while (hasNext);
-                            } catch (BreakInstruction.Break br) {
-                                // Silently exit loop
-                            }
+                                } catch (BreakOrContinueException br) {
+                                    if (br == BreakOrContinueException.BREAK_INSTANCE) {
+                                        break listLoop;
+                                    }
+                                }
+                                index++;
+                            } while (hasNext);
                             openedIterator = null;
                         } else {
                             // We will reuse this at the #iterms
@@ -383,30 +389,32 @@ final class IteratorBlock extends TemplateElement {
                     hashNotEmpty = keysIter.hasNext();
                     if (hashNotEmpty) {
                         if (loopVarName != null) {
-                            try {
-                                do {
-                                    loopVar = keysIter.next();
-                                    if (!(loopVar instanceof TemplateScalarModel)) {
-                                        throw new NonStringException(env,
-                                                new _ErrorDescriptionBuilder(
-                                                        "When listing key-value pairs of
traditional hash "
-                                                        + "implementations, all keys must
be strings, but one of them "
-                                                        + "was ",
-                                                        new _DelayedAOrAn(new _DelayedFTLTypeDescription(loopVar)),
"."
-                                                        ).tip("The listed value's TemplateModel
class was ",
-                                                                new _DelayedShortClassName(listedValue.getClass()),
-                                                                ", which doesn't implement
",
-                                                                new _DelayedShortClassName(TemplateHashModelEx2.class),
-                                                                ", which leads to this restriction."));
-                                    }
-                                    loopVar2 = listedHash.get(((TemplateScalarModel) loopVar).getAsString());
-                                    hasNext = keysIter.hasNext();
+                            listLoop: do {
+                                loopVar = keysIter.next();
+                                if (!(loopVar instanceof TemplateScalarModel)) {
+                                    throw new NonStringException(env,
+                                            new _ErrorDescriptionBuilder(
+                                                    "When listing key-value pairs of traditional
hash "
+                                                    + "implementations, all keys must be
strings, but one of them "
+                                                    + "was ",
+                                                    new _DelayedAOrAn(new _DelayedFTLTypeDescription(loopVar)),
"."
+                                                    ).tip("The listed value's TemplateModel
class was ",
+                                                            new _DelayedShortClassName(listedValue.getClass()),
+                                                            ", which doesn't implement ",
+                                                            new _DelayedShortClassName(TemplateHashModelEx2.class),
+                                                            ", which leads to this restriction."));
+                                }
+                                loopVar2 = listedHash.get(((TemplateScalarModel) loopVar).getAsString());
+                                hasNext = keysIter.hasNext();
+                                try {
                                     env.visit(childBuffer);
-                                    index++;
-                                } while (hasNext);
-                            } catch (BreakInstruction.Break br) {
-                                // Silently exit loop
-                            }
+                                } catch (BreakOrContinueException br) {
+                                    if (br == BreakOrContinueException.BREAK_INSTANCE) {
+                                        break listLoop;
+                                    }
+                                }
+                                index++;
+                            } while (hasNext);
                         } else {
                             env.visit(childBuffer);
                         }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/SwitchBlock.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/SwitchBlock.java b/src/main/java/freemarker/core/SwitchBlock.java
index 3f8a320..19c6a3e 100644
--- a/src/main/java/freemarker/core/SwitchBlock.java
+++ b/src/main/java/freemarker/core/SwitchBlock.java
@@ -86,7 +86,7 @@ final class SwitchBlock extends TemplateElement {
             if (!processedCase && defaultCase != null) {
                 env.visit(defaultCase);
             }
-        } catch (BreakInstruction.Break br) {}
+        } catch (BreakOrContinueException br) {}
         return null;
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/_CoreAPI.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index 1f10f81..3491568 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -78,6 +78,7 @@ public class _CoreAPI {
         addName(allNames, lcNames, ccNames, "case");
         addName(allNames, lcNames, ccNames, "comment");
         addName(allNames, lcNames, ccNames, "compress");
+        addName(allNames, lcNames, ccNames, "continue");
         addName(allNames, lcNames, ccNames, "default");
         addName(allNames, lcNames, ccNames, "else");
         addName(allNames, lcNames, ccNames, "elseif", "elseIf");

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index b7bdd48..ddbf78f 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -85,6 +85,11 @@ public class FMParser {
      */
     private int breakableDirectiveNesting;
     
+    /**
+     * Keeps track of the nesting depth of directives that support #continue.
+     */
+    private int continuableDirectiveNesting;
+    
     private boolean inMacro, inFunction;
     private LinkedList escapes = new LinkedList();
     private int mixedContentNesting; // for stripText
@@ -1019,6 +1024,8 @@ TOKEN:
     |
     <BREAK : <START_TAG> "break" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken,
DEFAULT); }
     |
+    <CONTINUE : <START_TAG> "continue" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken,
DEFAULT); }
+    |
     <SIMPLE_RETURN : <START_TAG> "return" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken,
DEFAULT); }
     |
     <HALT : <START_TAG> "stop" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken,
DEFAULT); }
@@ -2580,6 +2587,7 @@ TemplateElement List() :
         if (loopVar != null) {
             iterCtx.loopVarName = loopVar.image;
             breakableDirectiveNesting++;
+            continuableDirectiveNesting++;
             if (loopVar2 != null) {
                 iterCtx.loopVar2Name = loopVar2.image;
                 iterCtx.hashListing = true;
@@ -2596,6 +2604,7 @@ TemplateElement List() :
     {
         if (loopVar != null) {
             breakableDirectiveNesting--;
+            continuableDirectiveNesting--;
         } else if (iterCtx.kind != ITERATOR_BLOCK_KIND_ITEMS) {
             throw new ParseException(
                     "#list must have either \"as loopVar\" parameter or nested #items that
belongs to it.",
@@ -2660,6 +2669,7 @@ IteratorBlock ForEach() :
         iterCtx.loopVarName = loopVar.image;
         iterCtx.kind = ITERATOR_BLOCK_KIND_FOREACH;
         breakableDirectiveNesting++;
+        continuableDirectiveNesting++;
     }
     
     children = MixedContentElements()
@@ -2667,6 +2677,7 @@ IteratorBlock ForEach() :
     end = <END_FOREACH>
     {
         breakableDirectiveNesting--;
+        continuableDirectiveNesting--;
         popIteratorBlockContext();
                 
         IteratorBlock result = new IteratorBlock(exp, loopVar.image, null, children, false,
true);
@@ -2718,6 +2729,7 @@ Items Items() :
         }
     
         breakableDirectiveNesting++;
+        continuableDirectiveNesting++;
     }
     
     children = MixedContentElements()
@@ -2725,6 +2737,7 @@ Items Items() :
     end = <END_ITEMS>
     {
         breakableDirectiveNesting--;
+        continuableDirectiveNesting--;
         iterCtx.loopVarName = null;
         iterCtx.loopVar2Name = null;
         
@@ -2851,6 +2864,27 @@ BreakInstruction Break() :
 }
 
 /**
+ * Production used to skip an iteration in a loop.
+ */
+ContinueInstruction Continue() :
+{
+    Token start;
+}
+{
+    start = <CONTINUE>
+    {
+        if (continuableDirectiveNesting < 1) {
+            throw new ParseException(start.image + " must be nested inside a directive that
supports it: " 
+                    + " #list with \"as\", #items (or the deprecated " + forEachDirectiveSymbol()
+ ")",
+                    template, start);
+        }
+        ContinueInstruction result = new ContinueInstruction();
+        result.setLocation(template, start, start);
+        return result;
+    }
+}
+
+/**
  * Production used to jump out of a macro.
  * The stop instruction terminates the rendering of the template.
  */
@@ -3221,6 +3255,7 @@ Macro Macro() :
     Expression defValue = null;
     List lastIteratorBlockContexts;
     int lastBreakableDirectiveNesting;
+    int lastContiunableDirectiveNesting;
     TemplateElements children;
     boolean isFunction = false, hasDefaults = false;
     boolean isCatchAll = false;
@@ -3292,9 +3327,12 @@ Macro Macro() :
         iteratorBlockContexts = null;
         if (incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_23) {
 	        lastBreakableDirectiveNesting = breakableDirectiveNesting;
+	        lastContiunableDirectiveNesting = continuableDirectiveNesting;
 	        breakableDirectiveNesting = 0; 
+	        continuableDirectiveNesting = 0;
         } else {
             lastBreakableDirectiveNesting = 0; // Just to prevent uninitialized local variable
error later
+            lastContiunableDirectiveNesting = 0;
         }
     }
     children = MixedContentElements()
@@ -3313,6 +3351,7 @@ Macro Macro() :
         iteratorBlockContexts = lastIteratorBlockContexts;
         if (incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_23) {
             breakableDirectiveNesting = lastBreakableDirectiveNesting;
+            continuableDirectiveNesting = lastContiunableDirectiveNesting;
         }
         
         inMacro = inFunction = false;
@@ -3937,6 +3976,8 @@ TemplateElement FreemarkerDirective() :
         |
         tp = Break()
         |
+        tp = Continue()
+        |
         tp = Return()
         |
         tp = Stop()

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/manual/en_US/book.xml
----------------------------------------------------------------------
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index f1dc57a..21c3479 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -20428,7 +20428,7 @@ All rights reserved.</emphasis></programlisting>
       </section>
 
       <section xml:id="ref_directive_list">
-        <title>list, else, items, sep, break</title>
+        <title>list, else, items, sep, break, continue</title>
 
         <anchor xml:id="ref.directive.list"/>
 
@@ -20522,7 +20522,7 @@ All rights reserved.</emphasis></programlisting>
         <section>
           <title>Description</title>
 
-          <section>
+          <section xml:id="ref_list_simple">
             <title>Simplest form</title>
 
             <para>Assuming <literal>users</literal> contains the
@@ -20569,7 +20569,7 @@ All rights reserved.</emphasis></programlisting>
             <literal>Map</literal> objects can be listed.</para>
           </section>
 
-          <section>
+          <section xml:id="ref_list_else">
             <title>else directive</title>
 
             <anchor xml:id="ref.directive.list.else"/>
@@ -20609,7 +20609,7 @@ All rights reserved.</emphasis></programlisting>
             included template.</para>
           </section>
 
-          <section>
+          <section xml:id="ref_list_items">
             <title>items directive</title>
 
             <anchor xml:id="ref.directive.items"/>
@@ -20712,7 +20712,7 @@ All rights reserved.</emphasis></programlisting>
             </itemizedlist>
           </section>
 
-          <section>
+          <section xml:id="ref_list_sep">
             <title>sep directive</title>
 
             <anchor xml:id="ref.directive.sep"/>
@@ -20765,7 +20765,7 @@ All rights reserved.</emphasis></programlisting>
             will be called from).</para>
           </section>
 
-          <section>
+          <section xml:id="ref_list_break">
             <title>break directive</title>
 
             <anchor xml:id="ref.directive.list.break"/>
@@ -20792,24 +20792,100 @@ All rights reserved.</emphasis></programlisting>
             anywhere inside <literal>list</literal> as far as it has
             <literal>as <replaceable>item</replaceable></literal>
parameter,
             otherwise it can be placed anywhere inside the
-            <literal>items</literal> directive. If the
-            <literal>break</literal> is inside <literal>items</literal>,
it
-            will only exit from <literal>items</literal>, not from
-            <literal>list</literal>. In general, <literal>break</literal>
will
-            only exit from the directive whose body is called for each item,
-            and can only be placed inside such directive. So for example can't
-            use <literal>break</literal> inside <literal>list</literal>'s
+            <literal>items</literal> directive. However, it's strongly
+            recommended to place it either before or after all the other
+            things that you do inside the iteration. Otherwise it's easy to
+            end up with unclosed elements in the output, or otherwise make the
+            template harder to understand. Especially, avoid breaking out from
+            the nested content of custom directives (like <literal>&lt;#list
+            ...&gt;...&lt;@foo&gt;...&lt;#break&gt;...&lt;/@foo&gt;...&lt;/#list&gt;</literal>),
+            as the author of the directive may not expect that the closing tag
+            (<literal>&lt;/@foo&gt;</literal>) is never executed.</para>
+
+            <para>If the <literal>break</literal> is inside
+            <literal>items</literal>, it will only exit from
+            <literal>items</literal>, not from <literal>list</literal>.
In
+            general, <literal>break</literal> will only exit from the
+            directive whose body is called for each item, and can only be
+            placed inside such directive. So for example can't use
+            <literal>break</literal> inside <literal>list</literal>'s
             <literal>else</literal> section, unless there's the
             <literal>list</literal> is nested into another
             <literal>break</literal>-able directive.</para>
 
+            <para>Using <literal>break</literal> together with
+            <literal>sep</literal> is generally a bad idea, as
+            <literal>sep</literal> can't know if you will skip the rest of
+            items with <literal>break</literal>, and then you end up with a
+            separator after the item printed last.</para>
+
             <para>Just like <literal>else</literal> and
             <literal>items</literal>, <literal>break</literal> must
be
             literally inside body of the directive to break out from, and
             can't be moved out into a macro or included template.</para>
           </section>
 
-          <section>
+          <section xml:id="ref_list_continue">
+            <title>continue directive</title>
+
+            <anchor xml:id="ref.directive.list.continue"/>
+
+            <indexterm>
+              <primary>continue directive</primary>
+            </indexterm>
+
+            <note>
+              <para>The <literal>continue</literal> directive exists since
+              FreeMarker 2.3.27</para>
+            </note>
+
+            <para>You can skip the rest of the iteration body (the section
+            until the <literal>&lt;/#list&gt;</literal> or
+            <literal>&lt;/#items&gt;</literal> tag) with the
+            <literal>continue</literal> directive, then FreeMarker will
+            continue with the next item. For example:</para>
+
+            <programlisting role="template">&lt;#list 1..5 as x&gt;
+  &lt;#if x == 3&gt;
+    &lt;#continue&gt;
+  &lt;/#if&gt;
+  ${x}
+&lt;/#list&gt;</programlisting>
+
+            <programlisting role="output">  1
+  2
+  4
+  5</programlisting>
+
+            <para>The <literal>continue</literal> directives can be placed
+            anywhere inside <literal>list</literal> as far as it has
+            <literal>as <replaceable>item</replaceable></literal>
parameter,
+            otherwise it can be placed anywhere inside the
+            <literal>items</literal> directive. However, it's strongly
+            recommended to place it before all the other things you do inside
+            the iteration. Otherwise it's easy to end up with unclosed
+            elements in the output, or otherwise make the template harder to
+            understand. Especially, avoid breaking out from the nested content
+            of custom directives (like <literal>&lt;#list
+            ...&gt;...&lt;@foo&gt;...&lt;#continue&gt;...&lt;/@foo&gt;...&lt;/#list&gt;</literal>),
+            as the author of the directive may not expect that the closing tag
+            (<literal>&lt;/@foo&gt;</literal>) is never executed.</para>
+
+            <para>When you call <literal>continue</literal>, the
+            <literal>sep</literal> directive will not be executed for that
+            iteration. Using <literal>continue</literal> together with
+            <literal>sep</literal> is generally a bad idea, as
+            <literal>sep</literal> can't know if you will skip the rest of the
+            items, and then you end up with a separator after the item printed
+            last.</para>
+
+            <para>Just like <literal>break</literal>,
+            <literal>continue</literal> must be literally inside body of the
+            directive whose iteration need to be <quote>continued</quote>, and
+            can't be moved out into a macro or included template.</para>
+          </section>
+
+          <section xml:id="ref_list_accessing_state">
             <title>Accessing iteration state</title>
 
             <indexterm>
@@ -20876,7 +20952,7 @@ All rights reserved.</emphasis></programlisting>
             1}</literal>.</para>
           </section>
 
-          <section>
+          <section xml:id="ref_list_nesting">
             <title>Nesting loops into each other</title>
 
             <para>Naturally, <literal>list</literal> or
@@ -20919,7 +20995,7 @@ All rights reserved.</emphasis></programlisting>
   Outer again: 2</programlisting>
           </section>
 
-          <section>
+          <section xml:id="ref_list_java_notes">
             <title>Notes for Java programmers</title>
 
             <para><phrase role="forProgrammers">If classic compatible mode
@@ -26913,6 +26989,14 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
 
           <itemizedlist>
             <listitem>
+              <para>Added the <link
+              linkend="ref.directive.list.continue"><literal>continue</literal>
+              directive</link>, which can be used inside a
+              <literal>list</literal> to skip to the next iteration (similarly
+              as in Java).</para>
+            </listitem>
+
+            <listitem>
               <para>Added alternative syntaxes for the
               <literal>&amp;&amp;</literal> (logical <quote>and</quote>)
               operator: <literal>\and</literal> and

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java b/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java
new file mode 100644
index 0000000..e4ef01c
--- /dev/null
+++ b/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class BreakAndContinuePlacementTest extends TemplateTest {
+    
+    private static final String BREAK_NESTING_ERROR_MESSAGE_PART = "<#break> must be
nested";
+    private static final String CONTINUE_NESTING_ERROR_MESSAGE_PART = "<#continue>
must be nested";
+
+    @Test
+    public void testValidPlacements() throws IOException, TemplateException {
+        assertOutput("<#assign x = 1><#switch x><#case 1>one<#break><#case
2>two</#switch>", "one");
+        assertOutput("<#list 1..2 as x>${x}<#break></#list>", "1");
+        assertOutput("<#list 1..2 as x>${x}<#continue></#list>", "12");
+        assertOutput("<#list 1..2>[<#items as x>${x}<#break></#items>]</#list>",
"[1]");
+        assertOutput("<#list 1..2 as x>${x}<#list 1..3>B<#break>E<#items
as y></#items></#list>E</#list>.", "1B.");
+        assertOutput("<#list 1..2 as x>${x}<#list 3..4 as x>${x}<#break></#list>;</#list>",
"13;23;");
+        assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>[<#list xs as x>${x}<#else><#break></#list>]</#list>.",
+                "[12][34][.");
+        assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>"
+                + "<#list xs>[<#items as x>${x}</#items>]<#else><#break></#list>"
+                + "</#list>.",
+                "[12][34].");
+        assertOutput("<#forEach x in 1..2>${x}<#break></#forEach>", "1");
+        assertOutput("<#forEach x in 1..2>${x}<#continue></#forEach>",
"12");
+        assertOutput("<#switch 1><#case 1>1<#break></#switch>", "1");
+    }
+
+    @Test
+    public void testInvalidPlacements() throws IOException, TemplateException {
+        assertErrorContains("<#break>", BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#continue>", CONTINUE_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#switch 1><#case 1>1<#continue></#switch>",
CONTINUE_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#list 1..2 as x>${x}</#list><#break>", BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#if false><#break></#if>", BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#list xs><#break></#list>", BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains("<#list 1..2 as x>${x}<#else><#break></#list>",
BREAK_NESTING_ERROR_MESSAGE_PART);
+    }
+
+    @Test
+    public void testInvalidPlacementMacroLoophole() throws IOException, TemplateException
{
+        final String ftl = "<#list 1..2 as x>${x}<#macro m><#break></#macro></#list>";
+        getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_22);
+        assertOutput(ftl, "12");
+        getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_23);
+        assertErrorContains(ftl, BREAK_NESTING_ERROR_MESSAGE_PART);
+        assertErrorContains(ftl.replaceAll("#break", "#continue"), CONTINUE_NESTING_ERROR_MESSAGE_PART);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/test/java/freemarker/core/BreakPlacementTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/BreakPlacementTest.java b/src/test/java/freemarker/core/BreakPlacementTest.java
deleted file mode 100644
index 8844956..0000000
--- a/src/test/java/freemarker/core/BreakPlacementTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package freemarker.core;
-
-import java.io.IOException;
-
-import org.junit.Test;
-
-import freemarker.template.Configuration;
-import freemarker.template.TemplateException;
-import freemarker.test.TemplateTest;
-
-public class BreakPlacementTest extends TemplateTest {
-    
-    private static final String BREAK_NESTING_ERROR_MESSAGE_PART = "<#break> must be
nested";
-
-    @Test
-    public void testValidPlacements() throws IOException, TemplateException {
-        assertOutput("<#assign x = 1><#switch x><#case 1>one<#break><#case
2>two</#switch>", "one");
-        assertOutput("<#list 1..2 as x>${x}<#break></#list>", "1");
-        assertOutput("<#list 1..2>[<#items as x>${x}<#break></#items>]</#list>",
"[1]");
-        assertOutput("<#list 1..2 as x>${x}<#list 1..3>B<#break>E<#items
as y></#items></#list>E</#list>.", "1B.");
-        assertOutput("<#list 1..2 as x>${x}<#list 3..4 as x>${x}<#break></#list>;</#list>",
"13;23;");
-        assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>[<#list xs as x>${x}<#else><#break></#list>]</#list>.",
-                "[12][34][.");
-        assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>"
-                + "<#list xs>[<#items as x>${x}</#items>]<#else><#break></#list>"
-                + "</#list>.",
-                "[12][34].");
-        assertOutput("<#forEach x in 1..2>${x}<#break></#forEach>", "1");
-    }
-
-    @Test
-    public void testInvalidPlacements() throws IOException, TemplateException {
-        assertErrorContains("<#break>", BREAK_NESTING_ERROR_MESSAGE_PART);
-        assertErrorContains("<#list 1..2 as x>${x}</#list><#break>", BREAK_NESTING_ERROR_MESSAGE_PART);
-        assertErrorContains("<#if false><#break></#if>", BREAK_NESTING_ERROR_MESSAGE_PART);
-        assertErrorContains("<#list xs><#break></#list>", BREAK_NESTING_ERROR_MESSAGE_PART);
-        assertErrorContains("<#list 1..2 as x>${x}<#else><#break></#list>",
BREAK_NESTING_ERROR_MESSAGE_PART);
-    }
-
-    @Test
-    public void testInvalidPlacementMacroLoophole() throws IOException, TemplateException
{
-        final String ftl = "<#list 1..2 as x>${x}<#macro m><#break></#macro></#list>";
-        getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_22);
-        assertOutput(ftl, "12");
-        getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_23);
-        assertErrorContains(ftl, BREAK_NESTING_ERROR_MESSAGE_PART);
-    }
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/test/java/freemarker/core/ListBreakContinueTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/ListBreakContinueTest.java b/src/test/java/freemarker/core/ListBreakContinueTest.java
new file mode 100644
index 0000000..8ee4af8
--- /dev/null
+++ b/src/test/java/freemarker/core/ListBreakContinueTest.java
@@ -0,0 +1,93 @@
+package freemarker.core;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.test.TemplateTest;
+
+public class ListBreakContinueTest extends TemplateTest {
+    
+    @Test
+    public void testNonHash() throws IOException, TemplateException {
+        testNonHash(ImmutableList.of(1, 2, 3, 4, 5)); // Listing a TemplateSequenceModel
+        testNonHash(ImmutableSet.of(1, 2, 3, 4, 5)); // Listing a TemplateCollectionModel
+    }
+    
+    private void testNonHash(Object listed) throws IOException, TemplateException {
+        addToDataModel("listed", listed);
+        assertOutput(
+                "<#list listed as i>B(${i}) <#if i == 3>Break!<#break></#if>A(${i})<#sep>,
</#list>",
+                "B(1) A(1), B(2) A(2), B(3) Break!");
+        assertOutput(
+                "<#list listed as i>B(${i}) <#if i == 3>Continue! <#continue></#if>A(${i})<#sep>,
</#list>",
+                "B(1) A(1), B(2) A(2), B(3) Continue! B(4) A(4), B(5) A(5)");
+    }
+
+    @Test
+    public void testHash() throws IOException, TemplateException {
+        testHash(ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5)); // Listing a TemplateHashModelEx2
+        testHash(new NonEx2Hash((TemplateHashModelEx) getConfiguration().getObjectWrapper().wrap(
+                ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5)))); // Listing a
TemplateHashModelEx (non-Ex2)
+    }
+
+    private void testHash(Object listed) throws IOException, TemplateException {
+        addToDataModel("listed", listed);
+        assertOutput(
+                "<#list listed as k, v>B(${k}=${v}) <#if k == 'c'>Break!<#break></#if>A(${k}=${v})<#sep>,
</#list>",
+                "B(a=1) A(a=1), B(b=2) A(b=2), B(c=3) Break!");
+        assertOutput(
+                "<#list listed as k, v>B(${k}=${v}) <#if k == 'c'>Continue! <#continue></#if>A(${k}=${v})<#sep>,
</#list>",
+                "B(a=1) A(a=1), B(b=2) A(b=2), B(c=3) Continue! B(d=4) A(d=4), B(e=5) A(e=5)");
+    }
+    
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration conf = super.createConfiguration();
+        DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27);
+        owb.setForceLegacyNonListCollections(false);
+        conf.setObjectWrapper(owb.build());
+        return conf;
+    }
+    
+    /** Hides the Ex2 features of another hash */
+    static class NonEx2Hash implements TemplateHashModelEx {
+        private final TemplateHashModelEx delegate;
+
+        public NonEx2Hash(TemplateHashModelEx delegate) {
+            this.delegate = delegate;
+        }
+
+        public TemplateModel get(String key) throws TemplateModelException {
+            return delegate.get(key);
+        }
+
+        public int size() throws TemplateModelException {
+            return delegate.size();
+        }
+
+        public TemplateCollectionModel keys() throws TemplateModelException {
+            return delegate.keys();
+        }
+
+        public boolean isEmpty() throws TemplateModelException {
+            return delegate.isEmpty();
+        }
+
+        public TemplateCollectionModel values() throws TemplateModelException {
+            return delegate.values();
+        }
+    }
+    
+}



Mime
View raw message