Merge remote-tracking branch 'origin/2.3-gae' into 2.3
diff --git a/.travis.yml b/.travis.yml
index ffe0f62..21bd099 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,12 +15,32 @@
 # specific language governing permissions and limitations
 # under the License.
 
-language: java
+os: linux
+dist: focal
+
+arch:
+  - amd64
+  - arm64
+
+addons:
+  apt:
+    packages:
+      - openjdk-8-jdk
+      - ant-optional
+
+cache:
+  directories:
+    - $HOME/.ivy-freemarker/cache
+
 before_install:
-- sudo apt-get -qq update
-# ant-optional is needed for ant junit
-- sudo apt-get install ant-optional
-install: ant download-ivy
-script: ant ci
-jdk:
-  - openjdk8
\ No newline at end of file
+  - lscpu
+  - export JAVA_HOME="/usr/lib/jvm/java-8-openjdk-${TRAVIS_CPU_ARCH}/"
+  - export PATH="$JAVA_HOME/bin:$PATH"
+  - java -version
+  - ant -version
+
+install:
+  - ant download-ivy  
+
+script: 
+  - ant ci  
\ No newline at end of file
diff --git a/README.md b/README.md
index d8197cb..e69905c 100644
--- a/README.md
+++ b/README.md
@@ -107,7 +107,7 @@
 https://freemarker.apache.org/sourcecode.html
 
 You need JDK 8 (not JDK 9!), Apache Ant (tested with 1.9.6) and Ivy (tested
-with 2.4.0) to be installed. To install Ivy (but be sure it's not already
+with 2.5.0) to be installed. To install Ivy (but be sure it's not already
 installed), issue `ant download-ivy`; it will copy Ivy under `~/.ant/lib`.
 (Alternatively, you can copy `ivy-<version>.jar` into the Ant home `lib`
 subfolder manually.)
@@ -118,10 +118,7 @@
 
 To build `freemarker.jar`, just issue `ant` in the project root directory, and
 it should download all dependencies automatically and build `freemarker.jar`. 
-
-If later you change the dependencies in `ivy.xml`, or otherwise want to
-re-download some of them, it will not happen automatically anymore, and you
-must issue `ant update-deps`.
+(Depencies will be cached into the `.ivy/cache` subdirectory of the project.)
 
 To test your build, issue `ant test`.
 
diff --git a/build.xml b/build.xml
index 4b68750..54fe017 100644
--- a/build.xml
+++ b/build.xml
@@ -113,17 +113,11 @@
     <delete dir="build/coverage/classes" />
   </target>
 
-  <condition property="deps.available">
-    <available file=".ivy" />
-  </condition>
-  
-  <target name="init" depends="_autoget-deps"
-    description="Fetch dependencies if any are missing and create the build directory if necessary"
-  >
+  <target name="init">
     <mkdir dir="build"/>
   </target>
 
-  <property name="ivy.install.version" value="2.4.0" />
+  <property name="ivy.install.version" value="2.5.0" />
   <property name="ivy.home" value="${user.home}/.ant" />
   <property name="ivy.jar.dir" value="${ivy.home}/lib" />
   <property name="ivy.jar.file" value="${ivy.jar.dir}/ivy.jar" />
@@ -579,7 +573,7 @@
     <attribute name="locale" />
     <sequential>
       <ivy:cachepath conf="manual" pathid="ivy.dep" />
-      <taskdef resource="org/freemarker/docgen/antlib.properties"
+      <taskdef resource="org/freemarker/docgen/ant/antlib.properties"
         uri="http://freemarker.org/docgen"
         classpathref="ivy.dep"
       />
@@ -608,9 +602,9 @@
   </target>
   
 
-  <!-- ====================== -->
-  <!-- Distributuion building -->
-  <!-- ====================== -->
+  <!-- ===================== -->
+  <!-- Distribution building -->
+  <!-- ===================== -->
 
   <target name="dist"
     description="Build the FreeMarker distribution files"
@@ -954,105 +948,20 @@
   <!-- ================================================================= -->
   <!-- CI (like Travis).......................                           -->
   <!-- ================================================================= -->
-	
+
+  <target name="ci-setup">
+    <ivy:settings file="ivysettings-ci.xml" />
+  </target>
+
   <target name="ci"
-  	depends="clean, update-deps, jar, test, javadoc"
+  	depends="ci-setup, clean, jar, test, javadoc"
   	description="CI should invoke this task"
   />
-  
+
   <!-- ================================================================== -->
-  <!-- Dependency management (keep it exactly identical for all projects) -->
+  <!-- Dependency management                                              -->
   <!-- ================================================================== -->
   
-  <target name="_autoget-deps" unless="deps.available">
-    <antcall target="_autoget-deps-condition-workaround" />
-  </target>
-
-  <!--
-    Called with antcall to ensure that deps.available will be up-to-date.
-    Without this trick, if ant is called with multiple tasks, the local .ivy
-    directory might ends up corrupted if it was created by the 1st task.
-  -->
-  <target name="_autoget-deps-condition-workaround" unless="deps.available">
-    <antcall target="update-deps" />
-  </target>
-  
-  <target name="update-deps"
-    description="Gets the latest version of the dependencies from the Web"
-  >
-    <echo>Getting dependencies...</echo>
-    <echo>-------------------------------------------------------</echo>
-    <ivy:settings id="remote" url="https://freemarker.apache.org/repos/ivy/ivysettings-remote.xml" />
-    <!-- Build an own repository that will serve us even offline: -->
-    <ivy:retrieve settingsRef="remote" sync="true"
-      ivypattern=".ivy.part/repo/[organisation]/[module]/ivy-[revision].xml"
-      pattern=".ivy.part/repo/[organisation]/[module]/[artifact]-[revision].[ext]"
-    />
-    <echo>-------------------------------------------------------</echo>
-    <echo>*** Successfully acquired dependencies from the Web ***</echo>
-    <echo>Eclipse users: Now right-click on ivy.xml and Resolve! </echo>
-    <echo>-------------------------------------------------------</echo>
-    <!-- Only now that we got all the dependencies will we delete anything. -->
-    <!-- Thus a net or repo outage doesn't left us without the dependencies. -->
-
-    <!-- Save the resolution cache from the soon coming <delete>: -->
-    <move todir=".ivy.part/update-deps-reso-cache">
-      <fileset dir=".ivy/update-deps-reso-cache" />
-    </move>
-    <!-- Drop all the old stuff: -->
-    <delete dir=".ivy" />
-    <!-- And use the new stuff instead: -->
-    <move todir=".ivy">
-      <fileset dir=".ivy.part" />
-    </move>
-  </target>
-
-  <!-- Do NOT call this from 'clean'; offline guys would stuck after that. -->
-  <target name="clean-deps"
-    description="Deletes all dependencies"
-  >
-    <delete dir=".ivy" />
-  </target>
-
-  <target name="publish-override" depends="jar"
-    description="Ivy-publishes THIS project locally as an override"
-  >
-    <ivy:resolve />
-    <ivy:publish
-      pubrevision="${moduleBranch}-branch-head"
-      artifactspattern="build/[artifact].[ext]"
-      overwrite="true" forcedeliver="true"
-      resolver="freemarker-devel-local-override"
-    >
-      <artifact name="freemarker" type="jar" ext="jar" />
-    </ivy:publish>
-    <delete file="build/ivy.xml" />  <!-- ivy:publish makes this -->
-    <echo>-------------------------------------------------------</echo>
-    <echo>*** Don't forget to `ant unpublish-override` later! ***</echo>
-  </target>
-
-  <target name="unpublish-override"
-    description="Undoes publish-override (made in THIS project)"
-  >
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override/${moduleOrg}/${moduleName}" />
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override-cache/${moduleOrg}/${moduleName}" />
-  </target>  
-
-  <target name="unpublish-override-all"
-    description="Undoes publish-override-s made in ALL projects"
-  >
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override" />
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override-cache" />
-  </target>  
-
-  <target name="uninstall"
-    description="Deletes external files created by FreeMarker developement"
-  >
-    <delete dir="${user.home}/.ivy2/freemarker-devel-cache" />
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override" />
-    <delete dir="${user.home}/.ivy2/freemarker-devel-local-override-cache " />
-  </target>
-
   <target name="report-deps"
     description="Creates a HTML document that summarizes the dependencies."
   >
@@ -1087,28 +996,11 @@
       -->the ide-dependencies Ant task, because Eclipse OSGi support expects them to be here.<!--
     --></echo>
   </target>
-  
-  <!--
-    This meant to be called on the Continuous Integration server, so the
-    integration builds appear in the freemarker.org public Ivy repository.
-    The artifacts must be already built.
-  -->
-  <target name="server-publish-last-build"
-    description="(For the Continuous Integration server only)"
-  >
-    <delete dir="build/dummy-server-ivy-repo" />
-    <ivy:resolve />
-    <ivy:publish
-      pubrevision="${moduleBranch}-branch-head"
-      artifactspattern="build/[artifact].[ext]"
-      overwrite="true" forcedeliver="true"
-      resolver="server-publishing-target"
-    >
-      <artifact name="freemarker" type="jar" ext="jar" />
-    </ivy:publish>
-    <delete file="build/ivy.xml" />  <!-- ivy:publish makes this -->
-  </target>
-  
+
+  <!-- ================================================================== -->
+  <!-- Misc.                                                              -->
+  <!-- ================================================================== -->
+
   <target name="rat" description="Generates Apache RAT report">
     <ivy:cachepath conf="rat" pathid="ivy.dep" />
     <taskdef
@@ -1130,24 +1022,5 @@
     -->Rat reports were written into build/rat-report-*.txt<!--
     --></echo>
   </target>
-
-  <target name="archive" depends=""
-    description='Archives project with Git repo into the "archive" directory.'
-  >
-    <mkdir dir="archive" />
-    <tstamp>
-      <format property="tstamp" pattern="yyyyMMdd-HHmm" />
-    </tstamp>
-    <delete file="archive/freemarker-git-${tstamp}.tar" />
-    <delete file="archive/freemarker-git-${tstamp}.tar.bz2" />
-    <tar tarfile="archive/freemarker-git-${tstamp}.tar"
-      basedir="."
-      longfile="gnu"
-      excludes="build/** .build/** .bin/** .ivy/**  archive/**"
-    />
-    <bzip2 src="archive/freemarker-git-${tstamp}.tar"
-        zipfile="archive/freemarker-git-${tstamp}.tar.bz2" />
-    <delete file="archive/freemarker-git-${tstamp}.tar" />
-  </target>
     
 </project>
diff --git a/ivy.xml b/ivy.xml
index 251ec1b..e21534f 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -164,7 +164,7 @@
 
     <!-- docs -->
     
-    <dependency org="org.freemarker" name="docgen" rev="2.0-branch-head" conf="manual->default" changing="true" />
+    <dependency org="org.apache.freemarker.docgen" name="freemarker-docgen-ant" rev="0.0.2-SNAPSHOT" conf="manual->default" changing="true" />
     
     <!-- parser -->
     
diff --git a/ivysettings-ci.xml b/ivysettings-ci.xml
new file mode 100644
index 0000000..3f0dc4c
--- /dev/null
+++ b/ivysettings-ci.xml
@@ -0,0 +1,34 @@
+<!--
+  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.
+-->
+
+<ivysettings>
+  <!-- We want this to be cacheable, so it has to be outside the project checkout directory: -->
+  <caches defaultCacheDir="${user.home}/.ivy-freemarker/cache">
+    <cache name="freemarkerCache" useOrigin="true" />
+  </caches>
+  
+  <settings defaultResolver="default" />
+  <resolvers>
+    <chain name="default">
+      <ibiblio name="mavenCentral" m2compatible="true" />
+      <ibiblio name="mavenApacheStaging" m2compatible="true" root="https://repository.apache.org/content/repositories/staging/" />
+      <ibiblio name="mavenApacheSnapshot" m2compatible="true" root="https://repository.apache.org/content/repositories/snapshots/" />
+    </chain>
+  </resolvers>
+</ivysettings>
diff --git a/ivysettings.xml b/ivysettings.xml
index 3369274..e54516e 100644
--- a/ivysettings.xml
+++ b/ivysettings.xml
@@ -18,37 +18,24 @@
 -->
 
 <ivysettings>
-  <!-- Prevent IvyDE error: -->
-  <property name="server.ivy.repo.root" value="${ivy.project.dir}/NOT_SET" override="false" />
-  
+  <!-- As we use our own resolved, we rather don't use the user level Ivy cache: -->
   <caches defaultCacheDir="${ivy.project.dir}/.ivy/cache">
-    <cache name="cacheForPrivate" useOrigin="true" defaultTTL="1s" />
-    
-    <!--
-      Rather don't use useOrigin="true" here, as deleting from the target repo breaks the cache then.
-    -->
-    <cache name="cacheForLocalOverride"
-      basedir="${user.home}/.ivy2/freemarker-devel-local-override-cache"
-      defaultTTL="1s" lockStrategy="artifact-lock"
-    />
+    <cache name="freemarkerCache" useOrigin="true" />
   </caches>
+  
+  <settings defaultResolver="default" />
+  <property name="localMaveRepoDir" value="${user.home}/.m2/repository/" />
   <resolvers>
-    <chain name="freemarker-devel-local" returnFirst="true">
-      <filesystem name="freemarker-devel-local-override" cache="cacheForLocalOverride">
-        <ivy pattern="${user.home}/.ivy2/freemarker-devel-local-override/[organisation]/[module]/ivy-[revision].xml" />
-        <artifact pattern="${user.home}/.ivy2/freemarker-devel-local-override/[organisation]/[module]/[artifact]-[revision].[ext]" />
+    <chain name="default">
+      <!--
+      <filesystem name="mavenLocal" m2compatible="true">
+        <artifact pattern="${localMaveRepoDir}/[organisation]/[module]/[revision]/[module]-[revision].[ext]" />
+        <ivy pattern="${localMaveRepoDir}/[organisation]/[module]/[revision]/[module]-[revision].pom" />
       </filesystem>
-      <filesystem name="project-private" cache="cacheForPrivate">
-        <ivy pattern="${ivy.project.dir}/.ivy/repo/[organisation]/[module]/ivy-[revision].xml" />
-        <artifact pattern="${ivy.project.dir}/.ivy/repo/[organisation]/[module]/[artifact]-[revision].[ext]" />
-      </filesystem>
+      -->
+      <ibiblio name="mavenCentral" m2compatible="true" />
+      <ibiblio name="mavenApacheStaging" m2compatible="true" root="https://repository.apache.org/content/repositories/staging/" />
+      <ibiblio name="mavenApacheSnapshot" m2compatible="true" root="https://repository.apache.org/content/repositories/snapshots/" />
     </chain>
-    <filesystem name="server-publishing-target">
-      <ivy pattern="${server.ivy.repo.root}/[organisation]/[module]/ivy-[revision].xml" />
-      <artifact pattern="${server.ivy.repo.root}/[organisation]/[module]/[artifact]-[revision].[ext]" />
-    </filesystem>
   </resolvers>
