This is an automated email from the ASF dual-hosted git repository.
ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git
The following commit(s) were added to refs/heads/2.3-gae by this push:
new 7d9f5e57 #switch #on PR post-merge adjustments:
7d9f5e57 is described below
commit 7d9f5e57567d32f755e1e4767d7b922e4ce98169
Author: ddekany <[email protected]>
AuthorDate: Wed Aug 28 19:43:17 2024 +0200
#switch #on PR post-merge adjustments:
- Reorganized how #case/#on/#default is parsed in JavaCC, mostly to achieve
better error messages (also I guess it's also easier to follow).
- Reorganized SwitchBlock to branch out for the #switch+#case and the
#switch+#on logic earlier (less checks on runtime, also maybe easier to follow).
- Adjusted error message wording
- Added much more test cases
- Manual updates for #on, and added version history entry
---
.../src/main/java/freemarker/core/SwitchBlock.java | 73 ++++---
.../src/main/javacc/freemarker/core/FTL.jj | 84 ++++----
.../core/BreakAndContinuePlacementTest.java | 22 +--
.../src/test/java/freemarker/core/SwitchTest.java | 185 ++++++++++++++++++
.../test/templatesuite/templates/switch.ftl | 2 +-
freemarker-manual/src/main/docgen/en_US/book.xml | 212 +++++++++++++++++----
6 files changed, 449 insertions(+), 129 deletions(-)
diff --git a/freemarker-core/src/main/java/freemarker/core/SwitchBlock.java
b/freemarker-core/src/main/java/freemarker/core/SwitchBlock.java
index 5b6131c0..6c5641b8 100644
--- a/freemarker-core/src/main/java/freemarker/core/SwitchBlock.java
+++ b/freemarker-core/src/main/java/freemarker/core/SwitchBlock.java
@@ -29,6 +29,7 @@ import freemarker.template.TemplateException;
final class SwitchBlock extends TemplateElement {
private Case defaultCase;
+ private boolean usesOnDirective;
private final Expression searched;
private int firstCaseOrOnIndex;
@@ -46,9 +47,6 @@ final class SwitchBlock extends TemplateElement {
firstCaseOrOnIndex = ignoredCnt; // Note that normally
postParseCleanup will overwrite this
}
- /**
- * @param cas a Case element.
- */
void addCase(Case cas) {
if (cas.condition == null) {
defaultCase = cas;
@@ -56,40 +54,40 @@ final class SwitchBlock extends TemplateElement {
addChild(cas);
}
- /**
- * @param on an On element.
- */
void addOn(On on) {
addChild(on);
+ usesOnDirective = true;
}
@Override
- TemplateElement[] accept(Environment env)
- throws TemplateException, IOException {
- boolean processedCaseOrOn = false;
- boolean usingOn = false;
+ TemplateElement[] accept(Environment env) throws TemplateException,
IOException {
int ln = getChildCount();
- try {
- for (int i = firstCaseOrOnIndex; i < ln; i++) {
+ if (usesOnDirective) {
+ processOnDirectives: for (int i = firstCaseOrOnIndex; i < ln; i++)
{
TemplateElement tel = getChild(i);
- if (tel instanceof On) {
- usingOn = true;
+ // "default" is always the last; the parser ensures this
+ if (tel == defaultCase) {
+ env.visit(defaultCase);
+ break;
+ }
- 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;
+ for (Expression condition : ((On) tel).conditions) {
+ boolean processOn = EvalUtil.compare(
+ searched,
+ EvalUtil.CMP_OP_EQUALS, "on==", condition,
condition, env);
+ if (processOn) {
+ env.visit(tel);
+ break processOnDirectives;
}
- } else { // Case
+ }
+ }
+ } else { // case-s
+ try {
+ boolean processedCaseOrOn = false;
+ for (int i = firstCaseOrOnIndex; i < ln; i++) {
+ TemplateElement tel = getChild(i);
+
Expression condition = ((Case) tel).condition;
boolean processCase = false;
@@ -107,19 +105,14 @@ final class SwitchBlock extends TemplateElement {
processedCaseOrOn = true;
}
}
- }
-
- // If we didn't process any nestedElements, and we have a default,
- // process it.
- if (!processedCaseOrOn && defaultCase != null) {
- env.visit(defaultCase);
- }
- } 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.
+ // If we didn't process any nestedElements, and we have a
default,
+ // process it.
+ if (!processedCaseOrOn && defaultCase != null) {
+ env.visit(defaultCase);
+ }
+ } catch (BreakOrContinueException br) {
+ // This catches both break and continue, hence continue is
incorrectly treated as a break inside a case.
+ // ("on", doesn't have this bug.)
}
}
return null;
diff --git a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
index 87256917..48bb1d6d 100644
--- a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
+++ b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
@@ -3883,11 +3883,13 @@ SwitchBlock Switch() :
{
SwitchBlock switchBlock;
MixedContent ignoredSectionBeforeFirstCase = null;
- Case caseIns;
- On onIns;
+ Case caseOrDefault;
+ On on;
+ boolean hadCase = false;
+ boolean hadDefault = false;
+ boolean hadOn = false;
Expression switchExp;
Token start, end;
- boolean defaultFound = false;
}
{
(
@@ -3897,60 +3899,72 @@ SwitchBlock Switch() :
[ ignoredSectionBeforeFirstCase = WhitespaceAndComments() ]
)
{
- breakableDirectiveNesting++;
+ breakableDirectiveNesting++; // Note that this will be undone when we
find an "on" directive call.
switchBlock = new SwitchBlock(switchExp,
ignoredSectionBeforeFirstCase);
}
+
[
(
- (
- caseIns = Case()
- {
- if (caseIns.condition == null) {
- if (defaultFound) {
+ caseOrDefault = Case()
+ {
+ if (caseOrDefault.condition == null) {
+ if (hadDefault) {
+ throw new ParseException(
+ "You already had a #default in the #switch
block",
+ caseOrDefault);
+ }
+ hadDefault = true;
+ } else {
+ if (hadOn) {
+ if (caseOrDefault.condition != null) {
throw new ParseException(
- "You can only have one default case in a switch
statement", template, start);
+ "You can't use both #on, and #case in a
#switch block, and you already had an #on.",
+ caseOrDefault);
}
- defaultFound = true;
}
- switchBlock.addCase(caseIns);
+ hadCase = true;
}
- )+
+
+ switchBlock.addCase(caseOrDefault);
+ }
|
(
{
- // A Switch with Case supports break, but not one with On.
- // Do it this way to ensure backwards compatibility.
- breakableDirectiveNesting--;
+ if (!hadOn) {
+ // A #switch with #case handles #break specially, but
not #switch with #on.
+ // Also, this must be done before calling On(), as
that consumes the nested #break already.
+ breakableDirectiveNesting--;
+ }
}
- (
- onIns = On()
- {
- switchBlock.addOn(onIns);
+ on = On()
+ {
+ if (hadCase) {
+ throw new ParseException(
+ "You can't use both #on, and #case in a
#switch block, and you already had a #case.",
+ on);
}
- )+
- [
- 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);
+ if (hadDefault) {
+ throw new ParseException(
+ "You can't use #on after #default in a #switch
block; #default must come last.",
+ on);
}
- ]
+ hadOn = true;
- {
- breakableDirectiveNesting++;
+ switchBlock.addOn(on);
}
)
- )
+ )+
[<STATIC_TEXT_WS>]
]
+
end = <END_SWITCH>
{
- breakableDirectiveNesting--;
+ // If we had #on, then this was already decreased there
+ if (!hadOn) {
+ breakableDirectiveNesting--;
+ }
+
switchBlock.setLocation(template, start, end);
return switchBlock;
}
diff --git
a/freemarker-core/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java
b/freemarker-core/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java
index 0246688f..aad6ede4 100644
---
a/freemarker-core/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java
+++
b/freemarker-core/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java
@@ -50,22 +50,22 @@ public class BreakAndContinuePlacementTest extends
TemplateTest {
assertOutput("<#list 1..2 as x><#switch x><#on
1>one<#continue></#switch>;</#list>", "one;");
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");
- assertOutput("<#switch 1><#default>1<#break></#switch>", "1");
+ assertOutput("<#switch 1><#case 1>1<#break>unreachable</#switch>.",
"1.");
+ assertOutput("<#switch 1><#default>1<#break>unreachable</#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("<#switch 1><#on 1>1<#continue></#switch>",
CONTINUE_NESTING_ERROR_MESSAGE_PART);
- assertErrorContains("<#switch 1><#on 1>1<#break></#switch>",
BREAK_NESTING_ERROR_MESSAGE_PART);
+ assertErrorContains("<#break>", ParseException.class,
BREAK_NESTING_ERROR_MESSAGE_PART);
+ assertErrorContains("<#continue>", ParseException.class,
CONTINUE_NESTING_ERROR_MESSAGE_PART);
+ assertErrorContains("<#switch 1><#case 1>1<#continue></#switch>",
ParseException.class, CONTINUE_NESTING_ERROR_MESSAGE_PART);
+ assertErrorContains("<#switch 1><#on 1>1<#continue></#switch>",
ParseException.class, CONTINUE_NESTING_ERROR_MESSAGE_PART);
+ assertErrorContains("<#switch 1><#on 1>1<#break></#switch>",
ParseException.class, BREAK_NESTING_ERROR_MESSAGE_PART);
assertErrorContains("<#switch 1><#on 1>1<#default><#break></#switch>",
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}</#list><#break>",
ParseException.class, BREAK_NESTING_ERROR_MESSAGE_PART);
+ assertErrorContains("<#if false><#break></#if>", ParseException.class,
BREAK_NESTING_ERROR_MESSAGE_PART);
+ assertErrorContains("<#list xs><#break></#list>",
ParseException.class, BREAK_NESTING_ERROR_MESSAGE_PART);
+ assertErrorContains("<#list 1..2 as x>${x}<#else><#break></#list>",
ParseException.class, BREAK_NESTING_ERROR_MESSAGE_PART);
}
@Test
diff --git a/freemarker-core/src/test/java/freemarker/core/SwitchTest.java
b/freemarker-core/src/test/java/freemarker/core/SwitchTest.java
new file mode 100644
index 00000000..c4a33787
--- /dev/null
+++ b/freemarker-core/src/test/java/freemarker/core/SwitchTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.TemplateException;
+import freemarker.test.TemplateTest;
+
+/**
+ * There are more (older) test in "switch-builtin.ftl", but we prefer creating
new ones in Java.
+ */
+public class SwitchTest extends TemplateTest {
+
+ @Test
+ public void testCaseBasics() throws TemplateException, IOException {
+ testCaseBasics(true);
+ testCaseBasics(false);
+ }
+
+ private void testCaseBasics(boolean hasDefault) throws TemplateException,
IOException {
+ for (int i = 1 ; i <= 6; i++) {
+ assertOutput(
+ "[<#switch " + i + ">\n"
+ + "<#case 3>Case 3<#break>"
+ + "<#case 1>Case 1<#break>"
+ + "<#case 4><#case 5>Case 4 or 5<#break>"
+ + "<#case 2>Case 2<#break>"
+ + (hasDefault ? "<#default>D" : "")
+ + "</#switch>]",
+ i < 6
+ ? "[Case " + (i < 4 ? i : "4 or 5") + "]"
+ : (hasDefault ? "[D]" : "[]"));
+ }
+ }
+
+ /** Tolerated for backward compatibility */
+ @Test
+ public void testCasesWithOddlyPlacedDefault() throws TemplateException,
IOException {
+ assertOutput("<#list 1..3 as i><#switch i><#case 1>1<#default>D<#case
3>3</#switch>;</#list>", "1D3;D;3;");
+ }
+
+ @Test
+ public void testDefaultOnly() throws TemplateException, IOException {
+ assertOutput("<#switch 1><#default>D</#switch>", "D");
+ assertOutput("<#list 1..2 as i><#switch
1><#default>D<#break>unreachable</#switch></#list>", "DD");
+ }
+
+ @Test
+ public void testCaseWhitespace() throws TemplateException, IOException {
+ assertOutput(
+ ""
+ + "<#list 1..3 as i>\n"
+ + "[\n" // <#----> is to avoid unrelated old
white-space stripping bug
+ + " <#switch i>\n"
+ + " <#case 1>C1\n"
+ + " <#case 2>C2<#break>\n"
+ + " <#default>D\n"
+ + " </#switch>\n"
+ + "]\n"
+ + "</#list>",
+ "[\nC1\n C2]\n[\nC2]\n[\nD\n]\n");
+ }
+
+ @Test
+ public void testOn() throws TemplateException, IOException {
+ testOnBasics(true);
+ testOnBasics(false);
+ }
+
+ private void testOnBasics(boolean hasDefault) throws TemplateException,
IOException {
+ for (int i = 1 ; i <= 6; i++) {
+ assertOutput(
+ "[<#switch " + i + ">\n"
+ + "<#on 3>On 3"
+ + "<#on 1>On 1"
+ + "<#on 4, 5>On 4 or 5"
+ + "<#on 2>On 2"
+ + (hasDefault ? "<#default>D" : "")
+ + "</#switch>]",
+ i < 6
+ ? "[On " + (i < 4 ? i : "4 or 5") + "]"
+ : (hasDefault ? "[D]" : "[]"));
+ }
+ }
+
+ @Test
+ public void testOnParsingErrors() throws TemplateException, IOException {
+ assertErrorContains(
+ "<#switch x><#on 1>On 1<#default>D<#on 2>On 2</#switch>",
+ ParseException.class,
+ "#on after #default");
+ assertErrorContains(
+ "<#switch x><#on 1>On 1<#case 2>On 2</#switch>",
+ ParseException.class,
+ "can't use both #on, and #case", "already had an #on");
+ assertErrorContains(
+ "<#switch x><#case 1>On 1<#on 2>On 2</#switch>",
+ ParseException.class,
+ "can't use both #on, and #case", "already had a #case");
+ assertErrorContains(
+ "<#switch x><#on 1>On 1<#default>D<#case 2>On 2</#switch>",
+ ParseException.class,
+ "can't use both #on, and #case", "already had an #on");
+ assertErrorContains(
+ "<#switch x><#on 1>On 1<#default>D1<#default>D2</#switch>",
+ ParseException.class,
+ "already had a #default");
+ assertErrorContains(
+ "<#switch x><#case 1>On 1<#default>D1<#default>D2</#switch>",
+ ParseException.class,
+ "already had a #default");
+ assertErrorContains(
+ "<#switch x><#on 1>On 1<#default>D<#on 2>On 2</#switch>",
+ ParseException.class,
+ "#on after #default");
+ assertErrorContains(
+ "<#switch x><#default>D<#on 2>On 2</#switch>",
+ ParseException.class,
+ "#on after #default");
+ }
+
+ @Test
+ public void testOnWhitespace() throws TemplateException, IOException {
+ assertOutput(
+ ""
+ + "<#list 1..3 as i>\n"
+ + "[\n" // <#----> is to avoid unrelated old
white-space stripping bug
+ + " <#switch i>\n"
+ + " <#on 1>C1\n"
+ + " <#on 2>C2\n"
+ + " <#default>D\n"
+ + " </#switch>\n"
+ + "]\n"
+ + "</#list>",
+ "[\nC1\n ]\n[\nC2\n ]\n[\nD\n]\n");
+ assertOutput(
+ ""
+ + "<#list 1..3 as i>\n"
+ + "[\n" // <#----> is to avoid unrelated old
white-space stripping bug
+ + " <#switch i>\n"
+ + " <#on 1>C1<#t>\n"
+ + " <#on 2>C2<#t>\n"
+ + " <#default>D<#t>\n"
+ + " </#switch>\n"
+ + "]\n"
+ + "</#list>",
+ "[\nC1]\n[\nC2]\n[\nD]\n");
+ assertOutput(
+ ""
+ + "<#list 1..3 as i>\n"
+ + "[\n" // <#----> is to avoid unrelated old
white-space stripping bug
+ + " <#switch i>\n"
+ + " <#on 1>\n"
+ + " C1\n"
+ + " <#on 2>\n"
+ + " C2\n"
+ + " <#default>\n"
+ + " D\n"
+ + " </#switch>\n"
+ + "]\n"
+ + "</#list>",
+ "[\n C1\n]\n[\n C2\n]\n[\n D\n]\n");
+ }
+
+}
diff --git
a/freemarker-jython25/src/test/resources/freemarker/test/templatesuite/templates/switch.ftl
b/freemarker-jython25/src/test/resources/freemarker/test/templatesuite/templates/switch.ftl
index e9bd3775..f51f44a5 100644
---
a/freemarker-jython25/src/test/resources/freemarker/test/templatesuite/templates/switch.ftl
+++
b/freemarker-jython25/src/test/resources/freemarker/test/templatesuite/templates/switch.ftl
@@ -136,7 +136,7 @@
</#list>
<#-- two #default-s are parsing error -->
-<@assertFails message="can only have one default"><@"<#switch 1><#case
1><#default><#default></#switch>"?interpret /></@>
+<@assertFails message="already had a #default"><@"<#switch 1><#case
1><#default><#default></#switch>"?interpret /></@>
</body>
</html>
diff --git a/freemarker-manual/src/main/docgen/en_US/book.xml
b/freemarker-manual/src/main/docgen/en_US/book.xml
index 46936674..f999f39d 100644
--- a/freemarker-manual/src/main/docgen/en_US/book.xml
+++ b/freemarker-manual/src/main/docgen/en_US/book.xml
@@ -21199,6 +21199,10 @@ with_args_last:
<para><link linkend="ref.directive.nt">nt</link></para>
</listitem>
+ <listitem>
+ <para><link linkend="ref.directive.on">on</link></para>
+ </listitem>
+
<listitem>
<para><link
linkend="ref_directive_outputformat">outputformat</link></para>
@@ -25181,12 +25185,14 @@ or
</section>
<section xml:id="ref_directive_switch">
- <title>switch, case, default, break</title>
+ <title>switch, on, case, default, break</title>
<anchor xml:id="ref.directive.switch"/>
<anchor xml:id="ref.directive.case"/>
+ <anchor xml:id="ref.directive.on"/>
+
<anchor xml:id="ref.directive.default"/>
<anchor xml:id="ref.directive.switch.break"/>
@@ -25199,6 +25205,10 @@ or
<primary>case directive</primary>
</indexterm>
+ <indexterm>
+ <primary>on directive</primary>
+ </indexterm>
+
<indexterm>
<primary>default directive</primary>
</indexterm>
@@ -25210,13 +25220,32 @@ or
<section>
<title>Synopsis</title>
- <programlisting role="metaTemplate">
-<literal><#switch <replaceable>value</replaceable>>
- <#case <replaceable>refValue1</replaceable>>
+ <para>Recommended form (<literal>on</literal>-based), but this is
+ <emphasis role="bold">only supported since FreeMarker
+ 2.3.34</emphasis>:</para>
+
+ <programlisting role="metaTemplate"><literal><#switch
<replaceable>value</replaceable>>
+ <#on <replaceable>refValue1</replaceable>>
+ <replaceable>... (Handles refValue1)</replaceable>
+ <#on <replaceable>refValue2, refValue3</replaceable>>
+ <replaceable>... (Handles both refValue2 and refValue3)</replaceable>)
+ <replaceable>...</replaceable>
+ <#case <replaceable>refValueN</replaceable>>
<replaceable>...</replaceable>
<#break>
- <#case <replaceable>refValue2</replaceable>>
+ <#default>
<replaceable>...</replaceable>
+</#switch></literal></programlisting>
+
+ <para>Deprecated legacy form (<literal>case</literal>-based):</para>
+
+ <programlisting role="metaTemplate"><literal><#switch
<replaceable>value</replaceable>>
+ <#case <replaceable>refValue1</replaceable>>
+ <replaceable>... (Handles refValue1)</replaceable>
+ <#break>
+ <#case <replaceable>refValue2</replaceable>>
+ <#case <replaceable>refValue3</replaceable>>
+ <replaceable>... (Handles both refValue2 and refValue3)</replaceable>
<#break>
<replaceable>...</replaceable>
<#case <replaceable>refValueN</replaceable>>
@@ -25224,9 +25253,7 @@ or
<#break>
<#default>
<replaceable>...</replaceable>
-</#switch>
-</literal>
-</programlisting>
+</#switch></literal></programlisting>
<para>Where:</para>
@@ -25238,54 +25265,140 @@ or
</listitem>
</itemizedlist>
- <para>The <literal>break</literal>-s and <literal>default</literal>
- are optional.</para>
+ <para>Additional rules:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>A <literal>switch</literal> block either have
+ <literal>on</literal>-s, or <literal>case</literal>-s, but not
+ both.</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>default</literal>: Optional, can occur at most
+ once. Must be after the <literal>on</literal>-s. There's no
+ ordering restriction when used with
+ <literal>case</literal>-s.</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>break</literal>: </para>
+
+ <itemizedlist>
+ <listitem>
+ <para>If the <literal>switch</literal> uses
+ <literal>case</literal> then <literal>break</literal>
+ immediately exits from the closes enclosing
+ <literal>switch</literal>. This is used to prevent
+ fall-through into the next <literal>case</literal>, or into
+ the following (adjacent) <literal>default</literal>. (To be
+ precise, <literal>break</literal> behaves as described if
+ the <literal>switch</literal> doesn't contain
+ <literal>on</literal>, and thus also if all it contains is a
+ <literal>default</literal>.)</para>
+ </listitem>
+
+ <listitem>
+ <para>If the <literal>switch</literal> uses
+ <literal>on</literal> then <literal>break</literal> is not
+ supported by <literal>switch</literal> directly, but
+ naturally can be used when the <literal>switch</literal> is
+ inside something else that supports <literal>break</literal>
+ (like inside a <link
+ linkend="ref.directive.list">list</link>).</para>
+ </listitem>
+
+ <listitem>
+ <para>With <literal>case</literal>,
+ <literal>continue</literal> does the same as
+ <literal>break</literal>. This is an old bug that works for
+ backward compatibility, but don't utilize it. With
+ <literal>on</literal>, this is fixed.</para>
+ </listitem>
+ </itemizedlist>
+ </listitem>
+ </itemizedlist>
</section>
<section>
<title>Description</title>
- <para>The usage of this directive is not recommended, as it's
- error-prone because of the fall-through behavior. Use <link
- linkend="ref.directive.elseif"><literal>elseif</literal></link>-s
- instead unless you want to exploit the fall-through behavior.</para>
+ <note>
+ <para>Using this directive with <literal>case</literal> is not
+ recommended, as it's error-prone because of the fall-through
+ behavior. Starting from FreeMarker 2.3.34, use
+ <literal>on</literal> instead of <literal>case</literal>. In
+ earlier versions use <link
+ linkend="ref.directive.elseif"><literal>elseif</literal></link>-s
+ instead. </para>
+ </note>
<para>Switch is used to choose a fragment of template depending on
- the value of an expression:</para>
+ the value of an expression, and is a shorthand instead using <link
+
linkend="ref.directive.if"><literal>if</literal>-<literal>elseif</literal>-<literal>else</literal></link>:</para>
+
+ <programlisting role="template"><#switch animal.size>
+ <#on "small">
+ Processed if animal.size was "small"
+ <#on "medium">
+ Processed if animal.size was "medium"
+ <#on "large", "extra large">
+ Processed if animal.size was "large" or "extra large"
+ <#default>
+ Processed if animal.size is neither of the above
+</#switch></programlisting>
+
+ <para>Before FreeMarker 2.3.24, <literal>on</literal> wasn't
+ supported, only <literal>case</literal> (note the usage of
+ <literal>break</literal>, and the intentional omission of it after
+ <literal><#case "large"></literal>):</para>
<programlisting role="template"><#switch animal.size>
<#case "small">
- This will be processed if it is small
+ Processed if animal.size was "small"
<#break>
<#case "medium">
- This will be processed if it is medium
+ Processed if animal.size was "medium"
<#break>
<#case "large">
- This will be processed if it is large
+ <#case "extra large">
+ Processed if animal.size was "large" or "extra large"
<#break>
<#default>
- This will be processed if it is neither
+ Processed if animal.size is neither of the above
</#switch></programlisting>
- <para>Inside the <literal>switch</literal> must be one or more
- <literal><#case <replaceable>value</replaceable>></literal>,
- and after all such <literal>case</literal> tags optionally one
- <literal><#default></literal>. When FM reaches the
- <literal>switch</literal> directive, it chooses a
- <literal>case</literal> directive where
+ <para>Above examples are basically equivalent with this:</para>
+
+ <programlisting role="template"><#assign value = animal.size>
+<#if value == "small">
+ Processed if animal.size was "small"
+<#elseif value == "medium">
+ Processed if animal.size was "medium"
+<#elseif value == "large" || value == "extra large">
+ Processed if animal.size was "large" or "extra large"
+<#else>
+ Processed if animal.size is neither of the above
+</#if></programlisting>
+
+ <para>That is, when the <literal>switch</literal> directive is
+ processed, it chooses an <literal>on</literal> or
+ <literal>case</literal> directive where a
<literal><replaceable>refValue</replaceable></literal> equals with
- <literal><replaceable>value</replaceable></literal> and continues
+ <literal><replaceable>value</replaceable></literal>, and continues
the processing of the template there. If there is no
- <literal>case</literal> directive with appropriate value then it
- continues processing at the <literal>default</literal> directive if
- that exists, otherwise it continues the processing after the end-tag
- of <literal>switch</literal>. And now comes the confusing thing:
- when it has chosen a <literal>case</literal> directive, it will
- continue the processing there, and will go ahead until it reaches a
- <literal>break</literal> directive. That is, it will not
- automatically leave the <literal>switch</literal> directive when it
- reaches another <literal>case</literal> directive or the
- <literal><#default></literal> tag. Example:</para>
+ <literal>on</literal> or <literal>case</literal> directive with
+ appropriate <literal><replaceable>refValue</replaceable></literal>,
+ then it continues processing at the <literal>default</literal>
+ directive, if that exists, otherwise it continues the processing
+ after the end-tag of <literal>switch</literal>.</para>
+
+ <para>Be careful with <literal>case</literal>'s fall-through
+ behavior! It means that after processing have jumped on the matching
+ <literal>case</literal>, it will not leave the
+ <literal>switch</literal> directive when it reaches another
+ <literal>case</literal> or <literal>default</literal>, only when it
+ reaches a <literal>break</literal>. Example:</para>
<programlisting role="template"><#switch x>
<#case 1>
@@ -25296,12 +25409,14 @@ or
d
</#switch></programlisting>
- <para>If <literal>x</literal> is 1, then it will print 1 2 d; if
- <literal>x</literal> is 2 then it will print 2 d; if
- <literal>x</literal> is 3 then it will print d. This is the
- mentioned fall-through behavior. The <literal>break</literal> tag
- instructs FM to immediately skip past the <literal>switch</literal>
- end-tag.</para>
+ <para>If <literal>x</literal> is <literal>1</literal>, then it will
+ print <literal>1 2 d</literal> (actually with more white-space
+ between); if <literal>x</literal> is <literal>2</literal> then it
+ will print <literal>2 d</literal>; if <literal>x</literal> is
+ <literal>3</literal> then it will print <literal>d</literal>. This
+ is usually unintended, or if it is intended then probably not
+ obvious for the reader, and that's why <literal>on</literal> is
+ recommended over <literal>case</literal>.</para>
</section>
</section>
@@ -30349,6 +30464,19 @@ TemplateModel x = env.getVariable("x"); // get
variable x</programlisting>
<title>Changes on the FTL side</title>
<itemizedlist>
+ <listitem>
+ <para>Added <literal>#on</literal> directive that's now
+ recommended instead of <literal>#case</literal> inside
+ <literal>#switch</literal>. It has no fall-through, so you need
+ not, and must not use <literal>#break</literal> like you did
+ with <literal>#case</literal>. Also, <literal>#on</literal> can
+ have a list of matching values, so you can use <literal><#on
+ 1, 2, 3>Was 1-3</literal> instead of <literal><#case
+ 1><#case 2><#case 3>Was
+ 1-3<#break></literal> See more <link
+ linkend="ref.directive.switch">here...</link></para>
+ </listitem>
+
<listitem>
<para>Added new built-ins that allow handling empty or blank
strings like the same way as if they were missing values (Java