freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [08/12] incubator-freemarker git commit: Formatting to markup with ${numOrDate} and `something + numOrDate`. Incomplete: (a) String built-ins should fail instead of formatting to plain text; (a) More testing needed, like for TemplateDateFormatter-s
Date Thu, 24 Sep 2015 20:23:55 GMT
Formatting to markup with ${numOrDate} and `something + numOrDate`. Incomplete: (a) String built-ins should fail instead of formatting to plain text; (a) More testing needed, like for TemplateDateFormatter-s


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/b6bd3c2c
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/b6bd3c2c
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/b6bd3c2c

Branch: refs/heads/master
Commit: b6bd3c2cc0abb871f0a59c1e7115f76e45cfeb53
Parents: dde63a1
Author: ddekany <ddekany@apache.org>
Authored: Sun Sep 20 17:51:22 2015 +0200
Committer: ddekany <ddekany@apache.org>
Committed: Wed Sep 23 08:29:25 2015 +0200

----------------------------------------------------------------------
 .../freemarker/core/AddConcatExpression.java    |  56 ++++----
 src/main/java/freemarker/core/Assignment.java   |   9 +-
 .../core/BuiltInForLegacyEscaping.java          |  11 +-
 .../core/BuiltInsForMultipleTypes.java          |  17 ++-
 .../core/BuiltInsForOutputFormatRelated.java    |  21 ++-
 .../core/BuiltInsForStringsEncoding.java        |  25 ++++
 .../java/freemarker/core/DollarVariable.java    |  33 +++--
 src/main/java/freemarker/core/Environment.java  |  52 +++----
 src/main/java/freemarker/core/EvalUtil.java     | 107 ++++++++++++---
 src/main/java/freemarker/core/Expression.java   |   4 -
 .../core/JavaTemplateNumberFormat.java          |   2 +-
 src/main/java/freemarker/core/MessageUtil.java  |  27 +++-
 .../java/freemarker/core/NumberLiteral.java     |   2 +-
 .../freemarker/core/TemplateDateFormat.java     |   4 +-
 .../freemarker/core/TemplateFormatUtil.java     |  13 +-
 .../freemarker/core/TemplateNumberFormat.java   |  12 +-
 src/main/javacc/FTL.jj                          |  15 ++-
 .../core/BaseNTemplateNumberFormatFactory.java  |   2 +-
 .../core/HexTemplateNumberFormatFactory.java    |   2 +-
 ...aleSensitiveTemplateNumberFormatFactory.java |   2 +-
 .../java/freemarker/core/NumberFormatTest.java  |  31 ++++-
 .../PrintfGTemplateNumberFormatFactory.java     | 135 +++++++++++++++++++
 22 files changed, 440 insertions(+), 142 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/AddConcatExpression.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/AddConcatExpression.java b/src/main/java/freemarker/core/AddConcatExpression.java
