User-extensible escaping rules (not just HTML or no escaping)

This commit is contained in:
Nat Pryce
2013-07-22 22:36:51 +01:00
parent 9441d062a6
commit 4ae290fdaa
6 changed files with 89 additions and 34 deletions
@@ -0,0 +1,5 @@
package com.samskivert.mustache;
public interface Escaping {
String escape(String raw);
}
@@ -4,6 +4,9 @@
package com.samskivert.mustache; package com.samskivert.mustache;
import com.samskivert.mustache.formats.HtmlEscaping;
import com.samskivert.mustache.formats.NoEscaping;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.io.StringReader; import java.io.StringReader;
@@ -30,8 +33,8 @@ public class Mustache
/** An interface to the Mustache compilation process. See {@link Mustache}. */ /** An interface to the Mustache compilation process. See {@link Mustache}. */
public static class Compiler public static class Compiler
{ {
/** Whether or not HTML entities are escaped by default. */ /** How characters are escaped by default. */
public final boolean escapeHTML; public final Escaping escaping;
/** Whether or not standards mode is enabled. */ /** Whether or not standards mode is enabled. */
public final boolean standardsMode; public final boolean standardsMode;
@@ -79,7 +82,11 @@ public class Mustache
/** Returns a compiler that either does or does not escape HTML by default. */ /** Returns a compiler that either does or does not escape HTML by default. */
public Compiler escapeHTML (boolean escapeHTML) { public Compiler escapeHTML (boolean escapeHTML) {
return new Compiler(escapeHTML, this.standardsMode, this.nullValue, this.missingIsNull, return escaping(escapeHTML ? new HtmlEscaping() : new NoEscaping());
}
public Compiler escaping(Escaping escaping) {
return new Compiler(escaping, this.standardsMode, this.nullValue, this.missingIsNull,
this.emptyStringIsFalse, this.zeroIsFalse, this.loader, this.emptyStringIsFalse, this.zeroIsFalse, this.loader,
this.collector, this.delims); this.collector, this.delims);
} }
@@ -88,7 +95,7 @@ public class Mustache
* disables the non-standard JMustache extensions like looking up missing names in a parent * disables the non-standard JMustache extensions like looking up missing names in a parent
* context. */ * context. */
public Compiler standardsMode (boolean standardsMode) { public Compiler standardsMode (boolean standardsMode) {
return new Compiler(this.escapeHTML, standardsMode, this.nullValue, this.missingIsNull, return new Compiler(this.escaping, standardsMode, this.nullValue, this.missingIsNull,
this.emptyStringIsFalse, this.zeroIsFalse, this.loader, this.emptyStringIsFalse, this.zeroIsFalse, this.loader,
this.collector, this.delims); this.collector, this.delims);
} }
@@ -97,7 +104,7 @@ public class Mustache
* otherwise resolves to null. This is like {@link #nullValue} except that it returns the * otherwise resolves to null. This is like {@link #nullValue} except that it returns the
* supplied default for missing keys and existing keys that return null values. */ * supplied default for missing keys and existing keys that return null values. */
public Compiler defaultValue (String defaultValue) { public Compiler defaultValue (String defaultValue) {
return new Compiler(this.escapeHTML, this.standardsMode, defaultValue, true, return new Compiler(this.escaping, this.standardsMode, defaultValue, true,
this.emptyStringIsFalse, this.zeroIsFalse, this.loader, this.emptyStringIsFalse, this.zeroIsFalse, this.loader,
this.collector, this.delims); this.collector, this.delims);
} }
@@ -114,35 +121,35 @@ public class Mustache
* raised.</li> * raised.</li>
* </ul> */ * </ul> */
public Compiler nullValue (String nullValue) { public Compiler nullValue (String nullValue) {
return new Compiler(this.escapeHTML, this.standardsMode, nullValue, false, return new Compiler(this.escaping, this.standardsMode, nullValue, false,
this.emptyStringIsFalse, this.zeroIsFalse, this.loader, this.emptyStringIsFalse, this.zeroIsFalse, this.loader,
this.collector, this.delims); this.collector, this.delims);
} }
/** Returns a compiler that will treat empty string as a false value if parameter is true. */ /** Returns a compiler that will treat empty string as a false value if parameter is true. */
public Compiler emptyStringIsFalse (boolean emptyStringIsFalse) { public Compiler emptyStringIsFalse (boolean emptyStringIsFalse) {
return new Compiler(this.escapeHTML, this.standardsMode, this.nullValue, return new Compiler(this.escaping, this.standardsMode, this.nullValue,
this.missingIsNull, emptyStringIsFalse, this.zeroIsFalse, this.missingIsNull, emptyStringIsFalse, this.zeroIsFalse,
this.loader, this.collector, this.delims); this.loader, this.collector, this.delims);
} }
/** Returns a compiler that will treat zero as a false value if parameter is true. */ /** Returns a compiler that will treat zero as a false value if parameter is true. */
public Compiler zeroIsFalse (boolean zeroIsFalse) { public Compiler zeroIsFalse (boolean zeroIsFalse) {
return new Compiler(this.escapeHTML, this.standardsMode, this.nullValue, return new Compiler(this.escaping, this.standardsMode, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, zeroIsFalse, this.missingIsNull, this.emptyStringIsFalse, zeroIsFalse,
this.loader, this.collector, this.delims); this.loader, this.collector, this.delims);
} }
/** Returns a compiler configured to use the supplied template loader to handle partials. */ /** Returns a compiler configured to use the supplied template loader to handle partials. */
public Compiler withLoader (TemplateLoader loader) { public Compiler withLoader (TemplateLoader loader) {
return new Compiler(this.escapeHTML, this.standardsMode, this.nullValue, return new Compiler(this.escaping, this.standardsMode, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
loader, this.collector, this.delims); loader, this.collector, this.delims);
} }
/** Returns a compiler configured to use the supplied collector. */ /** Returns a compiler configured to use the supplied collector. */
public Compiler withCollector (Collector collector) { public Compiler withCollector (Collector collector) {
return new Compiler(this.escapeHTML, this.standardsMode, this.nullValue, return new Compiler(this.escaping, this.standardsMode, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.loader, collector, this.delims); this.loader, collector, this.delims);
} }
@@ -151,7 +158,7 @@ public class Mustache
* @param delims a string of the form {@code AB CD} or {@code A D} where A and B are * @param delims a string of the form {@code AB CD} or {@code A D} where A and B are
* opening delims and C and D are closing delims. */ * opening delims and C and D are closing delims. */
public Compiler withDelims (String delims) { public Compiler withDelims (String delims) {
return new Compiler(this.escapeHTML, this.standardsMode, this.nullValue, return new Compiler(this.escaping, this.standardsMode, this.nullValue,
this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse,
this.loader, this.collector, new Delims().updateDelims(delims)); this.loader, this.collector, new Delims().updateDelims(delims));
} }
@@ -170,10 +177,10 @@ public class Mustache
(zeroIsFalse && (value instanceof Number) && ((Number)value).longValue() == 0); (zeroIsFalse && (value instanceof Number) && ((Number)value).longValue() == 0);
} }
protected Compiler (boolean escapeHTML, boolean standardsMode, String nullValue, protected Compiler (Escaping escaping, boolean standardsMode, String nullValue,
boolean missingIsNull, boolean emptyStringIsFalse, boolean zeroIsFalse, boolean missingIsNull, boolean emptyStringIsFalse, boolean zeroIsFalse,
TemplateLoader loader, Collector collector, Delims delims) { TemplateLoader loader, Collector collector, Delims delims) {
this.escapeHTML = escapeHTML; this.escaping = escaping;
this.standardsMode = standardsMode; this.standardsMode = standardsMode;
this.nullValue = nullValue; this.nullValue = nullValue;
this.missingIsNull = missingIsNull; this.missingIsNull = missingIsNull;
@@ -234,7 +241,7 @@ public class Mustache
* Returns a compiler that escapes HTML by default and does not use standards mode. * Returns a compiler that escapes HTML by default and does not use standards mode.
*/ */
public static Compiler compiler () { public static Compiler compiler () {
return new Compiler(true, false, null, true, false, false, FAILING_LOADER, return new Compiler(new HtmlEscaping(), false, null, true, false, false, FAILING_LOADER,
new DefaultCollector(), new Delims()); new DefaultCollector(), new Delims());
} }
@@ -262,12 +269,11 @@ public class Mustache
} }
protected static String escapeHTML (String text) { protected static String escapeHTML (String text) {
for (String[] escape : ATTR_ESCAPES) { return htmlEscaping.escape(text);
text = text.replace(escape[0], escape[1]);
}
return text;
} }
public static Escaping htmlEscaping = new HtmlEscaping();
// TODO: this method was never called, what was my intention here? // TODO: this method was never called, what was my intention here?
protected static boolean allowsWhitespace (char typeChar) { protected static boolean allowsWhitespace (char typeChar) {
return (typeChar == '=') || // change delimiters return (typeChar == '=') || // change delimiters
@@ -574,12 +580,12 @@ public class Mustache
case '&': case '&':
requireNoNewlines(tag, tagLine); requireNoNewlines(tag, tagLine);
_segs.add(new VariableSegment(tag1, false, tagLine)); _segs.add(new VariableSegment(tag1, tagLine, new NoEscaping()));
return this; return this;
default: default:
requireNoNewlines(tag, tagLine); requireNoNewlines(tag, tagLine);
_segs.add(new VariableSegment(tag, _comp.escapeHTML, tagLine)); _segs.add(new VariableSegment(tag, tagLine, _comp.escaping));
return this; return this;
} }
} }
@@ -663,9 +669,9 @@ public class Mustache
/** A segment that substitutes the contents of a variable. */ /** A segment that substitutes the contents of a variable. */
protected static class VariableSegment extends NamedSegment { protected static class VariableSegment extends NamedSegment {
public VariableSegment (String name, boolean escapeHTML, int line) { public VariableSegment(String name, int line, Escaping escaping) {
super(name, line); super(name, line);
_escapeHTML = escapeHTML; _escaping = escaping;
} }
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) { @Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
Object value = tmpl.getValueOrDefault(ctx, _name, _line); Object value = tmpl.getValueOrDefault(ctx, _name, _line);
@@ -674,9 +680,9 @@ public class Mustache
"' on line " + _line, _name, _line); "' on line " + _line, _name, _line);
} }
String text = String.valueOf(value); String text = String.valueOf(value);
write(out, _escapeHTML ? escapeHTML(text) : text); write(out, _escaping.escape(text));
} }
protected boolean _escapeHTML; protected Escaping _escaping;
} }
/** A helper class for block segments. */ /** A helper class for block segments. */
@@ -752,16 +758,6 @@ public class Mustache
protected final Compiler _comp; protected final Compiler _comp;
} }
/** Map of strings that must be replaced inside html attributes and their replacements. (They
* need to be applied in order so amps are not double escaped.) */
protected static final String[][] ATTR_ESCAPES = {
{ "&", "&amp;" },
{ "'", "&#39;" },
{ "\"", "&quot;" },
{ "<", "&lt;" },
{ ">", "&gt;" },
};
/** Used when we have only a single character delimiter. */ /** Used when we have only a single character delimiter. */
protected static final char NO_CHAR = Character.MIN_VALUE; protected static final char NO_CHAR = Character.MIN_VALUE;
@@ -770,4 +766,5 @@ public class Mustache
throw new UnsupportedOperationException("Template loading not configured"); throw new UnsupportedOperationException("Template loading not configured");
} }
}; };
} }
@@ -0,0 +1,13 @@
package com.samskivert.mustache.formats;
public class HtmlEscaping extends SimpleEscaping {
public HtmlEscaping() {
super(new String[][]{
{"&", "&amp;"},
{"'", "&#39;"},
{"\"", "&quot;"},
{"<", "&lt;"},
{">", "&gt;"},
});
}
}
@@ -0,0 +1,10 @@
package com.samskivert.mustache.formats;
import com.samskivert.mustache.Escaping;
public class NoEscaping implements Escaping {
@Override
public String escape(String raw) {
return raw;
}
}
@@ -0,0 +1,19 @@
package com.samskivert.mustache.formats;
import com.samskivert.mustache.Escaping;
public class SimpleEscaping implements Escaping {
private final String[][] substringReplacements;
public SimpleEscaping(String[][] substringReplacements) {
this.substringReplacements = substringReplacements;
}
@Override
public String escape(String text) {
for (String[] escape : substringReplacements) {
text = text.replace(escape[0], escape[1]);
}
return text;
}
}
@@ -13,6 +13,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import com.samskivert.mustache.formats.SimpleEscaping;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -295,6 +296,16 @@ public class MustacheTest
execute(context("a", "<b>"))); execute(context("a", "<b>")));
} }
@Test public void testUserDefinedEscaping() {
Escaping escaping = new SimpleEscaping(new String[][] {
{"[", ":BEGIN:"},
{"]", ":END:"}
});
assertEquals(":BEGIN:b:END:", Mustache.compiler().escaping(escaping).compile("{{a}}").
execute(context("a", "[b]")));
}
@Test public void testPartialDelimiterMatch () { @Test public void testPartialDelimiterMatch () {
assertEquals("{bob}", Mustache.compiler().compile("{bob}").execute(context())); assertEquals("{bob}", Mustache.compiler().compile("{bob}").execute(context()));
assertEquals("bar", Mustache.compiler().compile("{{bob}bob}}").execute( assertEquals("bar", Mustache.compiler().compile("{{bob}bob}}").execute(