Merge pull request #83 from nolaviz/nolaviz-devel

Add fine-grained mixed-content support to markup output formats.
diff --git a/src/main/java/freemarker/core/CombinedMarkupOutputFormat.java b/src/main/java/freemarker/core/CombinedMarkupOutputFormat.java
index fd1e823..a8a6b85 100644
--- a/src/main/java/freemarker/core/CombinedMarkupOutputFormat.java
+++ b/src/main/java/freemarker/core/CombinedMarkupOutputFormat.java
@@ -70,6 +70,11 @@
     }
 
     @Override
+    public <MO2 extends TemplateMarkupOutputModel<MO2>> void outputForeign(MO2 mo, Writer out) throws IOException, TemplateModelException {
+        outer.outputForeign(mo, out);
+    }
+
+    @Override
     public String escapePlainText(String plainTextContent) throws TemplateModelException {
         return outer.escapePlainText(inner.escapePlainText(plainTextContent));
     }
diff --git a/src/main/java/freemarker/core/DollarVariable.java b/src/main/java/freemarker/core/DollarVariable.java
index 12fa02a..6823a5a 100644
--- a/src/main/java/freemarker/core/DollarVariable.java
+++ b/src/main/java/freemarker/core/DollarVariable.java
@@ -73,7 +73,9 @@
             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()) {
+            if (moOF == outputFormat) {
+                moOF.output(mo, out);
+            } else if (!outputFormat.isOutputFormatMixingAllowed()) {
                 final String srcPlainText;
                 // ATTENTION: Keep this logic in sync. ?esc/?noEsc's logic!
                 srcPlainText = moOF.getSourcePlainText(mo);
@@ -83,11 +85,13 @@
                             " format, which differs from the current output format, ",
                             new _DelayedToString(outputFormat), ". Format conversion wasn't possible.");
                 }
-                if (outputFormat instanceof MarkupOutputFormat) {
-                    ((MarkupOutputFormat) outputFormat).output(srcPlainText, out);
+                if (markupOutputFormat != null) {
+                    markupOutputFormat.output(srcPlainText, out);
                 } else {
                     out.write(srcPlainText);
                 }
+            } else if (markupOutputFormat != null) {
+                markupOutputFormat.outputForeign(mo, out);
             } else {
                 moOF.output(mo, out);
             }
