You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2022/12/26 22:55:04 UTC
[freemarker] 02/02: ?c now works on strings, and it outputs a quoted string literal that's compatible with JSON, and JavaScript. Added ?cn to format missing/null values. Added cFormat setting to specify in what language should ?c output the values.
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
commit 782daab2958f13181bee5bbc5d069787d0c2c9d7
Author: ddekany <dd...@apache.org>
AuthorDate: Wed Dec 21 21:12:27 2022 +0100
?c now works on strings, and it outputs a quoted string literal that's compatible with JSON, and JavaScript. Added ?cn to format missing/null values. Added cFormat setting to specify in what language should ?c output the values.
---
.../freemarker/core/AbstractJSONLikeFormat.java | 76 ++
.../freemarker/core/AbstractLegacyCFormat.java | 105 +++
src/main/java/freemarker/core/BuiltIn.java | 3 +-
.../freemarker/core/BuiltInsForMultipleTypes.java | 139 +---
src/main/java/freemarker/core/CFormat.java | 80 ++
.../freemarker/core/CTemplateNumberFormat.java | 44 +-
src/main/java/freemarker/core/Configurable.java | 203 +++---
.../java/freemarker/core/Default230CFormat.java | 73 ++
.../java/freemarker/core/Default2321CFormat.java | 72 ++
src/main/java/freemarker/core/Environment.java | 212 ++++--
src/main/java/freemarker/core/EvalUtil.java | 2 +-
src/main/java/freemarker/core/JSONCFormat.java | 56 ++
src/main/java/freemarker/core/JavaCFormat.java | 87 +++
.../java/freemarker/core/JavaScriptCFormat.java | 51 ++
.../freemarker/core/JavaTemplateNumberFormat.java | 2 +-
src/main/java/freemarker/core/PropertySetting.java | 32 +-
.../freemarker/core/TemplateConfiguration.java | 9 +-
src/main/java/freemarker/core/XSCFormat.java | 91 +++
.../java/freemarker/core/_StandardCLanguages.java | 40 +
.../java/freemarker/template/Configuration.java | 57 +-
.../java/freemarker/template/_TemplateAPI.java | 7 +-
.../freemarker/template/utility/StringUtil.java | 166 +++--
src/manual/en_US/book.xml | 812 ++++++++++++++++++---
.../core/BooleanFormatEnvironmentCachingTest.java | 57 ++
.../java/freemarker/core/CAndCnBuiltInTest.java | 142 ++++
.../java/freemarker/core/CFormatTemplateTest.java | 73 ++
.../freemarker/core/CTemplateNumberFormatTest.java | 11 +-
src/test/java/freemarker/core/CustomCFormat.java | 83 +++
.../java/freemarker/core/NumberFormatTest.java | 7 +-
.../freemarker/core/TemplateConfigurationTest.java | 1 +
.../freemarker/template/ConfigurationTest.java | 58 +-
.../template/utility/StringUtilTest.java | 2 +-
.../test/templatesuite/templates/number-format.ftl | 42 +-
33 files changed, 2409 insertions(+), 486 deletions(-)
diff --git a/src/main/java/freemarker/core/AbstractJSONLikeFormat.java b/src/main/java/freemarker/core/AbstractJSONLikeFormat.java
new file mode 100644
index 00000000..190ce697
--- /dev/null
+++ b/src/main/java/freemarker/core/AbstractJSONLikeFormat.java
@@ -0,0 +1,76 @@
+/*
+ * 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.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+/**
+ * Defines the methods in {@link CFormat} that are the same for all JSON-like languages.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @since 2.3.32
+ */
+public abstract class AbstractJSONLikeFormat extends CFormat {
+ private static final TemplateNumberFormat TEMPLATE_NUMBER_FORMAT = new CTemplateNumberFormat(
+ "Infinity", "-Infinity", "NaN",
+ "Infinity", "-Infinity", "NaN");
+
+ private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+ static {
+ DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+ symbols.setInfinity("Infinity");
+ symbols.setNaN("NaN");
+ LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+ }
+
+ // Visibility is not "protected" to avoid external implementations while this class is experimental.
+ AbstractJSONLikeFormat() {
+ }
+
+ @Override
+ String getTrueString() {
+ return "true";
+ }
+
+ @Override
+ String getFalseString() {
+ return "false";
+ }
+
+ @Override
+ final String getNullString() {
+ return "null";
+ }
+
+ @Override
+ final TemplateNumberFormat getTemplateNumberFormat() {
+ return TEMPLATE_NUMBER_FORMAT;
+ }
+
+ @Override
+ NumberFormat getLegacyNumberFormat() {
+ return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+ }
+}
diff --git a/src/main/java/freemarker/core/AbstractLegacyCFormat.java b/src/main/java/freemarker/core/AbstractLegacyCFormat.java
new file mode 100644
index 00000000..99440684
--- /dev/null
+++ b/src/main/java/freemarker/core/AbstractLegacyCFormat.java
@@ -0,0 +1,105 @@
+/*
+ * 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.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * Super class of {@link CFormat}-s that merely exist to mimic old {@code ?c} behavior for backward compatibility.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @since 2.3.32
+ * @see AbstractJSONLikeFormat
+ */
+public abstract class AbstractLegacyCFormat extends CFormat {
+ // Visibility is not "protected" to avoid external implementations while this class is experimental.
+ AbstractLegacyCFormat() {
+ }
+
+ @Override
+ final String formatString(String s, Environment env) throws TemplateException {
+ return StringUtil.jsStringEnc(s, true, true);
+ }
+
+ @Override
+ final TemplateNumberFormat getTemplateNumberFormat() {
+ return new LegacyCTemplateNumberFormat();
+ }
+
+ @Override
+ String getTrueString() {
+ return "true";
+ }
+
+ @Override
+ String getFalseString() {
+ return "false";
+ }
+
+ @Override
+ final String getNullString() {
+ return "null";
+ }
+
+ abstract NumberFormat getLegacyNumberFormat();
+
+ final class LegacyCTemplateNumberFormat extends JavaTemplateNumberFormat {
+
+ public LegacyCTemplateNumberFormat() {
+ super(getLegacyNumberFormat(), Environment.COMPUTER_FORMAT_STRING);
+ }
+
+ @Override
+ public String formatToPlainText(TemplateNumberModel numberModel) throws UnformattableValueException,
+ TemplateModelException {
+ Number number = TemplateFormatUtil.getNonNullNumber(numberModel);
+ return format(number);
+ }
+
+ @Override
+ public boolean isLocaleBound() {
+ return false;
+ }
+
+ @Override
+ String format(Number number) throws UnformattableValueException {
+ if (number instanceof Integer || number instanceof Long) {
+ // Accelerate these fairly common cases
+ return number.toString();
+ }
+ return super.format(number);
+ }
+
+ @Override
+ public String getDescription() {
+ return "LegacyC(" + super.getDescription() + ")";
+ }
+
+ }
+
+}
diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index 1631b76b..708c562e 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -85,7 +85,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
static final Set<String> CAMEL_CASE_NAMES = new TreeSet<>();
static final Set<String> SNAKE_CASE_NAMES = new TreeSet<>();
- static final int NUMBER_OF_BIS = 295;
+ static final int NUMBER_OF_BIS = 296;
static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new HashMap<>(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
static final String BI_NAME_SNAKE_CASE_WITH_ARGS = "with_args";
@@ -103,6 +103,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
putBI("boolean", new BuiltInsForStringsMisc.booleanBI());
putBI("byte", new byteBI());
putBI("c", new BuiltInsForMultipleTypes.cBI());
+ putBI("cn", new BuiltInsForMultipleTypes.cnBI());
putBI("cap_first", "capFirst", new BuiltInsForStringsBasic.cap_firstBI());
putBI("capitalize", new BuiltInsForStringsBasic.capitalizeBI());
putBI("ceiling", new ceilingBI());
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index b214d8c3..e2eb2731 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -56,95 +56,52 @@ import freemarker.template.utility.NumberUtil;
*/
class BuiltInsForMultipleTypes {
- static class cBI extends AbstractCBI implements ICIChainMember {
-
- static class BIBeforeICI2d3d21 extends AbstractCBI {
-
- @Override
- protected TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateModelException {
- Number num = EvalUtil.modelToNumber((TemplateNumberModel) model, target);
- if (num instanceof Integer || num instanceof Long) {
- // Accelerate these fairly common cases
- return new SimpleScalar(num.toString());
- } else {
- return new SimpleScalar(env.getCNumberFormat().format(num));
- }
- }
-
+ static class cBI extends AbstractCLikeBI {
+ final protected String formatNull(Environment env) throws InvalidReferenceException {
+ throw InvalidReferenceException.getInstance(target, env);
}
+ }
- static class BIBeforeICI2d3d32 extends AbstractCBI implements ICIChainMember {
- private final BIBeforeICI2d3d21 prevICIObj = new BIBeforeICI2d3d21();
-
- @Override
- protected TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateModelException {
- Number num = EvalUtil.modelToNumber((TemplateNumberModel) model, target);
- if (num instanceof Integer || num instanceof Long) {
- // Accelerate these fairly common cases
- return new SimpleScalar(num.toString());
- // INF etc. is properly handled by ?c since 2.3.21, but the getCNumberFormat returns the pre 2.3.21
- // format before IcI 2.3.31, so we keep these here:
- } else if (num instanceof Double) {
- double n = num.doubleValue();
- if (n == Double.POSITIVE_INFINITY) {
- return new SimpleScalar("INF");
- }
- if (n == Double.NEGATIVE_INFINITY) {
- return new SimpleScalar("-INF");
- }
- if (Double.isNaN(n)) {
- return new SimpleScalar("NaN");
- }
- // Deliberately falls through
- } else if (num instanceof Float) {
- float n = num.floatValue();
- if (n == Float.POSITIVE_INFINITY) {
- return new SimpleScalar("INF");
- }
- if (n == Float.NEGATIVE_INFINITY) {
- return new SimpleScalar("-INF");
- }
- if (Float.isNaN(n)) {
- return new SimpleScalar("NaN");
- }
- // Deliberately falls through
- }
-
- return new SimpleScalar(env.getCNumberFormat().format(num));
- }
-
- @Override
- public int getMinimumICIVersion() {
- return _VersionInts.V_2_3_21;
- }
-
- @Override
- public Object getPreviousICIChainMember() {
- return prevICIObj;
- }
+ static class cnBI extends AbstractCLikeBI {
+ final protected String formatNull(Environment env) {
+ return env.getCFormat().getNullString();
}
+ }
+
+ private static abstract class AbstractCLikeBI extends BuiltIn {
@Override
- protected TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateException {
- try {
- return new SimpleScalar(CTemplateNumberFormat.INSTANCE.formatToPlainText((TemplateNumberModel) model));
- } catch (TemplateValueFormatException e) {
- throw _MessageUtil.newCantFormatNumberException(CTemplateNumberFormat.INSTANCE, target, e, false);
+ final TemplateModel _eval(Environment env) throws TemplateException {
+ final String result;
+ final TemplateModel model = target.eval(env);
+ if (model instanceof TemplateNumberModel) {
+ TemplateNumberFormat cTemplateNumberFormat = env.getCTemplateNumberFormat();
+ try {
+ result = cTemplateNumberFormat.formatToPlainText((TemplateNumberModel) model);
+ } catch (TemplateValueFormatException e) {
+ throw _MessageUtil.newCantFormatNumberException(cTemplateNumberFormat, target, e, false);
+ }
+ } else if (model instanceof TemplateBooleanModel) {
+ boolean b = ((TemplateBooleanModel) model).getAsBoolean();
+ CFormat cFormat = env.getCFormat();
+ result = b ? cFormat.getTrueString() : cFormat.getFalseString();
+ } else if (model instanceof TemplateScalarModel) {
+ String s = EvalUtil.modelToString((TemplateScalarModel) model, target, env);
+ result = env.getCFormat().formatString(s, env);
+ } else if (model == null) {
+ result = formatNull(env);
+ } else {
+ throw new UnexpectedTypeException(
+ target, model,
+ "number, boolean, or string",
+ new Class[] { TemplateNumberModel.class, TemplateBooleanModel.class, TemplateScalarModel.class },
+ env);
}
+ return new SimpleScalar(result);
}
- private final BIBeforeICI2d3d32 prevICIObj = new BIBeforeICI2d3d32();
+ protected abstract String formatNull(Environment env) throws InvalidReferenceException;
- @Override
- public int getMinimumICIVersion() {
- return _VersionInts.V_2_3_32;
- }
-
- @Override
- public Object getPreviousICIChainMember() {
- return prevICIObj;
- }
-
}
static class dateBI extends BuiltIn {
@@ -802,26 +759,4 @@ class BuiltInsForMultipleTypes {
// Can't be instantiated
private BuiltInsForMultipleTypes() { }
- static abstract class AbstractCBI extends BuiltIn {
-
- @Override
- final TemplateModel _eval(Environment env) throws TemplateException {
- TemplateModel model = target.eval(env);
- if (model instanceof TemplateNumberModel) {
- return formatNumber(env, model);
- } else if (model instanceof TemplateBooleanModel) {
- return new SimpleScalar(((TemplateBooleanModel) model).getAsBoolean()
- ? MiscUtil.C_TRUE : MiscUtil.C_FALSE);
- } else {
- throw new UnexpectedTypeException(
- target, model,
- "number or boolean", new Class[] { TemplateNumberModel.class, TemplateBooleanModel.class },
- env);
- }
- }
-
- protected abstract TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateException;
-
- }
-
}
diff --git a/src/main/java/freemarker/core/CFormat.java b/src/main/java/freemarker/core/CFormat.java
new file mode 100644
index 00000000..3c04976c
--- /dev/null
+++ b/src/main/java/freemarker/core/CFormat.java
@@ -0,0 +1,80 @@
+/*
+ * 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.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+
+/**
+ * Defines a format (usually a computer language) that's used by the {@code c}, {@code cn} built-ins, and for the
+ * {@code "c"} and {@code "computer"} number formats (see {@link Configurable#setNumberFormat(String)}). A
+ * {@link CFormat} currently defines how numbers, booleans, and strings are converted to text that defines a similar
+ * value in the computer language that the {@link CFormat} is made for.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @since 2.3.32
+ */
+public abstract class CFormat {
+
+ // Visibility is not "protected" to avoid external implementations while this class is experimental.
+ CFormat() {
+ }
+
+ /**
+ * Gets/creates the number format to use; this is not always a cheap operation, so in case it will be called
+ * for many cases, the result should be cached or otherwise reused.
+ *
+ * <p>The returned object is typically not thread-safe. The implementation must ensure that if there's a singleton,
+ * which is mutable, or not thread-safe, then it's not returned, but a clone or copy of it. The caller of this
+ * method is not responsible for do any such cloning or copying.
+ */
+ abstract TemplateNumberFormat getTemplateNumberFormat();
+
+ /**
+ * Similar to {@link #getTemplateNumberFormat()}, but only exist to serve the deprecated
+ * {@link Environment#getCNumberFormat()} method. We don't expect the result of the formatting to be the same as
+ * with the {@link TemplateNumberFormat}, but this method should make some effort to be similar.
+ *
+ * @deprecated Use {@link #getTemplateNumberFormat()} instead, except in {@link Environment#getCNumberFormat()}.
+ */
+ @Deprecated
+ abstract NumberFormat getLegacyNumberFormat();
+
+ /**
+ * Format a {@link String} to a string literal.
+ *
+ * @param env
+ * Not {@code null}; is here mostly to be used to figure out escaping preferences (like based on
+ * {@link Environment#getOutputEncoding()}).
+ */
+ abstract String formatString(String s, Environment env) throws TemplateException;
+
+ abstract String getTrueString();
+
+ abstract String getFalseString();
+
+ abstract String getNullString();
+
+ public abstract String getName();
+}
diff --git a/src/main/java/freemarker/core/CTemplateNumberFormat.java b/src/main/java/freemarker/core/CTemplateNumberFormat.java
index 139e2d72..dc662b7a 100644
--- a/src/main/java/freemarker/core/CTemplateNumberFormat.java
+++ b/src/main/java/freemarker/core/CTemplateNumberFormat.java
@@ -19,23 +19,44 @@
package freemarker.core;
-
import java.math.BigDecimal;
import java.math.BigInteger;
+import freemarker.template.Configuration;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
/**
+ * The number format used by most {@link CFormat}-s starting from Incompatible Improvements
+ * {@link Configuration#VERSION_2_3_32}.
+ *
+ * <p>This {@link TemplateNumberFormat} implementation is thread-safe an immutable.
+ *
+ * <p>This is a lossless format, except that the number types are not kept. That is, the original number can always be
+ * exactly restored from the string representation, but only if you know what the type of the number was.
+ *
* @since 2.3.32
*/
final class CTemplateNumberFormat extends TemplateNumberFormat {
- static final TemplateNumberFormat INSTANCE = new CTemplateNumberFormat();
-
private static final float MAX_INCREMENT_1_FLOAT = 16777216f;
private static final double MAX_INCREMENT_1_DOUBLE = 9007199254740992d;
- private CTemplateNumberFormat() {
+ private final String doublePositiveInfinity;
+ private final String doubleNegativeInfinity;
+ private final String doubleNaN;
+ private final String floatPositiveInfinity;
+ private final String floatNegativeInfinity;
+ private final String floatNaN;
+
+ CTemplateNumberFormat(
+ String doublePositiveInfinity, String doubleNegativeInfinity, String doubleNaN,
+ String floatPositiveInfinity, String floatNegativeInfinity, String floatNaN) {
+ this.doublePositiveInfinity = doublePositiveInfinity;
+ this.doubleNegativeInfinity = doubleNegativeInfinity;
+ this.doubleNaN = doubleNaN;
+ this.floatPositiveInfinity = floatPositiveInfinity;
+ this.floatNegativeInfinity = floatNegativeInfinity;
+ this.floatNaN = floatNaN;
}
@Override
@@ -49,13 +70,13 @@ final class CTemplateNumberFormat extends TemplateNumberFormat {
} else if (num instanceof Double) {
double n = num.doubleValue();
if (n == Double.POSITIVE_INFINITY) {
- return "INF";
+ return doublePositiveInfinity;
}
if (n == Double.NEGATIVE_INFINITY) {
- return "-INF";
+ return doubleNegativeInfinity;
}
if (Double.isNaN(n)) {
- return "NaN";
+ return doubleNaN;
}
if (Math.floor(n) == n) {
if (Math.abs(n) <= MAX_INCREMENT_1_DOUBLE) {
@@ -84,13 +105,13 @@ final class CTemplateNumberFormat extends TemplateNumberFormat {
float n = num.floatValue();
if (n == Float.POSITIVE_INFINITY) {
- return "INF";
+ return floatPositiveInfinity;
}
if (n == Float.NEGATIVE_INFINITY) {
- return "-INF";
+ return floatNegativeInfinity;
}
if (Float.isNaN(n)) {
- return "NaN";
+ return floatNaN;
}
if (Math.floor(n) == n) {
if (Math.abs(n) <= MAX_INCREMENT_1_FLOAT) {
@@ -117,7 +138,8 @@ final class CTemplateNumberFormat extends TemplateNumberFormat {
if (scale <= 0) {
// A whole number. Myabe a long ID in a database or other system, and for those exponential form is not
// expected generally, so we avoid that. But then, it becomes too easy to write something like
- // 1e1000000000000 and kill the server with a terra byte long rendering of the number, so for lengths that
+ // 1e1000000000000 and kill the server with a terra byte long rendering of the number, so for lengths
+ // that
// realistically aren't ID-s or such, we use exponential format after all:
if (scale <= -100) {
return bd.toString(); // Will give exponential form for this scale
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index dd38163a..428ac41d 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -96,7 +96,14 @@ public class Configurable {
public static final String LOCALE_KEY_CAMEL_CASE = "locale";
/** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
public static final String LOCALE_KEY = LOCALE_KEY_SNAKE_CASE;
-
+
+ /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+ public static final String C_FORMAT_KEY_SNAKE_CASE = "c_format";
+ /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+ public static final String C_FORMAT_KEY_CAMEL_CASE = "cFormat";
+ /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+ public static final String C_FORMAT_KEY = C_FORMAT_KEY_SNAKE_CASE;
+
/** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
public static final String NUMBER_FORMAT_KEY_SNAKE_CASE = "number_format";
/** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
@@ -306,6 +313,7 @@ public class Configurable {
AUTO_IMPORT_KEY_SNAKE_CASE,
AUTO_INCLUDE_KEY_SNAKE_CASE,
BOOLEAN_FORMAT_KEY_SNAKE_CASE,
+ C_FORMAT_KEY_SNAKE_CASE,
CLASSIC_COMPATIBLE_KEY_SNAKE_CASE,
CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE,
CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE,
@@ -339,6 +347,7 @@ public class Configurable {
AUTO_IMPORT_KEY_CAMEL_CASE,
AUTO_INCLUDE_KEY_CAMEL_CASE,
BOOLEAN_FORMAT_KEY_CAMEL_CASE,
+ C_FORMAT_KEY_CAMEL_CASE,
CLASSIC_COMPATIBLE_KEY_CAMEL_CASE,
CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE,
@@ -368,6 +377,7 @@ public class Configurable {
private HashMap<Object, Object> customAttributes;
private Locale locale;
+ private CFormat cFormat;
private String numberFormat;
private String timeFormat;
private String dateFormat;
@@ -376,8 +386,8 @@ public class Configurable {
private TimeZone sqlDataAndTimeTimeZone;
private boolean sqlDataAndTimeTimeZoneSet;
private String booleanFormat;
- private String trueStringValue; // deduced from booleanFormat
- private String falseStringValue; // deduced from booleanFormat
+ private String booleanFormatCommaSplitTrueSide; // deduced from booleanFormat
+ private String booleanFormatCommaSplitFalseSide; // deduced from booleanFormat
private Integer classicCompatible;
private TemplateExceptionHandler templateExceptionHandler;
private AttemptExceptionReporter attemptExceptionReporter;
@@ -425,7 +435,7 @@ public class Configurable {
locale = _TemplateAPI.getDefaultLocale();
properties.setProperty(LOCALE_KEY, locale.toString());
-
+
timeZone = _TemplateAPI.getDefaultTimeZone();
properties.setProperty(TIME_ZONE_KEY, timeZone.getID());
@@ -443,7 +453,9 @@ public class Configurable {
dateTimeFormat = "";
properties.setProperty(DATETIME_FORMAT_KEY, dateTimeFormat);
-
+
+ cFormat = _TemplateAPI.getDefaultCFormat(incompatibleImprovements);
+
classicCompatible = Integer.valueOf(0);
properties.setProperty(CLASSIC_COMPATIBLE_KEY, classicCompatible.toString());
@@ -678,12 +690,43 @@ public class Configurable {
/**
* Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
*
+ * @since 2.3.32
+ */
+ public boolean isCFormatSet() {
+ return cFormat != null;
+ }
+
+ /**
+ * Sets the computer language that's used for the {@code c}, {@code cn} built-ins, and for the {@code "c"}
+ * (and {@code "computer"}) number format ({@link Environment#getCTemplateNumberFormat()}). That is, of the
+ * templates output pieces in a computer language (like JavaScript), you should set what's that here.
+ *
+ * @since 2.3.32
+ */
+ public void setCFormat(CFormat cFormat) {
+ NullArgumentException.check("cFormat", cFormat);
+ this.cFormat = cFormat;
+ }
+
+ /**
+ * Getter pair of {@link #setCFormat(CFormat)}. Not {@code null}.
+ *
+ * @since 2.3.32
+ */
+ public CFormat getCFormat() {
+ return cFormat != null ? cFormat : parent.getCFormat();
+ }
+
+ /**
+ * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+ *
* @since 2.3.24
*/
public boolean isLocaleSet() {
return locale != null;
}
-
+
+
/**
* Sets the time zone to use when formatting date/time values.
* Defaults to the system time zone ({@link TimeZone#getDefault()}), regardless of the "locale" FreeMarker setting,
@@ -815,7 +858,7 @@ public class Configurable {
* <li>{@code "number"}: The number format returned by {@link NumberFormat#getNumberInstance(Locale)}</li>
* <li>{@code "currency"}: The number format returned by {@link NumberFormat#getCurrencyInstance(Locale)}</li>
* <li>{@code "percent"}: The number format returned by {@link NumberFormat#getPercentInstance(Locale)}</li>
- * <li>{@code "c"} (recognized since 2.3.34), or {@code "computer"} (same as {@code "c"}, but also recognized by
+ * <li>{@code "c"} (recognized since 2.3.32), or {@code "computer"} (same as {@code "c"}, but also recognized by
* older versions): The number format used by FTL's {@code c} built-in (like in {@code someNumber?c}).</li>
* <li>{@link java.text.DecimalFormat} pattern (like {@code "0.##"}). This syntax is extended by FreeMarker
* so that you can specify options like the rounding mode and the symbols used after a 2nd semicolon. For
@@ -985,14 +1028,9 @@ public class Configurable {
public void setBooleanFormat(String booleanFormat) {
NullArgumentException.check("booleanFormat", booleanFormat);
- if (booleanFormat.equals(C_TRUE_FALSE)) {
- // C_TRUE_FALSE is the default for BC, but it's not a good default for human audience formatting, so we
- // pretend that it wasn't set.
- trueStringValue = null;
- falseStringValue = null;
- } else if (booleanFormat.equals(C_FORMAT_STRING)) {
- trueStringValue = MiscUtil.C_TRUE;
- falseStringValue = MiscUtil.C_FALSE;
+ if (booleanFormat.equals(C_TRUE_FALSE) || booleanFormat.equals(C_FORMAT_STRING)) {
+ booleanFormatCommaSplitTrueSide = null;
+ booleanFormatCommaSplitFalseSide = null;
} else {
int commaIdx = booleanFormat.indexOf(',');
if (commaIdx == -1) {
@@ -1001,8 +1039,8 @@ public class Configurable {
"or it must be \"" + C_FORMAT_STRING + "\", but it was " +
StringUtil.jQuote(booleanFormat) + ".");
}
- trueStringValue = booleanFormat.substring(0, commaIdx);
- falseStringValue = booleanFormat.substring(commaIdx + 1);
+ booleanFormatCommaSplitTrueSide = booleanFormat.substring(0, commaIdx);
+ booleanFormatCommaSplitFalseSide = booleanFormat.substring(commaIdx + 1);
}
this.booleanFormat = booleanFormat;
@@ -1015,91 +1053,40 @@ public class Configurable {
public String getBooleanFormat() {
return booleanFormat != null ? booleanFormat : parent.getBooleanFormat();
}
-
+
/**
- * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
- *
- * @since 2.3.24
+ * Non-{@code null} if the {@link #setBooleanFormat(String)} boolean_format} setting is comma separated true, and
+ * false strings.
+ *
+ * @since 2.3.32
*/
- public boolean isBooleanFormatSet() {
- return booleanFormat != null;
- }
-
- String formatBoolean(boolean value, boolean fallbackToTrueFalse) throws TemplateException {
- if (value) {
- String s = getTrueStringValue();
- if (s == null) {
- if (fallbackToTrueFalse) {
- return MiscUtil.C_TRUE;
- } else {
- throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
- }
- } else {
- return s;
- }
- } else {
- String s = getFalseStringValue();
- if (s == null) {
- if (fallbackToTrueFalse) {
- return MiscUtil.C_FALSE;
- } else {
- throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
- }
- } else {
- return s;
- }
+ String getBooleanFormatCommaSplitTrueSide() {
+ if (booleanFormat != null) {
+ return booleanFormatCommaSplitTrueSide;
}
- }
-
- private _ErrorDescriptionBuilder getNullBooleanFormatErrorDescription() {
- return new _ErrorDescriptionBuilder(
- "Can't convert boolean to string automatically, because the \"", BOOLEAN_FORMAT_KEY ,"\" setting was ",
- new _DelayedJQuote(getBooleanFormat()),
- (getBooleanFormat().equals(C_TRUE_FALSE)
- ? ", which is the legacy deprecated default, and we treat it as if no format was set. "
- + "This is the default configuration; you should provide the format explicitly for each "
- + "place where you print a boolean."
- : ".")
- ).tips(
- "Write something like myBool?string('yes', 'no') to specify boolean formatting in place.",
- new Object[]{
- "If you want \"true\"/\"false\" result as you are generating computer-language output "
- + "(not for direct human consumption), then use \"?c\", like ${myBool?c}. (If you "
- + "always generate computer-language output, then it's might be reasonable to set "
- + "the \"", BOOLEAN_FORMAT_KEY, "\" setting to \"c\" instead.)",
- },
- new Object[] {
- "If you need the same two values on most places, the programmers can set the \"",
- BOOLEAN_FORMAT_KEY ,"\" setting to something like \"yes,no\". However, then it will be easy to "
- + "unwillingly format booleans like that."
- }
- );
+ return parent != null ? parent.getBooleanFormatCommaSplitTrueSide() : null;
}
/**
- * Returns the string to which {@code true} is converted to for human audience, or {@code null} if automatic
- * coercion to string is not allowed. The default value is {@code null}.
- *
- * <p>This value is deduced from the {@code "boolean_format"} setting.
- * Confusingly, for backward compatibility (at least until 2.4) that defaults to {@code "true,false"}, yet this
- * defaults to {@code null}. That's so because {@code "true,false"} is treated exceptionally, as that default is a
- * historical mistake in FreeMarker, since it targets computer language output, not human writing. Thus it's
- * ignored.
- *
- * @since 2.3.20
+ * Non-{@code null} if the {@link #setBooleanFormat(String)} boolean_format} setting is comma separated true, and
+ * false strings.
+ *
+ * @since 2.3.32
*/
- String getTrueStringValue() {
- // The first step deliberately tests booleanFormat instead of trueStringValue!
- return booleanFormat != null ? trueStringValue : (parent != null ? parent.getTrueStringValue() : null);
+ String getBooleanFormatCommaSplitFalseSide() {
+ if (booleanFormat != null) {
+ return booleanFormatCommaSplitFalseSide;
+ }
+ return parent != null ? parent.getBooleanFormatCommaSplitFalseSide() : null;
}
/**
- * Same as {@link #getTrueStringValue()} but with {@code false}.
- * @since 2.3.20
+ * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+ *
+ * @since 2.3.24
*/
- String getFalseStringValue() {
- // The first step deliberately tests booleanFormat instead of falseStringValue!
- return booleanFormat != null ? falseStringValue : (parent != null ? parent.getFalseStringValue() : null);
+ public boolean isBooleanFormatSet() {
+ return booleanFormat != null;
}
/**
@@ -2178,7 +2165,7 @@ public class Configurable {
* See {@link #setLocale(Locale)}.
* <br>String value: local codes with the usual format in Java, such as {@code "en_US"}, or since 2.3.26,
* "JVM default" (ignoring case) to use the default locale of the Java environment.
- *
+ *
* <li><p>{@code "classic_compatible"}:
* See {@link #setClassicCompatible(boolean)} and {@link Configurable#setClassicCompatibleAsInt(int)}.
* <br>String value: {@code "true"}, {@code "false"}, also since 2.3.20 {@code 0} or {@code 1} or {@code 2}.
@@ -2194,7 +2181,15 @@ public class Configurable {
* <br>String value: Interpreted as an <a href="#fm_obe">object builder expression</a>.
* <br>Example: <code>{ "trade": com.example.TradeTemplateDateFormatFactory,
* "log": com.example.LogTemplateDateFormatFactory }</code>
- *
+ *
+ * <li><p>{@code "c_format"}:
+ * See {@link Configuration#setCFormat(CFormat)}.
+ * <br>String value: {@code "default"} (case insensitive) for the default (on {@link Configuration} only), or
+ * one of the predefined values {@value JSONCFormat#NAME}, {@value JavaScriptCFormat#NAME},
+ * {@value JavaCFormat#NAME}, {@value XSCFormat#NAME}, {@value Default230CFormat#NAME},
+ * {@value Default2321CFormat#NAME}, or an <a href="#fm_obe">object builder expression</a> that gives a
+ * {@link CFormat} object.
+ *
* <li><p>{@code "template_exception_handler"}:
* See {@link #setTemplateExceptionHandler(TemplateExceptionHandler)}.
* <br>String value: If the value contains dot, then it's interpreted as an <a href="#fm_obe">object builder
@@ -2249,7 +2244,7 @@ public class Configurable {
*
* <li><p>{@code "time_zone"}:
* See {@link #setTimeZone(TimeZone)}.
- * <br>String value: With the format as {@link TimeZone#getTimeZone} defines it. Also, since 2.3.21
+ * <br>String value: With the format as {@link TimeZone#getTimeZone(String)} defines it. Also, since 2.3.21
* {@code "JVM default"} can be used that will be replaced with the actual JVM default time zone when
* {@link #setSetting(String, String)} is called.
* For example {@code "GMT-8:00"} or {@code "America/Los_Angeles"}
@@ -2259,8 +2254,8 @@ public class Configurable {
* <li><p>{@code sql_date_and_time_time_zone}:
* See {@link #setSQLDateAndTimeTimeZone(TimeZone)}.
* Since 2.3.21.
- * <br>String value: With the format as {@link TimeZone#getTimeZone} defines it. Also, {@code "JVM default"}
- * can be used that will be replaced with the actual JVM default time zone when
+ * <br>String value: With the format as {@link TimeZone#getTimeZone(String)} defines it. Also,
+ * {@code "JVM default"} can be used that will be replaced with the actual JVM default time zone when
* {@link #setSetting(String, String)} is called. Also {@code "null"} can be used, which has the same effect
* as {@link #setSQLDateAndTimeTimeZone(TimeZone) setSQLDateAndTimeTimeZone(null)}.
*
@@ -2410,7 +2405,7 @@ public class Configurable {
* <br>String value: {@code "default"} (case insensitive) for the default, or an
* <a href="#fm_obe">object builder expression</a> that gives an {@link OutputFormat}, for example
* {@code HTMLOutputFormat} or {@code XMLOutputFormat}.
- *
+ *
* <li><p>{@code "registered_custom_output_formats"}:
* See {@link Configuration#setRegisteredCustomOutputFormats(Collection)}.
* <br>String value: an <a href="#fm_obe">object builder expression</a> that gives a {@link List} of
@@ -2750,6 +2745,20 @@ public class Configurable {
}
} else if (BOOLEAN_FORMAT_KEY_SNAKE_CASE.equals(name) || BOOLEAN_FORMAT_KEY_CAMEL_CASE.equals(name)) {
setBooleanFormat(value);
+ } else if (C_FORMAT_KEY_SNAKE_CASE.equals(name) || C_FORMAT_KEY_CAMEL_CASE.equals(name)) {
+ if (value.equalsIgnoreCase(DEFAULT)) {
+ if (this instanceof Configuration) {
+ ((Configuration) this).unsetCFormat();
+ } else {
+ throw invalidSettingValueException(name, value);
+ }
+ } else {
+ CFormat cFormat = StandardCFormats.STANDARD_C_FORMATS.get(value);
+ setCFormat(
+ cFormat != null ? cFormat
+ : (CFormat) _ObjectBuilderSettingEvaluator.eval(
+ value, CFormat.class, false, _SettingEvaluationEnvironment.getCurrent()));
+ }
} else if (OUTPUT_ENCODING_KEY_SNAKE_CASE.equals(name) || OUTPUT_ENCODING_KEY_CAMEL_CASE.equals(name)) {
setOutputEncoding(value);
} else if (URL_ESCAPING_CHARSET_KEY_SNAKE_CASE.equals(name)
diff --git a/src/main/java/freemarker/core/Default230CFormat.java b/src/main/java/freemarker/core/Default230CFormat.java
new file mode 100644
index 00000000..b1ba1b4e
--- /dev/null
+++ b/src/main/java/freemarker/core/Default230CFormat.java
@@ -0,0 +1,73 @@
+/*
+ * 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.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import freemarker.template.Configuration;
+import freemarker.template.Version;
+
+/**
+ * Corresponds to the behavior of {@code ?c} if
+ * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is less than
+ * {@link Configuration#VERSION_2_3_21}.
+ * The only good reason for using this is strict backward-compatibility.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @see Default2321CFormat
+ * @see JSONCFormat
+ *
+ * @since 2.3.32
+ */
+public class Default230CFormat extends AbstractLegacyCFormat {
+ public static final Default230CFormat INSTANCE = new Default230CFormat();
+ public static final String NAME = "default 2.3.0";
+
+ /**
+ * "c" number format as it was before Incompatible Improvements 2.3.21.
+ */
+ private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = new DecimalFormat(
+ "0.################",
+ new DecimalFormatSymbols(Locale.US));
+ static {
+ LEGACY_NUMBER_FORMAT_PROTOTYPE.setGroupingUsed(false);
+ LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalSeparatorAlwaysShown(false);
+ }
+
+ private Default230CFormat() {
+ }
+
+ @Override
+ NumberFormat getLegacyNumberFormat() {
+ // Note: DecimalFormat-s aren't thread-safe, so you must clone the static field value.
+ return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+}
diff --git a/src/main/java/freemarker/core/Default2321CFormat.java b/src/main/java/freemarker/core/Default2321CFormat.java
new file mode 100644
index 00000000..d9a6463a
--- /dev/null
+++ b/src/main/java/freemarker/core/Default2321CFormat.java
@@ -0,0 +1,72 @@
+/*
+ * 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.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+import freemarker.template.Configuration;
+import freemarker.template.Version;
+
+/**
+ * Corresponds to the behavior of {@code ?c} if
+ * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is between
+ * {@link Configuration#VERSION_2_3_21} and {@link Configuration#VERSION_2_3_31}.
+ * The only good reason for using this is strict backward-compatibility.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @see Default230CFormat
+ * @see JSONCFormat
+ *
+ * @since 2.3.32
+ */
+public class Default2321CFormat extends AbstractLegacyCFormat {
+ public static final Default2321CFormat INSTANCE = new Default2321CFormat();
+ public static final String NAME = "default 2.3.21";
+
+ /**
+ * "c" number format as it was starting from Incompatible Improvements 2.3.21.
+ */
+ private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+ static {
+ DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+ symbols.setInfinity("INF");
+ symbols.setNaN("NaN");
+ LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+ }
+
+ private Default2321CFormat() {
+ }
+
+ @Override
+ NumberFormat getLegacyNumberFormat() {
+ // Note: DecimalFormat-s aren't thread-safe, so you must clone the static field value.
+ return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+}
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index a078eaea..6a00a788 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -26,8 +26,6 @@ import java.io.Writer;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.Collator;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
@@ -104,28 +102,6 @@ public final class Environment extends Configurable {
private static final Logger LOG = Logger.getLogger("freemarker.runtime");
private static final Logger ATTEMPT_LOGGER = Logger.getLogger("freemarker.runtime.attempt");
- // Do not use this object directly; clone it first! DecimalFormat isn't
- // thread-safe.
- /** "c" number format as it was before Incompatible Improvements 2.3.21. */
- private static final DecimalFormat C_NUMBER_FORMAT_ICI_2_3_20 = new DecimalFormat(
- "0.################",
- new DecimalFormatSymbols(Locale.US));
- static {
- C_NUMBER_FORMAT_ICI_2_3_20.setGroupingUsed(false);
- C_NUMBER_FORMAT_ICI_2_3_20.setDecimalSeparatorAlwaysShown(false);
- }
-
- // Do not use this object directly; clone it first! DecimalFormat isn't
- // thread-safe.
- /** "c" number format as it was starting from Incompatible Improvements 2.3.21. */
- private static final DecimalFormat C_NUMBER_FORMAT_ICI_2_3_21 = (DecimalFormat) C_NUMBER_FORMAT_ICI_2_3_20.clone();
- static {
- DecimalFormatSymbols symbols = C_NUMBER_FORMAT_ICI_2_3_21.getDecimalFormatSymbols();
- symbols.setInfinity("INF");
- symbols.setNaN("NaN");
- C_NUMBER_FORMAT_ICI_2_3_21.setDecimalFormatSymbols(symbols);
- }
-
private final Configuration configuration;
private final boolean incompatibleImprovementsGE2328;
private final TemplateHashModel rootDataModel;
@@ -167,6 +143,16 @@ public final class Environment extends Configurable {
@Deprecated
private NumberFormat cNumberFormat;
private TemplateNumberFormat cTemplateNumberFormat;
+ private TemplateNumberFormat cTemplateNumberFormatWithPre2331IcIBug;
+
+ /**
+ * Should be a boolean "trueAndFalseStringsCached", but with Incompatible Improvements less than 2.3.22 the
+ * effective value of {@code boolean_format} could change because of {@code #import} and {@code #include},
+ * as those changed the parent template. So we need this cache invalidation trick.
+ */
+ private Configurable trueAndFalseStringsCachedForParent;
+ private String cachedTrueString;
+ private String cachedFalseString;
/**
* Used by the "iso_" built-ins to accelerate formatting.
@@ -1631,6 +1617,8 @@ public final class Environment extends Configurable {
return format;
}
+ static final String COMPUTER_FORMAT_STRING = "computer";
+
/**
* Returns the {@link TemplateNumberFormat} for the given parameters without using the {@link Environment}-level
* cache. Of course, the {@link TemplateNumberFormatFactory} involved might still uses its own cache.
@@ -1669,8 +1657,8 @@ public final class Environment extends Configurable {
return formatFactory.get(params, locale, this);
} else if (formatStringLen >= 1 && formatString.charAt(0) == 'c'
- && (formatStringLen == 1 || formatString.equals(COMPUTER))) {
- return getCTemplateNumberFormat();
+ && (formatStringLen == 1 || formatString.equals(COMPUTER_FORMAT_STRING))) {
+ return getCTemplateNumberFormatWithPre2331IcIBug();
} else {
return JavaTemplateNumberFormatFactory.INSTANCE.get(formatString, locale, this);
}
@@ -1684,41 +1672,65 @@ public final class Environment extends Configurable {
*
* @deprecated Use {@link #getCTemplateNumberFormat()} instead. This method can't return the format used when
* {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is 2.3.32,
- * or greater, and instead it will fall back to return the format that was used for 2.3.31.
+ * or greater, and instead it will fall back to return the format that was used for 2.3.31. Also, as its described
+ * earlier, this method was inconsistent with {@code ?c} between Incompatible Improvements 2.3.21 and 2.3.30, while
+ * {@link #getCTemplateNumberFormat()} behaves as {@code ?c} for all Incompatible Improvements value.
*/
@Deprecated
public NumberFormat getCNumberFormat() {
- ensureCNumberFormatInitialized();
+ if (cNumberFormat == null) {
+ cNumberFormat = getCFormatWithPre2331IcIBug().getLegacyNumberFormat();
+ }
return cNumberFormat;
}
/**
- * Returns the {@link TemplateNumberFormat} used for the <tt>c</tt> built-in uses.
- * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is 2.3.32 or greater.
+ * Returns the {@link TemplateNumberFormat} used for the <tt>c</tt> built-in currently uses in this environment.
+ * Calling this method for many times is fine, as it internally caches the result object.
+ * Remember that {@link TemplateNumberFormat}-s are not thread-safe objects, so the resulting object should only
+ * be used in the same thread where this {@link Environment} runs.
*
* @since 2.3.32
*/
public TemplateNumberFormat getCTemplateNumberFormat() {
- if (configuration.getIncompatibleImprovements().intValue() < _VersionInts.V_2_3_32) {
- ensureCNumberFormatInitialized();
- return cTemplateNumberFormat;
+ if (cTemplateNumberFormat == null) {
+ cTemplateNumberFormat = getCFormat().getTemplateNumberFormat();
+ }
+ return cTemplateNumberFormat;
+ }
+
+ /**
+ * Like {@link #getCTemplateNumberFormat()}, but emulates the same bug as
+ * {@link #getCNumberFormat()} if a legacy default {@link CFormat} is used.
+ */
+ private TemplateNumberFormat getCTemplateNumberFormatWithPre2331IcIBug() {
+ if (cTemplateNumberFormatWithPre2331IcIBug == null) {
+ cTemplateNumberFormatWithPre2331IcIBug = getCFormatWithPre2331IcIBug().getTemplateNumberFormat();
}
- return CTemplateNumberFormat.INSTANCE;
+ return cTemplateNumberFormatWithPre2331IcIBug;
}
- static final String COMPUTER = "computer";
+ private CFormat getCFormatWithPre2331IcIBug() {
+ CFormat cFormat = getCFormat();
+ if (cFormat == Default2321CFormat.INSTANCE
+ && configuration.getIncompatibleImprovements().intValue() < _VersionInts.V_2_3_31) {
+ return Default230CFormat.INSTANCE;
+ }
+ return cFormat;
+ }
- private void ensureCNumberFormatInitialized() {
- // Note: DecimalFormat-s aren't thread-safe, so you must clone the static field value.
- if (cNumberFormat == null) {
- if (configuration.getIncompatibleImprovements().intValue() >= _VersionInts.V_2_3_31) {
- cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT_ICI_2_3_21.clone();
- } else {
- cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT_ICI_2_3_20.clone();
+ @Override
+ public void setCFormat(CFormat cFormat) {
+ CFormat prevCFormat = getCFormat();
+ super.setCFormat(cFormat);
+ if (prevCFormat != cFormat) {
+ cTemplateNumberFormat = null;
+ cTemplateNumberFormatWithPre2331IcIBug = null;
+ if (cachedTemplateNumberFormats != null) {
+ cachedTemplateNumberFormats.remove(C_FORMAT_STRING);
+ cachedTemplateNumberFormats.remove(COMPUTER_FORMAT_STRING);
}
- // Note this uses the legacy name "computer", instead of "c". From IcI 2.3.32 we are using
- // CTemplateNumberFormat.INSTANCE instead, so users won't see this anymore.
- cTemplateNumberFormat = new JavaTemplateNumberFormat(cNumberFormat, COMPUTER);
+ clearCachedTrueAndFalseString();
}
}
@@ -1761,6 +1773,116 @@ public final class Environment extends Configurable {
}
}
+ @Override
+ public void setBooleanFormat(String booleanFormat) {
+ super.setBooleanFormat(booleanFormat);
+ clearCachedTrueAndFalseString();
+ }
+
+ String formatBoolean(boolean value, boolean fallbackToTrueFalse) throws TemplateException {
+ if (value) {
+ String s = getTrueStringValue();
+ if (s == null) {
+ if (fallbackToTrueFalse) {
+ return MiscUtil.C_TRUE;
+ } else {
+ throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
+ }
+ } else {
+ return s;
+ }
+ } else {
+ String s = getFalseStringValue();
+ if (s == null) {
+ if (fallbackToTrueFalse) {
+ return MiscUtil.C_FALSE;
+ } else {
+ throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
+ }
+ } else {
+ return s;
+ }
+ }
+ }
+
+ private _ErrorDescriptionBuilder getNullBooleanFormatErrorDescription() {
+ return new _ErrorDescriptionBuilder(
+ "Can't convert boolean to string automatically, because the \"", BOOLEAN_FORMAT_KEY ,"\" setting was ",
+ new _DelayedJQuote(getBooleanFormat()),
+ (getBooleanFormat().equals(C_TRUE_FALSE)
+ ? ", which is the legacy deprecated default, and we treat it as if no format was set. "
+ + "This is the default configuration; you should provide the format explicitly for each "
+ + "place where you print a boolean."
+ : ".")
+ ).tips(
+ "Write something like myBool?string('yes', 'no') to specify boolean formatting in place.",
+ new Object[]{
+ "If you want \"true\"/\"false\" result as you are generating computer-language output "
+ + "(not for direct human consumption), then use \"?c\", like ${myBool?c}. (If you "
+ + "always generate computer-language output, then it's might be reasonable to set "
+ + "the \"", BOOLEAN_FORMAT_KEY, "\" setting to \"c\" instead.)",
+ },
+ new Object[] {
+ "If you need the same two values on most places, the programmers can set the \"",
+ BOOLEAN_FORMAT_KEY ,"\" setting to something like \"yes,no\". However, then it will be easy to "
+ + "unwillingly format booleans like that."
+ }
+ );
+ }
+
+ /**
+ * Returns the string to which {@code true} is converted to for human audience, or {@code null} if automatic
+ * coercion to string is not allowed.
+ *
+ * <p>This value is deduced from the {@code "boolean_format"} setting.
+ * Confusingly, for backward compatibility (at least until 2.4) that defaults to {@code "true,false"}, yet this
+ * defaults to {@code null}. That's so because {@code "true,false"} is treated exceptionally, as that default is a
+ * historical mistake in FreeMarker, since it targets computer language output, not human writing. Thus it's
+ * ignored, and instead we admit that we don't know how to show boolean values.
+ */
+ String getTrueStringValue() {
+ if (trueAndFalseStringsCachedForParent == getParent()) {
+ return cachedTrueString;
+ }
+ cacheTrueAndFalseStrings();
+ return cachedTrueString;
+ }
+
+ /**
+ * Same as {@link #getTrueStringValue()} but with {@code false}.
+ */
+ String getFalseStringValue() {
+ if (trueAndFalseStringsCachedForParent == getParent()) {
+ return cachedFalseString;
+ }
+ cacheTrueAndFalseStrings();
+ return cachedFalseString;
+ }
+
+ private void clearCachedTrueAndFalseString() {
+ trueAndFalseStringsCachedForParent = null;
+ cachedTrueString = null;
+ cachedFalseString = null;
+ }
+
+ private void cacheTrueAndFalseStrings() {
+ String spitTrueSide = getBooleanFormatCommaSplitTrueSide();
+ if (spitTrueSide != null) {
+ cachedTrueString = spitTrueSide;
+ cachedFalseString = getBooleanFormatCommaSplitFalseSide();
+ } else if (getBooleanFormat().equals(C_FORMAT_STRING)) {
+ CFormat cFormat = getCFormat();
+ cachedTrueString = cFormat.getTrueString();
+ cachedFalseString = cFormat.getFalseString();
+ } else {
+ // This happens for C_TRUE_FALSE deliberately. That's the default for BC, but it's not a good default for human
+ // audience formatting, so we pretend that it wasn't set.
+ cachedTrueString = null;
+ cachedFalseString = null;
+ }
+ trueAndFalseStringsCachedForParent = getParent();
+ }
+
public Configuration getConfiguration() {
return configuration;
}
diff --git a/src/main/java/freemarker/core/EvalUtil.java b/src/main/java/freemarker/core/EvalUtil.java
index 3d3f900d..1b469c93 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -481,7 +481,7 @@ class EvalUtil {
throw InvalidReferenceException.getInstance(exp, env);
} else {
throw new InvalidReferenceException(
- "Null/missing value (no more informatoin avilable)",
+ "Null/missing value (no more information available)",
env);
}
}
diff --git a/src/main/java/freemarker/core/JSONCFormat.java b/src/main/java/freemarker/core/JSONCFormat.java
new file mode 100644
index 00000000..5cf4c52a
--- /dev/null
+++ b/src/main/java/freemarker/core/JSONCFormat.java
@@ -0,0 +1,56 @@
+/*
+ * 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 freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.template.Version;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * JSON {@link CFormat}; when this is used, values output by {@code ?c} are valid JSON values, and therefore also
+ * valid JavaScript values.
+ * This is the default of {@link Configurable#getCFormat()} starting from
+ * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements}
+ * {@link Configuration#VERSION_2_3_32}.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * most methods are not exposed outside FreeMarker. The class itself and some members are exposed as they are needed for
+ * configuring FreeMarker.
+ *
+ * @since 2.3.32
+ */
+public final class JSONCFormat extends AbstractJSONLikeFormat {
+ public static final String NAME = "JSON";
+ public static final JSONCFormat INSTANCE = new JSONCFormat();
+
+ private JSONCFormat() {
+ }
+
+ @Override
+ String formatString(String s, Environment env) throws TemplateException {
+ return StringUtil.jsStringEnc(s, true, true);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+}
diff --git a/src/main/java/freemarker/core/JavaCFormat.java b/src/main/java/freemarker/core/JavaCFormat.java
new file mode 100644
index 00000000..6a1d0d61
--- /dev/null
+++ b/src/main/java/freemarker/core/JavaCFormat.java
@@ -0,0 +1,87 @@
+/*
+ * 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.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * Java {@link CFormat}.
+ *
+ * @since 2.3.32
+ */
+public final class JavaCFormat extends CFormat {
+ public static final String NAME = "Java";
+ public static final JavaCFormat INSTANCE = new JavaCFormat();
+
+ private static final TemplateNumberFormat TEMPLATE_NUMBER_FORMAT = new CTemplateNumberFormat(
+ "Double.POSITIVE_INFINITY", "Double.NEGATIVE_INFINITY", "Double.NaN",
+ "Float.POSITIVE_INFINITY", "Float.NEGATIVE_INFINITY", "Float.NaN");
+
+ private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+ static {
+ DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+ symbols.setInfinity("Double.POSITIVE_INFINITY");
+ symbols.setNaN("Double.NaN");
+ LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+ }
+
+ private JavaCFormat() {
+ }
+
+ @Override
+ TemplateNumberFormat getTemplateNumberFormat() {
+ return TEMPLATE_NUMBER_FORMAT;
+ }
+
+ @Override
+ String formatString(String s, Environment env) throws TemplateException {
+ return StringUtil.javaStringEnc(s, true);
+ }
+
+ @Override
+ String getTrueString() {
+ return "true";
+ }
+
+ @Override
+ String getFalseString() {
+ return "false";
+ }
+
+ @Override
+ String getNullString() {
+ return "null";
+ }
+
+ @Override
+ NumberFormat getLegacyNumberFormat() {
+ return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+}
diff --git a/src/main/java/freemarker/core/JavaScriptCFormat.java b/src/main/java/freemarker/core/JavaScriptCFormat.java
new file mode 100644
index 00000000..d9ba7229
--- /dev/null
+++ b/src/main/java/freemarker/core/JavaScriptCFormat.java
@@ -0,0 +1,51 @@
+/*
+ * 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 freemarker.template.TemplateException;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * JavaScript {@link CFormat}. This is almost the same as {@link JSONCFormat}, but it uses shorter forms where
+ * the additional JavaScript features make that possible.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * most methods are not exposed outside FreeMarker. The class itself and some members are exposed as they are needed for
+ * configuring FreeMarker.
+ *
+ * @since 2.3.32
+ */
+public final class JavaScriptCFormat extends AbstractJSONLikeFormat {
+ public static final String NAME = "JavaScript";
+ public static final JavaScriptCFormat INSTANCE = new JavaScriptCFormat();
+
+ private JavaScriptCFormat() {
+ }
+
+ @Override
+ String formatString(String s, Environment env) throws TemplateException {
+ return StringUtil.jsStringEnc(s, false, true);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+}
diff --git a/src/main/java/freemarker/core/JavaTemplateNumberFormat.java b/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
index 59e3288c..5280bb39 100644
--- a/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
@@ -23,7 +23,7 @@ import java.text.NumberFormat;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
-final class JavaTemplateNumberFormat extends BackwardCompatibleTemplateNumberFormat {
+class JavaTemplateNumberFormat extends BackwardCompatibleTemplateNumberFormat {
private final String formatString;
private final NumberFormat javaNumberFormat;
diff --git a/src/main/java/freemarker/core/PropertySetting.java b/src/main/java/freemarker/core/PropertySetting.java
index 0c2e22ab..117a8c00 100644
--- a/src/main/java/freemarker/core/PropertySetting.java
+++ b/src/main/java/freemarker/core/PropertySetting.java
@@ -38,11 +38,14 @@ final class PropertySetting extends TemplateElement {
private final String key;
private final Expression value;
+ private final ValueSafetyChecker valueSafeyChecker;
static final String[] SETTING_NAMES = new String[] {
// Must be sorted alphabetically!
Configurable.BOOLEAN_FORMAT_KEY_CAMEL_CASE,
Configurable.BOOLEAN_FORMAT_KEY_SNAKE_CASE,
+ Configurable.C_FORMAT_KEY_CAMEL_CASE,
+ Configurable.C_FORMAT_KEY_SNAKE_CASE,
Configurable.CLASSIC_COMPATIBLE_KEY_CAMEL_CASE,
Configurable.CLASSIC_COMPATIBLE_KEY_SNAKE_CASE,
Configurable.DATE_FORMAT_KEY_CAMEL_CASE,
@@ -66,7 +69,7 @@ final class PropertySetting extends TemplateElement {
PropertySetting(Token keyTk, FMParserTokenManager tokenManager, Expression value, Configuration cfg)
throws ParseException {
- String key = keyTk.image;
+ final String key = keyTk.image;
if (Arrays.binarySearch(SETTING_NAMES, key) < 0) {
StringBuilder sb = new StringBuilder();
if (_TemplateAPI.getConfigurationSettingNames(cfg, true).contains(key)
@@ -107,6 +110,26 @@ final class PropertySetting extends TemplateElement {
this.key = key;
this.value = value;
+
+ if (key.equals(Configurable.C_FORMAT_KEY_SNAKE_CASE) || key.equals(Configurable.C_FORMAT_KEY_CAMEL_CASE)) {
+ valueSafeyChecker = new ValueSafetyChecker() {
+ @Override
+ public void check(Environment env, String actualValue) throws TemplateException {
+ if (actualValue.startsWith("@")
+ || StandardCFormats.STANDARD_C_FORMATS.containsKey(actualValue)
+ || actualValue.equals("default")) {
+ return;
+ }
+ throw new TemplateException("It's not allowed to set \"" + key + "\" to "
+ + StringUtil.jQuote(actualValue) + " in a template. Use a standard c format name ("
+ + String.join(", ", StandardCFormats.STANDARD_C_FORMATS.keySet()) + "), " +
+ "or registered custom c format name after a \"@\".",
+ env);
+ }
+ };
+ } else {
+ valueSafeyChecker = null;
+ }
}
@Override
@@ -122,6 +145,9 @@ final class PropertySetting extends TemplateElement {
} else {
strval = value.evalAndCoerceToStringOrUnsupportedMarkup(env);
}
+ if (valueSafeyChecker != null) {
+ valueSafeyChecker.check(env, strval);
+ }
env.setSetting(key, strval);
return null;
}
@@ -171,5 +197,9 @@ final class PropertySetting extends TemplateElement {
boolean isNestedBlockRepeater() {
return false;
}
+
+ private interface ValueSafetyChecker {
+ void check(Environment env, String value) throws TemplateException;
+ }
}
diff --git a/src/main/java/freemarker/core/TemplateConfiguration.java b/src/main/java/freemarker/core/TemplateConfiguration.java
index ff290f5b..121c18b6 100644
--- a/src/main/java/freemarker/core/TemplateConfiguration.java
+++ b/src/main/java/freemarker/core/TemplateConfiguration.java
@@ -191,6 +191,9 @@ public final class TemplateConfiguration extends Configurable implements ParserC
if (tc.isDateTimeFormatSet()) {
setDateTimeFormat(tc.getDateTimeFormat());
}
+ if (tc.isCFormatSet()) {
+ setCFormat(tc.getCFormat());
+ }
if (tc.isEncodingSet()) {
setEncoding(tc.getEncoding());
}
@@ -333,6 +336,9 @@ public final class TemplateConfiguration extends Configurable implements ParserC
if (isDateTimeFormatSet() && !template.isDateTimeFormatSet()) {
template.setDateTimeFormat(getDateTimeFormat());
}
+ if (isCFormatSet() && !template.isCFormatSet()) {
+ template.setCFormat(getCFormat());
+ }
if (isEncodingSet() && template.getEncoding() == null) {
template.setEncoding(getEncoding());
}
@@ -541,7 +547,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC
public boolean isOutputFormatSet() {
return outputFormat != null;
}
-
+
/**
* See {@link Configuration#setRecognizeStandardFileExtensions(boolean)}.
*/
@@ -680,6 +686,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC
|| isCustomNumberFormatsSet()
|| isDateFormatSet()
|| isDateTimeFormatSet()
+ || isCFormatSet()
|| isLazyImportsSet()
|| isLazyAutoImportsSet()
|| isLocaleSet()
diff --git a/src/main/java/freemarker/core/XSCFormat.java b/src/main/java/freemarker/core/XSCFormat.java
new file mode 100644
index 00000000..c3ee5774
--- /dev/null
+++ b/src/main/java/freemarker/core/XSCFormat.java
@@ -0,0 +1,91 @@
+/*
+ * 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.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+
+/**
+ * {@link CFormat} for outputting XML that follows the conventions of XML Schema.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * most methods are not exposed outside FreeMarker. The class itself and some members are exposed as they are needed for
+ * configuring FreeMarker.
+ */
+public final class XSCFormat extends CFormat {
+ public static final String NAME = "XS";
+ public static final XSCFormat INSTANCE = new XSCFormat();
+
+ private static final TemplateNumberFormat TEMPLATE_NUMBER_FORMAT = new CTemplateNumberFormat(
+ "INF", "-INF", "NaN",
+ "INF", "-INF", "NaN");
+
+ private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+ static {
+ DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+ symbols.setInfinity("INF");
+ symbols.setNaN("NaN");
+ LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+ }
+
+ @Override
+ NumberFormat getLegacyNumberFormat() {
+ return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+ }
+
+ private XSCFormat() {
+ }
+
+ @Override
+ TemplateNumberFormat getTemplateNumberFormat() {
+ return TEMPLATE_NUMBER_FORMAT;
+ }
+
+ @Override
+ String formatString(String s, Environment env) throws TemplateException {
+ return s; // So we don't escape here, as we assume that there's XML auto-escaping
+ }
+
+ @Override
+ String getTrueString() {
+ return "true";
+ }
+
+ @Override
+ String getFalseString() {
+ return "false";
+ }
+
+ @Override
+ String getNullString() {
+ // XSD has no null literal, and you have to leave out the whole element to represent null. We can achieve that
+ // here. The closet we can do is causing an empty element or attribute to be outputted. Some frameworks will
+ // interpret that as null if the XSD type is not string.
+ return "";
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+}
diff --git a/src/main/java/freemarker/core/_StandardCLanguages.java b/src/main/java/freemarker/core/_StandardCLanguages.java
new file mode 100644
index 00000000..37da6381
--- /dev/null
+++ b/src/main/java/freemarker/core/_StandardCLanguages.java
@@ -0,0 +1,40 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.Map;
+
+final class StandardCFormats {
+ private StandardCFormats() {
+ }
+
+ static final Map<String, CFormat> STANDARD_C_FORMATS;
+ static {
+ STANDARD_C_FORMATS = new LinkedHashMap<>();
+ STANDARD_C_FORMATS.put(JSONCFormat.INSTANCE.getName(), JSONCFormat.INSTANCE);
+ STANDARD_C_FORMATS.put(JavaScriptCFormat.INSTANCE.getName(), JavaScriptCFormat.INSTANCE);
+ STANDARD_C_FORMATS.put(JavaCFormat.INSTANCE.getName(), JavaCFormat.INSTANCE);
+ STANDARD_C_FORMATS.put(XSCFormat.INSTANCE.getName(), XSCFormat.INSTANCE);
+ STANDARD_C_FORMATS.put(Default230CFormat.INSTANCE.getName(), Default230CFormat.INSTANCE);
+ STANDARD_C_FORMATS.put(Default2321CFormat.INSTANCE.getName(), Default2321CFormat.INSTANCE);
+ }
+
+}
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index 93bfd83d..1ffa86f6 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -59,11 +59,15 @@ import freemarker.cache.TemplateLookupStrategy;
import freemarker.cache.TemplateNameFormat;
import freemarker.cache.URLTemplateLoader;
import freemarker.core.BugException;
+import freemarker.core.CFormat;
import freemarker.core.CSSOutputFormat;
import freemarker.core.CombinedMarkupOutputFormat;
import freemarker.core.Configurable;
+import freemarker.core.Default230CFormat;
+import freemarker.core.Default2321CFormat;
import freemarker.core.Environment;
import freemarker.core.HTMLOutputFormat;
+import freemarker.core.JSONCFormat;
import freemarker.core.JSONOutputFormat;
import freemarker.core.JavaScriptOutputFormat;
import freemarker.core.MarkupOutputFormat;
@@ -384,7 +388,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
STANDARD_OUTPUT_FORMATS.put(JavaScriptOutputFormat.INSTANCE.getName(), JavaScriptOutputFormat.INSTANCE);
STANDARD_OUTPUT_FORMATS.put(JSONOutputFormat.INSTANCE.getName(), JSONOutputFormat.INSTANCE);
}
-
+
/**
* The parser decides between {@link #ANGLE_BRACKET_TAG_SYNTAX} and {@link #SQUARE_BRACKET_TAG_SYNTAX} based on the
* first tag (like {@code [#if x]} or {@code <#if x>}) it mets. Note that {@code [=...]} is <em>not</em> a tag, but
@@ -572,6 +576,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
private boolean localeExplicitlySet;
private boolean defaultEncodingExplicitlySet;
private boolean timeZoneExplicitlySet;
+ private boolean cFormatExplicitlySet;
private HashMap/*<String, TemplateModel>*/ sharedVariables = new HashMap();
@@ -739,7 +744,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
* It won't be affected by {@code #include} and {@code #nested} anymore. This is unintended, a bug with
* {@code incompatible_improvement} 2.3.22 (a consequence of the lower level fixing described in the next
* point). The old behavior of {@code .template_name} is restored if you set
- * {@code incompatible_improvement} to 2.3.23 (while {@link Configurable#getParent()}) of
+ * {@code incompatible_improvement} to 2.3.23 (while {@link Configurable#getParent()} of
* {@link Environment} keeps the changed behavior shown in the next point).
* </li>
* <li><p>
@@ -1824,7 +1829,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
static TimeZone getDefaultTimeZone() {
return TimeZone.getDefault();
}
-
+
@Override
public void setTemplateExceptionHandler(TemplateExceptionHandler templateExceptionHandler) {
super.setTemplateExceptionHandler(templateExceptionHandler);
@@ -2019,7 +2024,12 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
logTemplateExceptionsExplicitlySet = true;
unsetLogTemplateExceptions();
}
-
+
+ if (!cFormatExplicitlySet) {
+ cFormatExplicitlySet = true;
+ unsetCFormat();
+ }
+
if (!wrapUncheckedExceptionsExplicitlySet) {
wrapUncheckedExceptionsExplicitlySet = true;
unsetWrapUncheckedExceptions();
@@ -2467,6 +2477,45 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
: recognizeStandardFileExtensions.booleanValue();
}
+ @Override
+ public void setCFormat(CFormat cFormat) {
+ super.setCFormat(cFormat);
+ cFormatExplicitlySet = true;
+ }
+
+ /**
+ * Resets the setting to its default, as if it was never set. This means that when you change the
+ * {@code incompatibe_improvements} setting later, the default will also change as appropriate. Also
+ * {@link #isCFormatExplicitlySet()} will return {@code false}.
+ *
+ * @since 2.3.32
+ */
+ public void unsetCFormat() {
+ if (cFormatExplicitlySet) {
+ setCFormat(getDefaultCFormat(incompatibleImprovements));
+ cFormatExplicitlySet = false;
+ }
+ }
+
+ static CFormat getDefaultCFormat(Version incompatibleImprovements) {
+ if (incompatibleImprovements.intValue() >= _VersionInts.V_2_3_32) {
+ return JSONCFormat.INSTANCE;
+ }
+ if (incompatibleImprovements.intValue() >= _VersionInts.V_2_3_21) {
+ return Default2321CFormat.INSTANCE;
+ }
+ return Default230CFormat.INSTANCE;
+ }
+
+ /**
+ * Tells if {@link #setCFormat(CFormat)} (or equivalent) was already called on this instance.
+ *
+ * @since 2.3.32
+ */
+ public boolean isCFormatExplicitlySet() {
+ return cFormatExplicitlySet;
+ }
+
/**
* Determines the tag syntax (like {@code <#if x>} VS {@code [#if x]}) of the template files
* that has no {@code #ftl} header to decide that. Don't confuse this with the interpolation syntax
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 51002b8b..0f7f23b4 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -27,6 +27,7 @@ import freemarker.cache.CacheStorage;
import freemarker.cache.TemplateLoader;
import freemarker.cache.TemplateLookupStrategy;
import freemarker.cache.TemplateNameFormat;
+import freemarker.core.CFormat;
import freemarker.core.Expression;
import freemarker.core.OutputFormat;
import freemarker.core.TemplateObject;
@@ -185,11 +186,15 @@ public class _TemplateAPI {
public static Locale getDefaultLocale() {
return Configuration.getDefaultLocale();
}
-
+
public static TimeZone getDefaultTimeZone() {
return Configuration.getDefaultTimeZone();
}
+ public static CFormat getDefaultCFormat(Version incompatibleImprovements) {
+ return Configuration.getDefaultCFormat(incompatibleImprovements);
+ }
+
public static void setPreventStrippings(Configuration conf, boolean preventStrippings) {
conf.setPreventStrippings(preventStrippings);
}
diff --git a/src/main/java/freemarker/template/utility/StringUtil.java b/src/main/java/freemarker/template/utility/StringUtil.java
index 33179556..0d76a48f 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -881,39 +881,7 @@ public class StringUtil {
if (s == null) {
return "null";
}
- int ln = s.length();
- StringBuilder b = new StringBuilder(ln + 4);
- b.append('"');
- for (int i = 0; i < ln; i++) {
- char c = s.charAt(i);
- if (c == '"') {
- b.append("\\\"");
- } else if (c == '\\') {
- b.append("\\\\");
- } else if (c < 0x20) {
- if (c == '\n') {
- b.append("\\n");
- } else if (c == '\r') {
- b.append("\\r");
- } else if (c == '\f') {
- b.append("\\f");
- } else if (c == '\b') {
- b.append("\\b");
- } else if (c == '\t') {
- b.append("\\t");
- } else {
- b.append("\\u00");
- int x = c / 0x10;
- b.append(toHexDigit(x));
- x = c & 0xF;
- b.append(toHexDigit(x));
- }
- } else {
- b.append(c);
- }
- } // for each characters
- b.append('"');
- return b.toString();
+ return javaStringEnc(s, true);
}
/**
@@ -934,7 +902,7 @@ public class StringUtil {
return "null";
}
int ln = s.length();
- StringBuilder b = new StringBuilder(ln + 4);
+ StringBuilder b = new StringBuilder(ln + 6);
b.append('"');
for (int i = 0; i < ln; i++) {
char c = s.charAt(i);
@@ -957,10 +925,8 @@ public class StringUtil {
b.append("\\t");
} else {
b.append("\\u00");
- int x = c / 0x10;
- b.append(toHexDigit(x));
- x = c & 0xF;
- b.append(toHexDigit(x));
+ b.append(toHexDigitLowerCase(c / 0x10));
+ b.append(toHexDigitLowerCase(c & 0xF));
}
} else {
b.append(c);
@@ -1281,25 +1247,39 @@ public class StringUtil {
public static boolean isBackslashEscapedFTLIdentifierCharacter(final char c) {
return c == '-' || c == '.' || c == ':' || c == '#';
}
-
+
/**
- * Escapes the <code>String</code> with the escaping rules of Java language
+ * Escapes the {@code String} with the escaping rules of Java language
* string literals, so it's safe to insert the value into a string literal.
* The resulting string will not be quoted.
- *
+ *
+ * See more details at {@link #javaStringEnc(String, boolean)}, as this just calls that with {@code false} as the
+ * 2nd argument.
+ */
+ public static String javaStringEnc(String s) {
+ return javaStringEnc(s, false);
+ }
+
+ /**
+ * Escapes the {@code String} with the escaping rules of Java language string literals, and then if {@code quote} is
+ * true, it also adds quotation marks before and after it.
+ *
* <p>All characters under UCS code point 0x20 will be escaped.
* Where they have no dedicated escape sequence in Java, they will
* be replaced with hexadecimal escape (<tt>\</tt><tt>u<i>XXXX</i></tt>).
*
* @see #jQuote(String)
*/
- public static String javaStringEnc(String s) {
+ public static String javaStringEnc(String s, boolean quote) {
int ln = s.length();
for (int i = 0; i < ln; i++) {
char c = s.charAt(i);
if (c == '"' || c == '\\' || c < 0x20) {
- StringBuilder b = new StringBuilder(ln + 4);
- b.append(s.substring(0, i));
+ StringBuilder b = new StringBuilder(ln + (quote ? 6 : 4));
+ if (quote) {
+ b.append("\"");
+ }
+ b.append(s, 0, i);
while (true) {
if (c == '"') {
b.append("\\\"");
@@ -1318,30 +1298,29 @@ public class StringUtil {
b.append("\\t");
} else {
b.append("\\u00");
- int x = c / 0x10;
- b.append((char)
- (x < 0xA ? x + '0' : x - 0xA + 'a'));
- x = c & 0xF;
- b.append((char)
- (x < 0xA ? x + '0' : x - 0xA + 'a'));
+ b.append(toHexDigitLowerCase(c / 0x10));
+ b.append(toHexDigitLowerCase(c & 0xF));
}
} else {
b.append(c);
}
i++;
if (i >= ln) {
+ if (quote) {
+ b.append("\"");
+ }
return b.toString();
}
c = s.charAt(i);
}
} // if has to be escaped
} // for each characters
- return s;
+ return quote ? '"' + s + '"' : s;
}
/**
* Escapes a {@link String} to be safely insertable into a JavaScript string literal; for more see
- * {@link #jsStringEnc(String, boolean) jsStringEnc(s, false)}.
+ * {@link #jsStringEnc(String, boolean, boolean) jsStringEnc(s, false, false)}.
*/
public static String javaScriptStringEnc(String s) {
return jsStringEnc(s, false);
@@ -1360,20 +1339,30 @@ public class StringUtil {
private static final int ESC_BACKSLASH = 3;
/**
- * Escapes a {@link String} to be safely insertable into a JavaScript or a JSON string literal.
- * The resulting string will <em>not</em> be quoted; the caller must ensure that they are there in the final
- * output. Note that for JSON, the quotation marks must be {@code "}, not {@code '}, because JSON doesn't escape
- * {@code '}.
- *
+ * Escapes a {@link String} to be safely insertable into a JSON or JavaScript string literal; for more see
+ * {@link #jsStringEnc(String, boolean, boolean) jsStringEnc(s, json, false)}.
+ *
+ * @since 2.3.20
+ */
+ public static String jsStringEnc(String s, boolean json) {
+ return jsStringEnc(s, json, false);
+ }
+
+ /**
+ * Escapes a {@link String} to be safely insertable into a JavaScript or a JSON string literal, and if the 3rd
+ * argument is {@code true}, also adds quotation marks around it.
+ * If instead the caller ensures that the quotation marks are there, then in JSON mode (2nd argument), the quotation
+ * marks must be {@code "}, not {@code '}, because for JSON we won't escape {@code '}.
+ *
* <p>The escaping rules guarantee that if the inside of the JavaScript/JSON string literal is from one or more
* touching pieces that were escaped with this, no character sequence can occur that closes the
* JavaScript/JSON string literal, or has a meaning in HTML/XML that causes the HTML script section to be closed.
* (If, however, the escaped section is preceded by or followed by strings from other sources, this can't be
* guaranteed in some rare cases. Like <tt>x = "</${a?js_string}"</tt> might closes the "script"
* element if {@code a} is {@code "script>"}.)
- *
+ *
* The escaped characters are:
- *
+ *
* <table style="width: auto; border-collapse: collapse" border="1" summary="Characters escaped by jsStringEnc">
* <tr>
* <th>Input
@@ -1382,7 +1371,7 @@ public class StringUtil {
* <td><tt>"</tt>
* <td><tt>\"</tt>
* <tr>
- * <td><tt>'</tt> if not in JSON-mode
+ * <td><tt>'</tt> if not in JSON-mode, nor is the {@code quited} argument {@code true}
* <td><tt>\'</tt>
* <tr>
* <td><tt>\</tt>
@@ -1394,7 +1383,7 @@ public class StringUtil {
* <td><tt>></tt> if the method can't know that it won't be directly after <tt>]]</tt> or <tt>--</tt>
* <td>JavaScript: <tt>\></tt>; JSON: <tt>\</tt><tt>u003E</tt>
* <tr>
- * <td><tt><</tt> if the method can't know that it won't be directly followed by <tt>!</tt> or <tt>?</tt>
+ * <td><tt><</tt> if the method can't know that it won't be directly followed by <tt>!</tt> or <tt>?</tt>
* <td><tt><tt>\</tt>u003C</tt>
* <tr>
* <td>
@@ -1408,14 +1397,25 @@ public class StringUtil {
* u2029 (Paragraph separator - source code line-break in ECMAScript)<br>
* <td><tt>\<tt>u</tt><i>XXXX</i></tt>
* </table>
- *
- * @since 2.3.20
+ *
+ * @param s The string to escape
+ * @param json If escaping should restrict itself to rules that are valid in both JSON and JavaScript.
+ * @param quote If quotation marks should be added around the result.
+ * Currently, it's always ({@code "}, not {@code '}).
+ *
+ * @since 2.3.32
*/
- public static String jsStringEnc(String s, boolean json) {
+ public static String jsStringEnc(String s, boolean json, boolean quote) {
NullArgumentException.check("s", s);
int ln = s.length();
- StringBuilder sb = null;
+ StringBuilder sb;
+ if (quote) {
+ sb = new StringBuilder(ln + 8);
+ sb.append('"');
+ } else {
+ sb = null;
+ }
for (int i = 0; i < ln; i++) {
final char c = s.charAt(i);
final int escapeType; //
@@ -1437,15 +1437,17 @@ public class StringUtil {
} else if (c == '"') {
escapeType = ESC_BACKSLASH;
} else if (c == '\'') {
- escapeType = json ? NO_ESC : ESC_BACKSLASH;
+ escapeType = json || quote ? NO_ESC : ESC_BACKSLASH;
} else if (c == '\\') {
escapeType = ESC_BACKSLASH;
- } else if (c == '/' && (i == 0 || s.charAt(i - 1) == '<')) { // against closing elements
- escapeType = ESC_BACKSLASH;
+ } else if (c == '/' && (i == 0 || s.charAt(i - 1) == '<')) { // against closing elements
+ escapeType = quote ? NO_ESC : ESC_BACKSLASH;
} else if (c == '>') { // against "]]> and "-->"
final boolean dangerous;
if (i == 0) {
dangerous = true;
+ } else if (quote) {
+ dangerous = false;
} else {
final char prevC = s.charAt(i - 1);
if (prevC == ']' || prevC == '-') {
@@ -1464,6 +1466,8 @@ public class StringUtil {
final boolean dangerous;
if (i == ln - 1) {
dangerous = true;
+ } else if (quote) {
+ dangerous = false;
} else {
char nextC = s.charAt(i + 1);
dangerous = nextC == '!' || nextC == '?';
@@ -1480,7 +1484,7 @@ public class StringUtil {
if (escapeType != NO_ESC) { // If needs escaping
if (sb == null) {
sb = new StringBuilder(ln + 6);
- sb.append(s.substring(0, i));
+ sb.append(s, 0, i);
}
sb.append('\\');
@@ -1489,15 +1493,15 @@ public class StringUtil {
} else if (escapeType == ESC_HEXA) {
if (!json && c < 0x100) {
sb.append('x');
- sb.append(toHexDigit(c >> 4));
- sb.append(toHexDigit(c & 0xF));
+ sb.append(toHexDigitUpperCase(c >> 4));
+ sb.append(toHexDigitUpperCase(c & 0xF));
} else {
sb.append('u');
int cp = c;
- sb.append(toHexDigit((cp >> 12) & 0xF));
- sb.append(toHexDigit((cp >> 8) & 0xF));
- sb.append(toHexDigit((cp >> 4) & 0xF));
- sb.append(toHexDigit(cp & 0xF));
+ sb.append(toHexDigitUpperCase((cp >> 12) & 0xF));
+ sb.append(toHexDigitUpperCase((cp >> 8) & 0xF));
+ sb.append(toHexDigitUpperCase((cp >> 4) & 0xF));
+ sb.append(toHexDigitUpperCase(cp & 0xF));
}
} else { // escapeType == ESC_BACKSLASH
sb.append(c);
@@ -1510,14 +1514,22 @@ public class StringUtil {
if (sb != null) sb.append(c);
} // for each characters
+
+ if (quote) {
+ sb.append('"');
+ }
return sb == null ? s : sb.toString();
}
- private static char toHexDigit(int d) {
+ private static char toHexDigitLowerCase(int d) {
+ return (char) (d < 0xA ? d + '0' : d - 0xA + 'a');
+ }
+
+ private static char toHexDigitUpperCase(int d) {
return (char) (d < 0xA ? d + '0' : d - 0xA + 'A');
}
-
+
/**
* Parses a name-value pair list, where the pairs are separated with comma,
* and the name and value is separated with colon.
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 2198ffc1..8faa1a74 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -20,10 +20,7 @@
<book conformance="docgen" version="5.0" xml:lang="en"
xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:ns5="http://www.w3.org/2000/svg"
- xmlns:ns4="http://www.w3.org/1998/Math/MathML"
- xmlns:ns3="http://www.w3.org/1999/xhtml"
- xmlns:ns="http://docbook.org/ns/docbook">
+>
<info>
<title>Apache FreeMarker Manual</title>
@@ -6013,6 +6010,119 @@ To prove that "s" didn't contain the value in escaped form:
</section>
</section>
+ <section xml:id="dgui_misc_computer_vs_human_format">
+ <title>Formatting for humans, or for computers</title>
+
+ <para>By default
+ <literal>${<replaceable>someValue</replaceable>}</literal> outputs
+ numbers, date/time/date-time and boolean values in a format that
+ targets normal users (<quote>humans</quote>). You have various
+ settings to specify how that format looks, like
+ <literal>number_format</literal>, <literal>date_format</literal>,
+ <literal>time_format</literal>, <literal>datetime_format</literal>,
+ <literal>boolan_format</literal>. The output also often depends on the
+ <literal>locale</literal> setting (i.e., on the language, and country
+ of the user). So 3000000 is possibly printed as
+ <literal>3,000,000</literal> (i.e., with grouping separators), or
+ <literal>3.14</literal> is possibly printed as <literal>3,14</literal>
+ (i.e., with a different decimal separator).</para>
+
+ <para>At some places you need to output values that will be read
+ (parsed) by some program, in which case always use the <link
+ linkend="ref_builtin_c"><literal>c</literal> built-in</link>, as in
+ <literal>${<replaceable>someValue</replaceable>?c}</literal> (the
+ <quote>c</quote> stands for Computer). <emphasis>Then the formatting
+ depends on the <literal>c_format</literal> <link
+ linkend="pgui_config_settings">setting</link></emphasis>, which
+ usually refers to a computer language, like <literal>"JSON"</literal>.
+ The output of <literal>?c</literal> is not influenced by
+ <literal>locale</literal>, <literal>number_format</literal>,
+ etc.</para>
+
+ <para>The <literal>c</literal> built-in will format string values to
+ string literals. Like if the <literal>c_format</literal> setting is
+ <quote>JSON</quote>, then <literal>{"fullName":
+ ${fullName?c}}</literal> will output something like
+ <literal>{"fullName": "John Doe"}</literal>, where the quotation marks
+ (and <literal>\</literal> escaping if needed) were added by
+ <literal>?c</literal>.</para>
+
+ <note>
+ <para>At least as of 2.3.32, the <literal>c</literal> built-in
+ doesn't support date/time/datetime values, only numbers, booleans,
+ and strings.</para>
+ </note>
+
+ <para>When formatting for computers, at some places you want to output
+ a <literal>null</literal> (or its equivalent in the target language)
+ if the value you print is missing/<literal>null</literal>. The
+ convenient shortcut for that is using the <literal>cn</literal>
+ built-in (the <quote>n</quote> stands for Nullable), as in
+ <literal>${<replaceable>someValue</replaceable>?cn}</literal>. It's
+ behaves like the <literal>c</literal> built-in, except that if
+ <literal><replaceable>someValue</replaceable></literal> evaluates to
+ <literal>null</literal>/missing, it outputs the
+ <literal>null</literal> literal in the syntax that the
+ <literal>c_format</literal> setting specifies, instead of stopping
+ with missing value error.</para>
+
+ <para>For the templates where the output is obviously only for
+ computer consumption, some prefers setting the
+ <literal>number_format</literal>, and
+ <literal>boolean_format</literal> settings to <literal>"c"</literal>.
+ (Before 2.3.32, for <literal>number_format</literal> you have to use
+ <literal>"computer"</literal> instead.) Then
+ <literal>${<replaceable>someValue</replaceable>}</literal>, for
+ number, and boolean values will format like <literal>?c</literal>
+ does. To output string literals (i.e, to add quotation and escaping),
+ you will still need an explicit <literal>?c</literal>. Also if you
+ need to output <literal>null</literal> literals, you still have to use
+ <literal>?cn</literal>.</para>
+
+ <para>These are the table of supported <literal>c_format</literal>
+ setting values (as of FreeMarker 2.3.32), and their intended
+ use:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para><literal>JSON</literal>: JSON generation</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>JavaScript</literal>: JavaScript generation</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>Java</literal>: Java source code generation</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>XS</literal>: XML Schema compliant XML
+ generation</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>default 2.3.0</literal>: Default for backward
+ compatibility if the <link
+ linkend="pgui_config_incompatible_improvements"><literal>incompatible_improvements</literal>
+ setting</link> is less than 2.3.21. Avoid!</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>default 2.3.21</literal>: Default for backward
+ compatibility if the <link
+ linkend="pgui_config_incompatible_improvements"><literal>incompatible_improvements</literal>
+ setting</link> is equal or greater than 2.3.21. Avoid!</para>
+ </listitem>
+ </itemizedlist>
+
+ <para>The behaviour of these is documented at the <literal>c</literal>
+ built-in: <link linkend="ref_builtin_c">for numbers</link>, <link
+ linkend="ref_builtin_c_boolean">for boolean</link>, <link
+ linkend="ref_builtin_c_string">for strings</link>, <link
+ linkend="ref_builtin_cn">for null-s</link>.</para>
+ </section>
+
<section xml:id="dgui_misc_whitespace">
<title>White-space handling</title>
@@ -12705,7 +12815,8 @@ grant codeBase "file:/path/to/freemarker.jar"
<listitem>
<para>c <link linkend="ref_builtin_c">for strings</link>, <link
- linkend="ref_builtin_c_boolean">for booleans</link></para>
+ linkend="ref_builtin_c_boolean">for booleans</link>, <link
+ linkend="ref_builtin_c_string">for strings</link></para>
</listitem>
<listitem>
@@ -12745,6 +12856,10 @@ grant codeBase "file:/path/to/freemarker.jar"
<para><link linkend="ref_builtin_chunk">chunk</link></para>
</listitem>
+ <listitem>
+ <para><link linkend="ref_builtin_cn">cn</link></para>
+ </listitem>
+
<listitem>
<para><link linkend="ref_builtin_contains">contains</link></para>
</listitem>
@@ -13322,6 +13437,115 @@ GreEN mouse
<literal>locale</literal> (language, country).</para>
</section>
+ <section xml:id="ref_builtin_c_string">
+ <title>c (for string value)</title>
+
+ <indexterm>
+ <primary>c built-in</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>format</primary>
+
+ <secondary>string</secondary>
+ </indexterm>
+
+ <note>
+ <para>The <literal>c</literal> built-in also works <link
+ linkend="ref_builtin_c">on numbers</link>, and <link
+ linkend="ref_builtin_c_boolean">on booleans</link>!</para>
+ </note>
+
+ <note>
+ <para>To provide a background, see <xref
+ linkend="dgui_misc_computer_vs_human_format"/></para>
+ </note>
+
+ <note>
+ <para>The <literal>c</literal> built-in supports strings since
+ FreeMarker 2.3.32.</para>
+ </note>
+
+ <para>This built-in converts a string to a <quote>computer
+ language</quote> literal, according the value of the <link
+ linkend="gloss.c_format"><literal>c_format</literal>
+ setting</link>.</para>
+
+ <para>For the <literal>c_format</literal>-s that are built into
+ FreeMarker the rules are the following:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para><quote>JSON</quote>, <quote>default 2.3.0</quote>, and
+ <quote>default 2.3.21</quote>: Gives a JSON string literal, that
+ is, it will be surrounded with quotation marks
+ (<literal>"</literal>), and will be escaped using backslash
+ (<literal>\</literal>) where needed. For the exact escaping
+ rules see <link linkend="ref_builtin_json_string">the
+ <literal>json_string</literal> built-in</link>.</para>
+ </listitem>
+
+ <listitem>
+ <para><quote>JavaScript</quote>: Almost the same as JSON, but
+ uses <literal>\x<replaceable>XX</replaceable></literal> instead
+ of <literal>\u<replaceable>XXXX</replaceable></literal> where
+ possible. For the exact escaping rules see <link
+ linkend="ref_builtin_js_string">the <literal>js_string</literal>
+ built-in</link>, except that this won't escape the apostrophe
+ quote (since it knows that it has used quotation marks around
+ the string literal).</para>
+ </listitem>
+
+ <listitem>
+ <para><quote>Java</quote>: Gives a Java string literal, that is,
+ it will be surrounded with quotation marks
+ (<literal>"</literal>), and will be escaped using backslash
+ (<literal>\</literal>) where needed. For the exact escaping
+ rules see <link linkend="ref_builtin_j_string">the
+ <literal>j_string</literal> built-in</link>.</para>
+ </listitem>
+
+ <listitem>
+ <para><quote>XS</quote>: Leaves the string as is. The idea is
+ that you will insert the value as the body of an XML element, or
+ into an XML attribute, so it needs no quotation, or any other
+ special syntax. While it does need XML encoding, that should be
+ handled by the <link linkend="dgui_misc_autoescaping">automatic
+ escaping facility</link>, and not by the
+ <literal>c_format</literal> facility.</para>
+ </listitem>
+ </itemizedlist>
+
+ <para>If the value the <literal>c</literal> built-in is applied on
+ is <literal>null</literal>/missing, it will stop the template
+ processing with error, just like most other built-ins. If instead
+ you want to output a <literal>null</literal> literal, see the <link
+ linkend="ref_builtin_cn_string"><literal>cn</literal>
+ built-in</link>.</para>
+ </section>
+
+ <section xml:id="ref_builtin_cn_string">
+ <title>cn (for string value)</title>
+
+ <indexterm>
+ <primary>cn built-in</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>format</primary>
+
+ <secondary>string</secondary>
+ </indexterm>
+
+ <para>This does the same as the <link
+ linkend="ref_builtin_c_string"><literal>c</literal> built-in</link>,
+ but when applied on a <literal>null</literal>/missing value, it will
+ output a <literal>null</literal> value according the <link
+ linkend="gloss.c_format"><literal>c_format</literal> setting</link>.
+ See more details about formatting a <literal>null</literal> <link
+ linkend="ref_builtin_cn">here</link>.</para>
+ </section>
+
<section xml:id="ref_builtin_c_lower_case">
<title>c_lower_case</title>
@@ -13944,10 +14168,11 @@ String BEAN_NAME = "${beanName?j_string}";</programlisting>
JSON strings must be quoted with <literal>"</literal>.</para>
<para>The escaping rules are almost identical to those <link
- linkend="ref_builtin_j_string">documented for
+ linkend="ref_builtin_js_string">documented for
<literal>js_string</literal></link>. The differences are that
- <literal>'</literal> is not escaped at all, that > is escaped as
- \u003E (not as \>), and that
+ <literal>'</literal> is not escaped at all, that
+ <literal>></literal> is escaped as <literal>\u003E</literal> (not
+ as <literal>\></literal>), and that
<literal>\u<replaceable>XXXX</replaceable></literal> escapes are
used instead of <literal>\x<replaceable>XX</replaceable></literal>
escapes.</para>
@@ -15700,7 +15925,7 @@ rif: foo XYr baar</programlisting>
</section>
<section xml:id="ref_builtin_c">
- <title>c (when used with numerical value)</title>
+ <title>c (for numbers)</title>
<indexterm>
<primary>c built-in</primary>
@@ -15721,69 +15946,141 @@ rif: foo XYr baar</programlisting>
</indexterm>
<note>
- <para>This built-in exists since FreeMarker 2.3.3.</para>
+ <para>The <literal>c</literal> built-in also works <link
+ linkend="ref_builtin_c_boolean">on booleans</link>, and <link
+ linkend="ref_builtin_c_string">on strings</link>!</para>
</note>
- <para>This built-in converts a number to string for a
- <quote>computer language</quote> as opposed to for human audience.
- That is, it formats with the rules that programming languages used
- to use, which is independent of all the locale and number format
- settings of FreeMarker. It always uses dot as decimal separator, and
- it never uses grouping separators (like in 3,000,000), nor
- <quote>+</quote> sign (like +1), nor superfluous leading or trailing
- 0-s (like 03 or 1.0). It usually avoids exponential form (like it
- will never format 10000000000 as 1E10), but see the details below
- regarding that.</para>
-
- <para>This built-in is crucial because be default (like with
+ <note>
+ <para>To provide a background, see <xref
+ linkend="dgui_misc_computer_vs_human_format"/></para>
+ </note>
+
+ <para>This built-in converts a number to a <quote>computer
+ language</quote> literal, as opposed to format it for human reading.
+ This format is independent of the <literal>locale</literal> (human
+ language, country) and <literal>number_format</literal> settings of
+ FreeMarker. Instead, it depends on the <link
+ linkend="gloss.c_format"><literal>c_format</literal> setting</link>,
+ which is usually something like <literal>"JSON"</literal>; a
+ computer language.</para>
+
+ <para>This built-in is crucial because by default (like with
<literal>${x}</literal>) numbers are converted to strings with the
- locale (language, country) specific number formatting, which is for
- human readers (like 3000000 is possibly printed as 3,000,000). When
- the number is printed not for human audience (e.g., for a database
- record ID used as the part of an URL, or as invisible field value in
- a HTML form, or for printing CSS/JavaScript numerical literals) you
- must use this built-in to format the number (i.e., use
+ locale specific number formatting, so 3000000 is possibly printed as
+ <literal>3,000,000</literal> (i.e., with grouping separators), or
+ <literal>3.14</literal> is possibly printed as
+ <literal>3,14</literal> (i.e., with a different decimal separator).
+ When the number is printed not for human audience (e.g., for a
+ database record ID used as the part of an URL, or as invisible field
+ value in a HTML form, or for printing CSS/JavaScript numerical
+ literals) you must use this built-in to format the number (i.e., use
<literal>${x?c}</literal> instead of <literal>${x}</literal>), or
- else the output will be possibly broken depending on the current
- number formatting settings and locale (like the decimal point is not
- dot, but comma in many countries) and the value of the number (like
- big numbers are possibly <quote>damaged</quote> by grouping
- separators).</para>
+ else the output will be possibly unparsable for the consumer.</para>
- <para>Further details:</para>
+ <para>The exact format of numbers depend on value of the
+ <literal>c_format</literal> setting, but for all the
+ <literal>c_format</literal>-s that are built into FreeMarker, these
+ sand:</para>
<itemizedlist>
<listitem>
- <para>Rounding: The conversion is only guaranteed to be lossless
- if the <link
- linkend="pgui_config_incompatible_improvements"><literal>incompatible_improvements</literal>
- FreeMarker configuration setting</link> is set to 2.3.32 or
- higher. Before that, the format never uses exponential form, and
- is limited to 16 digits after the decimal dot, so rounding can
- occur (e.g. 1E-17 will be shown as 0).</para>
- </listitem>
-
- <listitem>
- <para>Special floating point values: If the
- <literal>incompatible_improvements</literal> FreeMarker
- configuration setting is set to 2.3.24 or higher (also if it's
- set to 2.3.20 or higher and you are outside a string literal),
- this built-in will return <literal>"INF"</literal>,
- <literal>"-INF"</literal> and <literal>"NaN"</literal> for
- positive/negative infinity and IEEE floating point Not-a-Number,
- respectively. These are the XML Schema compatible
- representations of these special values. With too low
- <literal>incompatible_improvements</literal> it returns what
- <literal>java.text.DecimalFormat</literal> did with US locale,
- none of which is understood by any (common) computer
- language.</para>
+ <para>It always uses dot as decimal separator</para>
+ </listitem>
+
+ <listitem>
+ <para>It always uses dot as decimal separator</para>
+ </listitem>
+
+ <listitem>
+ <para>Never uses <quote>+</quote> sign (as in +1), except maybe
+ after the <quote>E</quote> that signifies the exponent
+ part</para>
+ </listitem>
+
+ <listitem>
+ <para>No superfluous leading or trailing 0-s (like 03 or
+ 1.0)</para>
+ </listitem>
+
+ <listitem>
+ <para>It usually avoids exponential form (like it will never
+ format 10000000000 as 1E10), but see the details below regarding
+ that.</para>
+ </listitem>
+ </itemizedlist>
+
+ <para>Finder details:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>Rounding:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>For all non-deprecated <literal>c_format</literal>-s
+ that are built into FreeMarker: There's no rounding.</para>
+ </listitem>
+
+ <listitem>
+ <para>For the deprecated <literal>c_format</literal>-s,
+ <quote>default 2.3.0</quote>, and <quote>default
+ 2.3.21</quote>: The numbers are limited to 16 digits after
+ the decimal dot, so rounding can occur. The these formats
+ never use exponential form either, so the decimal point
+ place is fixed. Thus, for example, 1E-17 will be formatted
+ as <literal>0</literal>. </para>
+ </listitem>
+ </itemizedlist>
</listitem>
<listitem>
- <para>Exponential form: Before
- <literal>incompatible_improvements</literal> 2.3.32, it was
- never used. After that, it's uses exponential form in these
- cases:</para>
+ <para>Special floating point values, positive infinity, negative
+ infinity, and NaN (for Not-a-Number):</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>For <literal>c_format</literal>-s <quote>JSON</quote>,
+ and <quote>JavaScript</quote>: <literal>Infinity</literal>,
+ <literal>-Infinity</literal>, <literal>NaN</literal></para>
+ </listitem>
+
+ <listitem>
+ <para>For <literal>c_format</literal> <quote>Java</quote>:
+ If the value has Java type <literal>double</literal> or
+ <literal>Double</literal>:
+ <literal>Double.POSITIVE_INFINITY</literal>,
+ <literal>Double.NEGATIVE_INFINITY</literal>,
+ <literal>Double.NaN</literal>. If the value has Java type
+ <literal>float</literal> or <literal>Float</literal>:
+ <literal>Float.POSITIVE_INFINITY</literal>,
+ <literal>Float.NEGATIVE_INFINITY</literal>,
+ <literal>Float.NaN</literal>.</para>
+ </listitem>
+
+ <listitem>
+ <para>For <literal>c_format</literal> <quote>XS</quote>, and
+ also for the deprecated <literal>c_format</literal>,
+ <quote>default 2.3.21</quote>: <literal>INF</literal>,
+ <literal>-INF</literal>, and <literal>NaN</literal>.</para>
+ </listitem>
+
+ <listitem>
+ <para>For the deprecated <literal>c_format</literal>,
+ <quote>default 2.3.0</quote>: Gives what
+ <literal>java.text.DecimalFormat</literal> does with US
+ locale, which are <literal>∞</literal>,
+ <literal>-∞</literal>, and <literal>�</literal> (U+FFFD,
+ replacement character).</para>
+ </listitem>
+ </itemizedlist>
+ </listitem>
+
+ <listitem>
+ <para>Exponential form is used by all non-deprecated
+ <literal>c_format</literal>-s that are built into FreeMarker
+ (but not by the deprecated <quote>default 2.3.0</quote>, and
+ <quote>default 2.3.21</quote>):</para>
<itemizedlist>
<listitem>
@@ -15828,26 +16125,128 @@ rif: foo XYr baar</programlisting>
<para>Note that by default FreeMarker doesn't use
<literal>double</literal> or <literal>float</literal>, so
- such values are likely can only come from the
- data-model.</para>
+ such values are likely come from the data-model.</para>
</listitem>
</itemizedlist>
</listitem>
<listitem>
- <para>The output never contains superfluous zeros after the
- decimal point. Thus, unlike in Java, you can't tell apart a
- <literal>double</literal> from an <literal>int</literal>, if
- they store the same number mathematically, because the output
- will look the same for both. (This is because the template
- language only have a single number type, so you don't have a
- good control over the backing Java type.)</para>
+ <para>Currently, in the <literal>c_format</literal>-s that are
+ built into FreeMarker, the output never contains superfluous
+ zeros after the decimal point. Thus, unlike in Java, you can't
+ tell apart a <literal>double</literal> from an
+ <literal>int</literal>, if they store the same number
+ mathematically, because the output will look the same for both.
+ (This is because the template language only have a single number
+ type, so you don't have a good control over the backing Java
+ type.)</para>
</listitem>
</itemizedlist>
- <para>Note that the <literal>c</literal> built-in <link
- linkend="ref_builtin_c_boolean">also works on
- booleans</link>.</para>
+ <para>If you only generate output that's computer language and isn't
+ read by end-users, you may prefer to set the
+ <literal>number_format</literal> configuration setting to
+ <literal>"c"</literal> (since FreeMarker 2.3.32,
+ <literal>"computer"</literal> before that), in which case
+ <literal>${<replaceable>aNumber</replaceable>}</literal> will have
+ the same output as
+ <literal>${<replaceable>aNumber</replaceable>?c}</literal>. (In this
+ case you should use a <literal>c_format</literal> like
+ <literal>"JSON"</literal>, and not some of the strictly backward
+ compatible defaults, as those are emulating some confusing old
+ glitches.)</para>
+
+ <para>If the value the <literal>c</literal> built-in is applied on
+ is <literal>null</literal>/missing, it will stop the template
+ processing with error, just like most other built-ins. If instead
+ you want to output a <literal>null</literal> literal, see the <link
+ linkend="ref_builtin_cn"><literal>cn</literal>
+ built-in</link>.</para>
+ </section>
+
+ <section xml:id="ref_builtin_cn">
+ <title>cn (for numbers)</title>
+
+ <indexterm>
+ <primary>cn built-in</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>type-casting</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>converting between types</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>format</primary>
+
+ <secondary>number</secondary>
+ </indexterm>
+
+ <note>
+ <para><literal>cn</literal> works with all types that
+ <literal>c</literal> does, and thus for strings and booleans as
+ well. The formatting of <literal>null</literal>/missing doesn't
+ depend on the type of course (as we have no value that could have
+ a type).</para>
+ </note>
+
+ <note>
+ <para>See <xref linkend="dgui_misc_computer_vs_human_format"/> for
+ background</para>
+ </note>
+
+ <note>
+ <para>This built-in exists since FreeMarker 2.3.32</para>
+ </note>
+
+ <para>This is the same as the <link
+ linkend="ref_builtin_c"><literal>c</literal> built-in</link>, except
+ if the value on its left side is <literal>null</literal>/missing,
+ this won't stop with error, but outputs a <literal>null</literal>
+ literal that's appropriate for the current
+ <literal>c_format</literal> setting:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>For <quote>JSON</quote>, <quote>Java</quote>,
+ <quote>JavaScript</quote>, <quote>default 2.3.0</quote>,
+ <quote>default 2.3.21</quote>: <literal>null</literal></para>
+ </listitem>
+
+ <listitem>
+ <para>For <quote>XS</quote> (used for generating XML that
+ follows XML Schema principles): 0 length string (i.e.,
+ <literal>${<replaceable>thisIsNull</replaceable>?cn}</literal>
+ prints nothing), which is often not good enough (see below), but
+ we can't do better with a
+ <literal><replaceable>stringValue</replaceable>?c</literal>
+ alone. The idea is that you write something like
+ <literal><full-name>${fullName?nc}</full-name></literal>,
+ or <literal><user <replaceable>...</replaceable>
+ full-name="${fullName?nc}" /></literal>, and then, in case
+ <literal>fullName</literal> is <literal>null</literal>, the
+ output will be
+ <literal><full-name></full-name></literal>, and
+ <literal><user <replaceable>...</replaceable> full-name=""
+ /></literal>. Some applications accept that as the equivalent
+ of a <literal>null</literal>, at least where a string value is
+ expected according the XML Schema.</para>
+
+ <para>Note that the XML Schema approach is that you skip
+ outputting the whole <literal>full-name</literal> XML element,
+ or XML attribute. For that you have to write <literal><#if
+ fullName??><full-name>${full-name?c}</full-name></#if></literal>,
+ and <literal><user <replaceable>...</replaceable> <#if
+ fullName??>full-name="${fullName?c}"</#if>
+ /></literal>. When using such condition, and the value is a
+ string (as with this example), you might as well just write
+ <literal>${fullName}</literal>, without the
+ <literal>?c</literal>.</para>
+ </listitem>
+ </itemizedlist>
</section>
<section xml:id="ref_builtin_is_infinite">
@@ -16991,7 +17390,7 @@ Tue, Apr 8, '03
</indexterm>
<section xml:id="ref_builtin_c_boolean">
- <title>c (when used with boolean)</title>
+ <title>c (for boolean value)</title>
<indexterm>
<primary>c built-in</primary>
@@ -17012,28 +17411,73 @@ Tue, Apr 8, '03
</indexterm>
<note>
- <para>This built-in exists since FreeMarker 2.3.20.</para>
+ <para>The <literal>c</literal> built-in also works <link
+ linkend="ref_builtin_c_boolean"> <link linkend="ref_builtin_c">on
+ numbers</link></link>, and <link linkend="ref_builtin_c_string">on
+ strings</link>!</para>
+ </note>
+
+ <note>
+ <para>To provide a background, see <xref
+ linkend="dgui_misc_computer_vs_human_format"/></para>
</note>
- <para>This built-in converts a boolean to string for a
- <quote>computer language</quote> as opposed to for human audience.
- The result will be <literal>"true"</literal> or
- <literal>"false"</literal>, regardless of the
- <literal>boolean_format</literal> configuration setting, as that
- setting is meant to specify the format for human consumption. When
- generating boolean literals for JavaScript and such, this should be
- used.</para>
+ <note>
+ <para>The <literal>c</literal> built-in supports booleans since
+ FreeMarker 2.3.20.</para>
+ </note>
+
+ <para>This built-in converts a boolean to a <quote>computer
+ language</quote> literal, as opposed to format it for human reading.
+ This formats is independent of the <literal>boolean_format</literal>
+ configuration setting, as that setting is meant to specify the
+ format for human readers. Instead, it depends on the <link
+ linkend="gloss.c_format"><literal>c_format</literal> setting</link>.
+ However, currently all <literal>c_format</literal> that's built into
+ FreeMarker result will give <literal>true</literal> or
+ <literal>false</literal>.</para>
+
+ <para>When outputting boolean literals for JavaScript, JSON, Java,
+ and many other languages, always use this built-in, instead of
+ relying on <literal>boolean_format</literal>.</para>
<para>If you only generate output that's computer language and isn't
- read by end-users, you may want to set the
+ read by end-users, you may prefer to set the
<literal>boolean_format</literal> configuration setting to
<literal>c</literal> (since FreeMarker 2.3.29), in which case
<literal>${<replaceable>aBoolean</replaceable>}</literal> will have
the same output as
<literal>${<replaceable>aBoolean</replaceable>?c}</literal>.</para>
- <para>Note that this built-in <link linkend="ref_builtin_c">also
- works on strings</link>.</para>
+ <para>If the value the <literal>c</literal> built-in is applied on
+ is <literal>null</literal>/missing, it will stop the template
+ processing with error, just like most other built-ins. If instead
+ you want to output a <literal>null</literal> literal, see the <link
+ linkend="ref_builtin_cn_string"><literal>cn</literal>
+ built-in</link>.</para>
+ </section>
+
+ <section xml:id="ref_builtin_cn_boolean">
+ <title>cn (for boolean value)</title>
+
+ <indexterm>
+ <primary>cn built-in</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>format</primary>
+
+ <secondary>boolean</secondary>
+ </indexterm>
+
+ <para>This does the same as the <link
+ linkend="ref_builtin_c_boolean"><literal>c</literal>
+ built-in</link>, but when applied on a
+ <literal>null</literal>/missing value, it will output a
+ <literal>null</literal> value according the <link
+ linkend="gloss.c_format"><literal>c_format</literal> setting</link>.
+ See more details about formatting a <literal>null</literal> <link
+ linkend="ref_builtin_cn">here</link>.</para>
</section>
<section xml:id="ref_builtin_string_for_boolean">
@@ -17212,10 +17656,6 @@ N
<primary>columnar printing of sequences</primary>
</indexterm>
- <note>
- <para>This built-in exists since FreeMarker 2.3.3.</para>
- </note>
-
<para>This built-in splits a sequence into multiple sequences of the
size given with the 1st parameter to the built-in (like
<literal>mySeq?chunk(3)</literal>). The result is the sequence of
@@ -23886,9 +24326,7 @@ ${"'{}"}
<section>
<title>Synopsis</title>
- <programlisting role="metaTemplate">
-<literal><#setting <replaceable>name</replaceable>=<replaceable>value</replaceable>></literal>
-</programlisting>
+ <programlisting role="metaTemplate"><literal><#setting <replaceable>name</replaceable>=<replaceable>value</replaceable>></literal></programlisting>
<para>Where:</para>
@@ -24318,6 +24756,18 @@ ${"'{}"}
<literal>Configurable.setSQLDateAndTimeTimeZone(TimeZone)</literal>.</phrase></para>
</listitem>
+ <listitem>
+ <para><indexterm>
+ <primary>c_format</primary>
+ </indexterm><literal>c_format</literal> (since FreeMarker
+ 2.3.32): Sets what format to use when formatting for computer
+ consumption, like <literal>"JSON"</literal>. Mostly prominently
+ this affects the <link
+ linkend="ref_builtin_c"><literal>c</literal> built-in</link>,
+ hence the name. See valid values and their meaning here: <xref
+ linkend="dgui_misc_computer_vs_human_format"/>.</para>
+ </listitem>
+
<listitem>
<para><indexterm>
<primary>url_escaping_charset</primary>
@@ -29584,6 +30034,129 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting>
<title>Changes on the FTL side</title>
<itemizedlist>
+ <listitem>
+ <para>Improved outputting values for computer/parser consumption
+ (such as for generating JSON, JavaScript values, values encoded
+ into URL-s):</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>Added new configuration setting, <link
+ linkend="gloss.c_format"><literal>c_format</literal></link>
+ (also settable via the <link
+ linkend="ref.directive.setting"><literal>setting</literal>
+ directive</link>). This specifies what format to use when
+ formatting for computer consumption, like
+ <literal>"JSON"</literal>. Most prominently, this affects
+ the <link linkend="ref_builtin_c"><literal>c</literal>
+ built-in</link>, hence the name. See valid setting values,
+ and their meaning here: <xref
+ linkend="dgui_misc_computer_vs_human_format"/>.</para>
+
+ <para>If you set the <link
+ linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incompatible_improvements</literal>
+ setting</link> to 2.3.32, the default of
+ <literal>c_format</literal> changes to
+ <literal>JSON</literal>, which is a format that most targets
+ can parse (because we just format simple values, not lists
+ and maps). With lower
+ <literal>incompatible_improvements</literal>, the default
+ value is a <literal>c_format</literal> that emulates the old
+ behavior of <literal>?c</literal> (where you can lose
+ numerical prevision, etc.), so it's recommended to set it to
+ something else.</para>
+ </listitem>
+
+ <listitem>
+ <para><literal><link
+ linkend="ref_builtin_c">?c</link></literal> now formats
+ string values to string literals (with quotation marks and
+ escaping), according the language specified in the <link
+ linkend="gloss.c_format"><literal>c_format</literal>
+ setting</link>, such as <literal>JSON</literal>,
+ <literal>Java</literal>, etc. Earlier, <literal>?c</literal>
+ only allowed numbers, and booleans.</para>
+
+ <para>To generate JSON, you can now write a piece of
+ template like this:</para>
+
+ <programlisting role="template">"fullName": ${user.fullName?c},</programlisting>
+
+ <para>Then the output will be like:</para>
+
+ <programlisting role="output">"fullName": "John Doe",</programlisting>
+
+ <para>Note that the quotation marks were added by
+ <literal>?c</literal>, and weren't typed into the
+ template.</para>
+ </listitem>
+
+ <listitem>
+ <para>Added <link linkend="ref_builtin_cn">?cn</link>, which
+ is like <literal><link
+ linkend="ref_builtin_c">?c</link></literal>, except if the
+ value is <literal>null</literal>/missing, it will output a
+ <literal>null</literal> literal, according the language
+ specified in the new <link
+ linkend="gloss.c_format"><literal>c_format</literal>
+ setting</link>.</para>
+
+ <para>Let's say, in the previous example
+ <literal>user.fullName</literal> is expected to be
+ <literal>null</literal> sometimes. Then you can just use
+ <literal>?cn</literal> instead if
+ <literal>?c</literal>:</para>
+
+ <programlisting role="template">"fullName": ${user.fullName?cn},</programlisting>
+
+ <para>If said variable is <literal>null</literal>, the
+ output will be like this (otherwise it will be a quoted
+ string like earlier):</para>
+
+ <programlisting role="output">"fullName": null,</programlisting>
+
+ <para>Note with this approach you don't complicate the
+ template anymore to avoid printing quotation marks. Of
+ course, <literal>?cn</literal> works on numerical and
+ boolean values as well.</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>c_format</literal>-s which are not merely for
+ backward compatibility, the number formatting changes
+ relatively to how <literal>?c</literal> did it in earlier
+ versions. The change affects some non-whole numbers, and
+ whole numbers with over 100 digits. The goal of this change
+ is to make the formatting lossless, and also to avoiding
+ huge output with exponents of high magnitude. See details at
+ the documentation of the <link
+ linkend="ref_builtin_c"><literal>c</literal>
+ built-in</link>.</para>
+
+ <para>Because setting the <link
+ linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incompatible_improvements</literal>
+ setting</link> to 2.3.32 will change the default of
+ <literal>c_format</literal> to <literal>JSON</literal>, it
+ will consequently also activate these changes. But this only
+ affects number formatting done with <literal>?c</literal>,
+ <literal>?cn</literal>, and with <quote>c</quote>
+ <literal>number_format</literal>, and not number formatting
+ in general.</para>
+ </listitem>
+
+ <listitem>
+ <para>For consistency, when setting the
+ <literal>number_format</literal> setting (also when
+ formatting with
+ <literal>?string(<replaceable>format</replaceable>)</literal>),
+ now <quote>c</quote> can be used instead of
+ <quote>computer</quote>. Both has the same effect on
+ formatting, but <quote>c</quote> is preferred from now
+ on.</para>
+ </listitem>
+ </itemizedlist>
+ </listitem>
+
<listitem>
<para><link
xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-208">FREEMARKER-208</link>:
@@ -29601,29 +30174,6 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting>
purposes), and not for humans.</para>
</listitem>
- <listitem>
- <para>When setting the <literal>number_format</literal> setting
- (also when formatting with
- <literal>?string(<replaceable>format</replaceable>)</literal>),
- now <quote>c</quote> can be used instead of
- <quote>computer</quote>. The two mean the same, and
- <quote>c</quote> is preferred from now on.</para>
- </listitem>
-
- <listitem>
- <para>If <link
- linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incomplatible_improvements</literal></link>
- is set to 2.3.32 (or higher), the behavior of
- <literal>?c</literal> (and thus the <quote>computer</quote> and
- <quote>c</quote> number format) has changed for some non-whole
- numbers, and for whole numbers with over 100 digits. The goal of
- this change is to make the formatting lossless, and also to
- avoiding huge output with exponents of high magnitude. See
- details at the documentation of the <link
- linkend="ref_builtin_c"><literal>c</literal>
- built-in</link>.</para>
- </listitem>
-
<listitem>
<para>In <literal>freemarker.ext.xml</literal>, which is the
old, long deprecated XML wrapper, that almost nobody uses
@@ -42584,6 +43134,36 @@ Apache Software Foundation.</programlisting>
</glossdef>
</glossentry>
+ <glossentry xml:id="gloss.c">
+ <glossterm>c</glossterm>
+
+ <glossdef>
+ <para>In the context of FreeMarker, this usually refers to the <link
+ linkend="ref_builtin_c"><literal>c</literal> built-in</link>, or
+ <quote>computer format</quote> in general.</para>
+ </glossdef>
+ </glossentry>
+
+ <glossentry xml:id="gloss.c_format">
+ <glossterm>c_format</glossterm>
+
+ <glossdef>
+ <para><literal>c_format</literal> (aka. <literal>cFormat</literal>,
+ <quote>c format</quote>) is a <link
+ linkend="pgui_config_settings">configuration setting</link> that
+ specifies how to format values for <quote>computer
+ consumption</quote>, that is, if the target is some computer language,
+ or other simpler parser, and not a human reader. The value of this
+ setting is usually a computer language, like <quote>JSON</quote>, or
+ <quote>Java</quote>. The name refers to the <link
+ linkend="ref_builtin_c"><literal>c</literal> built-in</link>, which is
+ the typical way of formatting simple values for such output (e.g.
+ <literal><a href="/product/${product.id?c}"></literal>). To
+ understand the topic more, see: <xref
+ linkend="dgui_misc_computer_vs_human_format"/></para>
+ </glossdef>
+ </glossentry>
+
<glossentry xml:id="gloss.character">
<glossterm>Character</glossterm>
diff --git a/src/test/java/freemarker/core/BooleanFormatEnvironmentCachingTest.java b/src/test/java/freemarker/core/BooleanFormatEnvironmentCachingTest.java
new file mode 100644
index 00000000..d8ca4d81
--- /dev/null
+++ b/src/test/java/freemarker/core/BooleanFormatEnvironmentCachingTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class BooleanFormatEnvironmentCachingTest extends TemplateTest {
+
+ @Override
+ protected Configuration createConfiguration() throws Exception {
+ Configuration conf = super.createConfiguration();
+ conf.setCFormat(CustomCFormat.INSTANCE);
+ conf.setBooleanFormat("c");
+ return conf;
+ }
+
+ @Test
+ public void test() throws TemplateException, IOException {
+ assertOutput(
+ ""
+ + "${true} ${true} ${false} ${false} "
+ + "<#setting cFormat='JSON'>${true} ${true} ${false} ${false} "
+ + "<#setting booleanFormat='y,n'>${true} ${true} ${false} ${false} "
+ + "<#setting cFormat='Java'>${true} ${true} ${false} ${false} "
+ + "<#setting booleanFormat='c'>${true} ${true} ${false} ${false} ",
+ ""
+ + "TRUE TRUE FALSE FALSE "
+ + "true true false false "
+ + "y y n n "
+ + "y y n n "
+ + "true true false false "
+ + "");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/core/CAndCnBuiltInTest.java b/src/test/java/freemarker/core/CAndCnBuiltInTest.java
new file mode 100644
index 00000000..af7d4622
--- /dev/null
+++ b/src/test/java/freemarker/core/CAndCnBuiltInTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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 java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.Timestamp;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.template.Version;
+import freemarker.template._VersionInts;
+import freemarker.test.TemplateTest;
+
+public class CAndCnBuiltInTest extends TemplateTest {
+
+ @Before
+ public void addModelVariables() {
+ addToDataModel("double1", 1.0);
+ addToDataModel("double2", 1.000000000000001);
+ addToDataModel("double3", 0.0000000000000001);
+ addToDataModel("double4", -0.0000000000000001);
+ addToDataModel("bigDecimal1", new BigDecimal("1"));
+ addToDataModel("bigDecimal2", new BigDecimal("0.0000000000000001"));
+ addToDataModel("doubleInf", Double.POSITIVE_INFINITY);
+ addToDataModel("doubleNegativeInf", Double.NEGATIVE_INFINITY);
+ addToDataModel("doubleNaN", Double.NaN);
+ addToDataModel("floatInf", Float.POSITIVE_INFINITY);
+ addToDataModel("floatNegativeInf", Float.NEGATIVE_INFINITY);
+ addToDataModel("floatNaN", Float.NaN);
+ addToDataModel("string", "a\nb");
+ addToDataModel("long", Long.MAX_VALUE);
+ addToDataModel("int", Integer.MAX_VALUE);
+ addToDataModel("bigInteger", new BigInteger("123456789123456789123456789123456789"));
+ addToDataModel("dateTime", new Timestamp(1671641049876L));
+ addToDataModel("booleanTrue", true);
+ addToDataModel("booleanFalse", false);
+ }
+
+ @Test
+ public void testCWithNumber() throws TemplateException, IOException {
+ testWithNumber("c");
+ }
+
+ @Test
+ public void testCnWithNumber() throws TemplateException, IOException {
+ testWithNumber("cn");
+ }
+
+ void testWithNumber(String builtInName) throws TemplateException, IOException {
+ testWithNumber(builtInName, Configuration.VERSION_2_3_20);
+ testWithNumber(builtInName, Configuration.VERSION_2_3_21);
+ testWithNumber(builtInName, Configuration.VERSION_2_3_31);
+ testWithNumber(builtInName, Configuration.VERSION_2_3_32);
+ }
+
+ void testWithNumber(String builtInName, Version ici) throws TemplateException, IOException {
+ // Always the same
+ assertOutput("${double1? " + builtInName + "}", "1");
+ assertOutput("${double2?" + builtInName + "}", "1.000000000000001");
+ assertOutput("${bigDecimal1? " + builtInName + "}", "1");
+ assertOutput("${int? " + builtInName + "}", String.valueOf(Integer.MAX_VALUE));
+ assertOutput("${long? " + builtInName + "}", String.valueOf(Long.MAX_VALUE));
+ assertOutput("${bigInteger? " + builtInName + "}", "123456789123456789123456789123456789");
+
+ getConfiguration().setIncompatibleImprovements(ici);
+
+ if (ici.intValue() >= _VersionInts.V_2_3_32) {
+ assertOutput("${double3?" + builtInName + "}", "1E-16");
+ assertOutput("${double4?" + builtInName + "}", "-1E-16");
+ assertOutput("${bigDecimal2?" + builtInName + "}", "1E-16");
+ } else {
+ assertOutput("${double3?" + builtInName + "}", "0.0000000000000001");
+ assertOutput("${double4?" + builtInName + "}", "-0.0000000000000001");
+ assertOutput("${bigDecimal2?" + builtInName + "}", "0.0000000000000001");
+ }
+
+ for (String type : new String[] {"float", "double"}) {
+ String expectedInf;
+ String expectedNaN;
+ if (ici.intValue() >= _VersionInts.V_2_3_32) {
+ expectedInf = "Infinity";
+ expectedNaN = "NaN";
+ } else if (ici.intValue() >= _VersionInts.V_2_3_21) {
+ expectedInf = "INF";
+ expectedNaN = "NaN";
+ } else {
+ expectedInf = "\u221E";
+ expectedNaN = "\uFFFD";
+ }
+
+ assertOutput("${" + type + "Inf?" + builtInName + "}", expectedInf);
+ assertOutput("${" + type + "NegativeInf?" + builtInName + "}", "-" + expectedInf);
+ assertOutput("${" + type + "NaN?" + builtInName + "}", expectedNaN);
+ }
+ }
+
+ @Test
+ public void testWithNonNumber() throws TemplateException, IOException {
+ for (Version ici : new Version[] {
+ Configuration.VERSION_2_3_0, Configuration.VERSION_2_3_31, Configuration.VERSION_2_3_32 }) {
+ testWithNonNumber("c", ici);
+ testWithNonNumber("cn", ici);
+ }
+ }
+
+ private void testWithNonNumber(String builtInName, Version ici) throws TemplateException, IOException {
+ assertOutput("${string?" + builtInName + "}", "\"a\\nb\"");
+ assertOutput("${booleanTrue?" + builtInName + "}", "true");
+ assertOutput("${booleanFalse?" + builtInName + "}", "false");
+ assertErrorContains("${dateTime?" + builtInName + "}",
+ "Expected a number, boolean, or string");
+ }
+
+ @Test
+ public void testWithNull() throws TemplateException, IOException {
+ assertOutput("${noSuchVar?cn}", "null");
+ assertErrorContains("${noSuchVar?c}", "null or missing");
+ }
+
+}
diff --git a/src/test/java/freemarker/core/CFormatTemplateTest.java b/src/test/java/freemarker/core/CFormatTemplateTest.java
new file mode 100644
index 00000000..414a9011
--- /dev/null
+++ b/src/test/java/freemarker/core/CFormatTemplateTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.Before;
+import org.junit.Test;
+
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class CFormatTemplateTest extends TemplateTest {
+
+ @Before
+ public void addDataModelVariables() {
+ addToDataModel("s", "a'b\"c\u0001");
+ }
+
+ @Test
+ public void testBooleanAndNullFormat() throws TemplateException, IOException {
+ getConfiguration().setCFormat(CustomCFormat.INSTANCE);
+ assertOutput(
+ ""
+ + "${true?c} ${false?c} ${null?cn} "
+ + "JSON: <#setting c_format='JSON'>${true?c} ${false?c} ${null?cn}",
+ ""
+ + "TRUE FALSE NULL "
+ + "JSON: true false null");
+ assertOutput(
+ "<#setting boolean_format='c'>"
+ + "${true} ${false} "
+ + "JSON: <#setting c_format='JSON'>${true} ${false}",
+ ""
+ + "TRUE FALSE "
+ + "JSON: true false");
+ }
+
+ @Test
+ public void testStringFormat() throws TemplateException, IOException {
+ assertOutput(
+ ""
+ + "Default: ${s?c} "
+ + "XS: <#setting c_format='XS'>${s?c} "
+ + "JavaScript: <#setting c_format='JavaScript'>${s?c} "
+ + "JSON: <#setting c_format='JSON'>${s?c} "
+ + "Java: <#setting c_format='Java'>${s?c} ",
+ ""
+ + "Default: \"a'b\\\"c\\u0001\" "
+ + "XS: a'b\"c\u0001 "
+ + "JavaScript: \"a'b\\\"c\\x01\" "
+ + "JSON: \"a'b\\\"c\\u0001\" "
+ + "Java: \"a'b\\\"c\\u0001\" ");
+ }
+
+}
diff --git a/src/test/java/freemarker/core/CTemplateNumberFormatTest.java b/src/test/java/freemarker/core/CTemplateNumberFormatTest.java
index 3ae7ad75..d20be03b 100644
--- a/src/test/java/freemarker/core/CTemplateNumberFormatTest.java
+++ b/src/test/java/freemarker/core/CTemplateNumberFormatTest.java
@@ -41,7 +41,8 @@ public class CTemplateNumberFormatTest {
testFormat(10000000000000000d, "1E16");
testFormat(12300000000000000d, "1.23E16");
testFormat(Double.NaN, "NaN");
- testFormat(Double.POSITIVE_INFINITY, "INF");
+ testFormat(Double.POSITIVE_INFINITY, "Infinity");
+ testFormat(Double.NEGATIVE_INFINITY, "-Infinity");
testFormat(1.9E-6, "0.0000019");
testFormat(9.5E-7, "9.5E-7");
testFormat(9999999.5, "9999999.5");
@@ -58,7 +59,8 @@ public class CTemplateNumberFormatTest {
testFormat(100000000f, "1E8");
testFormat(123000000f, "1.23E8");
testFormat(Float.NaN, "NaN");
- testFormat(Float.POSITIVE_INFINITY, "INF");
+ testFormat(Float.POSITIVE_INFINITY, "Infinity");
+ testFormat(Float.NEGATIVE_INFINITY, "-Infinity");
testFormat(1.9E-6f, "0.0000019");
testFormat(9.5E-7f, "9.5E-7");
testFormat(1000000.5f, "1000000.5");
@@ -100,11 +102,12 @@ public class CTemplateNumberFormatTest {
private void testFormat(Number n, String expectedResult) throws TemplateModelException,
TemplateValueFormatException {
- String actualResult = (String) CTemplateNumberFormat.INSTANCE.format(new SimpleNumber(n));
+ TemplateNumberFormat cTemplateNumberFormat = JSONCFormat.INSTANCE.getTemplateNumberFormat();
+ String actualResult = (String) cTemplateNumberFormat.format(new SimpleNumber(n));
assertFormatResult(n, actualResult, expectedResult);
if (!actualResult.equals("NaN") && !actualResult.equals("0") && !actualResult.startsWith("-")) {
Number negativeN = negate(n);
- actualResult = (String) CTemplateNumberFormat.INSTANCE.format(new SimpleNumber(negativeN));
+ actualResult = (String) cTemplateNumberFormat.format(new SimpleNumber(negativeN));
assertFormatResult(negativeN, actualResult, "-" + expectedResult);
}
}
diff --git a/src/test/java/freemarker/core/CustomCFormat.java b/src/test/java/freemarker/core/CustomCFormat.java
new file mode 100644
index 00000000..18573cb5
--- /dev/null
+++ b/src/test/java/freemarker/core/CustomCFormat.java
@@ -0,0 +1,83 @@
+/*
+ * 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.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+import freemarker.template.utility.StringUtil;
+
+class CustomCFormat extends CFormat {
+ final static CustomCFormat INSTANCE = new CustomCFormat();
+
+ private CustomCFormat() {
+ }
+
+ private static final TemplateNumberFormat TEMPLATE_NUMBER_FORMAT = new CTemplateNumberFormat(
+ "M:INF", "M:NINF", "M:NaN",
+ "M:INF", "M:NINF", "M:NaN");
+
+ private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE =
+ (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+
+ static {
+ DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+ symbols.setInfinity("Infinity");
+ symbols.setNaN("NaN");
+ LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+ }
+
+ @Override
+ TemplateNumberFormat getTemplateNumberFormat() {
+ return TEMPLATE_NUMBER_FORMAT;
+ }
+
+ @Override
+ NumberFormat getLegacyNumberFormat() {
+ return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+ }
+
+ @Override
+ String formatString(String s, Environment env) throws TemplateException {
+ return StringUtil.ftlQuote(s);
+ }
+
+ @Override
+ String getTrueString() {
+ return "TRUE";
+ }
+
+ @Override
+ String getFalseString() {
+ return "FALSE";
+ }
+
+ @Override
+ String getNullString() {
+ return "NULL";
+ }
+
+ @Override
+ public String getName() {
+ return "custom";
+ }
+}
diff --git a/src/test/java/freemarker/core/NumberFormatTest.java b/src/test/java/freemarker/core/NumberFormatTest.java
index 184fc213..4d4ce237 100644
--- a/src/test/java/freemarker/core/NumberFormatTest.java
+++ b/src/test/java/freemarker/core/NumberFormatTest.java
@@ -46,7 +46,6 @@ import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.template.Version;
import freemarker.test.TemplateTest;
-import net.jcip.annotations.Immutable;
@SuppressWarnings("boxing")
public class NumberFormatTest extends TemplateTest {
@@ -326,14 +325,16 @@ public class NumberFormatTest extends TemplateTest {
for (Version ici : new Version[] {
Configuration.VERSION_2_3_20,
Configuration.VERSION_2_3_21, Configuration.VERSION_2_3_30,
- Configuration.VERSION_2_3_31 } ) {
+ Configuration.VERSION_2_3_31,
+ Configuration.VERSION_2_3_32 } ) {
cfg.setIncompatibleImprovements(ici);
boolean cBuiltInBroken = ici.intValue() < Configuration.VERSION_2_3_21.intValue();
boolean cNumberFormatBroken = ici.intValue() < Configuration.VERSION_2_3_31.intValue();
String humanAudienceOutput = "\u221e -\u221e \ufffd";
- String computerAudienceOutput = "INF -INF NaN";
+ String computerAudienceOutput = ici.intValue() < Configuration.VERSION_2_3_32.intValue()
+ ? "INF -INF NaN" : "Infinity -Infinity NaN";
assertOutput(
"${pInf?c} ${nInf?c} ${nan?c}",
diff --git a/src/test/java/freemarker/core/TemplateConfigurationTest.java b/src/test/java/freemarker/core/TemplateConfigurationTest.java
index e48c38aa..ef4be2b2 100644
--- a/src/test/java/freemarker/core/TemplateConfigurationTest.java
+++ b/src/test/java/freemarker/core/TemplateConfigurationTest.java
@@ -164,6 +164,7 @@ public class TemplateConfigurationTest {
SETTING_ASSIGNMENTS.put("dateFormat", "yyyy-#DDD");
SETTING_ASSIGNMENTS.put("dateTimeFormat", "yyyy-#DDD-@HH:mm");
SETTING_ASSIGNMENTS.put("locale", NON_DEFAULT_LOCALE);
+ SETTING_ASSIGNMENTS.put("CFormat", JavaScriptCFormat.INSTANCE);
SETTING_ASSIGNMENTS.put("logTemplateExceptions", false);
SETTING_ASSIGNMENTS.put("wrapUncheckedExceptions", true);
SETTING_ASSIGNMENTS.put("newBuiltinClassResolver", TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
diff --git a/src/test/java/freemarker/template/ConfigurationTest.java b/src/test/java/freemarker/template/ConfigurationTest.java
index a3adc5ba..7e11be74 100644
--- a/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/src/test/java/freemarker/template/ConfigurationTest.java
@@ -54,12 +54,15 @@ import freemarker.cache.TemplateLookupResult;
import freemarker.cache.TemplateLookupStrategy;
import freemarker.cache.TemplateNameFormat;
import freemarker.core.BaseNTemplateNumberFormatFactory;
+import freemarker.core.CFormat;
import freemarker.core.CombinedMarkupOutputFormat;
import freemarker.core.Configurable;
import freemarker.core.Configurable.SettingValueAssignmentException;
import freemarker.core.Configurable.UnknownSettingException;
import freemarker.core.ConfigurableTest;
import freemarker.core.CustomHTMLOutputFormat;
+import freemarker.core.Default230CFormat;
+import freemarker.core.Default2321CFormat;
import freemarker.core.DefaultTruncateBuiltinAlgorithm;
import freemarker.core.DummyOutputFormat;
import freemarker.core.Environment;
@@ -67,6 +70,9 @@ import freemarker.core.EpochMillisDivTemplateDateFormatFactory;
import freemarker.core.EpochMillisTemplateDateFormatFactory;
import freemarker.core.HTMLOutputFormat;
import freemarker.core.HexTemplateNumberFormatFactory;
+import freemarker.core.JSONCFormat;
+import freemarker.core.JavaCFormat;
+import freemarker.core.JavaScriptCFormat;
import freemarker.core.MarkupOutputFormat;
import freemarker.core.OptInTemplateClassResolver;
import freemarker.core.OutputFormat;
@@ -79,6 +85,7 @@ import freemarker.core.UndefinedOutputFormat;
import freemarker.core.UnregisteredOutputFormatException;
import freemarker.core.XHTMLOutputFormat;
import freemarker.core.XMLOutputFormat;
+import freemarker.core.XSCFormat;
import freemarker.core._CoreStringUtils;
import freemarker.ext.beans.BeansWrapperBuilder;
import freemarker.ext.beans.LegacyDefaultMemberAccessPolicy;
@@ -96,7 +103,7 @@ public class ConfigurationTest extends TestCase {
public ConfigurationTest(String name) {
super(name);
}
-
+
public void testIncompatibleImprovementsChangesDefaults() {
Version newVersion = Configuration.VERSION_2_3_21;
Version oldVersion = Configuration.VERSION_2_3_20;
@@ -180,6 +187,20 @@ public class ConfigurationTest extends TestCase {
cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_27);
assertTrue(((DefaultObjectWrapper) cfg.getObjectWrapper()).getTreatDefaultMethodsAsBeanMembers());
assertFalse(((DefaultObjectWrapper) cfg.getObjectWrapper()).getPreferIndexedReadMethod());
+
+ cfg = new Configuration(Configuration.VERSION_2_3_0);
+ assertSame(Default230CFormat.INSTANCE, cfg.getCFormat());
+ cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_20);
+ assertSame(Default230CFormat.INSTANCE, cfg.getCFormat());
+ cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_21);
+ assertSame(Default2321CFormat.INSTANCE, cfg.getCFormat());
+ cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_31);
+ assertSame(Default2321CFormat.INSTANCE, cfg.getCFormat());
+ cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_32);
+ assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());
+ cfg.setCFormat(JSONCFormat.INSTANCE); // Same as default, but explicitly set now
+ cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_31);
+ assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());
}
private void assertUses2322ObjectWrapper(Configuration cfg) {
@@ -286,6 +307,14 @@ public class ConfigurationTest extends TestCase {
assertFalse(cfg.isCacheStorageExplicitlySet());
assertTrue(cfg.getCacheStorage() instanceof SoftCacheStorage);
}
+
+ assertFalse(cfg.isCFormatExplicitlySet());
+ //
+ cfg.setCFormat(XSCFormat.INSTANCE);
+ assertTrue(cfg.isCFormatExplicitlySet());
+ //
+ cfg.unsetCFormat();
+ assertFalse(cfg.isTemplateLookupStrategyExplicitlySet());
}
public void testTemplateLoadingErrors() throws Exception {
@@ -1888,7 +1917,32 @@ public class ConfigurationTest extends TestCase {
}
}
- @Test
+ public void testCFormat() throws TemplateException {
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_21);
+
+ assertSame(Default2321CFormat.INSTANCE, cfg.getCFormat());
+ cfg.setSetting(Configuration.C_FORMAT_KEY_SNAKE_CASE, Default230CFormat.NAME);
+ assertSame(Default230CFormat.INSTANCE, cfg.getCFormat());
+ cfg.setSetting(Configuration.C_FORMAT_KEY_CAMEL_CASE, JSONCFormat.NAME);
+ assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());
+
+ cfg.setSetting(Configuration.C_FORMAT_KEY_CAMEL_CASE, "default");
+ cfg.setSetting(Configuration.C_FORMAT_KEY_SNAKE_CASE, Default2321CFormat.NAME);
+
+ for (CFormat standardCFormat : new CFormat[] {
+ Default230CFormat.INSTANCE, Default2321CFormat.INSTANCE,
+ JSONCFormat.INSTANCE, JavaScriptCFormat.INSTANCE, JavaCFormat.INSTANCE,
+ XSCFormat.INSTANCE
+ }) {
+ cfg.setSetting(Configuration.C_FORMAT_KEY, standardCFormat.getName());
+ assertSame(standardCFormat, cfg.getCFormat());
+ }
+
+ // Object Builder value:
+ cfg.setSetting(Configuration.C_FORMAT_KEY, JSONCFormat.class.getName() + "()");
+ assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());
+ }
+
public void testFallbackOnNullLoopVariable() throws TemplateException {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_29);
assertTrue(cfg.getFallbackOnNullLoopVariable());
diff --git a/src/test/java/freemarker/template/utility/StringUtilTest.java b/src/test/java/freemarker/template/utility/StringUtilTest.java
index 557c5dc4..0bb29345 100644
--- a/src/test/java/freemarker/template/utility/StringUtilTest.java
+++ b/src/test/java/freemarker/template/utility/StringUtilTest.java
@@ -34,7 +34,7 @@ import freemarker.core.ParseException;
public class StringUtilTest {
@Test
- public void testV2319() {
+ public void testJavaScriptStringEncV2319() {
assertEquals("\\n\\r\\f\\b\\t\\x00\\x19", StringUtil.javaScriptStringEnc("\n\r\f\b\t\u0000\u0019"));
}
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/number-format.ftl b/src/test/resources/freemarker/test/templatesuite/templates/number-format.ftl
index 81501e54..9d4b61f6 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/number-format.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/number-format.ftl
@@ -34,23 +34,29 @@ ${100000.5}
${100000.5}
<#setting number_format = ",000.##">
${100000.5}
-<@assertEquals expected="1" actual=double?c />
-<@assertEquals expected="1.000000000000001" actual=double2?c />
-<@assertEquals expected=(iciIntValue gte 2003032)?then("1E-16", "0.0000000000000001") actual=double3?c />
-<@assertEquals expected=(iciIntValue gte 2003032)?then("-1E-16", "-0.0000000000000001") actual=double4?c />
-<@assertEquals expected="1" actual=bigDecimal?c />
-<@assertEquals expected=(iciIntValue gte 2003032)?then("1E-16", "0.0000000000000001") actual=bigDecimal2?c />
-<#if iciIntValue gte 2003021>
- <@assertEquals expected="INF" actual="INF"?number?c />
- <@assertEquals expected="INF" actual="INF"?number?c />
- <@assertEquals expected="-INF" actual="-INF"?number?c />
- <@assertEquals expected="-INF" actual="-INF"?number?float?c />
- <@assertEquals expected="NaN" actual="NaN"?number?float?c />
- <@assertEquals expected="NaN" actual="NaN"?number?float?c />
+<#setting number_format = "c">
+<@assertEquals expected="1" actual=double?string />
+<@assertEquals expected="1.000000000000001" actual=double2?string />
+<@assertEquals expected=(iciIntValue gte 2003032)?then("1E-16", "0.0000000000000001") actual=double3?string />
+<@assertEquals expected=(iciIntValue gte 2003032)?then("-1E-16", "-0.0000000000000001") actual=double4?string />
+<@assertEquals expected="1" actual=bigDecimal?string />
+<@assertEquals expected=(iciIntValue gte 2003032)?then("1E-16", "0.0000000000000001") actual=bigDecimal2?string />
+<#if iciIntValue gte 2003032>
+ <@assertEquals expected="Infinity" actual="INF"?number?string />
+ <@assertEquals expected="Infinity" actual="INF"?number?float?string />
+ <@assertEquals expected="-Infinity" actual="-INF"?number?string />
+ <@assertEquals expected="-Infinity" actual="-INF"?number?float?string />
+ <@assertEquals expected="NaN" actual="NaN"?number?string />
+ <@assertEquals expected="NaN" actual="NaN"?number?float?string />
+<#elseif iciIntValue == 2003031>
+ <@assertEquals expected="INF" actual="INF"?number?string />
+ <@assertEquals expected="INF" actual="INF"?number?float?string />
+ <@assertEquals expected="-INF" actual="-INF"?number?string />
+ <@assertEquals expected="-INF" actual="-INF"?number?float?string />
+ <@assertEquals expected="NaN" actual="NaN"?number?string />
+ <@assertEquals expected="NaN" actual="NaN"?number?float?string />
<#else>
- <#setting locale = "en_US">
- <#setting number_format = "0.#">
- <@assertEquals expected="INF"?number?string actual="INF"?number?c />
- <@assertEquals expected="-INF"?number?string actual="-INF"?number?c />
- <@assertEquals expected="NaN"?number?string actual="NaN"?number?c />
+ <@assertEquals expected="\x221E" actual="INF"?number?string />
+ <@assertEquals expected="-\x221E" actual="-INF"?number?string />
+ <@assertEquals expected="\xFFFD" actual="NaN"?number?string />
</#if>
\ No newline at end of file