diff --git a/freemarker-core/src/main/java/freemarker/core/On.java b/freemarker-core/src/main/java/freemarker/core/On.java new file mode 100644 index 000000000..1a9ebe796 --- /dev/null +++ b/freemarker-core/src/main/java/freemarker/core/On.java @@ -0,0 +1,95 @@ +/* + * 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.util.List; + +/** + * Represents an "on" in a switch statement. + * This is alternative to case that does not fall-though + * and instead supports multiple conditions. + */ +final class On extends TemplateElement { + + List conditions; + + On(List matchingValues, TemplateElements children) { + this.conditions = matchingValues; + setChildren(children); + } + + @Override + TemplateElement[] accept(Environment env) { + return getChildBuffer(); + } + + @Override + protected String dump(boolean canonical) { + StringBuilder sb = new StringBuilder(); + if (canonical) sb.append('<'); + sb.append(getNodeTypeSymbol()); + for (int i = 0; i < conditions.size(); i++) { + if (i != 0) { + sb.append(','); + } + sb.append(' '); + sb.append((conditions.get(i)).getCanonicalForm()); + } + if (canonical) { + sb.append('>'); + sb.append(getChildrenCanonicalForm()); + } + return sb.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#on"; + } + + @Override + int getParameterCount() { + return conditions.size(); + } + + @Override + Object getParameterValue(int idx) { + checkIndex(idx); + return conditions.get(idx); + } + + @Override + ParameterRole getParameterRole(int idx) { + checkIndex(idx); + return ParameterRole.CONDITION; + } + + private void checkIndex(int idx) { + if (conditions == null || idx >= conditions.size()) { + throw new IndexOutOfBoundsException(); + } + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +} diff --git a/freemarker-core/src/main/java/freemarker/core/SwitchBlock.java b/freemarker-core/src/main/java/freemarker/core/SwitchBlock.java index 19c6a3e7e..5b6131c00 100644 --- a/freemarker-core/src/main/java/freemarker/core/SwitchBlock.java +++ b/freemarker-core/src/main/java/freemarker/core/SwitchBlock.java @@ -24,13 +24,13 @@ import freemarker.template.TemplateException; /** - * An instruction representing a switch-case structure. + * An instruction representing a switch-case or switch-on structure. */ final class SwitchBlock extends TemplateElement { private Case defaultCase; private final Expression searched; - private int firstCaseIndex; + private int firstCaseOrOnIndex; /** * @param searched the expression to be tested. @@ -43,7 +43,7 @@ final class SwitchBlock extends TemplateElement { for (int i = 0; i < ignoredCnt; i++) { addChild(ignoredSectionBeforeFirstCase.getChild(i)); } - firstCaseIndex = ignoredCnt; // Note that normally postParseCleanup will overwrite this + firstCaseOrOnIndex = ignoredCnt; // Note that normally postParseCleanup will overwrite this } /** @@ -56,37 +56,72 @@ void addCase(Case cas) { addChild(cas); } + /** + * @param on an On element. + */ + void addOn(On on) { + addChild(on); + } + @Override TemplateElement[] accept(Environment env) throws TemplateException, IOException { - boolean processedCase = false; + boolean processedCaseOrOn = false; + boolean usingOn = false; int ln = getChildCount(); try { - for (int i = firstCaseIndex; i < ln; i++) { - Case cas = (Case) getChild(i); - boolean processCase = false; - - // Fall through if a previous case tested true. - if (processedCase) { - processCase = true; - } else if (cas.condition != null) { - // Otherwise, if this case isn't the default, test it. - processCase = EvalUtil.compare( - searched, - EvalUtil.CMP_OP_EQUALS, "case==", cas.condition, cas.condition, env); - } - if (processCase) { - env.visit(cas); - processedCase = true; + for (int i = firstCaseOrOnIndex; i < ln; i++) { + TemplateElement tel = getChild(i); + + if (tel instanceof On) { + usingOn = true; + + for (Expression condition : ((On) tel).conditions) { + boolean processOn = EvalUtil.compare( + searched, + EvalUtil.CMP_OP_EQUALS, "on==", condition, condition, env); + if (processOn) { + env.visit(tel); + processedCaseOrOn = true; + break; + } + } + if (processedCaseOrOn) { + break; + } + } else { // Case + Expression condition = ((Case) tel).condition; + boolean processCase = false; + + // Fall through if a previous case tested true. + if (processedCaseOrOn) { + processCase = true; + } else if (condition != null) { + // Otherwise, if this case isn't the default, test it. + processCase = EvalUtil.compare( + searched, + EvalUtil.CMP_OP_EQUALS, "case==", condition, condition, env); + } + if (processCase) { + env.visit(tel); + processedCaseOrOn = true; + } } } // If we didn't process any nestedElements, and we have a default, // process it. - if (!processedCase && defaultCase != null) { + if (!processedCaseOrOn && defaultCase != null) { env.visit(defaultCase); } - } catch (BreakOrContinueException br) {} + } catch (BreakOrContinueException br) { + // This catches both break and continue, + // hence continue is incorrectly treated as a break inside a case. + // Unless using On, do backwards compatible behavior. + if (usingOn) { + throw br; // On supports neither break nor continue. + } + } return null; } @@ -142,10 +177,12 @@ TemplateElement postParseCleanup(boolean stripWhitespace) throws ParseException // The first #case might have shifted in the child array, so we have to find it again: int ln = getChildCount(); int i = 0; - while (i < ln && !(getChild(i) instanceof Case)) { + while (i < ln + && !(getChild(i) instanceof Case) + && !(getChild(i) instanceof On)) { i++; } - firstCaseIndex = i; + firstCaseOrOnIndex = i; return result; } diff --git a/freemarker-core/src/main/java/freemarker/core/_CoreAPI.java b/freemarker-core/src/main/java/freemarker/core/_CoreAPI.java index df522a136..85c08d95d 100644 --- a/freemarker-core/src/main/java/freemarker/core/_CoreAPI.java +++ b/freemarker-core/src/main/java/freemarker/core/_CoreAPI.java @@ -105,6 +105,7 @@ private static void addName(Set allNames, Set lcNames, Set "case" > { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); } | + "on" > { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); } + | "assign" > { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); } | "global" > { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); } @@ -3875,15 +3877,16 @@ SwitchBlock Switch() : SwitchBlock switchBlock; MixedContent ignoredSectionBeforeFirstCase = null; Case caseIns; + On onIns; Expression switchExp; Token start, end; boolean defaultFound = false; } { ( - start = - switchExp = Expression() - + start = + switchExp = Expression() + [ ignoredSectionBeforeFirstCase = WhitespaceAndComments() ] ) { @@ -3891,20 +3894,52 @@ SwitchBlock Switch() : switchBlock = new SwitchBlock(switchExp, ignoredSectionBeforeFirstCase); } [ - ( - caseIns = Case() - { - if (caseIns.condition == null) { - if (defaultFound) { - throw new ParseException( - "You can only have one default case in a switch statement", template, start); - } - defaultFound = true; - } - switchBlock.addCase(caseIns); - } - )+ - [] + ( + ( + caseIns = Case() + { + if (caseIns.condition == null) { + if (defaultFound) { + throw new ParseException( + "You can only have one default case in a switch statement", template, start); + } + defaultFound = true; + } + switchBlock.addCase(caseIns); + } + )+ + | + ( + { + // A Switch with Case supports break, but not one with On. + // Do it this way to ensure backwards compatibility. + breakableDirectiveNesting--; + } + + ( + onIns = On() + { + switchBlock.addOn(onIns); + } + )+ + [ + caseIns = Case() + { + // When using on, you can have a default, but not a normal case + if (caseIns.condition != null) { + throw new ParseException( + "You cannot mix \"case\" and \"on\" in a switch statement", template, start); + } + switchBlock.addCase(caseIns); + } + ] + + { + breakableDirectiveNesting++; + } + ) + ) + [] ] end = { @@ -3934,6 +3969,24 @@ Case Case() : } } +On On() : +{ + ArrayList exps; + TemplateElements children; + Token start; +} +{ + ( + start = exps = PositionalArgs() + ) + children = MixedContentElements() + { + On result = new On(exps, children); + result.setLocation(template, start, start, children); + return result; + } +} + EscapeBlock Escape() : { Token variable, start, end; diff --git a/freemarker-core/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java b/freemarker-core/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java index e4ef01c1c..0246688fd 100644 --- a/freemarker-core/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java +++ b/freemarker-core/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java @@ -46,9 +46,12 @@ public void testValidPlacements() throws IOException, TemplateException { + "<#list xs>[<#items as x>${x}]<#else><#break>" + ".", "[12][34]."); + assertOutput("<#list 1..2 as x><#switch x><#on 1>one<#break>;", "one"); + assertOutput("<#list 1..2 as x><#switch x><#on 1>one<#continue>;", "one;"); assertOutput("<#forEach x in 1..2>${x}<#break>", "1"); assertOutput("<#forEach x in 1..2>${x}<#continue>", "12"); assertOutput("<#switch 1><#case 1>1<#break>", "1"); + assertOutput("<#switch 1><#default>1<#break>", "1"); } @Test @@ -56,6 +59,9 @@ 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>", CONTINUE_NESTING_ERROR_MESSAGE_PART); + assertErrorContains("<#switch 1><#on 1>1<#continue>", CONTINUE_NESTING_ERROR_MESSAGE_PART); + assertErrorContains("<#switch 1><#on 1>1<#break>", BREAK_NESTING_ERROR_MESSAGE_PART); + assertErrorContains("<#switch 1><#on 1>1<#default><#break>", BREAK_NESTING_ERROR_MESSAGE_PART); assertErrorContains("<#list 1..2 as x>${x}<#break>", BREAK_NESTING_ERROR_MESSAGE_PART); assertErrorContains("<#if false><#break>", BREAK_NESTING_ERROR_MESSAGE_PART); assertErrorContains("<#list xs><#break>", BREAK_NESTING_ERROR_MESSAGE_PART); diff --git a/freemarker-core/src/test/java/freemarker/core/TemplateProcessingTracerTest.java b/freemarker-core/src/test/java/freemarker/core/TemplateProcessingTracerTest.java index c73554d8c..91d61dcbf 100644 --- a/freemarker-core/src/test/java/freemarker/core/TemplateProcessingTracerTest.java +++ b/freemarker-core/src/test/java/freemarker/core/TemplateProcessingTracerTest.java @@ -65,6 +65,15 @@ public class TemplateProcessingTracerTest { "<#case 2>C3<#break>" + "<#default>D" + "" + + "<#switch 4>" + + "<#on 1>O1" + + "<#on 4>O4" + + "<#default>D" + + "" + + "<#switch 5>" + + "<#on 1>O1" + + "<#default>OD" + + "" + "<#macro m>Hello from m!" + "Calling macro: <@m />" + "<#assign t>captured" + @@ -121,6 +130,8 @@ public void test() throws Exception { "C2", "<#break>", "D", + "O4", + "OD", "Calling macro: ", "<@m />", "Hello from m!", @@ -191,6 +202,12 @@ public void test() throws Exception { " #switch 3", " #default", " text \"D\"", + " #switch 4", + " #on 4", + " text \"O4\"", + " #switch 5", + " #default", + " text \"OD\"", " #macro m", " text \"Calling macro: \"", " @m", diff --git a/freemarker-core/src/test/resources/freemarker/core/ast-1.ast b/freemarker-core/src/test/resources/freemarker/core/ast-1.ast index 853feb5b7..06dc279bf 100644 --- a/freemarker-core/src/test/resources/freemarker/core/ast-1.ast +++ b/freemarker-core/src/test/resources/freemarker/core/ast-1.ast @@ -112,6 +112,24 @@ - content: "more" // String #text // f.c.TextBlock - content: "\n6 " // String + #switch // f.c.SwitchBlock + - value: x // f.c.Identifier + #on // f.c.On + - condition: 1 // f.c.NumberLiteral + - condition: 2 // f.c.NumberLiteral + #text // f.c.TextBlock + - content: "one or two" // String + #on // f.c.On + - condition: 3 // f.c.NumberLiteral + #text // f.c.TextBlock + - content: "three" // String + #default // f.c.Case + - condition: null // Null + - AST-node subtype: "1" // Integer + #text // f.c.TextBlock + - content: "more" // String + #text // f.c.TextBlock + - content: "\n7 " // String #macro // f.c.Macro - assignment target: "foo" // String - parameter name: "x" // String @@ -128,7 +146,7 @@ - passed value: x // f.c.Identifier - passed value: y // f.c.Identifier #text // f.c.TextBlock - - content: "\n7 " // String + - content: "\n8 " // String #function // f.c.Macro - assignment target: "foo" // String - parameter name: "x" // String @@ -146,12 +164,12 @@ #return // f.c.ReturnInstruction - value: 1 // f.c.NumberLiteral #text // f.c.TextBlock - - content: "\n8 " // String + - content: "\n9 " // String #list // f.c.IteratorBlock - list source: xs // f.c.Identifier - target loop variable: "x" // String #text // f.c.TextBlock - - content: "\n9 " // String + - content: "\n10 " // String #list-#else-container // f.c.ListElseContainer #list // f.c.IteratorBlock - list source: xs // f.c.Identifier @@ -170,11 +188,11 @@ #text // f.c.TextBlock - content: "None" // String #text // f.c.TextBlock - - content: "\n10 " // String + - content: "\n11 " // String #--...-- // f.c.Comment - content: " A comment " // String #text // f.c.TextBlock - - content: "\n11 " // String + - content: "\n12 " // String #outputformat // f.c.OutputFormatBlock - value: "XML" // f.c.StringLiteral #noautoesc // f.c.NoAutoEscBlock diff --git a/freemarker-core/src/test/resources/freemarker/core/ast-1.ftl b/freemarker-core/src/test/resources/freemarker/core/ast-1.ftl index 8c8953a05..074ab615e 100644 --- a/freemarker-core/src/test/resources/freemarker/core/ast-1.ftl +++ b/freemarker-core/src/test/resources/freemarker/core/ast-1.ftl @@ -21,9 +21,10 @@ 3 <#assign x = 123><#assign x = 123 in ns><#global x = 123> 4 <#if x + 1 == 0>foo${y}bar<#else>${"static"}${'x${baaz * 10}y'} 5 <#switch x><#case 1>one<#case 2>two<#default>more -6 <#macro foo x y=2 z=y+1 q...><#nested x y> -7 <#function foo x y><#local x = 123><#return 1> -8 <#list xs as x> -9 <#list xs>[<#items as x>${x}<#sep>, ]<#else>None -10 <#-- A comment --> -11 <#outputFormat "XML"><#noAutoEsc>${a}<#autoEsc>${b}${c} \ No newline at end of file +6 <#switch x><#on 1, 2>one or two<#on 3>three<#default>more +7 <#macro foo x y=2 z=y+1 q...><#nested x y> +8 <#function foo x y><#local x = 123><#return 1> +9 <#list xs as x> +10 <#list xs>[<#items as x>${x}<#sep>, ]<#else>None +11 <#-- A comment --> +12 <#outputFormat "XML"><#noAutoEsc>${a}<#autoEsc>${b}${c} \ No newline at end of file