You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2015/10/04 16:49:31 UTC

[06/14] incubator-freemarker git commit: `${...}` inside string literals is equivalent to using the `+` operator again. This rule was broken by `+` supporting markup operands, while `${...}` inside string literals didn't. Now similarly as `"foo " + someM

`${...}` inside string literals is equivalent to using the `+` operator again. This rule was broken by `+` supporting markup operands, while `${...}` inside string literals didn't. Now similarly as `"foo " + someMarkup` works and gives a markup result, `"foo ${someMarkup}"` does too.


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

Branch: refs/heads/master
Commit: fa6ac0eea88c38c816298b518c5d249d4599f93e
Parents: d8487ff
Author: ddekany <dd...@apache.org>
Authored: Sun Oct 4 01:42:47 2015 +0200
Committer: ddekany <dd...@apache.org>
Committed: Sun Oct 4 01:43:45 2015 +0200

----------------------------------------------------------------------
 .../freemarker/core/AddConcatExpression.java    |  32 +-----
 .../java/freemarker/core/DollarVariable.java    |  10 +-
 src/main/java/freemarker/core/EvalUtil.java     |  28 ++++-
 .../java/freemarker/core/Interpolation.java     |   9 ++
 .../java/freemarker/core/NumericalOutput.java   |  18 ++-
 .../java/freemarker/core/ParameterRole.java     |   1 +
 .../java/freemarker/core/StringLiteral.java     | 105 +++++++++++------
 src/main/javacc/FTL.jj                          |  66 ++++++++++-
 src/manual/book.xml                             | 114 ++++++++-----------
 .../java/freemarker/core/NumberFormatTest.java  |   4 +-
 .../java/freemarker/core/OutputFormatTest.java  |  23 +++-
 .../core/StringLiteralInterpolationTest.java    |  23 ++++
 src/test/java/freemarker/test/TemplateTest.java |   6 +-
 src/test/resources/freemarker/core/ast-1.ast    |  19 ++--
 .../resources/freemarker/core/ast-builtins.ast  |   3 -
 .../resources/freemarker/core/ast-range.ast     |   2 -
 .../freemarker/core/ast-strlitinterpolation.ast |  72 ++++++++++--
 .../freemarker/core/ast-strlitinterpolation.ftl |   8 +-
 .../AutoEscapingExample-stringLiteral2.ftlh     |   2 +-
 .../AutoEscapingExample-stringLiteral2.ftlh.out |   2 +-
 20 files changed, 364 insertions(+), 183 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/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 7c1eb75..4dbfd78 100644
--- a/src/main/java/freemarker/core/AddConcatExpression.java
+++ b/src/main/java/freemarker/core/AddConcatExpression.java
@@ -85,18 +85,18 @@ final class AddConcatExpression extends Expression {
                         return new SimpleScalar(((String) leftOMOrStr).concat((String) rightOMOrStr));
                     } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
                         TemplateMarkupOutputModel<?> rightMO = (TemplateMarkupOutputModel<?>) rightOMOrStr; 
-                        return concatMarkupOutputs(parent,
+                        return EvalUtil.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,
+                        return EvalUtil.concatMarkupOutputs(parent,
                                 leftMO,
                                 leftMO.getOutputFormat().fromPlainTextByEscaping((String) rightOMOrStr));
                     } else { // rightOMOrStr instanceof TemplateMarkupOutputModel
-                        return concatMarkupOutputs(parent,
+                        return EvalUtil.concatMarkupOutputs(parent,
                                 leftMO,
                                 (TemplateMarkupOutputModel) rightOMOrStr);
                     }
@@ -124,32 +124,6 @@ final class AddConcatExpression extends Expression {
         }
     }
 