diff --git a/src/main/java/freemarker/core/MarkupOutputFormat.java b/src/main/java/freemarker/core/MarkupOutputFormat.java
index c947c93..e98961e 100644
--- a/src/main/java/freemarker/core/MarkupOutputFormat.java
+++ b/src/main/java/freemarker/core/MarkupOutputFormat.java
@@ -82,6 +82,14 @@
     public abstract void output(String textToEsc, Writer out) throws IOException, TemplateModelException;
     
     /**
+     * Outputs a value from a foreign output format; only used if {@link #isOutputFormatMixingAllowed()} is true.
+     * By default will just let the other output format handle the value, but can be overridden to support more nuanced conversions.
+     */
+    public <MO2 extends TemplateMarkupOutputModel<MO2>> void outputForeign(MO2 mo, Writer out) throws IOException, TemplateModelException {
+        mo.getOutputFormat().output(mo, out);
+    }
+
+    /**
      * If this {@link TemplateMarkupOutputModel} was created with {@link #fromPlainTextByEscaping(String)}, it returns
      * the original plain text, otherwise it returns {@code null}. Useful for converting between different types
      * of markups, as if the source format can be converted to plain text without loss, then that just has to be
diff --git a/src/main/java/freemarker/core/OutputFormat.java b/src/main/java/freemarker/core/OutputFormat.java
index 8ab52da..eb7d648 100644
--- a/src/main/java/freemarker/core/OutputFormat.java
+++ b/src/main/java/freemarker/core/OutputFormat.java
@@ -46,13 +46,19 @@
 
     /**
      * Tells if this output format allows inserting {@link TemplateMarkupOutputModel}-s of another output formats into
-     * it. If {@code true}, the foreign {@link TemplateMarkupOutputModel} will be inserted into the output as is (like
-     * if the surrounding output format was the same). This is usually a bad idea to allow, as such an event could
-     * indicate application bugs. If this method returns {@code false} (recommended), then FreeMarker will try to
-     * assimilate the inserted value by converting its format to this format, which will currently (2.3.24) cause
-     * exception, unless the inserted value is made by escaping plain text and the target format is non-escaping, in
-     * which case format conversion is trivially possible. (It's not impossible that conversions will be extended beyond
-     * this, if there will be demand for that.)
+     * it.
+     *
+     * <p>If {@code true}, the foreign {@link TemplateMarkupOutputModel} will be inserted into the output. If the current
+     * output format is a {@link MarkupOutputFormat} this is done using the
+     * {@link MarkupOutputFormat#outputForeign(TemplateMarkupOutputModel, Writer)} method, which can implement smart
+     * conversions. The default behavior (and the only behavior for non-markup outputs) is to behave as if the surrounding
+     * output format was the same; this is usually a bad idea to allow, as such an event could
+     * indicate application bugs.
+     *
+     * <p>If this method returns {@code false} (recommended), then FreeMarker will try to assimilate the inserted value by
+     * converting its format to this format, which will currently (2.3.24) cause exception, unless the inserted value is
+     * made by escaping plain text and the target format is non-escaping, in which case format conversion is trivially
+     * possible. (It's not impossible that conversions will be extended beyond this, if there will be demand for that.)
      * 
      * <p>
      * {@code true} value is used by {@link UndefinedOutputFormat}.
diff --git a/src/test/java/freemarker/core/DummyOutputFormat.java b/src/test/java/freemarker/core/DummyOutputFormat.java
index c37f45e..78186da 100644
--- a/src/test/java/freemarker/core/DummyOutputFormat.java
+++ b/src/test/java/freemarker/core/DummyOutputFormat.java
@@ -22,6 +22,8 @@
 import java.io.Writer;
 
 import freemarker.template.TemplateModelException;
+import freemarker.core._TemplateModelException;
+import freemarker.core._DelayedToString;
 
 public class DummyOutputFormat extends CommonMarkupOutputFormat<TemplateDummyOutputModel> {
     
@@ -47,6 +49,20 @@
     }
 
     @Override
+    public boolean isOutputFormatMixingAllowed() {
+        return true;
+    }
+
+    @Override
+    public <MO extends TemplateMarkupOutputModel<MO>> void outputForeign(MO mo, Writer out) throws IOException, TemplateModelException {
+        if (mo.getOutputFormat().getMimeType().equals("text/html")) {
+            mo.getOutputFormat().output(mo, out);
+        } else {
+            throw new _TemplateModelException("DummyOutputFormat is incompatible with ", new _DelayedToString(mo.getOutputFormat()));
+        }
+    }
+
+    @Override
     public String escapePlainText(String plainTextContent) {
         return plainTextContent.replaceAll("(\\.|\\\\)", "\\\\$1");
     }
@@ -61,4 +77,4 @@
         return new TemplateDummyOutputModel(plainTextContent, markupContent);
     }
     
-}
\ No newline at end of file
+}
diff --git a/src/test/java/freemarker/core/OutputFormatTest.java b/src/test/java/freemarker/core/OutputFormatTest.java
index 0c72faa..1856166 100644
--- a/src/test/java/freemarker/core/OutputFormatTest.java
+++ b/src/test/java/freemarker/core/OutputFormatTest.java
@@ -745,6 +745,15 @@
     }
     
     @Test
+    public void testMixedContent() throws Exception {
+        getConfiguration().setRegisteredCustomOutputFormats(Collections.singleton(DummyOutputFormat.INSTANCE));
+        addToDataModel("m1", HTMLOutputFormat.INSTANCE.fromMarkup("x"));
+        addToDataModel("m2", XMLOutputFormat.INSTANCE.fromMarkup("y"));
+        assertOutput("<#ftl outputFormat='dummy'>${m1}", "x");
+        assertErrorContains("<#ftl outputFormat='dummy'>${m2}", "is incompatible with");
+    }
+
+    @Test
     public void testExplicitAutoEscBannedForNonMarkup() throws Exception {
         // While this restriction is technically unnecessary, we can catch a dangerous and probably common user
         // misunderstanding.