freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject 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:47:29 GMT
Repository: incubator-freemarker
Updated Branches:
  refs/heads/3 eceaac29c -> 8dbd71579


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/8dbd7157
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/8dbd7157
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/8dbd7157

Branch: refs/heads/3
Commit: 8dbd715794daa71779096ea30e6ccd2d6e34285b
Parents: eceaac2
Author: ddekany <ddekany@apache.org>
Authored: Sun Sep 17 12:47:04 2017 +0200
Committer: ddekany <ddekany@apache.org>
Committed: Sun Sep 17 12:47:04 2017 +0200

----------------------------------------------------------------------
 .../core/BreakAndContinuePlacementTest.java     | 61 ++++++++++++++
 .../freemarker/core/BreakPlacementTest.java     | 56 -------------
 .../freemarker/core/ListBreakContinueTest.java  | 78 ++++++++++++++++++
 .../org/apache/freemarker/core/ASTDirBreak.java | 10 +--
 .../apache/freemarker/core/ASTDirContinue.java  | 62 +++++++++++++++
 .../org/apache/freemarker/core/ASTDirList.java  | 84 +++++++++++---------
 .../apache/freemarker/core/ASTDirSwitch.java    |  2 +-
 .../apache/freemarker/core/ASTDirective.java    |  1 +
 .../core/BreakOrContinueException.java          | 13 +++
 freemarker-core/src/main/javacc/FTL.jj          | 39 +++++++++
 10 files changed, 301 insertions(+), 105 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/8dbd7157/freemarker-core-test/src/test/java/org/apache/freemarker/core/BreakAndContinuePlacementTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/BreakAndContinuePlacementTest.java
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/BreakAndContinuePlacementTest.java
new file mode 100644
index 0000000..323ddfb
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/BreakAndContinuePlacementTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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 org.apache.freemarker.core;
+
+import java.io.IOException;
+
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+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("<#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);
+        assertErrorContains("<#list 1..2 as x>${x}<#macro m><#break></#macro></#list>",
BREAK_NESTING_ERROR_MESSAGE_PART);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/8dbd7157/freemarker-core-test/src/test/java/org/apache/freemarker/core/BreakPlacementTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/BreakPlacementTest.java
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/BreakPlacementTest.java
deleted file mode 100644
index 61ba02b..0000000
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/BreakPlacementTest.java
+++ /dev/null
@@ -1,56 +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 org.apache.freemarker.core;
-
-import java.io.IOException;
-
-import org.apache.freemarker.test.TemplateTest;
-import org.junit.Test;
-
-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].");
-    }
-
-    @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);
-        assertErrorContains("<#list 1..2 as x>${x}<#macro m><#break></#macro></#list>",
BREAK_NESTING_ERROR_MESSAGE_PART);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/8dbd7157/freemarker-core-test/src/test/java/org/apache/freemarker/core/ListBreakContinueTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ListBreakContinueTest.java
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ListBreakContinueTest.java
new file mode 100644
index 0000000..86f52f8
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ListBreakContinueTest.java
@@ -0,0 +1,78 @@
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class ListBreakContinueTest extends TemplateTest {
+    
+    @Test
+    public void testNonHash() throws IOException, TemplateException {
+        addToDataModel("listed", ImmutableSet.of(1, 2, 3, 4, 5));
+        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)");
+    }
+    
+    /** Hides the Ex2 features of another hash */
+    static class NonEx2Hash implements TemplateHashModelEx {
+        private final TemplateHashModelEx delegate;
+
+        public NonEx2Hash(TemplateHashModelEx delegate) {
+            this.delegate = delegate;
+        }
+
+        @Override
+        public TemplateModel get(String key) throws TemplateException {
+            return delegate.get(key);
+        }
+
+        @Override
+        public int getHashSize() throws TemplateException {
+            return delegate.getHashSize();
+        }
+
+        @Override
+        public TemplateCollectionModel keys() throws TemplateException {
+            return delegate.keys();
+        }
+
+        @Override
+        public boolean isEmptyHash() throws TemplateException {
+            return delegate.isEmptyHash();
+        }
+
+        @Override
+        public TemplateCollectionModel values() throws TemplateException {
+            return delegate.values();
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/8dbd7157/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirBreak.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirBreak.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirBreak.java
index ff82b8a..128e684 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirBreak.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirBreak.java
@@ -26,7 +26,7 @@ final class ASTDirBreak extends ASTDirective {
 
     @Override
     ASTElement[] accept(Environment env) {
-        throw Break.INSTANCE;
+        throw BreakOrContinueException.BREAK_INSTANCE;
     }
 
     @Override
@@ -54,17 +54,9 @@ final class ASTDirBreak extends ASTDirective {
         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/8dbd7157/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirContinue.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirContinue.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirContinue.java
new file mode 100644
index 0000000..4d18785
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirContinue.java
@@ -0,0 +1,62 @@
+/*
+ * 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 org.apache.freemarker.core;
+
+/**
+ * AST directive node: {@code #break}
+ */
+final class ASTDirContinue extends ASTDirective {
+
+    @Override
+    ASTElement[] accept(Environment env) {
+        throw BreakOrContinueException.CONTINUE_INSTANCE;
+    }
+
+    @Override
+    protected String dump(boolean canonical) {
+        return canonical ? "<" + getASTNodeDescriptor() + "/>" : getASTNodeDescriptor();
+    }
+    
+    @Override
+    String getASTNodeDescriptor() {
+        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/8dbd7157/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java
index 1d0927f..0262ded 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirList.java
@@ -266,16 +266,18 @@ final class ASTDirList extends ASTDirective {
                 listNotEmpty = iterModel.hasNext();
                 if (listNotEmpty) {
                     if (nestedContentParam1Name != null) {
-                        try {
-                            do {
+                        listLoop: do {
                                 nestedContentParam = 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 (ASTDirBreak.Break br) {
-                            // Silently exit loop
-                        }
                         openedIterator = null;
                     } else {
                         // We must reuse this later, because TemplateIterableModel-s that
wrap an Iterator only
@@ -313,18 +315,20 @@ final class ASTDirList extends ASTDirective {
                     hashNotEmpty = kvpIter.hasNext();
                     if (hashNotEmpty) {
                         if (nestedContentParam1Name != null) {
-                            try {
-                                do {
+                            listLoop: do {
                                     KeyValuePair kvp = kvpIter.next();
                                     nestedContentParam = kvp.getKey();
                                     nestedContentParam2 = kvp.getValue();
                                     hasNext = kvpIter.hasNext();
-                                    env.visit(childBuffer);
+                                    try {
+                                        env.visit(childBuffer);
+                                    } catch (BreakOrContinueException br) {
+                                        if (br == BreakOrContinueException.BREAK_INSTANCE)
{
+                                            break listLoop;
+                                        }
+                                    }
                                     index++;
                                 } while (hasNext);
-                            } catch (ASTDirBreak.Break br) {
-                                // Silently exit loop
-                            }
                             openedIterator = null;
                         } else {
                             // We will reuse this at the #iterms
@@ -337,34 +341,36 @@ final class ASTDirList extends ASTDirective {
                     hashNotEmpty = keysIter.hasNext();
                     if (hashNotEmpty) {
                         if (nestedContentParam1Name != null) {
-                            try {
-                                do {
-                                    nestedContentParam = keysIter.next();
-                                    if (!(nestedContentParam instanceof TemplateStringModel))
{
-                                        throw new TemplateException(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 _DelayedTemplateLanguageTypeDescription(
-                                                                        nestedContentParam)),
-                                                        "."
-                                                        ).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."));
-                                    }
-                                    nestedContentParam2 = listedHash.get(((TemplateStringModel)
nestedContentParam)
-                                            .getAsString());
-                                    hasNext = keysIter.hasNext();
+                            listLoop: do {
+                                nestedContentParam = keysIter.next();
+                                if (!(nestedContentParam instanceof TemplateStringModel))
{
+                                    throw new TemplateException(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 _DelayedTemplateLanguageTypeDescription(
+                                                                    nestedContentParam)),
+                                                    "."
+                                                    ).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."));
+                                }
+                                nestedContentParam2 = listedHash.get(((TemplateStringModel)
nestedContentParam)
+                                        .getAsString());
+                                hasNext = keysIter.hasNext();
+                                try {
                                     env.visit(childBuffer);
-                                    index++;
-                                } while (hasNext);
-                            } catch (ASTDirBreak.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/8dbd7157/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
index 1d5d6ea..296dd48 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirSwitch.java
@@ -81,7 +81,7 @@ final class ASTDirSwitch extends ASTDirective {
             if (!processedCase && defaultCase != null) {
                 env.visit(defaultCase);
             }
-        } catch (ASTDirBreak.Break br) {
+        } catch (BreakOrContinueException br) {
             // #break was called
         }
         return null;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/8dbd7157/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
index e20c0e3..6015376 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirective.java
@@ -37,6 +37,7 @@ abstract class ASTDirective extends ASTElement {
         names.add("break");
         names.add("case");
         names.add("compress");
+        names.add("continue");
         names.add("default");
         names.add("else");
         names.add("elseIf");

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/8dbd7157/freemarker-core/src/main/java/org/apache/freemarker/core/BreakOrContinueException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BreakOrContinueException.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/BreakOrContinueException.java
new file mode 100644
index 0000000..c83cb43
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BreakOrContinueException.java
@@ -0,0 +1,13 @@
+package org.apache.freemarker.core;
+
+/**
+ * Used for implementing #break and #continue. 
+ */
+// TODO [FM3] This is not a good mechanism (like what if we have <#list ...><@m><#break><@></#list>,
and inside `m`
+// there's <#list ...><#nested></#list>)
+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/8dbd7157/freemarker-core/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/javacc/FTL.jj b/freemarker-core/src/main/javacc/FTL.jj
index 54f9a45..2d25dcf 100644
--- a/freemarker-core/src/main/javacc/FTL.jj
+++ b/freemarker-core/src/main/javacc/FTL.jj
@@ -93,6 +93,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
@@ -810,6 +815,8 @@ TOKEN:
     |
     <BREAK : <START_TAG> "break" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken,
DEFAULT); }
     |
+    <CONTINUE : <START_TAG> "continue" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken,
DEFAULT); }
+    |
     <SIMPLE_RETURN : <START_TAG> "return" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken,
DEFAULT); }
     |
     <HALT : <START_TAG> "stop" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken,
DEFAULT); }
@@ -2365,6 +2372,7 @@ ASTElement List() :
         if (nestedContentParam != null) {
             iterCtx.nestedContentParamName = nestedContentParam.image;
             breakableDirectiveNesting++;
+            continuableDirectiveNesting++;
             if (nestedContentParam2 != null) {
                 iterCtx.nestedContentParam2Name = nestedContentParam2.image;
                 iterCtx.hashListing = true;
@@ -2382,6 +2390,7 @@ ASTElement List() :
     {
         if (nestedContentParam != null) {
             breakableDirectiveNesting--;
+            continuableDirectiveNesting--;
         } else if (iterCtx.kind != ITERATOR_BLOCK_KIND_ITEMS) {
             throw new ParseException(
                     "#list must have either \"as someItem\" parameter or nested #items that
belongs to it.",
@@ -2471,6 +2480,7 @@ ASTDirItems Items() :
         }
     
         breakableDirectiveNesting++;
+        continuableDirectiveNesting++;
     }
     
     children = MixedContentElements()
@@ -2478,6 +2488,7 @@ ASTDirItems Items() :
     end = <END_ITEMS>
     {
         breakableDirectiveNesting--;
+        continuableDirectiveNesting--;
         iterCtx.nestedContentParamName = null;
         iterCtx.nestedContentParam2Name = null;
         
@@ -2603,6 +2614,28 @@ ASTDirBreak Break() :
     }
 }
 
+
+/**
+ * Production used to skip an iteration in a loop.
+ */
+ASTDirContinue 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",
+                    template, start);
+        }
+        ASTDirContinue result = new ASTDirContinue();
+        result.setLocation(template, start, start);
+        return result;
+    }
+}
+
 /**
  * Production used to jump out of a macro.
  * The stop instruction terminates the rendering of the template.
@@ -2984,6 +3017,7 @@ ASTDirMacroOrFunction Macro() :
 
     List lastIteratorBlockContexts;
     int lastBreakableDirectiveNesting;
+    int lastContinuableDirectiveNesting;
 }
 {
     (
@@ -3177,7 +3211,9 @@ ASTDirMacroOrFunction Macro() :
         lastIteratorBlockContexts = iteratorBlockContexts;
         iteratorBlockContexts = null;
         lastBreakableDirectiveNesting = breakableDirectiveNesting;
+        lastContinuableDirectiveNesting = continuableDirectiveNesting;
         breakableDirectiveNesting = 0; 
+        continuableDirectiveNesting = 0; 
     }
     children = MixedContentElements()
     (
@@ -3194,6 +3230,7 @@ ASTDirMacroOrFunction Macro() :
     {
         iteratorBlockContexts = lastIteratorBlockContexts;
         breakableDirectiveNesting = lastBreakableDirectiveNesting;
+        continuableDirectiveNesting = lastContinuableDirectiveNesting;
         
         inMacro = inFunction = false;
 
@@ -3810,6 +3847,8 @@ ASTElement FreemarkerDirective() :
         |
         tp = Break()
         |
+        tp = Continue()
+        |
         tp = Return()
         |
         tp = Stop()


Mime
View raw message