-  <modules>
-    <module organisation="*" resolver="freemarker-devel-local" />
-  </modules>
 </ivysettings>
diff --git a/osgi.bnd b/osgi.bnd
index 08bac25..3cf11c1 100644
--- a/osgi.bnd
+++ b/osgi.bnd
@@ -5,9 +5,9 @@
 # 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
@@ -49,10 +49,10 @@
 # This is needed for "a.class.from.another.Bundle"?new() to work.
 DynamicImport-Package: *
 
-# The required minimum is 1.5, but we utilize versions up to 1.8 if available.
+# The required minimum is 1.7, but we utilize versions up to 1.8 if available.
 # See also: http://wiki.eclipse.org/Execution_Environments, "Compiling
 # against more than is required"
-Bundle-RequiredExecutionEnvironment: JavaSE-1.8, JavaSE-1.7, JavaSE-1.6, J2SE-1.5
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8, JavaSE-1.7
 
 # Non-OSGi meta:
 Main-Class: freemarker.core.CommandLine
@@ -63,3 +63,4 @@
 Implementation-Title: FreeMarker
 Implementation-Version: ${versionForMf}
 Implementation-Vendor: freemarker.org
+Automatic-Module-Name: freemarker
diff --git a/src/main/java/freemarker/core/Assignment.java b/src/main/java/freemarker/core/Assignment.java
index 0495f14..4d74447 100644
--- a/src/main/java/freemarker/core/Assignment.java
+++ b/src/main/java/freemarker/core/Assignment.java
@@ -118,11 +118,11 @@
                 throw new BugException("Unexpected scope type: " + scope);
             }
         } else {
-            TemplateModel namespaceTM = namespaceExp.eval(env);
+            TemplateModel uncheckedNamespace = namespaceExp.eval(env);
             try {
-                namespace = (Environment.Namespace) namespaceTM;
+                namespace = (Environment.Namespace) uncheckedNamespace;
             } catch (ClassCastException e) {
-                throw new NonNamespaceException(namespaceExp, namespaceTM, env);
+                throw new NonNamespaceException(namespaceExp, uncheckedNamespace, env);
             }
             if (namespace == null) {
                 throw InvalidReferenceException.getInstance(namespaceExp, env);
diff --git a/src/main/java/freemarker/core/BlockAssignment.java b/src/main/java/freemarker/core/BlockAssignment.java
index 8c9e403..cf54fd6 100644
--- a/src/main/java/freemarker/core/BlockAssignment.java
+++ b/src/main/java/freemarker/core/BlockAssignment.java
@@ -59,7 +59,17 @@
         }
         
         if (namespaceExp != null) {
-            ((Environment.Namespace) namespaceExp.eval(env)).put(varName, value);
+            final Environment.Namespace namespace;
+            TemplateModel uncheckedNamespace = namespaceExp.eval(env);
+            try {
+                namespace = (Environment.Namespace) uncheckedNamespace;
+            } catch (ClassCastException e) {
+                throw new NonNamespaceException(namespaceExp, uncheckedNamespace, env);
+            }
+            if (namespace == null) {
+                throw InvalidReferenceException.getInstance(namespaceExp, env);
+            }
+            namespace.put(varName, value);
         } else if (scope == Assignment.NAMESPACE) {
             env.setVariable(varName, value);
         } else if (scope == Assignment.GLOBAL) {
diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index 30be937..fcea193 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -65,6 +65,7 @@
 import freemarker.core.BuiltInsForSequences.sortBI;
 import freemarker.core.BuiltInsForSequences.sort_byBI;
 import freemarker.core.BuiltInsForStringsMisc.evalBI;
+import freemarker.core.BuiltInsForStringsMisc.evalJsonBI;
 import freemarker.template.Configuration;
 import freemarker.template.TemplateDateModel;
 import freemarker.template.TemplateModel;
@@ -84,7 +85,7 @@
 
     static final Set<String> CAMEL_CASE_NAMES = new TreeSet<>();
     static final Set<String> SNAKE_CASE_NAMES = new TreeSet<>();
-    static final int NUMBER_OF_BIS = 289;
+    static final int NUMBER_OF_BIS = 291;
     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";
@@ -120,6 +121,7 @@
         putBI("ensure_starts_with", "ensureStartsWith", new BuiltInsForStringsBasic.ensure_starts_withBI());
         putBI("esc", new escBI());
         putBI("eval", new evalBI());
+        putBI("eval_json", "evalJson", new evalJsonBI());
         putBI("exists", new BuiltInsForExistenceHandling.existsBI());
         putBI("filter", new BuiltInsForSequences.filterBI());
         putBI("first", new firstBI());
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index a8b1130..fbd528c 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -57,7 +57,7 @@
 
     static class cBI extends AbstractCBI implements ICIChainMember {
         
-        static class BIBeforeICE2d3d21 extends AbstractCBI {
+        static class BIBeforeICI2d3d21 extends AbstractCBI {
 
             @Override
             protected TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateModelException {
@@ -72,7 +72,7 @@
             
         }
         
-        private final BIBeforeICE2d3d21 prevICIObj = new BIBeforeICE2d3d21();
+        private final BIBeforeICI2d3d21 prevICIObj = new BIBeforeICI2d3d21();
 
         @Override
         TemplateModel _eval(Environment env) throws TemplateException {
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
index 012a007..284abee 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
@@ -113,6 +113,22 @@
         
     }
 
+    static class evalJsonBI extends BuiltInForString {
+        @Override
+        TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+            try {
+                return JSONParser.parse(s);
+            } catch (JSONParser.JSONParseException e) {
+                throw new _MiscTemplateException(this, env,
+                        "Failed to \"?", key, "\" string with this error:\n\n",
+                        _MessageUtil.EMBEDDED_MESSAGE_BEGIN,
+                        new _DelayedGetMessage(e),
+                        _MessageUtil.EMBEDDED_MESSAGE_END,
+                        "\n\nThe failing expression:");
+            }
+        }
+    }
+
     static class numberBI extends BuiltInForString {
         @Override
         TemplateModel calculateResult(String s, Environment env)  throws TemplateException {
@@ -170,5 +186,4 @@
 
     // Can't be instantiated
     private BuiltInsForStringsMisc() { }
-    
 }
diff --git a/src/main/java/freemarker/core/BuiltinVariable.java b/src/main/java/freemarker/core/BuiltinVariable.java
index 5002a03..ccc9d0b 100644
--- a/src/main/java/freemarker/core/BuiltinVariable.java
+++ b/src/main/java/freemarker/core/BuiltinVariable.java
@@ -53,26 +53,28 @@
     static final String DATA_MODEL_CC = "dataModel";
     static final String DATA_MODEL = "data_model";
     static final String LANG = "lang";
-    static final String LOCALE = "locale";
-    static final String LOCALE_OBJECT_CC = "localeObject";
-    static final String LOCALE_OBJECT = "locale_object";
+    static final String LOCALE = Configurable.LOCALE_KEY;
+    static final String LOCALE_OBJECT_CC = Configurable.LOCALE_KEY_CAMEL_CASE + "Object";
+    static final String LOCALE_OBJECT = Configurable.LOCALE_KEY + "_object";
+    static final String TIME_ZONE_CC = Configurable.TIME_ZONE_KEY_CAMEL_CASE;
+    static final String TIME_ZONE = Configurable.TIME_ZONE_KEY;
     static final String CURRENT_NODE_CC = "currentNode";
     static final String CURRENT_NODE = "current_node";
     static final String NODE = "node";
     static final String PASS = "pass";
     static final String VARS = "vars";
     static final String VERSION = "version";
-    static final String INCOMPATIBLE_IMPROVEMENTS_CC = "incompatibleImprovements";
-    static final String INCOMPATIBLE_IMPROVEMENTS = "incompatible_improvements";
+    static final String INCOMPATIBLE_IMPROVEMENTS_CC = Configuration.INCOMPATIBLE_IMPROVEMENTS_KEY_CAMEL_CASE;
+    static final String INCOMPATIBLE_IMPROVEMENTS = Configuration.INCOMPATIBLE_IMPROVEMENTS_KEY;
     static final String ERROR = "error";
-    static final String OUTPUT_ENCODING_CC = "outputEncoding";
-    static final String OUTPUT_ENCODING = "output_encoding";
-    static final String OUTPUT_FORMAT_CC = "outputFormat";
-    static final String OUTPUT_FORMAT = "output_format";
+    static final String OUTPUT_ENCODING_CC = Configurable.OUTPUT_ENCODING_KEY_CAMEL_CASE;
+    static final String OUTPUT_ENCODING = Configurable.OUTPUT_ENCODING_KEY;
+    static final String OUTPUT_FORMAT_CC = Configuration.OUTPUT_FORMAT_KEY_CAMEL_CASE;
+    static final String OUTPUT_FORMAT = Configuration.OUTPUT_FORMAT_KEY;
     static final String AUTO_ESC_CC = "autoEsc";
     static final String AUTO_ESC = "auto_esc";
-    static final String URL_ESCAPING_CHARSET_CC = "urlEscapingCharset";
-    static final String URL_ESCAPING_CHARSET = "url_escaping_charset";
+    static final String URL_ESCAPING_CHARSET_CC = Configurable.URL_ESCAPING_CHARSET_KEY_CAMEL_CASE;
+    static final String URL_ESCAPING_CHARSET = Configurable.URL_ESCAPING_CHARSET_KEY;
     static final String NOW = "now";
     static final String GET_OPTIONAL_TEMPLATE = "get_optional_template";
     static final String GET_OPTIONAL_TEMPLATE_CC = "getOptionalTemplate";
@@ -116,6 +118,8 @@
         PASS,
         TEMPLATE_NAME_CC,
         TEMPLATE_NAME,
+        TIME_ZONE_CC,
+        TIME_ZONE,
         URL_ESCAPING_CHARSET_CC,
         URL_ESCAPING_CHARSET,
         VARS,
@@ -270,6 +274,9 @@
             }
             return args;
         }
+        if (name == TIME_ZONE || name == TIME_ZONE_CC) {
+            return new SimpleScalar(env.getTimeZone().getID());
+        }
 
         throw new _MiscTemplateException(this,
                 "Invalid special variable: ", name);
diff --git a/src/main/java/freemarker/core/DynamicKeyName.java b/src/main/java/freemarker/core/DynamicKeyName.java
index b651f38..ec46af2 100644
--- a/src/main/java/freemarker/core/DynamicKeyName.java
+++ b/src/main/java/freemarker/core/DynamicKeyName.java
@@ -150,7 +150,7 @@
                     "sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC,
                     NUMERICAL_KEY_LHO_EXPECTED_TYPES,
                     (targetModel instanceof TemplateHashModel
-                            ? "You had a numberical value inside the []. Currently that's only supported for "
+                            ? "You had a numerical value inside the []. Currently that's only supported for "
                                     + "sequences (lists) and strings. To get a Map item with a non-string key, "
                                     + "use myMap?api.get(myKey)."
                             : null),
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 13bde79..b6b5b44 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -72,6 +72,7 @@
 import freemarker.template.TemplateSequenceModel;
 import freemarker.template.TemplateTransformModel;
 import freemarker.template.TransformControl;
+import freemarker.template.Version;
 import freemarker.template._TemplateAPI;
 import freemarker.template.utility.DateUtil;
 import freemarker.template.utility.DateUtil.DateToISO8601CalendarFactory;
@@ -104,13 +105,24 @@
 
     // Do not use this object directly; clone it first! DecimalFormat isn't
     // thread-safe.
-    private static final DecimalFormat C_NUMBER_FORMAT = new DecimalFormat(
+    /** "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.setGroupingUsed(false);
-        C_NUMBER_FORMAT.setDecimalSeparatorAlwaysShown(false);
+        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;
@@ -151,6 +163,7 @@
     /** Caches the result of {@link #isSQLDateAndTimeTimeZoneSameAsNormal()}. */
     private Boolean cachedSQLDateAndTimeTimeZoneSameAsNormal;
 
+    @Deprecated
     private NumberFormat cNumberFormat;
 
     /**
@@ -1659,18 +1672,35 @@
     }
 
     /**
-     * Returns the {@link NumberFormat} used for the <tt>c</tt> built-in. This is always US English
-     * <code>"0.################"</code>, without grouping and without superfluous decimal separator.
+     * Returns the {@link NumberFormat} used for the <tt>c</tt> built-in, except, if
+     * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is less than 2.3.31,
+     * this will wrongly give the format that the <tt>c</tt> built-in used before Incompatible Improvements 2.3.21.
+     * See more at {@link Configuration#Configuration(Version)}.
      */
     public NumberFormat getCNumberFormat() {
-        // It can't be cached in a static field, because DecimalFormat-s aren't
-        // thread-safe.
+        // Note: DecimalFormat-s aren't thread-safe, so you must clone the static field value.
         if (cNumberFormat == null) {
-            cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT.clone();
+            if (configuration.getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_3_31) {
+                cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT_ICI_2_3_21.clone();
+            } else {
+                cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT_ICI_2_3_20.clone();
+            }
         }
         return cNumberFormat;
     }
 
+    /**
+     * As we have a number format cache that's shared between {@link Configuration}-s, if the interpretation of a format
+     * is impacted by Incompatible Improvements, we must change the cache key.
+     */
+    String transformNumberFormatGlobalCacheKey(String keyPart) {
+        if (configuration.getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_3_31
+                && JavaTemplateNumberFormatFactory.COMPUTER.equals(keyPart)) {
+            return "computer\u00002";
+        }
+        return keyPart;
+    }
+
     @Override
     public void setTimeFormat(String timeFormat) {
         String prevTimeFormat = getTimeFormat();
diff --git a/src/main/java/freemarker/core/JSONParser.java b/src/main/java/freemarker/core/JSONParser.java
new file mode 100644
index 0000000..ddb01c0
--- /dev/null
+++ b/src/main/java/freemarker/core/JSONParser.java
@@ -0,0 +1,622 @@
+/*
+ * 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.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import freemarker.template.SimpleHash;
+import freemarker.template.SimpleNumber;
+import freemarker.template.SimpleScalar;
+import freemarker.template.SimpleSequence;
+import freemarker.template.Template;
+import freemarker.template.TemplateBooleanModel;
+import freemarker.template.TemplateHashModelEx2;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
+import freemarker.template.TemplateScalarModel;
+import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
+import freemarker.template.utility.Constants;
+import freemarker.template.utility.NumberUtil;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * JSON parser that returns a {@link TameplatModel}, similar to what FTL literals product (and so, what
+ * @code ?eval} would return). A notable difference compared to the result FTL literals is that this doesn't use the
+ * {@link ParserConfiguration#getArithmeticEngine()} to parse numbers, as JSON has its own fixed number syntax. For
+ * numbers this parser returns {@link SimpleNumberModel}-s, where the wrapped numbers will be {@link Integer}-s when
+ * they fit into that, otherwise they will be {@link Long}-s if they fit into that, otherwise they will be
+ * {@link BigDecimal}-s. Another difference to the result of FTL literals is that instead of
+ * {@code HashLiteral.SequenceHash} it uses {@link SimpleHash} with {@link LinkedHashMap} as backing store, for
+ * efficiency.
+ *
+ * <p>This parser allows certain things that are errors in pure JSON:
+ * <ul>
+ *     <li>JavaScript comments are supported</li>
+ *     <li>Non-breaking space (nbsp) and BOM are treated as whitespace</li>
+ * </ul>
+ */
+class JSONParser {
+
+    private static final String UNCLOSED_OBJECT_MESSAGE
+            = "This {...} was still unclosed when the end of the file was reached. (Look for a missing \"}\")";
+
+    private static final String UNCLOSED_ARRAY_MESSAGE
+            = "This [...] was still unclosed when the end of the file was reached. (Look for a missing \"]\")";
+
+    private static final BigDecimal MIN_INT_AS_BIGDECIMAL = BigDecimal.valueOf(Integer.MIN_VALUE);
+    private static final BigDecimal MAX_INT_AS_BIGDECIMAL = BigDecimal.valueOf(Integer.MAX_VALUE);
+    private static final BigDecimal MIN_LONG_AS_BIGDECIMAL = BigDecimal.valueOf(Long.MIN_VALUE);
+    private static final BigDecimal MAX_LONG_AS_BIGDECIMAL = BigDecimal.valueOf(Long.MAX_VALUE);
+
+    private final String src;
+    private final int ln;
+
+    private int p;
+
+    public static TemplateModel parse(String src) throws JSONParseException {
+        return new JSONParser(src).parse();
+    }
+
+    /**
+     * @param sourceLocation Only used in error messages, maybe {@code null}.
+     */
+    private JSONParser(String src) {
+        this.src = src;
+        this.ln = src.length();
+    }
+
+    private TemplateModel parse() throws JSONParseException {
+        skipWS();
+        TemplateModel result = consumeValue("Empty JSON (contains no value)", p);
+
+        skipWS();
+        if (p != ln) {
+            throw newParseException("End-of-file was expected but found further non-whitespace characters.");
+        }
+
+        return result;
+    }
+
+    private TemplateModel consumeValue(String eofErrorMessage, int eofBlamePosition) throws JSONParseException {
+        if (p == ln) {
+            throw newParseException(
+                    eofErrorMessage == null
+                            ? "A value was expected here, but end-of-file was reached." : eofErrorMessage,
+                    eofBlamePosition == -1 ? p : eofBlamePosition);
+        }
+
+        TemplateModel result;
+
+        result = tryConsumeString();
+        if (result != null) return result;
+
+        result = tryConsumeNumber();
+        if (result != null) return result;
+
+        result = tryConsumeObject();
+        if (result != null) return result;
+
+        result = tryConsumeArray();
+        if (result != null) return result;
+
+        result = tryConsumeTrueFalseNull();
+        if (result != null) return result != TemplateNullModel.INSTANCE ? result : null;
+
+        // Better error message for a frequent mistake:
+        if (p < ln && src.charAt(p) == '\'') {
+            throw newParseException("Unexpected apostrophe-quote character. "
+                    + "JSON strings must be quoted with quotation mark.");
+        }
+
+        throw newParseException(
+                "Expected either the beginning of a (negative) number or the beginning of one of these: "
+                        + "{...}, [...], \"...\", true, false, null. Found character " + StringUtil.jQuote(src.charAt(p))
+                        + " instead.");
+    }
+
+    private TemplateModel tryConsumeTrueFalseNull() throws JSONParseException {
+        int startP = p;
+        if (p < ln && isIdentifierStart(src.charAt(p))) {
+            p++;
+            while (p < ln && isIdentifierPart(src.charAt(p))) {
+                p++;
+            }
+        }
+
+        if (startP == p) return null;
+
+        String keyword = src.substring(startP, p);
+        if (keyword.equals("true")) {
+            return TemplateBooleanModel.TRUE;
+        } else if (keyword.equals("false")) {
+            return TemplateBooleanModel.FALSE;
+        } else if (keyword.equals("null")) {
+            return TemplateNullModel.INSTANCE;
+        }
+
+        throw newParseException(
+                "Invalid JSON keyword: " + StringUtil.jQuote(keyword)
+                        + ". Should be one of: true, false, null. "
+                        + "If it meant to be a string then it must be quoted.", startP);
+    }
+
+    private TemplateNumberModel tryConsumeNumber() throws JSONParseException {
+        if (p >= ln) {
+            return null;
+        }
+        char c = src.charAt(p);
+        boolean negative = c == '-';
+        if (!(negative || isDigit(c) || c == '.')) {
+            return null;
+        }
+
+        int startP = p;
+
+        if (negative) {
+            if (p + 1 >= ln) {
+                throw newParseException("Expected a digit after \"-\", but reached end-of-file.");
+            }
+            char lookAheadC = src.charAt(p + 1);
+            if (!(isDigit(lookAheadC) || lookAheadC == '.')) {
+                return null;
+            }
+            p++; // Consume "-" only, not the digit
+        }
+
+        long longSum = 0;
+        boolean firstDigit = true;
+        consumeLongFittingHead: do {
+            c = src.charAt(p);
+
+            if (!isDigit(c)) {
+                if (c == '.' && firstDigit) {
+                    throw newParseException("JSON doesn't allow numbers starting with \".\".");
+                }
+                break consumeLongFittingHead;
+            }
+
+            int digit = c - '0';
+            if (longSum == 0) {
+                if (!firstDigit) {
+                    throw newParseException("JSON doesn't allow superfluous leading 0-s.", p - 1);
+                }
+
+                longSum = !negative ? digit : -digit;
+                p++;
+            } else {
+                long prevLongSum = longSum;
+                longSum = longSum * 10 + (!negative ? digit : -digit);
+                if (!negative && prevLongSum > longSum || negative && prevLongSum < longSum) {
+                    // We had an overflow => Can't consume this digit as long-fitting
+                    break consumeLongFittingHead;
+                }
+                p++;
+            }
+            firstDigit = false;
+        } while (p < ln);
+
+        if (p < ln && isBigDecimalFittingTailCharacter(c)) {
+            char lastC = c;
+            p++;
+
+            consumeBigDecimalFittingTail: while (p < ln) {
+                c = src.charAt(p);
+                if (isBigDecimalFittingTailCharacter(c)) {
+                    p++;
+                } else if ((c == '+' || c == '-') && isE(lastC)) {
+                    p++;
+                } else {
+                    break consumeBigDecimalFittingTail;
+                }
+                lastC = c;
+            }
+
+            String numStr = src.substring(startP, p);
+            BigDecimal bd;
+            try {
+                bd = new BigDecimal(numStr);
+            } catch (NumberFormatException e) {
+                throw new JSONParseException("Malformed number: " + numStr, src, startP, e);
+            }
+
+            if (bd.compareTo(MIN_INT_AS_BIGDECIMAL) >= 0 && bd.compareTo(MAX_INT_AS_BIGDECIMAL) <= 0) {
+                if (NumberUtil.isIntegerBigDecimal(bd)) {
+                    return new SimpleNumber(bd.intValue());
+                }
+            } else if (bd.compareTo(MIN_LONG_AS_BIGDECIMAL) >= 0 && bd.compareTo(MAX_LONG_AS_BIGDECIMAL) <= 0) {
+                if (NumberUtil.isIntegerBigDecimal(bd)) {
+                    return new SimpleNumber(bd.longValue());
+                }
+            }
+            return new SimpleNumber(bd);
+        } else {
+            return new SimpleNumber(
+                    longSum <= Integer.MAX_VALUE && longSum >= Integer.MIN_VALUE
+                            ? (Number) (int) longSum
+                            : longSum);
+        }
+    }
+
+    private TemplateScalarModel tryConsumeString() throws JSONParseException {
+        int startP = p;
+        if (!tryConsumeChar('"')) return null;
+
+        StringBuilder sb = new StringBuilder();
+        char c = 0;
+        while (p < ln) {
+            c = src.charAt(p);
+
+            if (c == '"') {
+                p++;
+                return new SimpleScalar(sb.toString());  // Call normally returns here!
+            } else if (c == '\\') {
+                p++;
+                sb.append(consumeAfterBackslash());
+            } else if (c <= 0x1F) {
+                throw newParseException("JSON doesn't allow unescaped control characters in string literals, "
+                        + "but found character with code (decimal): " + (int) c);
+            } else {
+                p++;
+                sb.append(c);
+            }
+        }
+
+        throw newParseException("String literal was still unclosed when the end of the file was reached. "
+                + "(Look for missing or accidentally escaped closing quotation mark.)", startP);
+    }
+
+    private TemplateSequenceModel tryConsumeArray() throws JSONParseException {
+        int startP = p;
+        if (!tryConsumeChar('[')) return null;
+
+        skipWS();
+        if (tryConsumeChar(']')) return Constants.EMPTY_SEQUENCE;
+
+        boolean afterComma = false;
+        SimpleSequence elements = new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER);
+        do {
+            skipWS();
+            elements.add(consumeValue(afterComma ? null : UNCLOSED_ARRAY_MESSAGE, afterComma ? -1 : startP));
+
+            skipWS();
+            afterComma = true;
+        } while (consumeChar(',', ']', UNCLOSED_ARRAY_MESSAGE, startP) == ',');
+        return elements;
+    }
+
+    private TemplateHashModelEx2 tryConsumeObject() throws JSONParseException {
+        int startP = p;
+        if (!tryConsumeChar('{')) return null;
+
+        skipWS();
+        if (tryConsumeChar('}')) return Constants.EMPTY_HASH_EX2;
+
+        boolean afterComma = false;
+        Map<String, Object> map = new LinkedHashMap<>();  // Must keeps original order!
+        do {
+            skipWS();
+            int keyStartP = p;
+            Object key = consumeValue(afterComma ? null : UNCLOSED_OBJECT_MESSAGE, afterComma ? -1 : startP);
+            if (!(key instanceof TemplateScalarModel)) {
+                throw newParseException("Wrong key type. JSON only allows string keys inside {...}.", keyStartP);
+            }
+            String strKey = null;
+            try {
+                strKey = ((TemplateScalarModel) key).getAsString();
+            } catch (TemplateModelException e) {
+                throw new BugException(e);
+            }
+
+            skipWS();
+            consumeChar(':');
+
+            skipWS();
+            map.put(strKey, consumeValue(null, -1));
+
+            skipWS();
+            afterComma = true;
+        } while (consumeChar(',', '}', UNCLOSED_OBJECT_MESSAGE, startP) == ',');
+        return new SimpleHash(map, _TemplateAPI.SAFE_OBJECT_WRAPPER, 0);
+    }
+
+    private boolean isE(char c) {
+        return c == 'e' || c == 'E';
+    }
+
+    private boolean isBigDecimalFittingTailCharacter(char c) {
+        return c == '.' || isE(c) || isDigit(c);
+    }
+
+    private char consumeAfterBackslash() throws JSONParseException {
+        if (p == ln) {
+            throw newParseException("Reached the end of the file, but the escape is unclosed.");
+        }
+
+        final char c = src.charAt(p);
+        switch (c) {
+            case '"':
+            case '\\':
+            case '/':
+                p++;
+                return c;
+            case 'b':
+                p++;
+                return '\b';
+            case 'f':
+                p++;
+                return '\f';
+            case 'n':
+                p++;
+                return '\n';
+            case 'r':
+                p++;
+                return '\r';
+            case 't':
+                p++;
+                return '\t';
+            case 'u':
+                p++;
+                return consumeAfterBackslashU();
+        }
+        throw newParseException("Unsupported escape: \\" + c);
+    }
+
+    private char consumeAfterBackslashU() throws JSONParseException {
+        if (p + 3 >= ln) {
+            throw newParseException("\\u must be followed by exactly 4 hexadecimal digits");
+        }
+        final String hex = src.substring(p, p + 4);
+        try {
+            char r = (char) Integer.parseInt(hex, 16);
+            p += 4;
+            return r;
+        } catch (NumberFormatException e) {
+            throw newParseException("\\u must be followed by exactly 4 hexadecimal digits, but was followed by "
+                    + StringUtil.jQuote(hex) + ".");
+        }
+    }
+
+    private boolean tryConsumeChar(char c) {
+        if (p < ln && src.charAt(p) == c) {
+            p++;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private void consumeChar(char expected) throws JSONParseException {
+        consumeChar(expected, (char) 0, null, -1);
+    }
+
+    private char consumeChar(char expected1, char expected2, String eofErrorHint, int eofErrorP) throws JSONParseException {
+        if (p >= ln) {
+            throw newParseException(eofErrorHint == null
+                            ? "Expected " + StringUtil.jQuote(expected1)
+                            + ( expected2 != 0 ? " or " + StringUtil.jQuote(expected2) : "")
+                            + " character, but reached end-of-file. "
+                            : eofErrorHint,
+                    eofErrorP == -1 ? p : eofErrorP);
+        }
+        char c = src.charAt(p);
+        if (c == expected1 || (expected2 != 0 && c == expected2)) {
+            p++;
+            return c;
+        }
+        throw newParseException("Expected " + StringUtil.jQuote(expected1)
+                + ( expected2 != 0 ? " or " + StringUtil.jQuote(expected2) : "")
+                + " character, but found " + StringUtil.jQuote(c) + " instead.");
+    }
+
+    private void skipWS() throws JSONParseException {
+        do {
+            while (p < ln && isWS(src.charAt(p))) {
+                p++;
+            }
+        } while (skipComment());
+    }
+
+    private boolean skipComment() throws JSONParseException {
+        if (p + 1 < ln) {
+            if (src.charAt(p) == '/') {
+                char c2 = src.charAt(p + 1);
+                if (c2 == '/') {
+                    int eolP = p + 2;
+                    while (eolP < ln && !isLineBreak(src.charAt(eolP))) {
+                        eolP++;
+                    }
+                    p = eolP;
+                    return true;
+                } else if (c2 == '*') {
+                    int closerP = p + 3;
+                    while (closerP < ln && !(src.charAt(closerP - 1) == '*' && src.charAt(closerP) == '/')) {
+                        closerP++;
+                    }
+                    if (closerP >= ln) {
+                        throw newParseException("Unclosed comment");
+                    }
+                    p = closerP + 1;
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Whitespace as specified by JSON, plus non-breaking space (nbsp), and BOM.
+     */
+    private static boolean isWS(char c) {
+        return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == 0xA0 || c == '\uFEFF';
+    }
+
+    private static boolean isLineBreak(char c) {
+        return c == '\r' || c == '\n';
+    }
+
+    private static boolean isIdentifierStart(char c) {
+        return Character.isLetter(c) || c == '_' || c == '$';
+    }
+
+    private static boolean isDigit(char c) {
+        return c >= '0' && c <= '9';
+    }
+
+    private static boolean isIdentifierPart(char c) {
+        return isIdentifierStart(c) || isDigit(c);
+    }
+
+    private JSONParseException newParseException(String message) {
+        return newParseException(message, p);
+    }
+
+    private JSONParseException newParseException(String message, int p) {
+        return new JSONParseException(message, src, p);
+    }
+
+    static class JSONParseException extends Exception {
+        public JSONParseException(String message, String src, int position) {
+            super(createSourceCodeErrorMessage(message, src, position));
+        }
+
+        public JSONParseException(String message, String src, int position,
+                Throwable cause) {
+            super(createSourceCodeErrorMessage(message, src, position), cause);
+        }
+
+    }
+
+    private static int MAX_QUOTATION_LENGTH = 50;
+
+    private static String createSourceCodeErrorMessage(String message, String srcCode, int position) {
+        int ln = srcCode.length();
+        if (position < 0) {
+            position = 0;
+        }
+        if (position >= ln) {
+            return message + "\n"
+                    + "Error location: At the end of text.";
+        }
+
+        int i;
+        char c;
+        int rowBegin = 0;
+        int rowEnd;
+        int row = 1;
+        char lastChar = 0;
+        for (i = 0; i <= position; i++) {
+            c = srcCode.charAt(i);
+            if (lastChar == 0xA) {
+                rowBegin = i;
+                row++;
+            } else if (lastChar == 0xD && c != 0xA) {
+                rowBegin = i;
+                row++;
+            }
+            lastChar = c;
+        }
+        for (i = position; i < ln; i++) {
+            c = srcCode.charAt(i);
+            if (c == 0xA || c == 0xD) {
+                if (c == 0xA && i > 0 && srcCode.charAt(i - 1) == 0xD) {
+                    i--;
+                }
+                break;
+            }
+        }
+        rowEnd = i - 1;
+        if (position > rowEnd + 1) {
+            position = rowEnd + 1;
+        }
+        int col = position - rowBegin + 1;
+        if (rowBegin > rowEnd) {
+            return message + "\n"
+                    + "Error location: line "
+                    + row + ", column " + col + ":\n"
+                    + "(Can't show the line because it is empty.)";
+        }
+        String s1 = srcCode.substring(rowBegin, position);
+        String s2 = srcCode.substring(position, rowEnd + 1);
+        s1 = expandTabs(s1, 8);
+        int ln1 = s1.length();
+        s2 = expandTabs(s2, 8, ln1);
+        int ln2 = s2.length();
+        if (ln1 + ln2 > MAX_QUOTATION_LENGTH) {
+            int newLn2 = ln2 - ((ln1 + ln2) - MAX_QUOTATION_LENGTH);
+            if (newLn2 < 6) {
+                newLn2 = 6;
+            }
+            if (newLn2 < ln2) {
+                s2 = s2.substring(0, newLn2 - 3) + "...";
+                ln2 = newLn2;
+            }
+            if (ln1 + ln2 > MAX_QUOTATION_LENGTH) {
+                s1 = "..." + s1.substring((ln1 + ln2) - MAX_QUOTATION_LENGTH + 3);
+            }
+        }
+        StringBuilder res = new StringBuilder(message.length() + 80);
+        res.append(message);
+        res.append("\nError location: line ").append(row).append(", column ").append(col).append(":\n");
+        res.append(s1).append(s2).append("\n");
+        int x = s1.length();
+        while (x != 0) {
+            res.append(' ');
+            x--;
+        }
+        res.append('^');
+
+        return res.toString();
+    }
+
+    private static String expandTabs(String s, int tabWidth) {
+        return expandTabs(s, tabWidth, 0);
+    }
+
+    /**
+     * Replaces all tab-s with spaces in a single line.
+     */
+    private static String expandTabs(String s, int tabWidth, int startCol) {
+        int e = s.indexOf('\t');
+        if (e == -1) {
+            return s;
+        }
+        int b = 0;
+        StringBuilder buf = new StringBuilder(s.length() + Math.max(16, tabWidth * 2));
+        do {
+            buf.append(s, b, e);
+            int col = buf.length() + startCol;
+            for (int i = tabWidth * (1 + col / tabWidth) - col; i > 0; i--) {
+                buf.append(' ');
+            }
+            b = e + 1;
+            e = s.indexOf('\t', b);
+        } while (e != -1);
+        buf.append(s, b, s.length());
+        return buf.toString();
+    }
+
+}
diff --git a/src/main/java/freemarker/core/JavaTemplateNumberFormatFactory.java b/src/main/java/freemarker/core/JavaTemplateNumberFormatFactory.java
index 42262ed..8e8a327 100644
--- a/src/main/java/freemarker/core/JavaTemplateNumberFormatFactory.java
+++ b/src/main/java/freemarker/core/JavaTemplateNumberFormatFactory.java
@@ -31,13 +31,15 @@
 class JavaTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
     
     static final JavaTemplateNumberFormatFactory INSTANCE = new JavaTemplateNumberFormatFactory();
-    
+
+    static final String COMPUTER = "computer";
+
     private static final Logger LOG = Logger.getLogger("freemarker.runtime");
 
     private static final ConcurrentHashMap<CacheKey, NumberFormat> GLOBAL_FORMAT_CACHE
             = new ConcurrentHashMap<>();
     private static final int LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE = 1024;
-    
+
     private JavaTemplateNumberFormatFactory() {
         // Not meant to be instantiated
     }
@@ -45,7 +47,9 @@
     @Override
     public TemplateNumberFormat get(String params, Locale locale, Environment env)
             throws InvalidFormatParametersException {
-        CacheKey cacheKey = new CacheKey(params, locale);
+        CacheKey cacheKey = new CacheKey(
+                env != null ? env.transformNumberFormatGlobalCacheKey(params) : params,
+                locale);
         NumberFormat jFormat = GLOBAL_FORMAT_CACHE.get(cacheKey);
         if (jFormat == null) {
             if ("number".equals(params)) {
@@ -54,7 +58,7 @@
                 jFormat = NumberFormat.getCurrencyInstance(locale);
             } else if ("percent".equals(params)) {
                 jFormat = NumberFormat.getPercentInstance(locale);
-            } else if ("computer".equals(params)) {
+            } else if (COMPUTER.equals(params)) {
                 jFormat = env.getCNumberFormat();
             } else {
                 try {
diff --git a/src/main/java/freemarker/core/ParseException.java b/src/main/java/freemarker/core/ParseException.java
index 4307508..6ab3ed6 100644
--- a/src/main/java/freemarker/core/ParseException.java
+++ b/src/main/java/freemarker/core/ParseException.java
@@ -20,8 +20,9 @@
 package freemarker.core;
 
 import java.io.IOException;
-import java.util.HashSet;
-import java.util.Iterator;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.Set;
 
 import freemarker.template.Template;
@@ -44,6 +45,9 @@
  */
 public class ParseException extends IOException implements FMParserConstants {
 
+    private static final String END_TAG_SYNTAX_HINT
+            = "(Note that FreeMarker end-tags must have # or @ after the / character.)";
+
     /**
      * This is the last token that has been consumed successfully.  If
      * this object has been created due to a parse error, the token
@@ -371,138 +375,214 @@
             if (description != null) return description;  // When we already have it from the constructor
         }
 
-        String tokenErrDesc;
-        if (currentToken != null) {
-            tokenErrDesc = getCustomTokenErrorDescription();
-            if (tokenErrDesc == null) {
-                // The default JavaCC message generation stuff follows.
-                StringBuilder expected = new StringBuilder();
-                int maxSize = 0;
-                for (int i = 0; i < expectedTokenSequences.length; i++) {
-                    if (i != 0) {
-                        expected.append(eol);
-                    }
-                    expected.append("    ");
-                    if (maxSize < expectedTokenSequences[i].length) {
-                        maxSize = expectedTokenSequences[i].length;
-                    }
-                    for (int j = 0; j < expectedTokenSequences[i].length; j++) {
-                        if (j != 0) expected.append(' ');
-                        expected.append(tokenImage[expectedTokenSequences[i][j]]);
-                    }
-                }
-                tokenErrDesc = "Encountered \"";
-                Token tok = currentToken.next;
-                for (int i = 0; i < maxSize; i++) {
-                    if (i != 0) tokenErrDesc += " ";
-                    if (tok.kind == 0) {
-                        tokenErrDesc += tokenImage[0];
-                        break;
-                    }
-                    tokenErrDesc += add_escapes(tok.image);
-                    tok = tok.next;
-                }
-                tokenErrDesc += "\", but ";
+        if (currentToken == null) {
+            return null;
+        }
 
-                if (expectedTokenSequences.length == 1) {
-                    tokenErrDesc += "was expecting:" + eol;
-                } else {
-                    tokenErrDesc += "was expecting one of:" + eol;
+        Token unexpectedTok = currentToken.next;
+
+        if (unexpectedTok.kind == EOF) {
+            Set<String> endTokenDescs = getExpectedEndTokenDescs();
+            return "Unexpected end of file reached."
+                    + (endTokenDescs.size() == 0
+                            ? ""
+                            : " You have an unclosed " + joinWithAnds(endTokenDescs)
+                                    + ". Check if the FreeMarker end-tags are present, and aren't malformed. "
+                                    + END_TAG_SYNTAX_HINT);
+        }
+
+        int maxExpectedTokenSequenceLength = 0;
+        for (int i = 0; i < expectedTokenSequences.length; i++) {
+            int[] expectedTokenSequence = expectedTokenSequences[i];
+            if (maxExpectedTokenSequenceLength < expectedTokenSequence.length) {
+                maxExpectedTokenSequenceLength = expectedTokenSequence.length;
+            }
+        }
+
+        StringBuilder tokenErrDesc = new StringBuilder();
+        tokenErrDesc.append("Encountered ");
+        boolean encounteredEndTag = false;
+        for (int i = 0; i < maxExpectedTokenSequenceLength; i++) {
+            if (i != 0) {
+                tokenErrDesc.append(" ");
+            }
+            if (unexpectedTok.kind == 0) {
+                tokenErrDesc.append(tokenImage[0]);
+                break;
+            }
+
+            String image = unexpectedTok.image;
+            if (i == 0) {
+                if (image.startsWith("</") || image.startsWith("[/")) {
+                    encounteredEndTag = true;
                 }
-                tokenErrDesc += expected;
+            }
+            tokenErrDesc.append(StringUtil.jQuote(image));
+            unexpectedTok = unexpectedTok.next;
+        }
+        Set<String> expectedEndTokenDescs;
+        int unexpTokKind = currentToken.next.kind;
+        if (getIsEndToken(unexpTokKind) || unexpTokKind == ELSE || unexpTokKind == ELSE_IF) {
+            expectedEndTokenDescs = new LinkedHashSet<>(getExpectedEndTokenDescs());
+            if (unexpTokKind == ELSE || unexpTokKind == ELSE_IF) {
+                // If <\#if> was expected, yet #else or #elseif wasn't, then this isn't nesting related problem.
+                expectedEndTokenDescs.remove(getEndTokenDescIfIsEndToken(END_IF));
+            } else {
+                expectedEndTokenDescs.remove(getEndTokenDescIfIsEndToken(unexpTokKind));
             }
         } else {
-            tokenErrDesc = null;
+            expectedEndTokenDescs = Collections.emptySet();
         }
-        return tokenErrDesc;
+        // Generate more helpful error message if this was a nesting related problem:
+        if (!expectedEndTokenDescs.isEmpty()) {
+            if (unexpTokKind == ELSE || unexpTokKind == ELSE_IF) {
+                tokenErrDesc.append(", which can only be used where an #if");
+                if (unexpTokKind == ELSE) {
+                    tokenErrDesc.append(" or #list");
+                }
+                tokenErrDesc.append(" could be closed");
+            }
+            tokenErrDesc.append(", but at this place only ");
+            tokenErrDesc.append(expectedEndTokenDescs.size() > 1 ? "these" : "this");
+            tokenErrDesc.append(" can be closed: ");
+            boolean first = true;
+            for (String expectedEndTokenDesc : expectedEndTokenDescs) {
+                if (!first) {
+                    tokenErrDesc.append(", ");
+                } else {
+                    first = false;
+                }
+                tokenErrDesc.append(
+                        !expectedEndTokenDesc.startsWith("\"")
+                                ? StringUtil.jQuote(expectedEndTokenDesc)
+                                : expectedEndTokenDesc);
+            }
+            tokenErrDesc.append(".");
+            if (encounteredEndTag) {
+                tokenErrDesc.append(" This usually because of wrong nesting of FreeMarker directives, like a "
+                        + "missed or malformed end-tag somewhere. " + END_TAG_SYNTAX_HINT);
+            }
+            tokenErrDesc.append(eol);
+            tokenErrDesc.append("Was ");
+        } else {
+            tokenErrDesc.append(", but was ");
+        }
+
+        if (expectedTokenSequences.length == 1) {
+            tokenErrDesc.append("expecting pattern:");
+        } else {
+            tokenErrDesc.append("expecting one of these patterns:");
+        }
+        tokenErrDesc.append(eol);
+
+        for (int i = 0; i < expectedTokenSequences.length; i++) {
+            if (i != 0) {
+                tokenErrDesc.append(eol);
+            }
+            tokenErrDesc.append("    ");
+            int[] expectedTokenSequence = expectedTokenSequences[i];
+            for (int j = 0; j < expectedTokenSequence.length; j++) {
+                if (j != 0) {
+                    tokenErrDesc.append(' ');
+                }
+                tokenErrDesc.append(tokenImage[expectedTokenSequence[j]]);
+            }
+        }
+
+        return tokenErrDesc.toString();
     }
 
-    private String getCustomTokenErrorDescription() {
-        final Token nextToken = currentToken.next;
-        final int kind = nextToken.kind;
-        if (kind == EOF) {
-            Set/*<String>*/ endNames = new HashSet();
-            for (int i = 0; i < expectedTokenSequences.length; i++) {
-                int[] sequence = expectedTokenSequences[i];
-                for (int j = 0; j < sequence.length; j++) {
-                    switch (sequence[j]) {
-                    case END_FOREACH:
-                        endNames.add( "#foreach");
-                        break;
-                    case END_LIST:
-                        endNames.add( "#list");
-                        break;
-                    case END_SWITCH:
-                        endNames.add( "#switch");
-                        break;
-                    case END_IF:
-                        endNames.add( "#if");
-                        break;
-                    case END_COMPRESS:
-                        endNames.add( "#compress");
-                        break;
-                    case END_MACRO:
-                        endNames.add( "#macro");
-                    case END_FUNCTION:
-                        endNames.add( "#function");
-                        break;
-                    case END_TRANSFORM:
-                        endNames.add( "#transform");
-                        break;
-                    case END_ESCAPE:
-                        endNames.add( "#escape");
-                        break;
-                    case END_NOESCAPE:
-                        endNames.add( "#noescape");
-                        break;
-                    case END_ASSIGN:
-                        endNames.add( "#assign");
-                        break;
-                    case END_LOCAL:
-                        endNames.add( "#local");
-                        break;
-                    case END_GLOBAL:
-                        endNames.add( "#global");
-                        break;
-                    case END_ATTEMPT:
-                        endNames.add( "#attempt");
-                        break;
-                    case CLOSING_CURLY_BRACKET:
-                        endNames.add( "\"{\"");
-                        break;
-                    case CLOSE_BRACKET:
-                        endNames.add( "\"[\"");
-                        break;
-                    case CLOSE_PAREN:
-                        endNames.add( "\"(\"");
-                        break;
-                    case UNIFIED_CALL_END:
-                        endNames.add( "@...");
-                        break;
-                    }
+    /**
+     * Returns the descriptions end-tags (or expression closing tokens) that we could have at this point.
+     * This is for generating error messages.
+     */
+    private Set<String> getExpectedEndTokenDescs() {
+        Set<String> endTokenDescs = new LinkedHashSet<>();
+        for (int i = 0; i < expectedTokenSequences.length; i++) {
+            int[] sequence = expectedTokenSequences[i];
+            for (int j = 0; j < sequence.length; j++) {
+                int token = sequence[j];
+                String endTokenDesc = getEndTokenDescIfIsEndToken(token);
+                if (endTokenDesc != null) {
+                    endTokenDescs.add(endTokenDesc);
                 }
             }
-            return "Unexpected end of file reached."
-                    + (endNames.size() == 0 ? "" : " You have an unclosed " + concatWithOrs(endNames) + ".");
-        } else if (kind == ELSE) {
-            return "Unexpected directive, \"#else\". "
-                    + "Check if you have a valid #if-#elseif-#else or #list-#else structure.";
-        } else if (kind == END_IF || kind == ELSE_IF) {
-            return "Unexpected directive, "
-                    + StringUtil.jQuote(nextToken)
-                    + ". Check if you have a valid #if-#elseif-#else structure.";
         }
-        return null;
+        return endTokenDescs;
     }
 
-    private String concatWithOrs(Set/*<String>*/ endNames) {
-        StringBuilder sb = new StringBuilder(); 
-        for (Iterator/*<String>*/ it = endNames.iterator(); it.hasNext(); ) {
-            String endName = (String) it.next();
+    private boolean getIsEndToken(int token) {
+        return getEndTokenDescIfIsEndToken(token) != null;
+    }
+
+    private String getEndTokenDescIfIsEndToken(int token) {
+        String endTokenDesc = null;
+        switch (token) {
+        case END_FOREACH:
+            endTokenDesc = "#foreach";
+            break;
+        case END_LIST:
+            endTokenDesc = "#list";
+            break;
+        case END_SEP:
+            endTokenDesc = "#sep";
+            break;
+        case END_ITEMS:
+            endTokenDesc = "#items";
+            break;
+        case END_SWITCH:
+            endTokenDesc = "#switch";
+            break;
+        case END_IF:
+            endTokenDesc = "#if";
+            break;
+        case END_COMPRESS:
+            endTokenDesc = "#compress";
+            break;
+        case END_MACRO:
+        case END_FUNCTION:
+            endTokenDesc = "#macro or #function";
+            break;
+        case END_TRANSFORM:
+            endTokenDesc = "#transform";
+            break;
+        case END_ESCAPE:
+            endTokenDesc = "#escape";
+            break;
+        case END_NOESCAPE:
+            endTokenDesc = "#noescape";
+            break;
+        case END_ASSIGN:
+        case END_GLOBAL:
+        case END_LOCAL:
+            endTokenDesc = "#assign or #local or #global";
+            break;
+        case END_ATTEMPT:
+            endTokenDesc = "#attempt";
+            break;
+        case CLOSING_CURLY_BRACKET:
+            endTokenDesc = "\"{\"";
+            break;
+        case CLOSE_BRACKET:
+            endTokenDesc = "\"[\"";
+            break;
+        case CLOSE_PAREN:
+            endTokenDesc = "\"(\"";
+            break;
+        case UNIFIED_CALL_END:
+            endTokenDesc = "@...";
+            break;
+        }
+        return endTokenDesc;
+    }
+
+    private String joinWithAnds(Collection<String> strings) {
+        StringBuilder sb = new StringBuilder();
+        for (String s : strings) {
             if (sb.length() != 0) {
-                sb.append(" or ");
+                sb.append(" and ");
             }
-            sb.append(endName);
+            sb.append(s);
         }
         return sb.toString();
     }
diff --git a/src/main/java/freemarker/core/TemplateNullModel.java b/src/main/java/freemarker/core/TemplateNullModel.java
index b3ac0de..b9af584 100644
--- a/src/main/java/freemarker/core/TemplateNullModel.java
+++ b/src/main/java/freemarker/core/TemplateNullModel.java
@@ -24,9 +24,9 @@
 /**
  * Represents a {@code null} value; if we get this as the value of a variable from a scope, we do not fall back
  * to a higher scope to get the same variable again. If instead we get a {@code null}, that means that the variable
- * doesn't exist at all in the current scope, and so we fall back to a higher scope. This distinction wasn is only
+ * doesn't exist at all in the current scope, and so we fall back to a higher scope. This distinction is only
  * used for (and expected from) certain scopes, so be careful where you are using it. (As of this
- * writing, it's onlt for local variables, including loop variables). The user should never meet a
+ * writing, it's only for local variables, including loop variables). The user should never meet a
  * {@link TemplateNullModel}, it must not be returned from public API-s.
  *
  * @see Environment#getNullableLocalVariable(String)
diff --git a/src/main/java/freemarker/core/_CoreStringUtils.java b/src/main/java/freemarker/core/_CoreStringUtils.java
index e545d7c..dc87b02 100644
--- a/src/main/java/freemarker/core/_CoreStringUtils.java
+++ b/src/main/java/freemarker/core/_CoreStringUtils.java
@@ -46,7 +46,8 @@
         scanForQuotationType: for (int i = 0; i < name.length(); i++) {
             final char c = name.charAt(i);
             if (!(i == 0 ? StringUtil.isFTLIdentifierStart(c) : StringUtil.isFTLIdentifierPart(c)) && c != '@') {
-                if ((quotationType == 0 || quotationType == '\\') && (c == '-' || c == '.' || c == ':')) {
+                if ((quotationType == 0 || quotationType == '\\')
+                        && StringUtil.isBackslashEscapedFTLIdentifierCharacter(c)) {
                     quotationType = '\\';
                 } else {
                     quotationType = '"';
@@ -66,8 +67,27 @@
         }
     }
 
-    private static String backslashEscapeIdentifier(String name) {
-        return StringUtil.replace(StringUtil.replace(StringUtil.replace(name, "-", "\\-"), ".", "\\."), ":", "\\:");
+    /*
+     * Escapes an identifier. This assumes that the identifier was once accepted by the parser, thus it is properly
+     * escapeable. Invalid characters that can't be escaped will be left as is. (This is actually feature because of
+     * historically weirdness, like that a sole {@code *} is a valid subvariable name, which must not be escaped.)
+     */
+    public static String backslashEscapeIdentifier(String name) {
+        StringBuilder sb = null;
+        for (int i = 0; i < name.length(); i++) {
+            char c = name.charAt(i);
+            if (StringUtil.isBackslashEscapedFTLIdentifierCharacter(c)) {
+                if (sb == null) {
+                    sb = new StringBuilder(name.length() + 8);
+                    sb.append(name, 0, i);
+                }
+                sb.append('\\');
+            }
+            if (sb != null) {
+                sb.append(c);
+            }
+        }
+        return sb == null ? name : sb.toString();
     }
 
     /**
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
index 36b2511..98580fc 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
@@ -819,21 +819,21 @@
     // This is needed as java.bean.Introspector sometimes gives back a method that's actually not accessible,
     // as it's an override of an accessible method in a non-public subclass. While that's still a public method, calling
     // it directly via reflection will throw java.lang.IllegalAccessException, and we are supposed to call the overidden
-    // accessible method instead. Like, we migth get two PropertyDescriptor-s for the same property name, and only one
+    // accessible method instead. Like, we might get two PropertyDescriptor-s for the same property name, and only one
     // will have a reader method that we can actually call. So we have to find that method here.
-    // Furthermore, the return type of the inaccisable method is possibly different (more specific) than the return type
-    // of the overidden accessible method. Also Introspector behavior changed with Java 9, as earlier in such case the
-    // Introspector returned all variants of the method (so the accessible one was amongst them at least), while in
-    // Java 9 it apparently always returns one variant only, but that's sometimes (not sure if it's predictable) the
-    // inaccessbile one.
+    // Furthermore, the return type of the inaccessible method is possibly different (more specific) than the return
+    // type of the overridden accessible method. Also Introspector behavior changed with Java 9, as earlier in such
+    // case the Introspector returned all variants of the method (so the accessible one was amongst them at least),
+    // while in Java 9 it apparently always returns one variant only, but that's sometimes (not sure if it's
+    // predictable) the inaccessible one.
     private static Method getMatchingAccessibleMethod(Method m, Map<ExecutableMemberSignature, List<Method>> accessibles) {
         if (m == null) {
             return null;
         }
         List<Method> ams = accessibles.get(new ExecutableMemberSignature(m));
-        // Certainly we could return any of the accessbiles, as Java reflection will call the correct override of the
+        // Certainly we could return any of the accessibles, as Java reflection will call the correct override of the
         // method anyway. There's an ambiguity when the return type is "overloaded", but in practice it probably doesn't
-        // matter which variant we call. Though, technically, they could do totaly different things. So, to avoid any
+        // matter which variant we call. Though, technically, they could do totally different things. So, to avoid any
         // corner cases that cause problems after an upgrade, we make an effort to give same result as before 2.3.31.
         return ams != null ? _MethodUtil.getMethodWithClosestNonSubReturnType(m.getReturnType(), ams) : null;
     }
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index bd7585a..eb189ee 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -931,6 +931,17 @@
      *           {@code <#if x[0]>} works correctly without this fix as well. 
      *     </ul>
      *   </li>
+     *   <li><p>
+     *     2.3.31 (or higher):
+     *     <ul>
+     *       <li><p>When you set the {@code number_format} setting to {@code "computer"} (or you call
+     *       {@link Environment#getCNumberFormat()}), the format now matches the behavior of {@code ?c}, when formatting
+     *       infinite (positive and negative), and NaN. Matching the behavior of {@code ?c} was always the intent,
+     *       but before this incompatible improvement, the {@code "computer"} format always behaved like {@code ?c}
+     *       before Incompatible Improvements 2.3.21, where instead of INF, and NaN, the results used unicode characters
+     *       U+221E, and U+FFFD.
+     *     </ul>
+     *   </li>
      * </ul>
      * 
      * @throws IllegalArgumentException
diff --git a/src/main/java/freemarker/template/DefaultObjectWrapper.java b/src/main/java/freemarker/template/DefaultObjectWrapper.java
index c0337db..eb44ce6 100644
--- a/src/main/java/freemarker/template/DefaultObjectWrapper.java
+++ b/src/main/java/freemarker/template/DefaultObjectWrapper.java
@@ -73,8 +73,10 @@
     private boolean useAdaptersForContainers;
     private boolean forceLegacyNonListCollections;
     private boolean iterableSupport;
+    private boolean domNodeSupport;
+    private boolean jythonSupport;
     private final boolean useAdapterForEnumerations;
-    
+
     /**
      * Creates a new instance with the incompatible-improvements-version specified in
      * {@link Configuration#DEFAULT_INCOMPATIBLE_IMPROVEMENTS}.
@@ -134,6 +136,8 @@
                 && getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_3_26;
         forceLegacyNonListCollections = dowDowCfg.getForceLegacyNonListCollections();
         iterableSupport = dowDowCfg.getIterableSupport();
+        domNodeSupport = dowDowCfg.getDOMNodeSupport();
+        jythonSupport = dowDowCfg.getJythonSupport();
         finalizeConstruction(writeProtected);
     }
 
@@ -255,9 +259,10 @@
      * Called for an object that isn't considered to be of a "basic" Java type, like for an application specific type,
      * or for a W3C DOM node. In its default implementation, W3C {@link Node}-s will be wrapped as {@link NodeModel}-s
      * (allows DOM tree traversal), Jython objects will be delegated to the {@code JythonWrapper}, others will be
-     * wrapped using {@link BeansWrapper#wrap(Object)}. Note that if {@link #getMemberAccessPolicy()} doesn't return
-     * a {@link DefaultMemberAccessPolicy} or {@link LegacyDefaultMemberAccessPolicy}, then Jython wrapper will be
-     * skipped for security reasons.
+     * wrapped using {@link BeansWrapper#wrap(Object)}. However, these can be turned off with the
+     * {@link #setDOMNodeSupport(boolean)} and {@link #setJythonSupport(boolean)}. Note that if
+     * {@link #getMemberAccessPolicy()} doesn't return a {@link DefaultMemberAccessPolicy} or
+     * {@link LegacyDefaultMemberAccessPolicy}, then Jython wrapper will be skipped for security reasons.
      * 
      * <p>
      * When you override this method, you should first decide if you want to wrap the object in a custom way (and if so
@@ -265,16 +270,20 @@
      * behavior is fine with you).
      */
     protected TemplateModel handleUnknownType(Object obj) throws TemplateModelException {
-        if (obj instanceof Node) {
+        if (domNodeSupport && obj instanceof Node) {
             return wrapDomNode(obj);
         }
-        MemberAccessPolicy memberAccessPolicy = getMemberAccessPolicy();
-        if (memberAccessPolicy instanceof DefaultMemberAccessPolicy
-                || memberAccessPolicy instanceof LegacyDefaultMemberAccessPolicy) {
-            if (JYTHON_WRAPPER != null && JYTHON_OBJ_CLASS.isInstance(obj)) {
-                return JYTHON_WRAPPER.wrap(obj);
+
+        if (jythonSupport) {
+            MemberAccessPolicy memberAccessPolicy = getMemberAccessPolicy();
+            if (memberAccessPolicy instanceof DefaultMemberAccessPolicy
+                    || memberAccessPolicy instanceof LegacyDefaultMemberAccessPolicy) {
+                if (JYTHON_WRAPPER != null && JYTHON_OBJ_CLASS.isInstance(obj)) {
+                    return JYTHON_WRAPPER.wrap(obj);
+                }
             }
         }
+
         return super.wrap(obj); 
     }
     
@@ -389,6 +398,51 @@
     }
 
     /**
+     * Getter pair of {@link #setDOMNodeSupport(boolean)}; see there.
+     *
+     * @since 2.3.31
+     */
+    public final boolean getDOMNodeSupport() {
+        return domNodeSupport;
+    }
+
+    /**
+     * Enables wrapping {@link Node}-s on a special way (as described in the "XML Processing Guide" in the Manual);
+     * defaults to {@code true}.. If this is {@code true}, {@link Node}+s will be wrapped like any other generic object.
+     *
+     * @see #handleUnknownType(Object)
+     *
+     * @since 2.3.31
+     */
+    public void setDOMNodeSupport(boolean domNodeSupport) {
+        checkModifiable();
+        this.domNodeSupport = domNodeSupport;
+    }
+
+    /**
+     * Getter pair of {@link #setJythonSupport(boolean)}; see there.
+     *
+     * @since 2.3.31
+     */
+    public final boolean getJythonSupport() {
+        return jythonSupport;
+    }
+
+    /**
+     * Enables wrapping Jython objects in a special way; defaults to {@code true}. If this is {@code false}, they will
+     * be wrapped like any other generic object. Note that Jython wrapping is legacy feature, and might by disabled by
+     * the selected {@link MemberAccessPolicy}, even if this is {@code true}; see {@link #handleUnknownType(Object)}.
+     *
+     * @see #handleUnknownType(Object)
+     *
+     * @since 2.3.31
+     */
+    public void setJythonSupport(boolean jythonSupport) {
+        checkModifiable();
+        this.jythonSupport = jythonSupport;
+    }
+
+    /**
      * Returns the lowest version number that is equivalent with the parameter version.
      * 
      * @since 2.3.22
@@ -416,8 +470,12 @@
             }
         }
         
-        return "useAdaptersForContainers=" + useAdaptersForContainers + ", forceLegacyNonListCollections="
-                + forceLegacyNonListCollections + ", iterableSupport=" + iterableSupport + bwProps;
+        return "useAdaptersForContainers=" + useAdaptersForContainers
+                + ", forceLegacyNonListCollections=" + forceLegacyNonListCollections
+                + ", iterableSupport=" + iterableSupport
+                + ", domNodeSupport=" + domNodeSupport
+                + ", jythonSupport=" + jythonSupport
+                + bwProps;
     }
     
 }
diff --git a/src/main/java/freemarker/template/DefaultObjectWrapperConfiguration.java b/src/main/java/freemarker/template/DefaultObjectWrapperConfiguration.java
index ff474fa..a9575bc 100644
--- a/src/main/java/freemarker/template/DefaultObjectWrapperConfiguration.java
+++ b/src/main/java/freemarker/template/DefaultObjectWrapperConfiguration.java
@@ -35,6 +35,8 @@
     private boolean useAdaptersForContainers;
     private boolean forceLegacyNonListCollections;
     private boolean iterableSupport;
+    private boolean domNodeSupport;
+    private boolean jythonSupport;
 
     protected DefaultObjectWrapperConfiguration(Version incompatibleImprovements) {
         super(DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(incompatibleImprovements), true);
@@ -43,6 +45,8 @@
                 "freemarker.configuration", "DefaultObjectWrapper");
         useAdaptersForContainers = getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_3_22;
         forceLegacyNonListCollections = true; // [2.4]: = IcI < _TemplateAPI.VERSION_INT_2_4_0;
+        domNodeSupport = true;
+        jythonSupport = true;
     }
 
     /** See {@link DefaultObjectWrapper#getUseAdaptersForContainers()}. */
@@ -65,6 +69,26 @@
         this.forceLegacyNonListCollections = legacyNonListCollectionWrapping;
     }
 
+    /** See {@link DefaultObjectWrapper#getDOMNodeSupport()}. */
+    public boolean getDOMNodeSupport() {
+        return domNodeSupport;
+    }
+
+    /** See {@link DefaultObjectWrapper#setDOMNodeSupport(boolean)}. */
+    public void setDOMNodeSupport(boolean domNodeSupport) {
+        this.domNodeSupport = domNodeSupport;
+    }
+
+    /** See {@link DefaultObjectWrapper#getJythonSupport()}. */
+    public boolean getJythonSupport() {
+        return jythonSupport;
+    }
+
+    /** See {@link DefaultObjectWrapper#setJythonSupport(boolean)}. */
+    public void setJythonSupport(boolean jythonSupport) {
+        this.jythonSupport = jythonSupport;
+    }
+
     /**
      * See {@link DefaultObjectWrapper#getIterableSupport()}.
      * 
@@ -90,6 +114,8 @@
         result = result * prime + (useAdaptersForContainers ? 1231 : 1237);
         result = result * prime + (forceLegacyNonListCollections ? 1231 : 1237);
         result = result * prime + (iterableSupport ? 1231 : 1237);
+        result = result * prime + (domNodeSupport ? 1231 : 1237);
+        result = result * prime + (jythonSupport ? 1231 : 1237);
         return result;
     }
 
@@ -99,7 +125,9 @@
         final DefaultObjectWrapperConfiguration thatDowCfg = (DefaultObjectWrapperConfiguration) that;
         return useAdaptersForContainers == thatDowCfg.getUseAdaptersForContainers()
                 && forceLegacyNonListCollections == thatDowCfg.forceLegacyNonListCollections
-                && iterableSupport == thatDowCfg.iterableSupport;
+                && iterableSupport == thatDowCfg.iterableSupport
+                && domNodeSupport == thatDowCfg.domNodeSupport
+                && jythonSupport == thatDowCfg.jythonSupport;
     }
 
 }
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 71feecb..5ff74de 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -60,13 +60,13 @@
     /**
      * Kind of a dummy {@link ObjectWrapper} used at places where the internal code earlier used the
      * {@link ObjectWrapper#DEFAULT_WRAPPER} singleton, because it wasn't supposed to wrap/unwrap anything with it;
-     * never use this {@link ObjectWrapper}r in situations where values of arbitrary types need to be wrapped!
+     * never use this {@link ObjectWrapper} in situations where values of arbitrary types need to be wrapped!
      * The typical situation is that we are using {@link SimpleSequence}, or {@link SimpleHash}, which always has an
      * {@link ObjectWrapper} field, even if we don't care in the given situation, and so we didn't set it explicitly.
      * The concern with the old way is that the {@link ObjectWrapper} set in the {@link Configuration} is possibly
      * more restrictive than the default, so if the template author can somehow make FreeMarker wrap something with the
      * default {@link ObjectWrapper}, then we got a security problem. So we try not to have that around, if possible.
-     * The obvious fix, and the better engineering would be just use a such {@link TemplateSequenceModel} or
+     * The obvious fix, and the better engineering would be just use a {@link TemplateSequenceModel} or
      * {@link TemplateHashModelEx2} implementation at those places, which doesn't have an {@link ObjectWrapper} (and
      * doesn't have the overhead of said implementations either). But, some user code might casts the values it
      * receives (as directive argument for example) to {@link SimpleSequence} or {@link SimpleHash}, instead of to
diff --git a/src/main/java/freemarker/template/utility/StringUtil.java b/src/main/java/freemarker/template/utility/StringUtil.java
index 1238dd9..3317955 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -1270,6 +1270,17 @@
     public static boolean isFTLIdentifierPart(final char c) {
         return isFTLIdentifierStart(c) || (c >= '0' && c <= '9');  
     }
+
+    /**
+     * Tells if a character can occur in an FTL identifier if it's preceded with a backslash. For example, {@code "-"}
+     * is a such character (as you can have an identifier like {@code foo\-bar} in FTL), but {@code "f"} is not, as
+     * it needn't be, and can't be escaped.
+     *
+     * @since 2.3.31
+     */
+    public static boolean isBackslashEscapedFTLIdentifierCharacter(final char c) {
+        return c == '-' || c == '.' || c == ':' || c ==  '#';
+    }
     
     /**
      * Escapes the <code>String</code> with the escaping rules of Java language
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index cb80218..a6ee15b 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -1544,7 +1544,8 @@
         ]
     >
     |
-    <#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":")>
+    // Keep this in sync with StringUtil.isBackslashEscapedFTLIdentifierCharacter
+    <#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":" | "#")>
     |
     <#ID_START_CHAR: <NON_ESCAPED_ID_START_CHAR>|<ESCAPED_ID_CHAR>>
     |
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 29dce43..8ef2763 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -2681,8 +2681,8 @@
             non-Latin digits), underline (<literal>_</literal>), dollar
             (<literal>$</literal>), at sign (<literal>@</literal>).
             Furthermore, the first character can't be an ASCII digit
-            (<literal>0</literal>-<literal>9</literal>). Starting from
-            FreeMarker 2.3.22, the variable name can also contain minus
+            (<literal>0</literal>-<literal>9</literal>). Since FreeMarker
+            2.3.22 the variable name can also contain minus
             (<literal>-</literal>), dot (<literal>.</literal>), and colon
             (<literal>:</literal>) at any position, but these must be escaped
             with a preceding backslash (<literal>\</literal>), otherwise they
@@ -2690,7 +2690,10 @@
             whose name is <quote>data-id</quote>, the expression is
             <literal>data\-id</literal>, as <literal>data-id</literal> would
             be interpreted as <quote>data minus id</quote>. (Note that these
-            escapes only work in identifiers, not in string literals.)</para>
+            escapes only work in identifiers, not in string literals.)
+            Furthermore, since FreeMarker 2.3.31, hash mark
+            (<literal>#</literal>) can also be used, but must be escaped with
+            a preceding backslash (<literal>\</literal>).</para>
           </section>
 
           <section xml:id="dgui_template_exp_var_hash">
@@ -12795,6 +12798,11 @@
           </listitem>
 
           <listitem>
+            <para><link
+            linkend="ref_builtin_eval_json">eval_json</link></para>
+          </listitem>
+
+          <listitem>
             <para><link linkend="ref_builtin_filter">filter</link></para>
           </listitem>
 
@@ -15881,13 +15889,16 @@
           meaning of these is locale (nationality) specific, and is controlled
           by the Java platform installation, not by FreeMarker, except for
           <literal>computer</literal>, which uses the same formatting as <link
-          linkend="ref_builtin_c">the <literal>c</literal> built-in</link>.
-          There can also be programmer-defined formats, whose name starts with
-          <literal>@</literal> (programmers <link
-          linkend="pgui_config_custom_formats">see more here...</link>). You
-          can use these predefined formats like this:</para>
+          linkend="ref_builtin_c">the <literal>c</literal> built-in</link>
+          (assuming <link
+          linkend="pgui_config_incompatible_improvements">incompatible
+          improvements</link> set to 2.3.31, or higher, or else infinity and
+          NaN isn't formatted like that). There can also be programmer-defined
+          formats, whose name starts with <literal>@</literal> (programmers
+          <link linkend="pgui_config_custom_formats">see more here...</link>).
+          You can use these predefined formats like this:</para>
 
-          <programlisting role="template">&lt;#assign x=42&gt;
+          <programlisting role="template">&lt;#assign x=4200000&gt;
 ${x}
 ${x?string}  &lt;#-- the same as ${x} --&gt;
 ${x?string.number}
@@ -15897,12 +15908,12 @@
 
           <para>If your locale is US English, this will print:</para>
 
-          <programlisting role="output">42
-42
-42
-$42.00
-4,200%
-42</programlisting>
+          <programlisting role="output">4,200,000
+4,200,000  
+4,200,000
+$4,200,000.00
+420,000,000%
+4200000</programlisting>
 
           <para>The output of first three expressions is identical because the
           first two expressions use the default format, which is
@@ -15910,7 +15921,7 @@
           setting:</para>
 
           <programlisting role="template">&lt;#setting number_format="currency"&gt;
-&lt;#assign x=42&gt;
+&lt;#assign x=4200000&gt;
 ${x}
 ${x?string}  &lt;#-- the same as ${x} --&gt;
 ${x?string.number}
@@ -15919,11 +15930,11 @@
 
           <para>Will now output:</para>
 
-          <programlisting role="output">$42.00
-$42.00
-42
-$42.00
-4,200%</programlisting>
+          <programlisting role="output">$4,200,000.00
+$4,200,000.00  
+4,200,000
+$4,200,000.00
+420,000,000%</programlisting>
 
           <para>since the default number format was set to
           <quote>currency</quote>.</para>
@@ -19168,6 +19179,16 @@
           linkend="ref_builtin_interpret"><literal>interpret</literal>
           built-in</link> instead.)</para>
 
+          <warning>
+            <para>Do not use this to evaluate JSON! For that use the <link
+            linkend="ref_builtin_eval_json"><literal>eval_json</literal>
+            built-in</link> instead. While FTL expression language looks
+            similar to JSON, not all JSON is valid FTL expression. Also, FTL
+            expressions can access variables, and call Java methods on them,
+            so if you <literal>?eval</literal> strings coming from untrusted
+            source, it can become an attack vector.</para>
+          </warning>
+
           <para>The evaluated expression sees the same variables (such as
           locals) that are visible at the place of the invocation of
           <literal>eval</literal>. That is, it behaves similarly as if in
@@ -19184,6 +19205,75 @@
           built-in</link>.</para>
         </section>
 
+        <section xml:id="ref_builtin_eval_json">
+          <title>eval_json</title>
+
+          <indexterm>
+            <primary>eval_json</primary>
+          </indexterm>
+
+          <indexterm>
+            <primary>evaluate string</primary>
+          </indexterm>
+
+          <indexterm>
+            <primary>JSON</primary>
+          </indexterm>
+
+          <note>
+            <para>This built-in is available since FreeMarker 2.3.31.</para>
+          </note>
+
+          <para>This built-in evaluates a string as a JSON
+          <emphasis>expression</emphasis>, so that you can extract data from
+          inside it. For example, if you receive data in the
+          <literal>dataJson</literal> variable, but it's unfortunately just a
+          flat string that contains <literal>{"name": "foo", "ids": [11,
+          22]}</literal>, then you can extract data from it like this:</para>
+
+          <programlisting role="template">&lt;#assign data = dataJson<emphasis>?eval_json</emphasis>&gt;
+&lt;p&gt;Name: ${data.name}
+&lt;p&gt;Ids:
+&lt;ul&gt;
+  &lt;#list data.ids as id&gt;
+    &lt;li&gt;${id}
+  &lt;/#list&gt;
+&lt;/ul&gt;</programlisting>
+
+          <para>Ideally, you shouldn't need <literal>eval_json</literal>,
+          since the template should receive data already parsed (to
+          <literal>List</literal>-s, <literal>Map</literal>-s, Java beans,
+          etc.). This built-in is there as a workaround, if you can't improve
+          the data-model.</para>
+
+          <para>The evaluated JSON expression doesn't have to be a JSON object
+          (key-value pairs), it can be any kind of JSON value, like JSON
+          array, JSON number, etc.</para>
+
+          <para>The syntax understood by this built-in is a superset of
+          JSON:</para>
+
+          <itemizedlist>
+            <listitem>
+              <para>Java-style comments are supported
+              (<literal>/*<replaceable>...</replaceable>*/</literal> and
+              <literal>//<replaceable>...</replaceable></literal>)</para>
+            </listitem>
+
+            <listitem>
+              <para>BOM (byte order mark) and non-breaking space
+              (<quote>nbsp</quote>) are treated as whitespace (in a stricter
+              JSON parser they are errors of occurring around tokens).</para>
+            </listitem>
+          </itemizedlist>
+
+          <para>No other non-JSON extras are implemented, notably, it's
+          impossible to refer to variables (unlike in the <link
+          linkend="ref_builtin_eval"><literal>eval</literal> built-in</link>).
+          This is important for safety, when receiving JSON from untrusted
+          sources.</para>
+        </section>
+
         <section xml:id="ref_builtin_has_content">
           <title>has_content</title>
 
@@ -25379,6 +25469,18 @@
           contains dash and further info after the numbers, like in
           2.3.21-nightly_20140726T151800Z.</para>
         </listitem>
+
+        <listitem>
+          <para><indexterm>
+              <primary>time zone</primary>
+            </indexterm><indexterm>
+              <primary>time_zone</primary>
+            </indexterm><literal>time_zone</literal> (exists since FreeMarker
+          2.3.31): The current value of the <literal>time_zone</literal>
+          setting, as a string. This is the ID of the time zone, like
+          <literal>GMT+01:00</literal>, or
+          <literal>America/Los_Angeles</literal>.</para>
+        </listitem>
       </itemizedlist>
 
       <simplesect xml:id="ref_specvar_get_optional_template">
@@ -29095,18 +29197,18 @@
                 <para>If you are using the default object wrapper class
                 (<literal>freemarker.template.DefaultObjectWrapper</literal>),
                 or a subclass of it, you should disable the XML (DOM) wrapping
-                feature of it, by overriding <literal>wrapDomNode(Object
-                obj)</literal> so that it does this: <literal>return
-                getModelFactory(obj.getClass()).create(obj, this);</literal>.
-                The problem with the XML wrapping feature, which wraps
-                <literal>org.w3c.dom.Node</literal> objects on special way to
-                make them easier to work with in templates, is that this
-                facility by design lets template authors evaluate arbitrary
-                XPath expressions, and XPath can do too much in certain
-                setups. If you really need the XML wrapping facility, review
-                carefully what XPath expressions are possible in your setup.
-                Also, be sure you don't use the long deprecated, and more
-                dangerous <literal>freemarker.ext.xml</literal> package, only
+                feature of it, by setting its
+                <literal>DOMNodeSupport</literal> property to
+                <literal>false</literal>. The problem with the XML wrapping
+                feature, which wraps <literal>org.w3c.dom.Node</literal>
+                objects on special way to make them easier to work with in
+                templates, is that this facility by design lets template
+                authors evaluate arbitrary XPath expressions, and XPath can do
+                too much in certain setups. If you really need the XML
+                wrapping facility, review carefully what XPath expressions are
+                possible in your setup. Also, be sure you don't use the long
+                deprecated, and more dangerous
+                <literal>freemarker.ext.xml</literal> package, only
                 <literal>freemarker.ext.dom</literal>. Also, note that when
                 using the XML wrapping feature, not allowing
                 <literal>org.w3c.dom.Node</literal> methods in the
@@ -29317,10 +29419,87 @@
         <para>Release date: FIXME</para>
 
         <section>
+          <title>Changes on the FTL side</title>
+
+          <itemizedlist>
+            <listitem>
+              <para>Added <literal>?eval_json</literal> to evaluate JSON given
+              as flat string. This was added as <literal>?eval</literal> is
+              routinely misused for the same purpose, which not only doesn't
+              work for all JSON-s, but can be a security problem. <link
+              linkend="ref_builtin_eval_json">See more here...</link></para>
+            </listitem>
+
+            <listitem>
+              <para>Added new special variable, <literal>time_zone</literal>
+              (referred like <literal>.time_zone</literal>, like all special
+              variables), to retrieve the current value of the
+              <literal>time_zone</literal> setting as a string.</para>
+            </listitem>
+
+            <listitem>
+              <para>Allowed escaping <literal>#</literal> with backlash in
+              identifier names (not in string), as it used to occur in
+              database column names. Like if you have a column name like
+              <literal>#users</literal>, you can refer to it as
+              <literal>row.\#users</literal>. (Alternatively,
+              <literal>row['#users']</literal> always worked, but is often
+              less convenient.)</para>
+            </listitem>
+
+            <listitem>
+              <para><link
+              xlink:href="https://issues.apache.org/jira/projects/FREEMARKER/issues/FREEMARKER-169">FREEMARKER-169</link>:
+              Fixed bug that made <literal>?c</literal> and
+              <quote>computer</quote> number format inconsistent. If <link
+              linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incomplatible_improvements</literal></link>
+              is set to 2.3.31 (or higher), when you set the
+              <literal>number_format</literal> setting to
+              <literal>computer</literal> (or you call
+              <literal>Environment.getCNumberFormat()</literal>), the format
+              now matches the behavior of <literal>?c</literal>, when
+              formatting infinite (positive and negative), and NaN. Matching
+              the behavior of <literal>?c</literal> was always the intent, but
+              before this incompatible improvement, the
+              <literal>computer</literal> format always behaved like
+              <literal>?c</literal> before incompatible improvements 2.3.21,
+              where instead of <quote>INF</quote>, and <quote>NaN</quote>, the
+              results used Unicode characters U+221E, and U+FFFD.</para>
+            </listitem>
+
+            <listitem>
+              <para>Fixed bug where <literal>.globals</literal> weren't seen
+              as namesapce, so something like <literal>&lt;#assign
+              <replaceable>name</replaceable> =
+              <replaceable>value</replaceable> in .globals&gt;</literal>
+              failed (although you should use <literal>&lt;#global
+              <replaceable>name</replaceable> =
+              <replaceable>value</replaceable>&gt;</literal> instead
+              anyway).</para>
+            </listitem>
+          </itemizedlist>
+        </section>
+
+        <section>
           <title>Changes on the Java side</title>
 
           <itemizedlist>
             <listitem>
+              <para>More helpful parser error messages for nesting problems
+              (caused by missed or malformed end-tags usually).</para>
+            </listitem>
+
+            <listitem>
+              <para>Added <literal>DOMNodeSupport</literal> and
+              <literal>JythonSupport</literal> <literal>boolean</literal>
+              properties to <literal>DefaultObjectWrapper</literal>. This
+              allows disabling the special wrapping of DOM nodes and Jython
+              classes. This might be desirable <link
+              linkend="faq_template_uploading_security">for security
+              reasons</link>.</para>
+            </listitem>
+
+            <listitem>
               <para><link
               xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-145">FREEMARKER-145</link>:
               Fixed bug where methods with "overloaded" return type may become
@@ -29346,6 +29525,38 @@
               very least can depend on the Java version), and in what
               order.</para>
             </listitem>
+
+            <listitem>
+              <para>Fixed bug where OSGi
+              <literal>Bundle-RequiredExecutionEnvironment</literal> in
+              <literal>META-INF/MANIFEST.FM</literal> has incorrectly
+              contained JavaSE-1.6, J2SE-1.5</para>
+            </listitem>
+
+            <listitem>
+              <para><link
+              xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-159">FREEMARKER-159</link>:
+              Set <literal>Automatic-Module-Name</literal> to
+              <literal>freemarker</literal> in
+              <literal>META-INF/MANIFEST.FM</literal>. In most cases this was
+              the deduced Java 9 module name earlier, but that was fragile, as
+              Java has deduced it from the jar file name.</para>
+            </listitem>
+
+            <listitem>
+              <para><link
+              xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-165">FREEMARKER-165</link>:
+              Fixed bug where where if the namespace expression in a block
+              assignment (like <literal>&lt;#assign
+              <replaceable>x</replaceable> in
+              <replaceable>someNamespace</replaceable>&gt;<replaceable>...</replaceable>&lt;/#assign&gt;</literal>)
+              refers to a missing variable, or has the wrong type, FreeMarker
+              has thrown <literal>NullPointerException</literal> or
+              <literal>ClassCastException</literal>, instead of
+              <literal>InvalidReferenceException</literal> and
+              <literal>NonNamespaceException</literal> with proper helpful
+              message.</para>
+            </listitem>
           </itemizedlist>
         </section>
       </section>
diff --git a/src/test/java/freemarker/core/CamelCaseTest.java b/src/test/java/freemarker/core/CamelCaseTest.java
index b1e1dcd..4e5d537 100644
--- a/src/test/java/freemarker/core/CamelCaseTest.java
+++ b/src/test/java/freemarker/core/CamelCaseTest.java
@@ -25,6 +25,7 @@
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Set;
+import java.util.TimeZone;
 
 import org.junit.Test;
 
@@ -43,6 +44,8 @@
         assertOutput("${.data_model?is_hash?c}", "true");
         assertOutput("${.localeObject.toString()}", "de_DE");
         assertOutput("${.locale_object.toString()}", "de_DE");
+        assertOutput("${.time_zone}", getConfiguration().getTimeZone().getID());
+        assertOutput("${.timeZone}", getConfiguration().getTimeZone().getID());
         assertOutput("${.templateName?length}", "0");
         assertOutput("${.template_name?length}", "0");
         assertOutput("${.outputEncoding}", "utf-8");
diff --git a/src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaround.java b/src/test/java/freemarker/core/EvalJsonBuiltInTest.java
similarity index 70%
copy from src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaround.java
copy to src/test/java/freemarker/core/EvalJsonBuiltInTest.java
index 05fa695..ff62a61 100644
--- a/src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaround.java
+++ b/src/test/java/freemarker/core/EvalJsonBuiltInTest.java
@@ -17,22 +17,25 @@
  * under the License.
  */
 
-package freemarker.ext.beans;
+package freemarker.core;
 
 import java.io.IOException;
-import java.nio.file.Paths;
 
 import org.junit.Test;
 
 import freemarker.template.TemplateException;
 import freemarker.test.TemplateTest;
 
-public class Java9InstrospectorBugWorkaround extends TemplateTest {
+public class EvalJsonBuiltInTest extends TemplateTest {
 
     @Test
-    public void test() throws IOException, TemplateException {
-        addToDataModel("path", Paths.get("foo", "bar"));
-        assertOutput("<#assign _ = path.parent>", "");
+    public void test() throws Exception {
+        assertOutput("${'1'?eval_json}", "1");
+        assertOutput("${'1'?evalJson}", "1");
+
+        assertOutput("${'null'?evalJson!'-'}", "-");
+
+        assertOutput("<#list '{\"a\": 1e2, \"b\": null}'?evalJson as k, v>${k}=${v!'NULL'}<#sep>, </#list>", "a=100, b=NULL");
     }
 
 }
diff --git a/src/test/java/freemarker/core/JSONParserTest.java b/src/test/java/freemarker/core/JSONParserTest.java
new file mode 100644
index 0000000..dfdf5b6
--- /dev/null
+++ b/src/test/java/freemarker/core/JSONParserTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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 static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.utility.DeepUnwrap;
+
+public class JSONParserTest {
+
+    @Test
+    public void testObjects() throws JSONParser.JSONParseException {
+        assertEquals(ImmutableMap.of("a", 1, "b", 2), JSONParser.parse("{\"a\": 1, \"b\": 2}"));
+        assertEquals(Collections.emptyMap(), JSONParser.parse("{}"));
+        try {
+            JSONParser.parse("{1: 1}");
+            fail();
+        } catch (JSONParser.JSONParseException e) {
+            assertThat(e.getMessage(), containsString("string key"));
+        }
+    }
+
+    @Test
+    public void testLists() throws JSONParser.JSONParseException {
+        assertEquals(ImmutableList.of(1, 2), JSONParser.parse("[1, 2]"));
+        assertEquals(Collections.emptyList(), JSONParser.parse("[]"));
+    }
+
+    @Test
+    public void testStrings() throws JSONParser.JSONParseException {
+        assertEquals("", JSONParser.parse("\"\""));
+        assertEquals(" ", JSONParser.parse("\" \""));
+        assertEquals("'", JSONParser.parse("\"'\""));
+        assertEquals("foo", JSONParser.parse("\"foo\""));
+        assertEquals("\" \\ / \b \f \n \r \t \ufeff",
+                JSONParser.parse(
+                        "\"" +
+                        "\\\" \\\\ \\/ \\b \\f \\n \\r \\t \\uFEFF" +
+                        "\""));
+    }
+
+    @Test
+    public void testNumbers() throws JSONParser.JSONParseException {
+        assertEquals(0, JSONParser.parse("0"));
+        assertEquals(123, JSONParser.parse("123"));
+        assertEquals(-123, JSONParser.parse("-123"));
+        assertNotEquals(123L, JSONParser.parse("123"));
+        assertEquals(2147483647, JSONParser.parse("2147483647"));
+        assertEquals(2147483648L, JSONParser.parse("2147483648"));
+        assertEquals(-2147483648, JSONParser.parse("-2147483648"));
+        assertEquals(-2147483649L, JSONParser.parse("-2147483649"));
+        assertEquals(-123, JSONParser.parse("-1.23E2"));
+        assertEquals(new BigDecimal("1.23"), JSONParser.parse("1.23"));
+        assertEquals(new BigDecimal("-1.23"), JSONParser.parse("-1.23"));
+        assertEquals(new BigDecimal("12.3"), JSONParser.parse("1.23E1"));
+        assertEquals(new BigDecimal("0.123"), JSONParser.parse("123E-3"));
+    }
+
+    @Test
+    public void testKeywords() throws JSONParser.JSONParseException {
+        assertNull(JSONParser.parse("null"));
+        assertEquals(true, JSONParser.parse("true"));
+        assertEquals(false, JSONParser.parse("false"));
+        try {
+            JSONParser.parse("NULL");
+            fail();
+        } catch (JSONParser.JSONParseException e) {
+            assertThat(e.getMessage(), containsString("quoted"));
+        }
+    }
+
+    @Test
+    public void testBlockComments() throws JSONParser.JSONParseException {
+        assertEquals(ImmutableList.of(1, 2), JSONParser.parse("/**/[/**/1/**/, /**/2/**/]/**/"));
+        assertEquals(ImmutableList.of(1, 2), JSONParser.parse("/*x*/[/*x*/1/*x*/, /*x*/2/*x*/]/*x*/"));
+        assertEquals(ImmutableList.of(1), JSONParser.parse(" /*x*/ /**//**/ [ /*x*/ /*\n*//***/ 1 ]"));
+        try {
+            JSONParser.parse("/*");
+            fail();
+        } catch (JSONParser.JSONParseException e) {
+            assertThat(e.getMessage(), containsString("Unclosed comment"));
+        }
+        try {
+            JSONParser.parse("[/*]");
+            fail();
+        } catch (JSONParser.JSONParseException e) {
+            assertThat(e.getMessage(), containsString("Unclosed comment"));
+        }
+    }
+
+    @Test
+    public void testLineComments() throws JSONParser.JSONParseException {
+        assertEquals(ImmutableList.of(1, 2), JSONParser.parse("//c1\n[ //c2\n1, //c3\n 2//c5\n] //c4"));
+        assertEquals(ImmutableList.of(1, 2), JSONParser.parse("// c1\n//\r// c2\r\n// c3\r\n[ 1, 2 ]//"));
+        assertEquals(ImmutableList.of(1, 2), JSONParser.parse("[1, 2]\n//\n"));
+    }
+
+    @Test
+    public void testWhitespace() throws JSONParser.JSONParseException {
+        assertEquals(ImmutableList.of(1, 2), JSONParser.parse("  [  1  ,\n2  ]  "));
+        assertEquals(ImmutableList.of(1, 2), JSONParser.parse("\uFEFF[\u00A01\u00A0,2]"));
+    }
+
+    @Test
+    public void testMixed() throws JSONParser.JSONParseException {
+        LinkedHashMap<String, Object> m = new LinkedHashMap<>();
+        m.put("x", 1);
+        m.put("y", null);
+        assertEquals(
+                ImmutableList.of(
+                        ImmutableMap.of("a", Collections.emptyMap()),
+                        ImmutableMap.of("b",
+                                Arrays.asList(
+                                        m,
+                                        true,
+                                        null
+                                ))
+                ),
+                JSONParser.parse("" +
+                        "[\n" +
+                            "{\"a\":{}},\n" +
+                            "{\"b\":\n" +
+                                    "[" +
+                                        "{\"x\":1, \"y\": null}," +
+                                        "true," +
+                                        "null" +
+                                    "] // comment\n" +
+                            "}\n" +
+                        "]"));
+    }
+
+    private static void assertEquals(Object expected, TemplateModel actual) {
+        try {
+            Assert.assertEquals(expected, DeepUnwrap.unwrap(actual));
+        } catch (TemplateModelException e) {
+            throw new BugException(e);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/core/MiscErrorMessagesTest.java b/src/test/java/freemarker/core/MiscErrorMessagesTest.java
index df10fde..6e98fcc 100644
--- a/src/test/java/freemarker/core/MiscErrorMessagesTest.java
+++ b/src/test/java/freemarker/core/MiscErrorMessagesTest.java
@@ -62,5 +62,17 @@
         assertErrorContains("<#global x += 2>", "\"x\"", "+=", "global scope");
         assertErrorContains("<#macro m><#local x--></#macro><@m/>", "\"x\"", "--", "local scope");
     }
-    
+
+    @Test
+    public void assignmentNamespaceChecks() {
+        assertErrorContains("<#assign x = 1 in noSuchVar>", InvalidReferenceException.class, "noSuchVar");
+        assertErrorContains("<#assign x =1 in 'notANamespace'>", NonNamespaceException.class, "notANamespace");
+    }
+
+    @Test
+    public void blockAssignmentNamespaceChecks() {
+        assertErrorContains("<#assign x in noSuchVar>1</#assign>", InvalidReferenceException.class, "noSuchVar");
+        assertErrorContains("<#assign x in 'notANamespace'>1</#assign>", NonNamespaceException.class, "notANamespace");
+    }
+
 }
diff --git a/src/test/java/freemarker/core/NumberFormatTest.java b/src/test/java/freemarker/core/NumberFormatTest.java
index 29920c4..184fc21 100644
--- a/src/test/java/freemarker/core/NumberFormatTest.java
+++ b/src/test/java/freemarker/core/NumberFormatTest.java
@@ -44,11 +44,13 @@
 import freemarker.template.TemplateModel;
 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 {
-    
+
     @Before
     public void setup() {
         Configuration cfg = getConfiguration();
@@ -313,7 +315,48 @@
             assertOutput("${0.0000123?string.@printfG}", "1.23000E-05");
         }
     }
-    
+
+    @Test
+    public void testCFormatOfSpecialNumbers() throws IOException, TemplateException {
+        addToDataModel("pInf", Double.POSITIVE_INFINITY);
+        addToDataModel("nInf", Double.NEGATIVE_INFINITY);
+        addToDataModel("nan", Double.NaN);
+
+        Configuration cfg = getConfiguration();
+        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 } ) {
+            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";
+
+            assertOutput(
+                    "${pInf?c} ${nInf?c} ${nan?c}",
+                    cBuiltInBroken ? humanAudienceOutput : computerAudienceOutput);
+
+            assertOutput(
+                    "<#setting numberFormat='computer'>${pInf} ${nInf} ${nan}",
+                    cNumberFormatBroken ? humanAudienceOutput : computerAudienceOutput);
+
+            assertOutput(
+                    "${pInf} ${nInf} ${nan}",
+                    humanAudienceOutput);
+
+            Environment env = new Template(null, "", cfg)
+                    .createProcessingEnvironment(null, null);
+            assertEquals(
+                    cNumberFormatBroken ? humanAudienceOutput : computerAudienceOutput,
+                    env.getCNumberFormat().format(Double.POSITIVE_INFINITY)
+                            + " " + env.getCNumberFormat().format(Double.NEGATIVE_INFINITY)
+                            + " " + env.getCNumberFormat().format(Double.NaN));
+        }
+    }
+
     private static class MutableTemplateNumberModel implements TemplateNumberModel {
         
         private Number number;
diff --git a/src/test/java/freemarker/core/ParsingErrorMessagesTest.java b/src/test/java/freemarker/core/ParsingErrorMessagesTest.java
index 73797ba..09af694 100644
--- a/src/test/java/freemarker/core/ParsingErrorMessagesTest.java
+++ b/src/test/java/freemarker/core/ParsingErrorMessagesTest.java
@@ -91,7 +91,29 @@
             assertErrorContains("[#ftl]${'x'>", "end of file");
         }
     }
-    
+
+    @Test
+    public void testNestingErrors() throws Exception {
+        assertErrorContains(
+                "<#if true><#list xs as x></list></#if>",
+                "</#if>", "#list", "end-tag");
+        assertErrorContains(
+                "<#if true><#assign x><#else></#assign></#if>",
+                "<#else>", "#if", "#list", "#assign");
+        assertErrorContains(
+                "<#list xs><#items as x></#list>",
+                "</#list>", "#items", "end-tag");
+        assertErrorContains(
+                "<#list xs as x><#sep></#if></#list>",
+                "</#if>", "#list", "#sep", "end-tag");
+        assertErrorContains(
+                "<#list xs as x>",
+                "end of file", "#list", "end-tag");
+        assertErrorContains(
+                "<#if true>text<#list xs as x></#list>",
+                "end of file", "#if", "end-tag");
+    }
+
     /**
      * "assertErrorContains" with both angle bracket and square bracket tag syntax, by converting the input tag syntax.
      * Beware, it uses primitive search-and-replace.
diff --git a/src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaround.java b/src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaroundTest.java
similarity index 93%
rename from src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaround.java
rename to src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaroundTest.java
index 05fa695..0201b41 100644
--- a/src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaround.java
+++ b/src/test/java/freemarker/ext/beans/Java9InstrospectorBugWorkaroundTest.java
@@ -27,7 +27,7 @@
 import freemarker.template.TemplateException;
 import freemarker.test.TemplateTest;
 
-public class Java9InstrospectorBugWorkaround extends TemplateTest {
+public class Java9InstrospectorBugWorkaroundTest extends TemplateTest {
 
     @Test
     public void test() throws IOException, TemplateException {
diff --git a/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java b/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java
index 66038c7..e2c3976 100644
--- a/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java
+++ b/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java
@@ -37,7 +37,7 @@
         super(name);
     }
     
-    public void testNumberical() {
+    public void testNumerical() {
         // Note: the signature lists consists of the same elements, only their order changes depending on the type
         // of the argument value.
         
diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index b973358..d9924f1 100644
--- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
+++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
@@ -46,7 +46,9 @@
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 
+import org.hamcrest.Matchers;
 import org.junit.Test;
+import org.python.core.PyString;
 import org.w3c.dom.Document;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
@@ -58,6 +60,7 @@
 import freemarker.ext.beans.EnumerationModel;
 import freemarker.ext.beans.HashAdapter;
 import freemarker.ext.beans.WhitelistMemberAccessPolicy;
+import freemarker.ext.jython.JythonSequenceModel;
 import freemarker.ext.util.WrapperTemplateModel;
 
 public class DefaultObjectWrapperTest {
@@ -1033,11 +1036,49 @@
     @Test
     public void testCanWrapDOM() throws SAXException, IOException, ParserConfigurationException,
             TemplateModelException {
+        assertTrue(OW22.wrap(createDocument()) instanceof TemplateNodeModel);
+    }
+
+    @Test
+    public void testDisabledDOMNodeWrapping() throws SAXException, IOException, ParserConfigurationException,
+            TemplateModelException {
+        Document doc = createDocument();
+        DefaultObjectWrapperBuilder dowB = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_31);
+        dowB.setDOMNodeSupport(false);
+        DefaultObjectWrapper ow = dowB.build();
+
+        testDisabledDomWrappingInternal(doc, ow);
+
+        ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_31);
+        ow.setDOMNodeSupport(false);
+        testDisabledDomWrappingInternal(doc, ow);
+    }
+
+    private void testDisabledDomWrappingInternal(Document doc, DefaultObjectWrapper ow) throws TemplateModelException {
+        TemplateModel model = ow.wrap(doc);
+        assertFalse(model instanceof TemplateNodeModel);
+        assertTrue(model instanceof TemplateHashModel);
+        assertNotNull(((TemplateHashModel) model).get("getDoctype"));
+        assertNotNull(((TemplateHashModel) model).get("class"));
+    }
+
+    @Test
+    public void testDisabledJythonWrapping() throws SAXException, IOException, ParserConfigurationException,
+            TemplateModelException {
+        PyString pyString = new PyString("foo");
+
+        DefaultObjectWrapper ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_31);
+        assertThat(ow.wrap(pyString), Matchers.instanceOf(JythonSequenceModel.class));
+
+        ow.setJythonSupport(false);
+        assertThat(ow.wrap(pyString), not(Matchers.instanceOf(JythonSequenceModel.class)));
+    }
+
+    private Document createDocument() throws ParserConfigurationException, SAXException, IOException {
         DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
         InputSource is = new InputSource();
         is.setCharacterStream(new StringReader("<doc><sub a='1' /></doc>"));
-        Document doc = db.parse(is);        
-        assertTrue(OW22.wrap(doc) instanceof TemplateNodeModel);
+        return db.parse(is);
     }
 
     @Test
diff --git a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl
index 75d52f1..11b1172 100644
--- a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl
+++ b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl
@@ -30,9 +30,9 @@
 </#function>
 ${f\-a("f-a")}
 
-<#assign \-\-\-\.\: = 'dash-dash-dash etc.'>
-${\-\-\-\.\:}
-${.vars['---.:']}
+<#assign \-\-\-\.\:\# = 'dash-dash-dash etc.'>
+${\-\-\-\.\:\#}
+${.vars['---.:#']}
 <#assign hash = { '--moz-prop': 'propVal' }>
 ${hash.\-\-moz\-prop}
 ${hash['--moz-prop']}
diff --git a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out
index 17e2b4e..96eff6b 100644
--- a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out
+++ b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out
@@ -21,8 +21,8 @@
 
 <#function f\-a(p\-a)><#return p\-a + " works"/></#function>${f\-a("f-a")}
 
-<#assign \-\-\-\.\: = "dash-dash-dash etc.">${\-\-\-\.\:}
-${.vars["---.:"]}
+<#assign \-\-\-\.\:\# = "dash-dash-dash etc.">${\-\-\-\.\:\#}
+${.vars["---.:#"]}
 <#assign hash = {"--moz-prop": "propVal"}>${hash.\-\-moz\-prop}
 ${hash["--moz-prop"]}
 
diff --git a/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt b/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt
index 1c62bd5..5afbaec 100644
--- a/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt
+++ b/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt
@@ -40,7 +40,7 @@
 
 <catchAll x=1 y=2 a:b.c=5 data-foo=4 z=3 />
 
----.: = dash-dash-dash etc.
+---.:# = dash-dash-dash etc.
 @as@_a = as1
 as/b = as3
 as'c = as4
diff --git a/src/test/resources/freemarker/test/templatesuite/expected/specialvars.txt b/src/test/resources/freemarker/test/templatesuite/expected/specialvars.txt
index e2fa13e..2c7bac3 100644
--- a/src/test/resources/freemarker/test/templatesuite/expected/specialvars.txt
+++ b/src/test/resources/freemarker/test/templatesuite/expected/specialvars.txt
@@ -18,6 +18,7 @@
  */
 en == en
 en_US == en_US
+GMT+01:00 == GMT+01:00
 utf-8 == utf-8
 specialvars.ftl == specialvars.ftl
 iso-8859-1 == iso-8859-1
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl b/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl
index 9b39235..e29d997 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl
@@ -30,9 +30,9 @@
 </#function>
 ${f\-a("f-a")}
 
-<#assign \-\-\-\.\: = 'dash-dash-dash etc.'>
-${\-\-\-\.\:}
-${.vars['---.:']}
+<#assign \-\-\-\.\:\# = 'dash-dash-dash etc.'>
+${\-\-\-\.\:\#}
+${.vars['---.:#']}
 <#assign hash = { '--moz-prop': 'propVal' }>
 ${hash.\-\-moz\-prop}
 ${hash['--moz-prop']}
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/if.ftl b/src/test/resources/freemarker/test/templatesuite/templates/if.ftl
index 97c3f4b..71013ae 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/if.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/if.ftl
@@ -103,7 +103,7 @@
 </#list></#list>
 
 <#-- parsing errors -->
-<@assertFails message="valid #if-#elseif-#else"><@"<#if t><#else><#elseif t2></#if>"?interpret /></@>
-<@assertFails message="valid #if-#elseif-#else"><@"<#if t><#else><#else></#if>"?interpret /></@>
-<@assertFails message="valid #if-#elseif-#else"><@"<#else></#else>"?interpret /></@>
-<@assertFails message="valid #if-#elseif-#else"><@"<#elseif t></#elseif>"?interpret /></@>
+<@assertFails message="<#elseif"><@"<#if t><#else><#elseif t2></#if>"?interpret /></@>
+<@assertFails message="<#else>"><@"<#if t><#else><#else></#if>"?interpret /></@>
+<@assertFails message="<#else>"><@"<#else></#else>"?interpret /></@>
+<@assertFails message="<#elseif"><@"<#elseif t></#elseif>"?interpret /></@>
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/setting.ftl b/src/test/resources/freemarker/test/templatesuite/templates/setting.ftl
index fcf9605..715bbe2 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/setting.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/setting.ftl
@@ -37,8 +37,10 @@
 <@assertEquals expected='dtf' actual=.now?string />
 
 <#setting time_zone='GMT+00'>
+<@assertEquals expected='GMT+00:00' actual=.time_zone />
 <#assign t1='2000'?datetime('yyyy')>
 <#setting time_zone='GMT+01'>
+<@assertEquals expected='GMT+01:00' actual=.time_zone />
 <#assign t2='2000'?datetime('yyyy')>
 <@assertEquals expected=1000*60*60 actual=t1?long-t2?long />
 
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl b/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
index 50416c9..abf53df 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/specialvars.ftl
@@ -26,6 +26,7 @@
 <#assign works = .globals>
 ${.lang} == en
 ${.locale} == en_US
+${.time_zone} == GMT+01:00
 <#assign works = .locals!>
 <#assign works = .main>
 <#assign works = .node!>