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:02 UTC

[freemarker] branch 2.3-gae updated (f6f5207a -> 782daab2)

This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a change to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git


    from f6f5207a FREEMARKER-208: Added ?c_lower_case, and ?c_upper_case, which are the non-localized (computer language) variants of ?lower_case, and ?upper_case. The primary problem people run into with the localized versions is that with Turkish locale the letter i, and I has different conversions as in most languages, which causes problem if the conversion was for computer consumption (for technical purposes), and not for humans.
     new f3d9d89e (Javadoc adjustments)
     new 782daab2 ?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.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../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 +-
 .../freemarker/core/CommonMarkupOutputFormat.java  |  10 +
 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 +++
 ...backInstruction.java => JavaScriptCFormat.java} |  56 +-
 .../freemarker/core/JavaTemplateNumberFormat.java  |   2 +-
 .../java/freemarker/core/MarkupOutputFormat.java   |   8 +-
 src/main/java/freemarker/core/PropertySetting.java |  32 +-
 .../freemarker/core/TemplateConfiguration.java     |   9 +-
 .../freemarker/core/TemplateMarkupOutputModel.java |   4 +-
 src/main/java/freemarker/core/XSCFormat.java       |  91 +++
 ...{BugException.java => _StandardCLanguages.java} |  38 +-
 .../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 ++++++++++++++++++---
 ...va => BooleanFormatEnvironmentCachingTest.java} |  42 +-
 .../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 +-
 36 files changed, 2328 insertions(+), 577 deletions(-)
 create mode 100644 src/main/java/freemarker/core/AbstractJSONLikeFormat.java
 create mode 100644 src/main/java/freemarker/core/AbstractLegacyCFormat.java
 create mode 100644 src/main/java/freemarker/core/CFormat.java
 create mode 100644 src/main/java/freemarker/core/Default230CFormat.java
 create mode 100644 src/main/java/freemarker/core/Default2321CFormat.java
 create mode 100644 src/main/java/freemarker/core/JSONCFormat.java
 create mode 100644 src/main/java/freemarker/core/JavaCFormat.java
 copy src/main/java/freemarker/core/{FallbackInstruction.java => JavaScriptCFormat.java} (50%)
 create mode 100644 src/main/java/freemarker/core/XSCFormat.java
 copy src/main/java/freemarker/core/{BugException.java => _StandardCLanguages.java} (50%)
 copy src/test/java/freemarker/core/{NumberBiTest.java => BooleanFormatEnvironmentCachingTest.java} (55%)
 create mode 100644 src/test/java/freemarker/core/CAndCnBuiltInTest.java
 create mode 100644 src/test/java/freemarker/core/CFormatTemplateTest.java
 create mode 100644 src/test/java/freemarker/core/CustomCFormat.java


