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.