index 3c12da9..c90cb19 100644
--- a/src/main/java/freemarker/core/AddConcatExpression.java
+++ b/src/main/java/freemarker/core/AddConcatExpression.java
@@ -45,15 +45,17 @@ final class AddConcatExpression extends Expression {
 
     private final Expression left;
     private final Expression right;
+    private final MarkupOutputFormat markupOutputFormat;
 
-    AddConcatExpression(Expression left, Expression right) {
+    AddConcatExpression(Expression left, Expression right, MarkupOutputFormat markupOutputFormat) {
         this.left = left;
         this.right = right;
+        this.markupOutputFormat = markupOutputFormat;
     }
 
     @Override
     TemplateModel _eval(Environment env) throws TemplateException {
-        return _eval(env, this, left, left.eval(env), right, right.eval(env));
+        return _eval(env, this, left, left.eval(env), right, right.eval(env), markupOutputFormat);
     }
 
     /**
@@ -65,7 +67,8 @@ final class AddConcatExpression extends Expression {
     static TemplateModel _eval(Environment env,
             TemplateObject parent,
             Expression leftExp, TemplateModel leftModel,
-            Expression rightExp, TemplateModel rightModel)
+            Expression rightExp, TemplateModel rightModel,
+            MarkupOutputFormat markupOutputFormat)
             throws TemplateModelException, TemplateException, NonStringException {
         if (leftModel instanceof TemplateNumberModel && rightModel instanceof TemplateNumberModel) {
             Number first = EvalUtil.modelToNumber((TemplateNumberModel) leftModel, leftExp);
@@ -75,27 +78,33 @@ final class AddConcatExpression extends Expression {
             return new ConcatenatedSequence((TemplateSequenceModel) leftModel, (TemplateSequenceModel) rightModel);
         } else {
             try {
-                String leftStr = EvalUtil.coerceModelToString(leftModel, leftExp, (String) null, true, env);
-                String rightStr = EvalUtil.coerceModelToString(rightModel, rightExp, (String) null, true, env);
-                
-                if (leftStr == null) {  // Signals that the model is markup output 
-                    if (leftModel instanceof TemplateMarkupOutputModel) {
-                        TemplateMarkupOutputModel<?> leftMO = (TemplateMarkupOutputModel<?>) leftModel; 
-                        if (rightStr == null) {  // Signals that the model is markup output
-                            return concatMarkupOutputs(parent, leftMO, (TemplateMarkupOutputModel) rightModel);
-                        }
-                        return concatMarkupOutputs(parent, leftMO, leftMO.getOutputFormat().fromPlainTextByEscaping(rightStr));
-                    } else {
-                        leftStr = "null";  // For B.C. only; should be an error 
-                        // Falls through
+                Object leftOMOrStr = EvalUtil.coerceModelToMarkupOutputOrString(
+                        leftModel, leftExp, (String) null, markupOutputFormat, env);
+                Object rightOMOrStr = EvalUtil.coerceModelToMarkupOutputOrString(
+                        rightModel, rightExp, (String) null, markupOutputFormat, env);
+                // TODO prove that neither can be null
+
+                if (leftOMOrStr instanceof String) {
+                    if (rightOMOrStr instanceof String) {
+                        return new SimpleScalar(((String) leftOMOrStr).concat((String) rightOMOrStr));
+                    } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
+                        TemplateMarkupOutputModel<?> rightMO = (TemplateMarkupOutputModel<?>) rightOMOrStr; 
+                        return concatMarkupOutputs(parent,
+                                rightMO.getOutputFormat().fromPlainTextByEscaping((String) leftOMOrStr),
+                                rightMO);
+                    }                    
+                } else { // leftOMOrStr instanceof TemplateMarkupOutputModel 
+                    TemplateMarkupOutputModel<?> leftMO = (TemplateMarkupOutputModel<?>) leftOMOrStr; 
+                    if (rightOMOrStr instanceof String) {  // markup output
+                        return concatMarkupOutputs(parent,
+                                leftMO,
+                                leftMO.getOutputFormat().fromPlainTextByEscaping((String) rightOMOrStr));
+                    } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
+                        return concatMarkupOutputs(parent,
+                                leftMO,
+                                (TemplateMarkupOutputModel) rightOMOrStr);
                     }
                 }
-                if (rightStr == null) {  // Signals that the model is markup output
-                    TemplateMarkupOutputModel<?> rightMO = (TemplateMarkupOutputModel<?>) rightModel; 
-                    return concatMarkupOutputs(parent, rightMO.getOutputFormat().fromPlainTextByEscaping(leftStr), rightMO);
-                }
-                
-                return new SimpleScalar(leftStr.concat(rightStr));
             } catch (NonStringOrTemplateOutputException e) {
                 if (leftModel instanceof TemplateHashModel && rightModel instanceof TemplateHashModel) {
                     if (leftModel instanceof TemplateHashModelEx && rightModel instanceof TemplateHashModelEx) {
@@ -161,7 +170,8 @@ final class AddConcatExpression extends Expression {
             String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
     	return new AddConcatExpression(
     	left.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
-    	right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
+    	right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
+    	markupOutputFormat);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/Assignment.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/Assignment.java b/src/main/java/freemarker/core/Assignment.java
index 0521250..8f5206b 100644
--- a/src/main/java/freemarker/core/Assignment.java
+++ b/src/main/java/freemarker/core/Assignment.java
@@ -41,6 +41,7 @@ final class Assignment extends TemplateElement {
     private final String variableName;
     private final int operatorType;
     private final Expression valueExp;
+    private final MarkupOutputFormat markupOutputFormat;
     private Expression namespaceExp;
 
     static final int NAMESPACE = 1;
@@ -57,7 +58,8 @@ final class Assignment extends TemplateElement {
     Assignment(String variableName,
             int operator,
             Expression valueExp,
-            int scope) {
+            int scope,
+            MarkupOutputFormat markupOutputFormat) {
         this.scope = scope;
         
         this.variableName = variableName;
@@ -93,6 +95,8 @@ final class Assignment extends TemplateElement {
         }
         
         this.valueExp = valueExp;
+        
+        this.markupOutputFormat = markupOutputFormat;
     }
     
     void setNamespaceExp(Expression namespaceExp) {
@@ -165,7 +169,8 @@ final class Assignment extends TemplateElement {
                         throw InvalidReferenceException.getInstance(valueExp, env);
                     }
                 }
-                value = AddConcatExpression._eval(env, namespaceExp, null, lhoValue, valueExp, value);
+                value = AddConcatExpression._eval(env,
+                        namespaceExp, null, lhoValue, valueExp, value, markupOutputFormat);
             } else {  // Numerical operation
                 Number lhoNumber;
                 if (lhoValue instanceof TemplateNumberModel) {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/BuiltInForLegacyEscaping.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/BuiltInForLegacyEscaping.java b/src/main/java/freemarker/core/BuiltInForLegacyEscaping.java
index bd1c19e..2ccd491 100644
--- a/src/main/java/freemarker/core/BuiltInForLegacyEscaping.java
+++ b/src/main/java/freemarker/core/BuiltInForLegacyEscaping.java
@@ -31,17 +31,20 @@ abstract class BuiltInForLegacyEscaping extends BuiltInBannedWhenAutoEscaping {
     TemplateModel _eval(Environment env)
     throws TemplateException {
         TemplateModel tm = target.eval(env);
-        String targetString = EvalUtil.coerceModelToString(tm, target, null, true, env);
-        if (targetString == null) {
-            TemplateMarkupOutputModel<?> mo = (TemplateMarkupOutputModel<?>) tm;
+        Object moOrStr = EvalUtil.coerceModelToMarkupOutputOrString(tm, target, null, getMarkupOutputFormat(), env);
+        if (moOrStr instanceof String) {
+            return calculateResult((String) moOrStr, env);
+        } else {
+            TemplateMarkupOutputModel<?> mo = (TemplateMarkupOutputModel<?>) moOrStr;
             if (mo.getOutputFormat().isLegacyBuiltInBypassed(key)) {
                 return mo;
             }
             throw new NonStringException(target, tm, env);
         }
-        return calculateResult(targetString, env);
     }
     
     abstract TemplateModel calculateResult(String s, Environment env) throws TemplateException;
     
+    abstract MarkupOutputFormat getMarkupOutputFormat();
+    
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index 6a6a86e..5a0ee73 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -560,7 +560,7 @@ class BuiltInsForMultipleTypes {
             private TemplateModel formatWith(String key)
             throws TemplateModelException {
                 try {
-                    return new SimpleScalar(env.formatDate(dateModel, key, target, stringBI.this, true));
+                    return new SimpleScalar(env.formatDateToString(dateModel, key, target, stringBI.this, true));
                 } catch (TemplateException e) {
                     // `e` should always be a TemplateModelException here, but to be sure: 
                     throw _CoreAPI.ensureIsTemplateModelException("Failed to format value", e); 
@@ -580,7 +580,12 @@ class BuiltInsForMultipleTypes {
                         }
                         cachedValue = defaultFormat.format(dateModel);
                     } catch (TemplateValueFormatException e) {
-                        throw MessageUtil.newCantFormatDateException(target, e);
+                        try {
+                            throw MessageUtil.newCantFormatDateException(defaultFormat, target, e, true);
+                        } catch (TemplateException e2) {
+                            // `e` should always be a TemplateModelException here, but to be sure: 
+                            throw _CoreAPI.ensureIsTemplateModelException("Failed to format date/time/datetime", e2); 
+                        }
                     }
                 }
                 return cachedValue;
@@ -633,9 +638,9 @@ class BuiltInsForMultipleTypes {
                 String result;
                 try {
                     if (format instanceof BackwardCompatibleTemplateNumberFormat) {
-                        result = env.formatNumber(number, (BackwardCompatibleTemplateNumberFormat) format, target);
+                        result = env.formatNumberToString(number, (BackwardCompatibleTemplateNumberFormat) format, target);
                     } else {
-                        result = env.formatNumber(numberModel, format, target, true);
+                        result = env.formatNumberToString(numberModel, format, target, true);
                     }
                 } catch (TemplateException e) {
                     // `e` should always be a TemplateModelException here, but to be sure: 
@@ -649,10 +654,10 @@ class BuiltInsForMultipleTypes {
                 if (cachedValue == null) {
                     try {
                         if (defaultFormat instanceof BackwardCompatibleTemplateNumberFormat) {
-                            cachedValue = env.formatNumber(
+                            cachedValue = env.formatNumberToString(
                                     number, (BackwardCompatibleTemplateNumberFormat) defaultFormat, target);
                         } else {
-                            cachedValue = env.formatNumber(numberModel, defaultFormat, target, true);
+                            cachedValue = env.formatNumberToString(numberModel, defaultFormat, target, true);
                         }
                     } catch (TemplateException e) {
                         // `e` should always be a TemplateModelException here, but to be sure: 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/BuiltInsForOutputFormatRelated.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/BuiltInsForOutputFormatRelated.java b/src/main/java/freemarker/core/BuiltInsForOutputFormatRelated.java
index a9b5d8d..556d12b 100644
--- a/src/main/java/freemarker/core/BuiltInsForOutputFormatRelated.java
+++ b/src/main/java/freemarker/core/BuiltInsForOutputFormatRelated.java
@@ -48,24 +48,24 @@ class BuiltInsForOutputFormatRelated {
         @Override
         protected TemplateModel calculateResult(Environment env) throws TemplateException {
             TemplateModel lhoTM = target.eval(env);
-            String lhoStr = EvalUtil.coerceModelToString(lhoTM, target, null, true, env);
             MarkupOutputFormat contextOF = outputFormat;
-            if (lhoStr == null) { // should indicate that lhoTM is a TemplateMarkupOutputModel
-                TemplateMarkupOutputModel lhoMO;
-                try {
-                    lhoMO = (TemplateMarkupOutputModel) lhoTM;
-                } catch (ClassCastException e) {
+            Object lhoMOOrStr = EvalUtil.coerceModelToMarkupOutputOrString(lhoTM, target, null, contextOF, env);
+            if (lhoMOOrStr instanceof String) { // TemplateMarkupOutputModel
+                return calculateResult((String) lhoMOOrStr, contextOF, env);
+            } else {
+                if (lhoMOOrStr == null) {
                     throw EvalUtil.newModelHasStoredNullException(null, lhoTM, target);
                 }
+                TemplateMarkupOutputModel lhoMO = (TemplateMarkupOutputModel) lhoMOOrStr;
                 MarkupOutputFormat lhoOF = lhoMO.getOutputFormat();
                 // ATTENTION: Keep this logic in sync. with ${...}'s logic!
                 if (lhoOF == contextOF || contextOF.isOutputFormatMixingAllowed()) {
                     // bypass
-                    return lhoTM;
+                    return lhoMO;
                 } else {
                     // ATTENTION: Keep this logic in sync. with ${...}'s logic!
-                    lhoStr = lhoOF.getSourcePlainText(lhoMO);
-                    if (lhoStr == null) {
+                    String lhoPlainTtext = lhoOF.getSourcePlainText(lhoMO);
+                    if (lhoPlainTtext == null) {
                         throw new _TemplateModelException(target,
                                 "The left side operand of ?", key, " is in ", new _DelayedToString(lhoOF),
                                 " format, which differs from the current output format, ",
@@ -73,10 +73,9 @@ class BuiltInsForOutputFormatRelated {
                     }
                     // Here we know that lho is escaped plain text. So we re-escape it to the current format and
                     // bypass it, just as if the two output formats were the same earlier.
-                    return contextOF.fromPlainTextByEscaping(lhoStr);
+                    return contextOF.fromPlainTextByEscaping(lhoPlainTtext);
                 }
             }
-            return calculateResult(lhoStr, contextOF, env);
         }
         
         protected abstract TemplateModel calculateResult(String lho, MarkupOutputFormat outputFormat, Environment env)

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/BuiltInsForStringsEncoding.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsEncoding.java b/src/main/java/freemarker/core/BuiltInsForStringsEncoding.java
index c47f9ec..f182430 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsEncoding.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsEncoding.java
@@ -39,6 +39,11 @@ class BuiltInsForStringsEncoding {
             TemplateModel calculateResult(String s, Environment env) {
                 return new SimpleScalar(StringUtil.HTMLEnc(s));
             }
+
+            @Override
+            MarkupOutputFormat getMarkupOutputFormat() {
+                return HTMLOutputFormat.INSTANCE;
+            }
         }
         
         private final BIBeforeICI2d3d20 prevICIObj = new BIBeforeICI2d3d20();
@@ -55,6 +60,11 @@ class BuiltInsForStringsEncoding {
         public Object getPreviousICIChainMember() {
             return prevICIObj;
         }
+
+        @Override
+        MarkupOutputFormat getMarkupOutputFormat() {
+            return HTMLOutputFormat.INSTANCE;
+        }
     }
 
     static class j_stringBI extends BuiltInForString {
@@ -83,6 +93,11 @@ class BuiltInsForStringsEncoding {
         TemplateModel calculateResult(String s, Environment env) {
             return new SimpleScalar(StringUtil.RTFEnc(s));
         }
+
+        @Override
+        MarkupOutputFormat getMarkupOutputFormat() {
+            return RTFOutputFormat.INSTANCE;
+        }
     }
 
     static class urlBI extends BuiltInForString {
@@ -134,6 +149,11 @@ class BuiltInsForStringsEncoding {
         TemplateModel calculateResult(String s, Environment env) {
             return new SimpleScalar(StringUtil.XHTMLEnc(s));
         }
+
+        @Override
+        MarkupOutputFormat getMarkupOutputFormat() {
+            return XMLOutputFormat.INSTANCE; // TODO XHTMLOutputFormat
+        }
     }
 
     static class xmlBI extends BuiltInForLegacyEscaping {
@@ -141,6 +161,11 @@ class BuiltInsForStringsEncoding {
         TemplateModel calculateResult(String s, Environment env) {
             return new SimpleScalar(StringUtil.XMLEnc(s));
         }
+
+        @Override
+        MarkupOutputFormat getMarkupOutputFormat() {
+            return XMLOutputFormat.INSTANCE;
+        }
     }
 
     // Can't be instantiated

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/DollarVariable.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/DollarVariable.java b/src/main/java/freemarker/core/DollarVariable.java
index a277a68..d08f37c 100644
--- a/src/main/java/freemarker/core/DollarVariable.java
+++ b/src/main/java/freemarker/core/DollarVariable.java
@@ -38,15 +38,18 @@ final class DollarVariable extends Interpolation {
     
     /** For OutputFormat-based auto-escaping */
     private final OutputFormat outputFormat;
-    private final MarkupOutputFormat autoEscapeOutputFormat;
+    private final MarkupOutputFormat markupOutputFormat;
+    private final boolean autoEscape;
 
     DollarVariable(
             Expression expression, Expression escapedExpression,
-            OutputFormat outputFormat, MarkupOutputFormat autoEscapeOutputFormat) {
+            OutputFormat outputFormat, boolean autoEscape) {
         this.expression = expression;
         this.escapedExpression = escapedExpression;
         this.outputFormat = outputFormat;
-        this.autoEscapeOutputFormat = autoEscapeOutputFormat;
+        this.markupOutputFormat
+                = (MarkupOutputFormat) (outputFormat instanceof MarkupOutputFormat ? outputFormat : null);
+        this.autoEscape = autoEscape;
     }
 
     /**
@@ -54,26 +57,28 @@ final class DollarVariable extends Interpolation {
      */
     @Override
     void accept(Environment env) throws TemplateException, IOException {
-        TemplateModel tm = escapedExpression.eval(env);
-        Writer out = env.getOut();
-        String s = EvalUtil.coerceModelToString(tm, escapedExpression, null, true, env);
-        if (s != null) {
-            if (autoEscapeOutputFormat != null) {
-                autoEscapeOutputFormat.output(s, out);
+        final TemplateModel tm = escapedExpression.eval(env);
+        final Writer out = env.getOut();
+        final Object moOrStr = EvalUtil.coerceModelToMarkupOutputOrString(
+                tm, escapedExpression, null, markupOutputFormat, out, env);
+        if (moOrStr instanceof String) {
+            final String s = (String) moOrStr;
+            if (autoEscape) {
+                markupOutputFormat.output(s, out);
             } else {
                 out.write(s);
             }
-        } else {
-            TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) tm;
-            MarkupOutputFormat moOF = mo.getOutputFormat();
+        } else if (moOrStr != null) { // moOrStr wasn't output yet
+            final TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) moOrStr;
+            final MarkupOutputFormat moOF = mo.getOutputFormat();
             // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
             if (moOF != outputFormat && !outputFormat.isOutputFormatMixingAllowed()) {
-                String srcPlainText;
+                final String srcPlainText;
                 // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
                 srcPlainText = moOF.getSourcePlainText(mo);
                 if (srcPlainText == null) {
                     throw new _TemplateModelException(escapedExpression,
-                            "Tha value to print is in ", new _DelayedToString(moOF),
+                            "The value to print is in ", new _DelayedToString(moOF),
                             " format, which differs from the current output format, ",
                             new _DelayedToString(outputFormat), ". Format conversion wasn't possible.");
                 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/Environment.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 1e6aa73..5078eb7 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -1033,20 +1033,9 @@ public final class Environment extends Configurable {
      * @param exp
      *            The blamed expression if an error occurs; it's only needed for better error messages
      */
-    String formatNumber(TemplateNumberModel number, Expression exp, boolean useTempModelExc) throws TemplateException {
-        return formatNumber(number, getTemplateNumberFormat(exp, useTempModelExc), exp, useTempModelExc);
-    }
-
-    /**
-     * Format number with the number format specified as the parameter, with the current locale.
-     * 
-     * @param exp
-     *            The blamed expression if an error occurs; it's only needed for better error messages
-     */
-    String formatNumber(
-            TemplateNumberModel number, String formatString, Expression exp,
-            boolean useTempModelExc) throws TemplateException {
-        return formatNumber(number, getTemplateNumberFormat(formatString, exp, useTempModelExc), exp, useTempModelExc);
+    String formatNumberToString(TemplateNumberModel number, Expression exp, boolean useTempModelExc)
+            throws TemplateException {
+        return formatNumberToString(number, getTemplateNumberFormat(exp, useTempModelExc), exp, useTempModelExc);
     }
 
     /**
@@ -1055,19 +1044,14 @@ public final class Environment extends Configurable {
      * @param exp
      *            The blamed expression if an error occurs; it's only needed for better error messages
      */
-    String formatNumber(
+    String formatNumberToString(
             TemplateNumberModel number, TemplateNumberFormat format, Expression exp,
             boolean useTempModelExc)
             throws TemplateException {
         try {
             return format.format(number);
         } catch (TemplateValueFormatException e) {
-            _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
-                    "Failed to format number with format ", new _DelayedJQuote(format.getDescription()), ": ",
-                    e.getMessage())
-                    .blame(exp); 
-            throw useTempModelExc
-                    ? new _TemplateModelException(e, this, desc) : new _MiscTemplateException(e, this, desc);
+            throw MessageUtil.newCantFormatNumberException(format, exp, e, useTempModelExc);
         }
     }
 
@@ -1077,7 +1061,7 @@ public final class Environment extends Configurable {
      * @param exp
      *            The blamed expression if an error occurs; it's only needed for better error messages
      */
-    String formatNumber(Number number, BackwardCompatibleTemplateNumberFormat format, Expression exp)
+    String formatNumberToString(Number number, BackwardCompatibleTemplateNumberFormat format, Expression exp)
             throws TemplateModelException, _MiscTemplateException {
         try {
             return format.format(number);
@@ -1335,18 +1319,14 @@ public final class Environment extends Configurable {
      * @param tdmSourceExpr
      *            The blamed expression if an error occurs; only used for error messages.
      */
-    String formatDate(TemplateDateModel tdm, Expression tdmSourceExpr,
+    String formatDateToString(TemplateDateModel tdm, Expression tdmSourceExpr,
             boolean useTempModelExc) throws TemplateException {
-        Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr);
-        
-        TemplateDateFormat format = getTemplateDateFormat(
-                tdm.getDateType(), date.getClass(), tdmSourceExpr,
-                useTempModelExc);
+        TemplateDateFormat format = getTemplateDateFormat(tdm, tdmSourceExpr, useTempModelExc);
         
         try {
             return format.format(tdm);
         } catch (TemplateValueFormatException e) {
-            throw MessageUtil.newCantFormatDateException(tdmSourceExpr, e);
+            throw MessageUtil.newCantFormatDateException(format, tdmSourceExpr, e, useTempModelExc);
         }
     }
 
@@ -1356,7 +1336,7 @@ public final class Environment extends Configurable {
      * @param blamedFormatterExp
      *            The blamed expression if an error occurs; only used for error messages.
      */
-    String formatDate(TemplateDateModel tdm, String formatString,
+    String formatDateToString(TemplateDateModel tdm, String formatString,
             Expression blamedDateSourceExp, Expression blamedFormatterExp,
             boolean useTempModelExc) throws TemplateException {
         Date date = EvalUtil.modelToDate(tdm, blamedDateSourceExp);
@@ -1369,7 +1349,7 @@ public final class Environment extends Configurable {
         try {
             return format.format(tdm);
         } catch (TemplateValueFormatException e) {
-            throw MessageUtil.newCantFormatDateException(blamedDateSourceExp, e);
+            throw MessageUtil.newCantFormatDateException(format, blamedDateSourceExp, e, useTempModelExc);
         }
     }
 
@@ -1537,6 +1517,16 @@ public final class Environment extends Configurable {
         return getTemplateDateFormatWithoutCache(formatString, dateType, locale, timeZone, zonelessInput);
     }
     
+    TemplateDateFormat getTemplateDateFormat(TemplateDateModel tdm, Expression tdmSourceExpr, boolean useTempModelExc)
+            throws TemplateModelException, TemplateException {
+        Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr);
+        
+        TemplateDateFormat format = getTemplateDateFormat(
+                tdm.getDateType(), date.getClass(), tdmSourceExpr,
+                useTempModelExc);
+        return format;
+    }
+
     /**
      * Same as {@link #getTemplateDateFormat(int, Class)}, but translates the exceptions to {@link TemplateException}-s.
      */

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/EvalUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/EvalUtil.java b/src/main/java/freemarker/core/EvalUtil.java
index ff41523..517a29e 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -19,6 +19,8 @@
 
 package freemarker.core;
 
+import java.io.IOException;
+import java.io.Writer;
 import java.util.Date;
 
 import freemarker.ext.beans.BeanModel;
@@ -44,7 +46,7 @@ class EvalUtil {
     static final int CMP_OP_LESS_THAN_EQUALS = 5;
     static final int CMP_OP_GREATER_THAN_EQUALS = 6;
     // If you add a new operator here, update the "compare" and "cmpOpToString" methods!
-
+    
     // Prevents instantination.
     private EvalUtil() { }
     
@@ -341,24 +343,93 @@ class EvalUtil {
         }
     }
 
-    static String coerceModelToString(TemplateModel tm, Expression exp, String seqHint, Environment env) throws TemplateException {
-        return coerceModelToString(tm, exp, seqHint, false, env);
-    }
-    
-    /**
-     * @param allowTOM
-     *            Instead of throwing exception, return {@code null} for a {@link TemplateMarkupOutputModel}.
-     */
     static String coerceModelToString(TemplateModel tm, Expression exp, String seqHint,
-            boolean allowTOM,
             Environment env) throws TemplateException {
         if (tm instanceof TemplateNumberModel) {
-            return env.formatNumber((TemplateNumberModel) tm, exp, false);
+            return env.formatNumberToString((TemplateNumberModel) tm, exp, false);
+        } else if (tm instanceof TemplateDateModel) {
+            return env.formatDateToString((TemplateDateModel) tm, exp, false);
+        } else {
+            return coerceModelToStringCommon(tm, exp, seqHint, false, env);
+        }
+    }
+
+    static Object coerceModelToMarkupOutputOrString(TemplateModel tm, Expression exp, String seqHint,
+            MarkupOutputFormat markupOutputFormat, Environment env) throws TemplateException {
+        try {
+            return coerceModelToMarkupOutputOrString(tm, exp, seqHint, markupOutputFormat, null, env);
+        } catch (IOException e) {
+            throw new BugException("Unexpected exception", e);
+        }
+    }
+
+    static Object coerceModelToMarkupOutputOrString(TemplateModel tm, Expression exp, String seqHint,
+            MarkupOutputFormat markupOutputFormat, Writer out, Environment env) throws TemplateException, IOException {
+        if (tm instanceof TemplateNumberModel) {
+            TemplateNumberModel tnm = (TemplateNumberModel) tm; 
+            TemplateNumberFormat format = env.getTemplateNumberFormat(exp, false);
+            try {
+                if (markupOutputFormat != null) {
+                    // Try to return markup output:
+                    if (out == null) {
+                        TemplateMarkupOutputModel r = format.format(tnm, markupOutputFormat);
+                        if (r != null) {
+                            return r;
+                        }
+                        // Falls through
+                    } else {
+                        if (format.format(tnm, markupOutputFormat, out)) {
+                            return null;
+                        }
+                        // Falls through
+                    }
+                }
+                
+                // Return a String:
+                return format.format(tnm);
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatNumberException(format, exp, e, false);
+            }
         } else if (tm instanceof TemplateDateModel) {
-            return env.formatDate((TemplateDateModel) tm, exp, false);
-        } else if (allowTOM && tm instanceof TemplateMarkupOutputModel) {
-            return null;
-        } else if (tm instanceof TemplateScalarModel) {
+            TemplateDateModel tdm = (TemplateDateModel) tm;
+            TemplateDateFormat format = env.getTemplateDateFormat(tdm, exp, false);
+            try {
+                if (markupOutputFormat != null) {
+                    // Try to return markup output:
+                    if (out == null) {
+                        TemplateMarkupOutputModel r = format.format(tdm, markupOutputFormat);
+                        if (r != null) {
+                            return r;
+                        }
+                        // Falls through
+                    } else {
+                        if (format.format(tdm, markupOutputFormat, out)) {
+                            return null;
+                        }
+                        // Falls through
+                    }
+                }
+                
+                // Return a String:
+                return format.format(tdm);
+            } catch (TemplateValueFormatException e) {
+                throw MessageUtil.newCantFormatDateException(format, exp, e, false);
+            }
+        } else if (tm instanceof TemplateMarkupOutputModel) {
+            return tm;
+        } else { 
+            return coerceModelToStringCommon(tm, exp, seqHint, true, env);
+        }
+    }
+
+    /**
+     * @param supportsTOM
+     *            Whether the caller {@code coerceModelTo...} method could handle a {@link TemplateMarkupOutputModel}.
+     */
+    private static String coerceModelToStringCommon(TemplateModel tm, Expression exp, String seqHint, boolean supportsTOM,
+            Environment env) throws TemplateModelException, InvalidReferenceException, TemplateException,
+                    NonStringOrTemplateOutputException, NonStringException {
+        if (tm instanceof TemplateScalarModel) {
             return modelToString((TemplateScalarModel) tm, exp, env);
         } else if (tm == null) {
             if (env.isClassicCompatible()) {
@@ -399,13 +470,13 @@ class EvalUtil {
                 return _BeansAPI.getAsClassicCompatibleString((BeanModel) tm);
             }
             if (seqHint != null && (tm instanceof TemplateSequenceModel || tm instanceof TemplateCollectionModel)) {
-                if (allowTOM) {
+                if (supportsTOM) {
                     throw new NonStringOrTemplateOutputException(exp, tm, seqHint, env);
                 } else {
                     throw new NonStringException(exp, tm, seqHint, env);
                 }
             } else {
-                if (allowTOM) {
+                if (supportsTOM) {
                     throw new NonStringOrTemplateOutputException(exp, tm, env);
                 } else {
                     throw new NonStringException(exp, tm, env);
@@ -413,7 +484,7 @@ class EvalUtil {
             }
         }
     }
-    
+
     /**
      * Returns an {@link ArithmeticEngine} even if {@code env} is {@code null}, because we are in parsing phase.
      */

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/Expression.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/Expression.java b/src/main/java/freemarker/core/Expression.java
index fef08cd..c1d5355 100644
--- a/src/main/java/freemarker/core/Expression.java
+++ b/src/main/java/freemarker/core/Expression.java
@@ -93,10 +93,6 @@ abstract public class Expression extends TemplateObject {
         return EvalUtil.coerceModelToString(eval(env), this, seqTip, env);
     }
     
-    static String coerceModelToString(TemplateModel tm, Expression exp, Environment env) throws TemplateException {
-        return EvalUtil.coerceModelToString(tm, exp, null, env);
-    }
-    
     Number evalToNumber(Environment env) throws TemplateException {
         TemplateModel model = eval(env);
         return modelToNumber(model, env);

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/JavaTemplateNumberFormat.java b/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
index 4144af8..ccbfb9d 100644
--- a/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
@@ -40,7 +40,7 @@ final class JavaTemplateNumberFormat extends BackwardCompatibleTemplateNumberFor
     }
 
     @Override
-    public <MO extends TemplateMarkupOutputModel> MO format(TemplateNumberModel dateModel,
+    public <MO extends TemplateMarkupOutputModel> MO format(TemplateNumberModel numberModel,
             MarkupOutputFormat<MO> outputFormat) throws UnformattableValueException, TemplateModelException {
         return null;
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/MessageUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/MessageUtil.java b/src/main/java/freemarker/core/MessageUtil.java
index 2875aa3..3aebacc 100644
--- a/src/main/java/freemarker/core/MessageUtil.java
+++ b/src/main/java/freemarker/core/MessageUtil.java
@@ -296,13 +296,28 @@ class MessageUtil {
                 .tips(MessageUtil.UNKNOWN_DATE_TO_STRING_TIPS));
     }
 
-    static TemplateModelException newCantFormatDateException(
-            Expression dateSourceExpr, TemplateValueFormatException cause) {
-        return new _TemplateModelException(cause, null, new _ErrorDescriptionBuilder(
-                cause.getMessage())
-                .blame(dateSourceExpr));
+    static TemplateException newCantFormatDateException(TemplateDateFormat format, Expression dataSrcExp,
+            TemplateValueFormatException e, boolean useTempModelExc) {
+        _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                "Failed to format date/time/datetime with format ", new _DelayedJQuote(format.getDescription()), ": ",
+                e.getMessage())
+                .blame(dataSrcExp); 
+        return useTempModelExc
+                ? new _TemplateModelException(e, (Environment) null, desc)
+                : new _MiscTemplateException(e, (Environment) null, desc);
     }
-
+    
+    static TemplateException newCantFormatNumberException(TemplateNumberFormat format, Expression dataSrcExp,
+            TemplateValueFormatException e, boolean useTempModelExc) {
+        _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
+                "Failed to format number with format ", new _DelayedJQuote(format.getDescription()), ": ",
+                e.getMessage())
+                .blame(dataSrcExp); 
+        return useTempModelExc
+                ? new _TemplateModelException(e, (Environment) null, desc)
+                : new _MiscTemplateException(e, (Environment) null, desc);
+    }
+    
     /**
      * @return "a" or "an" or "a(n)" (or "" for empty string) for an FTL type name
      */

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/NumberLiteral.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/NumberLiteral.java b/src/main/java/freemarker/core/NumberLiteral.java
index f008aa9..91ded3c 100644
--- a/src/main/java/freemarker/core/NumberLiteral.java
+++ b/src/main/java/freemarker/core/NumberLiteral.java
@@ -43,7 +43,7 @@ final class NumberLiteral extends Expression implements TemplateNumberModel {
 
     @Override
     public String evalAndCoerceToString(Environment env) throws TemplateException {
-        return env.formatNumber(this, this, false);
+        return env.formatNumberToString(this, this, false);
     }
 
     public Number getAsNumber() {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/TemplateDateFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/TemplateDateFormat.java b/src/main/java/freemarker/core/TemplateDateFormat.java
index 980d543..cb58aac 100644
--- a/src/main/java/freemarker/core/TemplateDateFormat.java
+++ b/src/main/java/freemarker/core/TemplateDateFormat.java
@@ -43,11 +43,11 @@ public abstract class TemplateDateFormat extends TemplateValueFormat {
     
     /**
      * @param dateModel
-     *            The date/time/dateTime to format. Most implementations will just work with the return value of
+     *            The date/time/dateTime to format; not {@code null}. Most implementations will just work with the return value of
      *            {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of
      *            a custom {@link TemplateDateModel} implementation.
      * 
-     * @return The date/time/dateTime as text, with no escaping (like no HTML escaping). Can't be {@code null}.
+     * @return The date/time/dateTime as text, with no escaping (like no HTML escaping); can't be {@code null}.
      * 
      * @throws TemplateValueFormatException
      *             When a problem occurs during the formatting of the value. Notable subclass:

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/TemplateFormatUtil.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/TemplateFormatUtil.java b/src/main/java/freemarker/core/TemplateFormatUtil.java
index 66e10da..034d5cb 100644
--- a/src/main/java/freemarker/core/TemplateFormatUtil.java
+++ b/src/main/java/freemarker/core/TemplateFormatUtil.java
@@ -20,11 +20,14 @@ package freemarker.core;
 
 import java.util.Date;
 
+import freemarker.template.ObjectWrapper;
 import freemarker.template.TemplateDateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateNumberModel;
 
 /**
+ * Utility classes for implementing {@link TemplateValueFormat}-s.
+ * 
  * @since 2.3.24 
  */
 public final class TemplateFormatUtil {
@@ -42,8 +45,10 @@ public final class TemplateFormatUtil {
     }
 
     /**
-     * Utility method to extract the {@link Number} from an {@link TemplateNumberModel}, and throw
-     * {@link UnformattableValueException} with a standard error message if that's {@code null}.
+     * Utility method to extract the {@link Number} from an {@link TemplateNumberModel}, and throws
+     * {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateNumberModel}
+     * that store {@code null} are in principle not allowed, and so are considered to be bugs in the
+     * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation.
      */
     public static Number getNonNullNumber(TemplateNumberModel numberModel)
             throws TemplateModelException, UnformattableValueException {
@@ -56,7 +61,9 @@ public final class TemplateFormatUtil {
 
     /**
      * Utility method to extract the {@link Date} from an {@link TemplateDateModel}, and throw
-     * {@link UnformattableValueException} with a standard error message if that's {@code null}.
+     * {@link TemplateModelException} with a standard error message if that's {@code null}. {@link TemplateDateModel}
+     * that store {@code null} are in principle not allowed, and so are considered to be bugs in the
+     * {@link ObjectWrapper} or {@link TemplateNumberModel} implementation.
      */
     public static Date getNonNullDate(TemplateDateModel dateModel) throws TemplateModelException {
         Date date = dateModel.getAsDate();

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/java/freemarker/core/TemplateNumberFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/TemplateNumberFormat.java b/src/main/java/freemarker/core/TemplateNumberFormat.java
index 68d2c4d..3c29738 100644
--- a/src/main/java/freemarker/core/TemplateNumberFormat.java
+++ b/src/main/java/freemarker/core/TemplateNumberFormat.java
@@ -42,11 +42,11 @@ public abstract class TemplateNumberFormat extends TemplateValueFormat {
 
     /**
      * @param numberModel
-     *            The date/time/dateTime to format. Most implementations will just work with the return value of
+     *            The number to format; not {@code null}. Most implementations will just work with the return value of
      *            {@link TemplateDateModel#getAsDate()}, but some may format differently depending on the properties of
      *            a custom {@link TemplateDateModel} implementation.
-     * 
-     * @return The date/time/dateTime as text, with no escaping (like no HTML escaping). Can't be {@code null}.
+     *            
+     * @return The number as text, with no escaping (like no HTML escaping); can't be {@code null}.
      * 
      * @throws TemplateValueFormatException
      *             If any problem occurs while parsing/getting the format. Notable subclass:
@@ -64,7 +64,7 @@ public abstract class TemplateNumberFormat extends TemplateValueFormat {
      * {@link #format(TemplateNumberModel)} escaped, it should return {@code null}.
      */
     public abstract <MO extends TemplateMarkupOutputModel> MO format(
-            TemplateNumberModel dateModel, MarkupOutputFormat<MO> outputFormat)
+            TemplateNumberModel numberModel, MarkupOutputFormat<MO> outputFormat)
                     throws TemplateValueFormatException, TemplateModelException;
     
     /**
@@ -79,9 +79,9 @@ public abstract class TemplateNumberFormat extends TemplateValueFormat {
      * {@link #format(TemplateNumberModel, MarkupOutputFormat)} and writes its result to the {@link Writer}.
      */
     public <MO extends TemplateMarkupOutputModel> boolean format(
-            TemplateNumberModel dateModel, MarkupOutputFormat<MO> outputFormat, Writer out)
+            TemplateNumberModel numberModel, MarkupOutputFormat<MO> outputFormat, Writer out)
                     throws TemplateValueFormatException, TemplateModelException, IOException {
-        MO mo = format(dateModel, outputFormat);
+        MO mo = format(numberModel, outputFormat);
         if (mo == null) {
             return false;
         }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index c36bad7..33c8ecb 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -326,6 +326,10 @@ public class FMParser {
             autoEscaping = false;
         }
     }
+    
+    MarkupOutputFormat getMarkupOutputFormat() {
+        return outputFormat instanceof MarkupOutputFormat ? (MarkupOutputFormat) outputFormat : null;
+    }
 
     /**
      * Don't use it, unless you are developing FreeMarker itself.
@@ -1661,7 +1665,7 @@ Expression AdditiveExpression() :
             if (plus) {
 	            // plus is treated separately, since it is also
 	            // used for concatenation.
-                result = new AddConcatExpression(lhs, rhs);
+                result = new AddConcatExpression(lhs, rhs, getMarkupOutputFormat());
             } else {
                 numberLiteralOnly(lhs);
                 numberLiteralOnly(rhs);
@@ -2322,8 +2326,7 @@ DollarVariable StringOutput() :
         DollarVariable result = new DollarVariable(
                 exp, escapedExpression(exp),
                 outputFormat,
-                autoEscaping && outputFormat instanceof MarkupOutputFormat
-                        ? (MarkupOutputFormat)  outputFormat : null);
+                autoEscaping);
         result.setLocation(template, begin, end);
         return result;
     }
@@ -2930,7 +2933,7 @@ TemplateElement Assign() :
 		        )
 	        )
 	        {
-	            ass = new Assignment(varName, equalsOp.kind, exp, scope);
+	            ass = new Assignment(varName, equalsOp.kind, exp, scope, getMarkupOutputFormat());
                 if (exp != null) {
                    ass.setLocation(template, nameExp, exp);
                 } else {
@@ -2970,7 +2973,7 @@ TemplateElement Assign() :
 	                )
 	            )
 	            {
-	                ass = new Assignment(varName, equalsOp.kind, exp, scope);
+	                ass = new Assignment(varName, equalsOp.kind, exp, scope, getMarkupOutputFormat());
 	                if (exp != null) {
 	                   ass.setLocation(template, nameExp, exp);
 	                } else {
@@ -3043,7 +3046,7 @@ TemplateElement Assign() :
 	        {
 	            BlockAssignment ba = new BlockAssignment(
 	                   block, varName, scope, nsExp,
-	                   outputFormat instanceof MarkupOutputFormat ? (MarkupOutputFormat) outputFormat : null);
+	                   getMarkupOutputFormat());
 	            ba.setLocation(template, start, end);
 	            return ba;
 	        }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/test/java/freemarker/core/BaseNTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/BaseNTemplateNumberFormatFactory.java b/src/test/java/freemarker/core/BaseNTemplateNumberFormatFactory.java
index d527aa0..18a06ea 100644
--- a/src/test/java/freemarker/core/BaseNTemplateNumberFormatFactory.java
+++ b/src/test/java/freemarker/core/BaseNTemplateNumberFormatFactory.java
@@ -96,7 +96,7 @@ public class BaseNTemplateNumberFormatFactory extends TemplateNumberFormatFactor
         }
 
         @Override
-        public <MO extends TemplateMarkupOutputModel> MO format(TemplateNumberModel dateModel,
+        public <MO extends TemplateMarkupOutputModel> MO format(TemplateNumberModel numberModel,
                 MarkupOutputFormat<MO> outputFormat) throws UnformattableValueException, TemplateModelException {
             return null;
         }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/test/java/freemarker/core/HexTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/HexTemplateNumberFormatFactory.java b/src/test/java/freemarker/core/HexTemplateNumberFormatFactory.java
index f18d97b..8687b58 100644
--- a/src/test/java/freemarker/core/HexTemplateNumberFormatFactory.java
+++ b/src/test/java/freemarker/core/HexTemplateNumberFormatFactory.java
@@ -57,7 +57,7 @@ public class HexTemplateNumberFormatFactory extends TemplateNumberFormatFactory
         }
 
         @Override
-        public <MO extends TemplateMarkupOutputModel> MO format(TemplateNumberModel dateModel,
+        public <MO extends TemplateMarkupOutputModel> MO format(TemplateNumberModel numberModel,
                 MarkupOutputFormat<MO> outputFormat) throws UnformattableValueException, TemplateModelException {
             return null;
         }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/test/java/freemarker/core/LocaleSensitiveTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/LocaleSensitiveTemplateNumberFormatFactory.java b/src/test/java/freemarker/core/LocaleSensitiveTemplateNumberFormatFactory.java
index 3fb2fae..fb989b9 100644
--- a/src/test/java/freemarker/core/LocaleSensitiveTemplateNumberFormatFactory.java
+++ b/src/test/java/freemarker/core/LocaleSensitiveTemplateNumberFormatFactory.java
@@ -58,7 +58,7 @@ public class LocaleSensitiveTemplateNumberFormatFactory extends TemplateNumberFo
         }
     
         @Override
-        public <MO extends TemplateMarkupOutputModel> MO format(TemplateNumberModel dateModel,
+        public <MO extends TemplateMarkupOutputModel> MO format(TemplateNumberModel numberModel,
                 MarkupOutputFormat<MO> outputFormat) throws UnformattableValueException, TemplateModelException {
             return null;
         }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/test/java/freemarker/core/NumberFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/NumberFormatTest.java b/src/test/java/freemarker/core/NumberFormatTest.java
index b30b72a..8db57dc 100644
--- a/src/test/java/freemarker/core/NumberFormatTest.java
+++ b/src/test/java/freemarker/core/NumberFormatTest.java
@@ -22,6 +22,8 @@ import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.*;
 
 import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.util.Collections;
 import java.util.Locale;
 import java.util.Map;
@@ -56,7 +58,8 @@ public class NumberFormatTest extends TemplateTest {
         cfg.setCustomNumberFormats(ImmutableMap.of(
                 "hex", HexTemplateNumberFormatFactory.INSTANCE,
                 "loc", LocaleSensitiveTemplateNumberFormatFactory.INSTANCE,
-                "base", BaseNTemplateNumberFormatFactory.INSTANCE));
+                "base", BaseNTemplateNumberFormatFactory.INSTANCE,
+                "printfG", PrintfGTemplateNumberFormatFactory.INSTANCE));
     }
 
     @Test
@@ -283,6 +286,32 @@ public class NumberFormatTest extends TemplateTest {
                 "1.0_en 1.0_en_GB 1.0_en_GB 1,0_fr_FR 1,0");
     }
     
+    @Test
+    public void testMarkupFormat() throws IOException, TemplateException {
+        getConfiguration().setNumberFormat("@printfG_3");
+
+        String commonFTL = "${1234567} ${'cat:' + 1234567} ${0.0000123}";
+        assertOutput(commonFTL,
+                "1.23E+06 cat:1.23E+06 1.23E-05");
+        assertOutput("<#ftl outputFormat='HTML'>" + commonFTL,
+                "1.23*10<sup>6</sup> cat:1.23*10<sup>6</sup> 1.23*10<sup>-5</sup>");
+        assertOutput("<#ftl outputFormat='HTML'>${\"" + commonFTL + "\"}",
+                "1.23E+06 cat:1.23E+06 1.23E-05");
+    }
+
+    @Test
+    public void testPrintG() throws IOException, TemplateException {
+        for (Number n : new Number[] {
+                1234567, 1234567L, 1234567d, 1234567f, BigInteger.valueOf(1234567), BigDecimal.valueOf(1234567) }) {
+            addToDataModel("n", n);
+            
+            assertOutput("${n?string.@printfG}", "1.23457E+06");
+            assertOutput("${n?string.@printfG_3}", "1.23E+06");
+            assertOutput("${n?string.@printfG_7}", "1234567");
+            assertOutput("${0.0000123?string.@printfG}", "1.23000E-05");
+        }
+    }
+    
     private static class MutableTemplateNumberModel implements TemplateNumberModel {
         
         private Number number;

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b6bd3c2c/src/test/java/freemarker/core/PrintfGTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/PrintfGTemplateNumberFormatFactory.java b/src/test/java/freemarker/core/PrintfGTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..a44dac3
--- /dev/null
+++ b/src/test/java/freemarker/core/PrintfGTemplateNumberFormatFactory.java
@@ -0,0 +1,135 @@
+/*
+ * 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.math.BigInteger;
+import java.util.Locale;
+
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * Formats like {@code %G} in {@code printf}, with the specified number of significant digits. Also has special
+ * formatter for HTML output format, where it uses the HTML "sup" element for exponents.
+ */
+public class PrintfGTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+
+    public static final PrintfGTemplateNumberFormatFactory INSTANCE = new PrintfGTemplateNumberFormatFactory();
+    
+    private PrintfGTemplateNumberFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws InvalidFormatParametersException {
+        Integer significantDigits;
+        if (!params.isEmpty()) {
+            try {
+                significantDigits = Integer.valueOf(params);
+            } catch (NumberFormatException e) {
+                throw new InvalidFormatParametersException(
+                        "The format parameter must be an integer, but was (shown quoted) "
+                        + StringUtil.jQuote(params) + ".");
+            }
+        } else {
+            // Use the default of %G
+            significantDigits = null;
+        }
+        return new PrintfGTemplateNumberFormat(significantDigits, locale);
+    }
+
+    private static class PrintfGTemplateNumberFormat extends TemplateNumberFormat {
+        
+        private final Locale locale;
+        private final String printfFormat; 
+
+        private PrintfGTemplateNumberFormat(Integer significantDigits, Locale locale) {
+            this.printfFormat = "%" + (significantDigits != null ? "." + significantDigits : "") + "G";
+            this.locale = locale;
+        }
+        
+        @Override
+        public String format(TemplateNumberModel numberModel)
+                throws UnformattableValueException, TemplateModelException {
+            final Number n = TemplateFormatUtil.getNonNullNumber(numberModel);
+            
+            // printf %G only accepts Double, BigDecimal and Float 
+            final Number gCompatibleN;
+            if (n instanceof Double  || n instanceof BigDecimal || n instanceof Float) {
+                gCompatibleN = n;
+            } else {
+                if (n instanceof BigInteger) {
+                    gCompatibleN = new BigDecimal((BigInteger) n);                        
+                } else if (n instanceof Long) {
+                    gCompatibleN = BigDecimal.valueOf(((Long) n).longValue());
+                } else {
+                    gCompatibleN = Double.valueOf(n.doubleValue());
+                }
+            }
+            
+            return String.format(locale, printfFormat, gCompatibleN);
+        }
+
+        @Override
+        public <MO extends TemplateMarkupOutputModel> MO format(TemplateNumberModel numberModel,
+                MarkupOutputFormat<MO> outputFormat) throws UnformattableValueException, TemplateModelException {
+            // TODO XHTMLOutputFormat
+            if (!(outputFormat instanceof HTMLOutputFormat)) {
+                return null;
+            }
+            
+            String s = StringUtil.XHTMLEnc(format(numberModel));
+            int eIdx = s.indexOf('E');
+            if (eIdx == -1) {
+                return outputFormat.fromMarkup(s);
+            }
+                
+            String expStr = s.substring(eIdx + 1);
+            int expNumStart = 0;
+            while (expNumStart < expStr.length() && isExpSignificantDigitPrefix(expStr.charAt(expNumStart))) {
+                expNumStart++;
+            }
+            
+            return outputFormat.fromMarkup(
+                    s.substring(0, eIdx)
+                    + "*10<sup>"
+                    + (expStr.charAt(0) == '-' ? "-" : "") + expStr.substring(expNumStart)
+                    + "</sup>");
+        }
+
+        private boolean isExpSignificantDigitPrefix(char c) {
+            return c == '+' || c == '-' || c == '0';
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return true;
+        }
+
+        @Override
+        public String getDescription() {
+            return "printf " + printfFormat;
+        }
+        
+    }
+
+}


Mime
View raw message