[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.

Posted by dd...@apache.org.
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 = "&lt;/${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>&gt;</tt> if the method can't know that it won't be directly after <tt>]]</tt> or <tt>--</tt>
      *   <td>JavaScript: <tt>\&gt;</tt>; JSON: <tt>\</tt><tt>u003E</tt>
      * <tr>
-     *   <td><tt>&lt;</tt> if the method can't know that it won't be directly followed by <tt>!</tt> or <tt>?</tt> 
+     *   <td><tt>&lt;</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 &gt; is escaped as
-          \u003E (not as \&gt;), and that
+          <literal>'</literal> is not escaped at all, that
+          <literal>&gt;</literal> is escaped as <literal>\u003E</literal> (not
+          as <literal>\&gt;</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>&lt;full-name&gt;${fullName?nc}&lt;/full-name&gt;</literal>,
+              or <literal>&lt;user <replaceable>...</replaceable>
+              full-name="${fullName?nc}" /&gt;</literal>, and then, in case
+              <literal>fullName</literal> is <literal>null</literal>, the
+              output will be
+              <literal>&lt;full-name&gt;&lt;/full-name&gt;</literal>, and
+              <literal>&lt;user <replaceable>...</replaceable> full-name=""
+              /&gt;</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>&lt;#if
+              fullName??&gt;&lt;full-name&gt;${full-name?c}&lt;/full-name&gt;&lt;/#if&gt;</literal>,
+              and <literal>&lt;user <replaceable>...</replaceable> &lt;#if
+              fullName??&gt;full-name="${fullName?c}"&lt;/#if&gt;
+              /&gt;</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>&lt;#setting <replaceable>name</replaceable>=<replaceable>value</replaceable>&gt;</literal>
-</programlisting>
+          <programlisting role="metaTemplate"><literal>&lt;#setting <replaceable>name</replaceable>=<replaceable>value</replaceable>&gt;</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>&lt;a href="/product/${product.id?c}"&gt;</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


[freemarker] 01/02: (Javadoc adjustments)

Posted by dd...@apache.org.
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 f3d9d89e59c882c12e4c04b5e20524ec127ba43d
Author: ddekany <dd...@apache.org>
AuthorDate: Mon Dec 19 18:45:05 2022 +0100

    (Javadoc adjustments)
---
 src/main/java/freemarker/core/CommonMarkupOutputFormat.java  | 10 ++++++++++
 src/main/java/freemarker/core/MarkupOutputFormat.java        |  8 ++++----
 src/main/java/freemarker/core/TemplateMarkupOutputModel.java |  4 ++--
 3 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/src/main/java/freemarker/core/CommonMarkupOutputFormat.java b/src/main/java/freemarker/core/CommonMarkupOutputFormat.java
index c9200bf8..ff1fe788 100644
--- a/src/main/java/freemarker/core/CommonMarkupOutputFormat.java
+++ b/src/main/java/freemarker/core/CommonMarkupOutputFormat.java
@@ -117,6 +117,16 @@ public abstract class CommonMarkupOutputFormat<MO extends CommonTemplateMarkupOu
 
     /**
      * Creates a new {@link CommonTemplateMarkupOutputModel} that's bound to this {@link OutputFormat} instance.
+     * When this is called from {@link CommonMarkupOutputFormat}, exactly one of the parameters will be
+     * non-{@code null}.
+     *
+     * @param plainTextContent
+     *      {@code null} if the content is markup that possibly can be described as plain text without loss.
+     * @param markupContent
+     *      Typically will be {@code null} if {@code plainTextContent} is not {@code null}. While the plain text always
+     *      can be converted to markup via {@link #escapePlainText(String)}, it's up to the implementation if it wants
+     *      to do that now and store the markup, or later on demand (like each time when
+     *      {@link #getMarkupString(CommonTemplateMarkupOutputModel)} is called).
      */
     protected abstract MO newTemplateMarkupOutputModel(String plainTextContent, String markupContent)
             throws TemplateModelException;
diff --git a/src/main/java/freemarker/core/MarkupOutputFormat.java b/src/main/java/freemarker/core/MarkupOutputFormat.java
index 81ffe8b2..e8b3d969 100644
--- a/src/main/java/freemarker/core/MarkupOutputFormat.java
+++ b/src/main/java/freemarker/core/MarkupOutputFormat.java
@@ -26,8 +26,8 @@ import freemarker.template.TemplateModelException;
 
 /**
  * Superclass of {@link OutputFormat}-s that represent a "markup" format, which is any format where certain character
- * sequences have special meaning and thus may need escaping. (Escaping is important for FreeMarker, as typically it has
- * to insert non-markup text from the data-model into the output markup. See also:
+ * sequences have special meaning, and thus may need escaping. (Escaping is important for FreeMarker, as typically it
+ * has to insert non-markup text from the data-model into the output markup. See also:
  * {@link Configuration#setOutputFormat(OutputFormat)}.)
  * 
  * <p>
@@ -77,7 +77,7 @@ public abstract class MarkupOutputFormat<MO extends TemplateMarkupOutputModel> e
 
     /**
      * Equivalent to calling {@link #fromPlainTextByEscaping(String)} and then
-     * {@link #output(TemplateMarkupOutputModel, Writer)}, but the implementation may uses a more efficient solution.
+     * {@link #output(TemplateMarkupOutputModel, Writer)}, but the implementation may use a more efficient solution.
      */
     public abstract void output(String textToEsc, Writer out) throws IOException, TemplateModelException;
     
@@ -105,7 +105,7 @@ public abstract class MarkupOutputFormat<MO extends TemplateMarkupOutputModel> e
     
     /**
      * Should give the same result as {@link #fromPlainTextByEscaping(String)} and then
-     * {@link #getMarkupString(TemplateMarkupOutputModel)}, but the implementation may uses a more efficient solution.
+     * {@link #getMarkupString(TemplateMarkupOutputModel)}, but the implementation may use a more efficient solution.
      */
     public abstract String escapePlainText(String plainTextContent) throws TemplateModelException;
 
diff --git a/src/main/java/freemarker/core/TemplateMarkupOutputModel.java b/src/main/java/freemarker/core/TemplateMarkupOutputModel.java
index 7043fba2..19a02ad8 100644
--- a/src/main/java/freemarker/core/TemplateMarkupOutputModel.java
+++ b/src/main/java/freemarker/core/TemplateMarkupOutputModel.java
@@ -27,8 +27,8 @@ import freemarker.template.TemplateScalarModel;
  * mechanism. Values of this kind are exempt from {@link OutputFormat}-based automatic escaping.
  * 
  * <p>
- * Each implementation of this type has a {@link OutputFormat} subclass pair, whose singleton instance is returned by
- * {@link #getOutputFormat()}. See more about how markup output values work at {@link OutputFormat}.
+ * Each implementation of this type has a corresponding {@link OutputFormat} subclass, whose singleton instance is
+ * returned by {@link #getOutputFormat()}. See more about how markup output values work at {@link OutputFormat}.
  * 
  * <p>
  * Note that {@link TemplateMarkupOutputModel}-s are by design not treated like {@link TemplateScalarModel}-s, and so