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 <[email protected]> Authored: Sun Sep 17 12:47:04 2017 +0200 Committer: ddekany <[email protected]> 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()