-    private static TemplateModel concatMarkupOutputs(TemplateObject parent, TemplateMarkupOutputModel leftMO,
-            TemplateMarkupOutputModel rightMO) throws TemplateModelException {
-        MarkupOutputFormat leftOF = leftMO.getOutputFormat();
-        MarkupOutputFormat rightOF = rightMO.getOutputFormat();
-        if (rightOF != leftOF) {
-            String rightPT;
-            String leftPT;
-            if ((rightPT = rightOF.getSourcePlainText(rightMO)) != null) {
-                return leftOF.concat(leftMO, leftOF.fromPlainTextByEscaping(rightPT));
-            } else if ((leftPT = leftOF.getSourcePlainText(leftMO)) != null) {
-                return rightOF.concat(rightOF.fromPlainTextByEscaping(leftPT), rightMO);
-            } else {
-                Object[] message = { "Concatenation left hand operand is in ", new _DelayedToString(leftOF),
-                        " format, while the right hand operand is in ", new _DelayedToString(rightOF),
-                        ". Conversion to common format wasn't possible." };
-                if (parent instanceof Expression) {
-                    throw new _TemplateModelException((Expression) parent, message);
-                } else {
-                    throw new _TemplateModelException(message);
-                }
-            }
-        } else {
-            return leftOF.concat(leftMO, rightMO);
-        }
-    }
-
     static TemplateModel _evalOnNumbers(Environment env, TemplateObject parent, Number first, Number second)
             throws TemplateException {
         ArithmeticEngine ae = EvalUtil.getArithmeticEngine(env, parent);

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/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 a3abc31..2ecab8c 100644
--- a/src/main/java/freemarker/core/DollarVariable.java
+++ b/src/main/java/freemarker/core/DollarVariable.java
@@ -23,7 +23,6 @@ import java.io.IOException;
 import java.io.Writer;
 
 import freemarker.template.TemplateException;
-import freemarker.template.TemplateModel;
 import freemarker.template.utility.StringUtil;
 
 /**
@@ -57,10 +56,8 @@ final class DollarVariable extends Interpolation {
      */
     @Override
     void accept(Environment env) throws TemplateException, IOException {
-        final TemplateModel tm = escapedExpression.eval(env);
+        final Object moOrStr = calculateInterpolatedStringOrMarkup(env);
         final Writer out = env.getOut();
-        final Object moOrStr = EvalUtil.coerceModelToStringOrMarkup(
-                tm, escapedExpression, null, env);
         if (moOrStr instanceof String) {
             final String s = (String) moOrStr;
             if (autoEscape) {
@@ -94,6 +91,11 @@ final class DollarVariable extends Interpolation {
     }
 
     @Override
+    protected Object calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException {
+        return EvalUtil.coerceModelToStringOrMarkup(escapedExpression.eval(env), escapedExpression, null, env);
+    }
+
+    @Override
     protected String dump(boolean canonical, boolean inStringLiteral) {
         StringBuilder sb = new StringBuilder();
         sb.append("${");

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/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 ca21ac5..886308c 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -528,10 +528,36 @@ class EvalUtil {
         throw new NullPointerException("TemplateValueFormatter result can't be null");
     }
 
+    static TemplateMarkupOutputModel concatMarkupOutputs(TemplateObject parent, TemplateMarkupOutputModel leftMO,
+            TemplateMarkupOutputModel rightMO) throws TemplateException {
+        MarkupOutputFormat leftOF = leftMO.getOutputFormat();
+        MarkupOutputFormat rightOF = rightMO.getOutputFormat();
+        if (rightOF != leftOF) {
+            String rightPT;
+            String leftPT;
+            if ((rightPT = rightOF.getSourcePlainText(rightMO)) != null) {
+                return leftOF.concat(leftMO, leftOF.fromPlainTextByEscaping(rightPT));
+            } else if ((leftPT = leftOF.getSourcePlainText(leftMO)) != null) {
+                return rightOF.concat(rightOF.fromPlainTextByEscaping(leftPT), rightMO);
+            } else {
+                Object[] message = { "Concatenation left hand operand is in ", new _DelayedToString(leftOF),
+                        " format, while the right hand operand is in ", new _DelayedToString(rightOF),
+                        ". Conversion to common format wasn't possible." };
+                if (parent instanceof Expression) {
+                    throw new _MiscTemplateException((Expression) parent, message);
+                } else {
+                    throw new _MiscTemplateException(message);
+                }
+            }
+        } else {
+            return leftOF.concat(leftMO, rightMO);
+        }
+    }
+
     /**
      * Returns an {@link ArithmeticEngine} even if {@code env} is {@code null}, because we are in parsing phase.
      */
-    public static ArithmeticEngine getArithmeticEngine(Environment env, TemplateObject tObj) {
+    static ArithmeticEngine getArithmeticEngine(Environment env, TemplateObject tObj) {
         return env != null
                 ? env.getArithmeticEngine()
                 : tObj.getTemplate().getParserConfiguration().getArithmeticEngine();

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/main/java/freemarker/core/Interpolation.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/Interpolation.java b/src/main/java/freemarker/core/Interpolation.java
index 733fafa..2dcb11a 100644
--- a/src/main/java/freemarker/core/Interpolation.java
+++ b/src/main/java/freemarker/core/Interpolation.java
@@ -18,6 +18,8 @@
  */
 package freemarker.core;
 
+import freemarker.template.TemplateException;
+
 abstract class Interpolation extends TemplateElement {
 
     protected abstract String dump(boolean canonical, boolean inStringLiteral);
@@ -31,4 +33,11 @@ abstract class Interpolation extends TemplateElement {
         return dump(true, true);
     }
 
+    /**
+     * Returns the already type-converted value that this interpolation will insert into the output.
+     * 
+     * @return A {@link String} or {@link TemplateMarkupOutputModel}. Not {@code null}.
+     */
+    protected abstract Object calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException;
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/main/java/freemarker/core/NumericalOutput.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/NumericalOutput.java b/src/main/java/freemarker/core/NumericalOutput.java
index 0ded83f..c9028c6 100644
--- a/src/main/java/freemarker/core/NumericalOutput.java
+++ b/src/main/java/freemarker/core/NumericalOutput.java
@@ -60,6 +60,17 @@ final class NumericalOutput extends Interpolation {
 
     @Override
     void accept(Environment env) throws TemplateException, IOException {
+        String s = calculateInterpolatedStringOrMarkup(env);
+        Writer out = env.getOut();
+        if (autoEscapeOutputFormat != null) {
+            autoEscapeOutputFormat.output(s, out);
+        } else {
+            out.write(s);
+        }
+    }
+
+    @Override
+    protected String calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException {
         Number num = expression.evalToNumber(env);
         
         FormatHolder fmth = formatCache;  // atomic sampling
@@ -85,12 +96,7 @@ final class NumericalOutput extends Interpolation {
         // Some locales may use non-Arabic digits, thus replacing the
         // decimal separator in the result of toString() is not enough.
         String s = fmth.format.format(num);
-        Writer out = env.getOut();
-        if (autoEscapeOutputFormat != null) {
-            autoEscapeOutputFormat.output(s, out);
-        } else {
-            out.write(s);
-        }
+        return s;
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/main/java/freemarker/core/ParameterRole.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/ParameterRole.java b/src/main/java/freemarker/core/ParameterRole.java
index d178881..1293d5c 100644
--- a/src/main/java/freemarker/core/ParameterRole.java
+++ b/src/main/java/freemarker/core/ParameterRole.java
@@ -62,6 +62,7 @@ final class ParameterRole {
     static final ParameterRole ARGUMENT_VALUE = new ParameterRole("argument value");
     static final ParameterRole CONTENT = new ParameterRole("content");
     static final ParameterRole EMBEDDED_TEMPLATE = new ParameterRole("embedded template");
+    static final ParameterRole VALUE_PART = new ParameterRole("value part");
     static final ParameterRole MINIMUM_DECIMALS = new ParameterRole("minimum decimals");
     static final ParameterRole MAXIMUM_DECIMALS = new ParameterRole("maximum decimals");
     static final ParameterRole NODE = new ParameterRole("node");

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/main/java/freemarker/core/StringLiteral.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/StringLiteral.java b/src/main/java/freemarker/core/StringLiteral.java
index 051c0aa..aa32d3e 100644
--- a/src/main/java/freemarker/core/StringLiteral.java
+++ b/src/main/java/freemarker/core/StringLiteral.java
@@ -19,14 +19,12 @@
 
 package freemarker.core;
 
-import java.io.IOException;
 import java.io.StringReader;
-import java.util.Enumeration;
+import java.util.List;
 
 import freemarker.template.SimpleScalar;
 import freemarker.template.Template;
 import freemarker.template.TemplateException;
-import freemarker.template.TemplateExceptionHandler;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.utility.StringUtil;
@@ -34,7 +32,9 @@ import freemarker.template.utility.StringUtil;
 final class StringLiteral extends Expression implements TemplateScalarModel {
     
     private final String value;
-    private TemplateElement dynamicValue;
+    
+    /** {@link List} of {@link String}-s and {@link Interpolation}-s. */
+    private List<Object> dynamicValue;
     
     StringLiteral(String value) {
         this.value = value;
@@ -45,8 +45,9 @@ final class StringLiteral extends Expression implements TemplateScalarModel {
      *            The token source of the template that contains this string literal. As of this writing, we only need
      *            this to share the {@code namingConvetion} with that.
      */
-    // TODO This should be the part of the "parent" parsing; now it contains hacks like those with namingConvention.  
-    void parseValue(FMParserTokenManager parentTkMan) throws ParseException {
+    void parseValue(FMParserTokenManager parentTkMan, OutputFormat outputFormat) throws ParseException {
+        // The way this work is incorrect (the literal should be parsed without un-escaping),
+        // but we can't fix this backward compatibly.
         if (value.length() > 3 && (value.indexOf("${") >= 0 || value.indexOf("#{") >= 0)) {
             
             Template parentTemplate = getTemplate();
@@ -60,9 +61,9 @@ final class StringLiteral extends Expression implements TemplateScalarModel {
                 
                 FMParser parser = new FMParser(parentTemplate, false, tkMan, parentTemplate.getParserConfiguration());
                 // We continue from the parent parser's current state:
-                parser.setupStringLiteralMode(parentTkMan);
+                parser.setupStringLiteralMode(parentTkMan, outputFormat);
                 try {
-                    dynamicValue = parser.FreeMarkerText();
+                    dynamicValue = parser.StaticTextAndInterpolations();
                 } finally {
                     // The parent parser continues from this parser's current state:
                     parser.tearDownStringLiteralMode(parentTkMan);
@@ -77,7 +78,51 @@ final class StringLiteral extends Expression implements TemplateScalarModel {
     
     @Override
     TemplateModel _eval(Environment env) throws TemplateException {
-        return new SimpleScalar(evalAndCoerceToPlainText(env));
+        if (dynamicValue == null) {
+            return new SimpleScalar(value);
+        } else {
+            // This should behave like concatenating the values with `+`. Thus, an interpolated expression that
+            // returns markup promotes the result of the whole expression to markup.
+            
+            // Exactly one of these is non-null, depending on if the result will be plain text or markup, which can
+            // change during evaluation, depending on the result of the interpolations:
+            StringBuilder plainTextResult = null;
+            TemplateMarkupOutputModel<?> markupResult = null;
+            
+            for (Object part : dynamicValue) {
+                Object calcedPart =
+                        part instanceof String ? part
+                        : ((Interpolation) part).calculateInterpolatedStringOrMarkup(env);
+                if (markupResult != null) {
+                    TemplateMarkupOutputModel<?> partMO = calcedPart instanceof String
+                            ? markupResult.getOutputFormat().fromPlainTextByEscaping((String) calcedPart)
+                            : (TemplateMarkupOutputModel<?>) calcedPart;
+                    markupResult = EvalUtil.concatMarkupOutputs(this, markupResult, partMO);
+                } else { // We are using `plainTextOutput` (or nothing yet)
+                    if (calcedPart instanceof String) {
+                        String partStr = (String) calcedPart;
+                        if (plainTextResult == null) {
+                            plainTextResult = new StringBuilder(partStr);
+                        } else {
+                            plainTextResult.append(partStr);
+                        }
+                    } else { // `calcedPart` is TemplateMarkupOutputModel
+                        TemplateMarkupOutputModel<?> moPart = (TemplateMarkupOutputModel<?>) calcedPart;
+                        if (plainTextResult != null) {
+                            TemplateMarkupOutputModel<?> leftHandMO = moPart.getOutputFormat()
+                                    .fromPlainTextByEscaping(plainTextResult.toString());
+                            markupResult = EvalUtil.concatMarkupOutputs(this, leftHandMO, moPart);
+                            plainTextResult = null;
+                        } else {
+                            markupResult = moPart;
+                        }
+                    }
+                }
+            } // for each part
+            return markupResult != null ? markupResult
+                    : plainTextResult != null ? new SimpleScalar(plainTextResult.toString())
+                    : SimpleScalar.EMPTY_STRING;
+        }
     }
 
     public String getAsString() {
@@ -88,40 +133,22 @@ final class StringLiteral extends Expression implements TemplateScalarModel {
      * Tells if this is something like <tt>"${foo}"</tt>, which is usually a user mistake.
      */
     boolean isSingleInterpolationLiteral() {
-        return dynamicValue != null && dynamicValue.getChildCount() == 1
-                && dynamicValue.getChildAt(0) instanceof DollarVariable;
+        return dynamicValue != null && dynamicValue.size() == 1
+                && dynamicValue.get(0) instanceof Interpolation;
     }
     
     @Override
-    String evalAndCoerceToPlainText(Environment env) throws TemplateException {
-        if (dynamicValue == null) {
-            return value;
-        } else {
-            TemplateExceptionHandler teh = env.getTemplateExceptionHandler();
-            env.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
-            try {
-               return env.renderElementToString(dynamicValue);
-            } catch (IOException ioe) {
-                throw new _MiscTemplateException(ioe, env);
-            } finally {
-                env.setTemplateExceptionHandler(teh);
-            }
-        }
-    }
-
-    @Override
     public String getCanonicalForm() {
         if (dynamicValue == null) {
             return StringUtil.ftlQuote(value);
         } else {
             StringBuilder sb = new StringBuilder();
             sb.append('"');
-            for (Enumeration childrenEnum = dynamicValue.children(); childrenEnum.hasMoreElements(); ) {
-                TemplateElement child = (TemplateElement) childrenEnum.nextElement();
+            for (Object child : dynamicValue) {
                 if (child instanceof Interpolation) {
                     sb.append(((Interpolation) child).getCanonicalFormInStringLiteral());
                 } else {
-                    sb.append(StringUtil.FTLStringLiteralEnc(child.getCanonicalForm(), '"'));
+                    sb.append(StringUtil.FTLStringLiteralEnc((String) child, '"'));
                 }
             }
             sb.append('"');
@@ -150,19 +177,25 @@ final class StringLiteral extends Expression implements TemplateScalarModel {
 
     @Override
     int getParameterCount() {
-        return 1;
+        return dynamicValue == null ? 0 : dynamicValue.size();
     }
 
     @Override
     Object getParameterValue(int idx) {
-        if (idx != 0) throw new IndexOutOfBoundsException();
-        return dynamicValue;
+        checkIndex(idx);
+        return dynamicValue.get(idx);
+    }
+
+    private void checkIndex(int idx) {
+        if (dynamicValue == null || idx >= dynamicValue.size()) {
+            throw new IndexOutOfBoundsException();
+        }
     }
 
     @Override
     ParameterRole getParameterRole(int idx) {
-        if (idx != 0) throw new IndexOutOfBoundsException();
-        return ParameterRole.EMBEDDED_TEMPLATE;
+        checkIndex(idx);
+        return ParameterRole.VALUE_PART;
     }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index 9c6bae1..34f737b 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -254,13 +254,13 @@ public class FMParser {
         }
     }
     
-    void setupStringLiteralMode(FMParserTokenManager parentTokenSource) {
+    void setupStringLiteralMode(FMParserTokenManager parentTokenSource, OutputFormat outputFormat) {
         token_source.initialNamingConvention = parentTokenSource.initialNamingConvention;
         token_source.namingConvention = parentTokenSource.namingConvention;
         token_source.namingConventionEstabilisher = parentTokenSource.namingConventionEstabilisher;
         token_source.SwitchTo(NODIRECTIVE);
         
-        outputFormat = PlainTextOutputFormat.INSTANCE;
+        this.outputFormat = outputFormat;
         recalculateAutoEscapingField();                                
         if (incompatibleImprovements < _TemplateAPI.VERSION_INT_2_3_24) {
             // Emulate bug, where the string literal parser haven't inherited the IcI:
@@ -2271,7 +2271,7 @@ StringLiteral StringLiteral(boolean interpolate) :
         result.setLocation(template, t, t);
         if (interpolate && !raw) {
             // TODO: This logic is broken. It can't handle literals that contains both ${...} and $\{...}. 
-            if (t.image.indexOf("${") >= 0 || t.image.indexOf("#{") >= 0) result.parseValue(token_source);
+            if (t.image.indexOf("${") >= 0 || t.image.indexOf("#{") >= 0) result.parseValue(token_source, outputFormat);
         }
         return result;
     }
@@ -4205,6 +4205,66 @@ Map ParamList() :
 }
 
 /**
+ * Parses the already un-escaped content of a string literal (input must not include the quotation marks).
+ *
+ * @return A {@link List} of {@link String}-s and {@link Interpolation}-s. 
+ */
+List<Object> StaticTextAndInterpolations() :
+{
+    Token t;
+    Interpolation interpolation;
+    StringBuilder staticTextCollector = null;
+    ArrayList<Object> parts = new ArrayList<Object>();
+}
+{
+    (
+	    (
+		    t = <STATIC_TEXT_WS>
+		    |
+		    t = <STATIC_TEXT_NON_WS>
+		    |
+		    t = <STATIC_TEXT_FALSE_ALARM>
+	    )
+	    {
+	       String s = t.image;
+	       if (!s.isEmpty()) {
+	           if (staticTextCollector == null) {
+	               staticTextCollector = new StringBuilder(t.image);
+	           } else {
+	               staticTextCollector.append(t.image);
+	           }
+	       }
+	    }
+	    |
+	    (
+	        LOOKAHEAD(<DOLLAR_INTERPOLATION_OPENING>)
+		    (
+		        interpolation = StringOutput()
+	        )
+		    |
+            LOOKAHEAD(<HASH_INTERPOLATION_OPENING>)
+		    (
+                interpolation = NumericalOutput()
+		    )
+	    )
+	    {
+            if (staticTextCollector != null) {
+                parts.add(staticTextCollector.toString());
+                staticTextCollector.setLength(0);
+            }
+            parts.add(interpolation);
+	    }
+    )*
+    {
+        if (staticTextCollector != null && staticTextCollector.length() != 0) {
+            parts.add(staticTextCollector.toString());
+        }
+        parts.trimToSize();
+        return parts;
+    }
+}
+
+/**
  * Root production to be used when parsing
  * an entire file.
  */

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/manual/book.xml
----------------------------------------------------------------------
diff --git a/src/manual/book.xml b/src/manual/book.xml
index 6decbd9..3546923 100644
--- a/src/manual/book.xml
+++ b/src/manual/book.xml
@@ -2798,14 +2798,7 @@ baz
             literals <link linkend="dgui_template_valueinsertion">behaves
             similarly as in <phrase role="markedText">text</phrase>
             sections</link> (so it goes through the same <emphasis>locale
-            sensitive</emphasis> number and date/time formatting), except that
-            no automatic escaping will happen (the deprecated <link
-            linkend="ref.directive.escape">escaping by
-            <literal>escape</literal> directive</link> has no effect there,
-            nor <link linkend="dgui_misc_autoescaping">auto-escaping</link>
-            happens as <link
-            linkend="dgui_misc_autoescaping_stringliteral">explained
-            here</link>).</para>
+            sensitive</emphasis> number and date/time formatting).</para>
 
             <para>Example (assume that user is <quote>Big Joe</quote>):</para>
 
@@ -2865,17 +2858,6 @@ ${s} &lt;#-- Just to see what the value of s is --&gt;
               something like <literal>"someUrl?id=1234"</literal>, regardless
               of locale and format settings.</para>
             </warning>
-
-            <para>Note for advanced users that <literal>+</literal> is more
-            flexible than <literal>${<replaceable>...</replaceable>}</literal>
-            when it comes to handling <link
-            linkend="dgui_misc_autoescaping_movalues">markup output
-            values</link>, because <link
-            linkend="dgui_misc_autoescaping_concatenation"><literal>+</literal>
-            can have markup output result</link>, while <link
-            linkend="dgui_misc_autoescaping_stringliteral">string literal
-            interpolation can't</link>, as the literal must yield a
-            string.</para>
           </section>
 
           <section xml:id="dgui_template_exp_get_character">
@@ -5690,51 +5672,6 @@ XML:  &lt;p&gt;Test&lt;/p&gt;
 RTF:  \par Test</programlisting>
           </section>
 
-          <section xml:id="dgui_misc_autoescaping_stringliteral">
-            <title>Auto-escaping and ${...} inside string literals</title>
-
-            <para>Inside string literals (quoted text expressions), the <link
-            linkend="dgui_misc_autoescaping_outputformat">output format</link>
-            is <literal>plainText</literal>. Thus no auto-escaping will occur
-            there:</para>
-
-            <programlisting role="template">&lt;#-- We assume that we have "HTML" output format by default. --&gt;
-&lt;#assign s = "Foo &amp; bar"&gt;
-${s}
-${"${s} &amp; baz"}</programlisting>
-
-            <programlisting role="output">Foo &amp;amp; bar
-Foo &amp;amp; bar &amp;amp; baz</programlisting>
-
-            <para>Above, because inside the string literal
-            <literal>${s}</literal> did no auto-escaping, when we print the
-            whole string we don't end up with double-escaping.</para>
-
-            <para>The <literal>plainText</literal> output format only allows
-            the inserting of markup output values that were created by
-            escaping plain text
-            (<literal><replaceable>plainText</replaceable>?esc</literal>), as
-            those are trivially convertible back to plain text. Thus only such
-            markup output values can be insert into string literals with
-            <literal>${<replaceable>...</replaceable>}</literal> (the
-            <literal>+</literal> operator is more flexible, but see that
-            later):</para>
-
-            <programlisting role="template">&lt;#-- We assume that we have "HTML" output format by default. --&gt;
-
-&lt;#-- Markup output value created by escaping plain text: --&gt;
-&lt;#assign mo1 = "Foo &amp; bar"?esc&gt;
-
-&lt;#-- Markup output value created outherwise: --&gt;
-&lt;#assign mo2 = "&lt;p&gt;Foo"?no_esc&gt;
-
-${"${mo1} baz"}
-&lt;#attempt&gt;${"${mo2} baz"}&lt;#recover&gt;Failed&lt;/#attempt&gt;</programlisting>
-
-            <programlisting role="output">Foo &amp;amp; bar baz
-Failed</programlisting>
-          </section>
-
           <section xml:id="dgui_misc_autoescaping_concatenation">
             <title>Markup output values and the <quote>+</quote>
             operator</title>
@@ -5764,6 +5701,32 @@ ${"&lt;h1&gt;"?no_esc + "Foo &amp; bar" + "&lt;/h1&gt;"?no_esc}</programlisting>
             of conversions here</link>.)</para>
           </section>
 
+          <section xml:id="dgui_misc_autoescaping_stringliteral">
+            <title>Auto-escaping and ${...} inside string literals</title>
+
+            <para>A string <emphasis>expression</emphasis> like
+            <literal>"Hello ${name}!"</literal> is just a shorthand for
+            <literal>"Hello" + name + "!"</literal>, so that
+            <literal>${<replaceable>...</replaceable>}</literal> doesn't
+            auto-escape.</para>
+
+            <programlisting role="template">&lt;#-- We assume that we have "HTML" output format by default. --&gt;
+&lt;#assign name = "Foo &amp; Bar"&gt;
+
+&lt;#assign s = "&lt;p&gt;Hello ${name}!"&gt;
+${s}
+&lt;p&gt;Hello ${name}!
+
+To prove that s didn't contain the value escaped:
+${s?replace('&amp;'), 'and'}</programlisting>
+
+            <programlisting role="output">&amp;lt;p&amp;gt;Hello Foo &amp;amp; Bar!
+&lt;p&gt;Hello Foo &amp;amp; Bar!
+
+To prove that s didn't contain the value escaped:
+&amp;lt;p&amp;gt;Hello Foo and Bar!</programlisting>
+          </section>
+
           <section>
             <title>Combined output formats</title>
 
@@ -26158,6 +26121,18 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
               which can't be loaded due to zip format errors in the error
               message.</para>
             </listitem>
+
+            <listitem>
+              <para>The non-public AST API of
+              <literal>freemarker.core.StringLiteral</literal>-s has been
+              changed. In principle it doesn't mater as it isn't a public API,
+              but some might used these regardless to introspect templates.
+              Earlier it had an <quote>embedded template</quote> parameter
+              inside, now it has 0 (for purely static string literals), one or
+              multiple <quote>value part</quote>-ts, which are
+              <literal>String</literal>-s and
+              <literal>Interpolation</literal>-s.</para>
+            </listitem>
           </itemizedlist>
         </section>
 
@@ -26267,6 +26242,17 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
             </listitem>
 
             <listitem>
+              <para><literal>${<replaceable>...</replaceable>}</literal>
+              inside string literals is equivalent to using the
+              <literal>+</literal> operator again. This rule was broken by
+              <literal>+</literal> supporting markup operands, while
+              <literal>${<replaceable>...</replaceable>}</literal> inside
+              string literals didn't. Now similarly as <literal>"foo " +
+              someMarkup</literal> works and gives a markup result,
+              <literal>"foo ${someMarkup}"</literal> does too.</para>
+            </listitem>
+
+            <listitem>
               <para>Added <literal>XHTMLOutputFormat</literal> and
               <literal>TemplateXHTMLOutputModel</literal>.</para>
             </listitem>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/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 ddfbf28..19d2cba 100644
--- a/src/test/java/freemarker/core/NumberFormatTest.java
+++ b/src/test/java/freemarker/core/NumberFormatTest.java
@@ -297,9 +297,7 @@ public class NumberFormatTest extends TemplateTest {
         assertOutput("<#escape x as x?html>" + commonFTL + "</#escape>", commonOutput);
         assertOutput("<#escape x as x?xhtml>" + commonFTL + "</#escape>", commonOutput);
         assertOutput("<#escape x as x?xml>" + commonFTL + "</#escape>", commonOutput);
-        // TODO: Should give markup, but currently does interpolation in plain text:
-        // assertOutput("${\"" + commonFTL + "\"}",
-        //        "1.23*10<sup>6</sup> cat:1.23*10<sup>6</sup> 1.23*10<sup>-5</sup>");
+        assertOutput("${\"" + commonFTL + "\"}", "1.23*10<sup>6</sup> cat:1.23*10<sup>6</sup> 1.23*10<sup>-5</sup>");
         assertErrorContains("<#ftl outputFormat='plainText'>" + commonFTL, "HTML", "plainText", "conversion");
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/java/freemarker/core/OutputFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/OutputFormatTest.java b/src/test/java/freemarker/core/OutputFormatTest.java
index f7614c3..b36761e 100644
--- a/src/test/java/freemarker/core/OutputFormatTest.java
+++ b/src/test/java/freemarker/core/OutputFormatTest.java
@@ -436,12 +436,21 @@ public class OutputFormatTest extends TemplateTest {
     }
 
     @Test
-    public void testStringLiteralTemplateModificationBug() throws IOException, TemplateException {
+    public void testStringLiteralInterpolation() throws IOException, TemplateException {
         Template t = new Template(null, "<#ftl outputFormat='XML'>${'&'} ${\"(${'&'})\"?noEsc}", getConfiguration());
         assertEquals(XMLOutputFormat.INSTANCE, t.getOutputFormat());
-        assertOutput("${.outputFormat} ${'${.outputFormat}'} ${.outputFormat}", "undefined plainText undefined");
-        assertOutput("${'foo ${xmlPlain}'}", "foo a < {x'}");
-        assertErrorContains("${'${xmlMarkup}'}", "plainText", "XML", "conversion");
+        
+        assertOutput("${.outputFormat} ${'${.outputFormat}'} ${.outputFormat}",
+                "undefined undefined undefined");
+        assertOutput("<#ftl outputFormat='HTML'>${.outputFormat} ${'${.outputFormat}'} ${.outputFormat}",
+                "HTML HTML HTML");
+        assertOutput("${.outputFormat} <#outputFormat 'XML'>${'${.outputFormat}'}</#outputFormat> ${.outputFormat}",
+                "undefined XML undefined");
+        assertOutput("${'foo ${xmlPlain}'}", "foo a &lt; {x&apos;}");
+        assertOutput("${'${xmlMarkup}'}", "<p>c</p>");
+        assertErrorContains("${'${\"x\"?esc}'}", "?esc", "undefined");
+        assertOutput("<#ftl outputFormat='XML'>${'${xmlMarkup?esc} ${\"<\"?esc} ${\">\"} ${\"&amp;\"?noEsc}'}",
+                "<p>c</p> &lt; &gt; &amp;");
     }
     
     @Test
@@ -875,11 +884,13 @@ public class OutputFormatTest extends TemplateTest {
                 assertOutput(commonFTL, "x");
                 assertErrorContains("<#ftl outputFormat='HTML'>" + commonFTL,
                         "?" + biName, "HTML", "double-escaping");
+                assertErrorContains("<#ftl outputFormat='HTML'>${'${\"x\"?" + biName + "}'}",
+                        "?" + biName, "HTML", "double-escaping");
                 assertOutput("<#ftl outputFormat='plainText'>" + commonFTL, "x");
                 assertOutput("<#ftl outputFormat='HTML' autoEsc=false>" + commonFTL, "x");
                 assertOutput("<#ftl outputFormat='HTML'><#noAutoEsc>" + commonFTL + "</#noAutoEsc>", "x");
-                assertOutput("<#ftl outputFormat='HTML'><#outputFormat 'plainText'>" + commonFTL + "</#outputFormat>", "x");
-                assertOutput("<#ftl outputFormat='HTML'>${'${\"x\"?" + biName + "}'}", "x");
+                assertOutput("<#ftl outputFormat='HTML'><#outputFormat 'plainText'>" + commonFTL + "</#outputFormat>",
+                        "x");
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/java/freemarker/core/StringLiteralInterpolationTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/StringLiteralInterpolationTest.java b/src/test/java/freemarker/core/StringLiteralInterpolationTest.java
index fd6fb14..b7bfeee 100644
--- a/src/test/java/freemarker/core/StringLiteralInterpolationTest.java
+++ b/src/test/java/freemarker/core/StringLiteralInterpolationTest.java
@@ -19,6 +19,7 @@
 package freemarker.core;
 
 import java.io.IOException;
+import java.util.Collections;
 
 import org.junit.Test;
 
@@ -36,6 +37,7 @@ public class StringLiteralInterpolationTest extends TemplateTest {
         assertOutput("${'#{x}'}", "1");
         assertOutput("${'a${x}b${x*2}c'}", "a1b2c");
         assertOutput("${'a#{x}b#{x*2}c'}", "a1b2c");
+        assertOutput("${'a#{x; m2}'}", "a1.00");
         assertOutput("${'${x} ${x}'}", "1 1");
         assertOutput("${'$\\{x}'}", "${x}");
         assertOutput("${'$\\{x} $\\{x}'}", "${x} ${x}");
@@ -73,6 +75,13 @@ public class StringLiteralInterpolationTest extends TemplateTest {
         assertOutput("${'${1}'}", "1");
         assertErrorContains("${'${  '}", "");
     }
+    
+    @Test
+    public void testErrors() {
+        addToDataModel("x", 1);
+        assertErrorContains("${'${noSuchVar}'}", InvalidReferenceException.class, "missing", "noSuchVar");
+        assertErrorContains("${'${x/0}'}", ArithmeticException.class, "zero");
+    }
 
     @Test
     public void escaping() throws IOException, TemplateException {
@@ -90,4 +99,18 @@ public class StringLiteralInterpolationTest extends TemplateTest {
         assertOutput("${'&\\''?html} ${\"${'&\\\\\\''?html}\"}", "&amp;&#39; &amp;&#39;");
     }
     
+    @Test
+    public void markup() throws IOException, TemplateException {
+        Configuration cfg = getConfiguration();
+        cfg.setCustomNumberFormats(Collections.singletonMap("G", PrintfGTemplateNumberFormatFactory.INSTANCE));
+        cfg.setNumberFormat("@G 3");
+        
+        assertOutput("${\"${1000}\"}", "1.00*10<sup>3</sup>");
+        assertOutput("${\"&_${1000}\"}", "&amp;_1.00*10<sup>3</sup>");
+        assertOutput("${\"${1000}_&\"}", "1.00*10<sup>3</sup>_&amp;");
+        assertOutput("${\"${1000}, ${2000}\"}", "1.00*10<sup>3</sup>, 2.00*10<sup>3</sup>");
+        assertOutput("${\"& ${'x'}, ${2000}\"}", "&amp; x, 2.00*10<sup>3</sup>");
+        assertOutput("${\"& ${'x'}, #{2000}\"}", "& x, 2000");
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/java/freemarker/test/TemplateTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/test/TemplateTest.java b/src/test/java/freemarker/test/TemplateTest.java
index 0b1e03d..6047e2e 100644
--- a/src/test/java/freemarker/test/TemplateTest.java
+++ b/src/test/java/freemarker/test/TemplateTest.java
@@ -229,11 +229,13 @@ public abstract class TemplateTest {
             }
             assertContainsAll(e.getEditorMessage(), expectedSubstrings);
             return e;
-        } catch (IOException e) {
+        } catch (Exception e) {
             if (exceptionClass != null) {
                 assertThat(e, instanceOf(exceptionClass));
+                return e;
+            } else {
+                throw new RuntimeException("Unexpected exception class: " + e.getClass().getName(), e);
             }
-            throw new RuntimeException("Unexpected exception class: " + e.getClass().getName(), e);
         }
     }
     

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/resources/freemarker/core/ast-1.ast
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/core/ast-1.ast b/src/test/resources/freemarker/core/ast-1.ast
index 6c5a753..9c3b8d1 100644
--- a/src/test/resources/freemarker/core/ast-1.ast
+++ b/src/test/resources/freemarker/core/ast-1.ast
@@ -64,19 +64,15 @@
             - AST-node subtype: "1"  // Integer
             ${...}  // f.c.DollarVariable
                 - content: "static"  // f.c.StringLiteral
-                    - embedded template: null  // Null
             ${...}  // f.c.DollarVariable
                 - content: dynamic "..."  // f.c.StringLiteral
-                    - embedded template: #mixed_content  // f.c.MixedContent
-                        #text  // f.c.TextBlock
-                            - content: "x"  // String
-                        ${...}  // f.c.DollarVariable
-                            - content: *  // f.c.ArithmeticExpression
-                                - left-hand operand: baaz  // f.c.Identifier
-                                - right-hand operand: 10  // f.c.NumberLiteral
-                                - AST-node subtype: "1"  // Integer
-                        #text  // f.c.TextBlock
-                            - content: "y"  // String
+                    - value part: "x"  // String
+                    - value part: ${...}  // f.c.DollarVariable
+                        - content: *  // f.c.ArithmeticExpression
+                            - left-hand operand: baaz  // f.c.Identifier
+                            - right-hand operand: 10  // f.c.NumberLiteral
+                            - AST-node subtype: "1"  // Integer
+                    - value part: "y"  // String
     #text  // f.c.TextBlock
         - content: "\n5 "  // String
     #switch  // f.c.SwitchBlock
@@ -163,7 +159,6 @@
         - content: "\n11 "  // String
     #outputformat  // f.c.OutputFormatBlock
         - value: "XML"  // f.c.StringLiteral
-            - embedded template: null  // Null
         #noautoesc  // f.c.NoAutoEscBlock
             ${...}  // f.c.DollarVariable
                 - content: a  // f.c.Identifier

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/resources/freemarker/core/ast-builtins.ast
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/core/ast-builtins.ast b/src/test/resources/freemarker/core/ast-builtins.ast
index 164c790..9278d8b 100644
--- a/src/test/resources/freemarker/core/ast-builtins.ast
+++ b/src/test/resources/freemarker/core/ast-builtins.ast
@@ -20,7 +20,6 @@
                 - right-hand operand: "left_pad"  // String
             - argument value: 5  // f.c.NumberLiteral
             - argument value: "-"  // f.c.StringLiteral
-                - embedded template: null  // Null
     #text  // f.c.TextBlock
         - content: "\n"  // String
     ${...}  // f.c.DollarVariable
@@ -28,9 +27,7 @@
             - left-hand operand: x  // f.c.Identifier
             - right-hand operand: "then"  // String
             - argument value: "y"  // f.c.StringLiteral
-                - embedded template: null  // Null
             - argument value: "n"  // f.c.StringLiteral
-                - embedded template: null  // Null
     #text  // f.c.TextBlock
         - content: "\n"  // String
     ${...}  // f.c.DollarVariable

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/resources/freemarker/core/ast-range.ast
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/core/ast-range.ast b/src/test/resources/freemarker/core/ast-range.ast
index eb862f4..3ddae45 100644
--- a/src/test/resources/freemarker/core/ast-range.ast
+++ b/src/test/resources/freemarker/core/ast-range.ast
@@ -102,13 +102,11 @@
                     - left-hand operand: n  // f.c.Identifier
                     - right-hand operand: "index_of"  // String
                 - argument value: "x"  // f.c.StringLiteral
-                    - embedded template: null  // Null
             - right-hand operand: ...(...)  // f.c.MethodCall
                 - callee: ?index_of  // f.c.BuiltInsForStringsBasic$index_ofBI
                     - left-hand operand: m  // f.c.Identifier
                     - right-hand operand: "index_of"  // String
                 - argument value: "y"  // f.c.StringLiteral
-                    - embedded template: null  // Null
         - variable scope: "1"  // Integer
         - namespace: null  // Null
     #assign  // f.c.Assignment

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/resources/freemarker/core/ast-strlitinterpolation.ast
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/core/ast-strlitinterpolation.ast b/src/test/resources/freemarker/core/ast-strlitinterpolation.ast
index f8f57bd..c55a320 100644
--- a/src/test/resources/freemarker/core/ast-strlitinterpolation.ast
+++ b/src/test/resources/freemarker/core/ast-strlitinterpolation.ast
@@ -1,10 +1,64 @@
-@  // f.c.UnifiedCall
-    - callee: m  // f.c.Identifier
-    - argument name: "x"  // String
-    - argument value: dynamic "..."  // f.c.StringLiteral
-        - embedded template: #mixed_content  // f.c.MixedContent
-            ${...}  // f.c.DollarVariable
+#mixed_content  // f.c.MixedContent
+    #text  // f.c.TextBlock
+        - content: "1. "  // String
+    @  // f.c.UnifiedCall
+        - callee: m  // f.c.Identifier
+        - argument name: "x"  // String
+        - argument value: dynamic "..."  // f.c.StringLiteral
+            - value part: ${...}  // f.c.DollarVariable
                 - content: e1  // f.c.Identifier
-    - argument name: "y"  // String
-    - argument value: "$\\{e2}"  // f.c.StringLiteral
-        - embedded template: null  // Null
\ No newline at end of file
+        - argument name: "y"  // String
+        - argument value: "$\\{e2}"  // f.c.StringLiteral
+    #text  // f.c.TextBlock
+        - content: "\n2. "  // String
+    ${...}  // f.c.DollarVariable
+        - content: dynamic "..."  // f.c.StringLiteral
+            - value part: "a"  // String
+            - value part: ${...}  // f.c.DollarVariable
+                - content: x  // f.c.Identifier
+            - value part: "b"  // String
+            - value part: ${...}  // f.c.DollarVariable
+                - content: x  // f.c.Identifier
+            - value part: "c"  // String
+    #text  // f.c.TextBlock
+        - content: "\n3. "  // String
+    ${...}  // f.c.DollarVariable
+        - content: dynamic "..."  // f.c.StringLiteral
+            - value part: ${...}  // f.c.DollarVariable
+                - content: x  // f.c.Identifier
+            - value part: "b"  // String
+    #text  // f.c.TextBlock
+        - content: "\n4. "  // String
+    ${...}  // f.c.DollarVariable
+        - content: dynamic "..."  // f.c.StringLiteral
+            - value part: "a"  // String
+            - value part: ${...}  // f.c.DollarVariable
+                - content: x  // f.c.Identifier
+    #text  // f.c.TextBlock
+        - content: "\n5. "  // String
+    ${...}  // f.c.DollarVariable
+        - content: dynamic "..."  // f.c.StringLiteral
+            - value part: ${...}  // f.c.DollarVariable
+                - content: x  // f.c.Identifier
+            - value part: #{...}  // f.c.NumericalOutput
+                - content: y  // f.c.Identifier
+                - minimum decimals: "0"  // Integer
+                - maximum decimals: "0"  // Integer
+    #text  // f.c.TextBlock
+        - content: "\n6. "  // String
+    ${...}  // f.c.DollarVariable
+        - content: dynamic "..."  // f.c.StringLiteral
+            - value part: "a b "  // String
+            - value part: ${...}  // f.c.DollarVariable
+                - content: x  // f.c.Identifier
+            - value part: " c d"  // String
+    #text  // f.c.TextBlock
+        - content: "\n7. "  // String
+    ${...}  // f.c.DollarVariable
+        - content: dynamic "..."  // f.c.StringLiteral
+            - value part: ${...}  // f.c.DollarVariable
+                - content: x  // f.c.Identifier
+            - value part: " a b "  // String
+            - value part: ${...}  // f.c.DollarVariable
+                - content: y  // f.c.Identifier
+            - value part: " c$d"  // String

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/resources/freemarker/core/ast-strlitinterpolation.ftl
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/core/ast-strlitinterpolation.ftl b/src/test/resources/freemarker/core/ast-strlitinterpolation.ftl
index efaced8..f67ba55 100644
--- a/src/test/resources/freemarker/core/ast-strlitinterpolation.ftl
+++ b/src/test/resources/freemarker/core/ast-strlitinterpolation.ftl
@@ -1 +1,7 @@
-<@m x='${e1}' y='$\\{e2}' />
\ No newline at end of file
+1. <@m x='${e1}' y='$\\{e2}' />
+2. ${'a${x}b${x}c'}
+3. ${'${x}b'}
+4. ${'a${x}'}
+5. ${'${x}#{y}'}
+6. ${'a b ${x} c d'}
+7. ${'${x} a b ${y} c$d'}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh b/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh
index 6d951a6..060b000 100644
--- a/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh
+++ b/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh
@@ -6,4 +6,4 @@
 <#assign mo2 = "<p>Foo"?no_esc>
 
 ${"${mo1} baz"}
-<#attempt>${"${mo2} baz"}<#recover>Failed</#attempt>
\ No newline at end of file
+${"${mo2} baz"}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fa6ac0ee/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh.out
----------------------------------------------------------------------
diff --git a/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh.out b/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh.out
index bc280c6..b8e929b 100644
--- a/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh.out
+++ b/src/test/resources/freemarker/manual/AutoEscapingExample-stringLiteral2.ftlh.out
@@ -1,3 +1,3 @@
 
 Foo &amp; bar baz
-Failed
\ No newline at end of file
+<p>Foo baz
\ No newline at end of file