diff --git a/src/main/java/com/samskivert/mustache/Escaping.java b/src/main/java/com/samskivert/mustache/Escaping.java new file mode 100644 index 0000000..704fb8b --- /dev/null +++ b/src/main/java/com/samskivert/mustache/Escaping.java @@ -0,0 +1,5 @@ +package com.samskivert.mustache; + +public interface Escaping { + String escape(String raw); +} diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 41c9b8f..a4da867 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -4,6 +4,9 @@ package com.samskivert.mustache; +import com.samskivert.mustache.formats.HtmlEscaping; +import com.samskivert.mustache.formats.NoEscaping; + import java.io.IOException; import java.io.Reader; import java.io.StringReader; @@ -30,8 +33,8 @@ public class Mustache /** An interface to the Mustache compilation process. See {@link Mustache}. */ public static class Compiler { - /** Whether or not HTML entities are escaped by default. */ - public final boolean escapeHTML; + /** How characters are escaped by default. */ + public final Escaping escaping; /** Whether or not standards mode is enabled. */ public final boolean standardsMode; @@ -79,7 +82,11 @@ public class Mustache /** Returns a compiler that either does or does not escape HTML by default. */ 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.collector, this.delims); } @@ -88,7 +95,7 @@ public class Mustache * disables the non-standard JMustache extensions like looking up missing names in a parent * context. */ 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.collector, this.delims); } @@ -97,7 +104,7 @@ public class Mustache * 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. */ 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.collector, this.delims); } @@ -114,35 +121,35 @@ public class Mustache * raised. * */ 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.collector, this.delims); } /** Returns a compiler that will treat empty string as a false value if parameter is true. */ 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.loader, this.collector, this.delims); } /** Returns a compiler that will treat zero as a false value if parameter is true. */ 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.loader, this.collector, this.delims); } /** Returns a compiler configured to use the supplied template loader to handle partials. */ 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, loader, this.collector, this.delims); } /** Returns a compiler configured to use the supplied 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.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 * opening delims and C and D are closing 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.loader, this.collector, new Delims().updateDelims(delims)); } @@ -170,10 +177,10 @@ public class Mustache (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, TemplateLoader loader, Collector collector, Delims delims) { - this.escapeHTML = escapeHTML; + this.escaping = escaping; this.standardsMode = standardsMode; this.nullValue = nullValue; this.missingIsNull = missingIsNull; @@ -234,7 +241,7 @@ public class Mustache * Returns a compiler that escapes HTML by default and does not use standards mode. */ 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()); } @@ -262,12 +269,11 @@ public class Mustache } protected static String escapeHTML (String text) { - for (String[] escape : ATTR_ESCAPES) { - text = text.replace(escape[0], escape[1]); - } - return text; + return htmlEscaping.escape(text); } + public static Escaping htmlEscaping = new HtmlEscaping(); + // TODO: this method was never called, what was my intention here? protected static boolean allowsWhitespace (char typeChar) { return (typeChar == '=') || // change delimiters @@ -574,12 +580,12 @@ public class Mustache case '&': requireNoNewlines(tag, tagLine); - _segs.add(new VariableSegment(tag1, false, tagLine)); + _segs.add(new VariableSegment(tag1, tagLine, new NoEscaping())); return this; default: requireNoNewlines(tag, tagLine); - _segs.add(new VariableSegment(tag, _comp.escapeHTML, tagLine)); + _segs.add(new VariableSegment(tag, tagLine, _comp.escaping)); return this; } } @@ -663,9 +669,9 @@ public class Mustache /** A segment that substitutes the contents of a variable. */ 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); - _escapeHTML = escapeHTML; + _escaping = escaping; } @Override public void execute (Template tmpl, Template.Context ctx, Writer out) { Object value = tmpl.getValueOrDefault(ctx, _name, _line); @@ -674,9 +680,9 @@ public class Mustache "' on line " + _line, _name, _line); } 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. */ @@ -752,16 +758,6 @@ public class Mustache 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 = { - { "&", "&" }, - { "'", "'" }, - { "\"", """ }, - { "<", "<" }, - { ">", ">" }, - }; - /** Used when we have only a single character delimiter. */ protected static final char NO_CHAR = Character.MIN_VALUE; @@ -770,4 +766,5 @@ public class Mustache throw new UnsupportedOperationException("Template loading not configured"); } }; + } diff --git a/src/main/java/com/samskivert/mustache/formats/HtmlEscaping.java b/src/main/java/com/samskivert/mustache/formats/HtmlEscaping.java new file mode 100644 index 0000000..eb23eaa --- /dev/null +++ b/src/main/java/com/samskivert/mustache/formats/HtmlEscaping.java @@ -0,0 +1,13 @@ +package com.samskivert.mustache.formats; + +public class HtmlEscaping extends SimpleEscaping { + public HtmlEscaping() { + super(new String[][]{ + {"&", "&"}, + {"'", "'"}, + {"\"", """}, + {"<", "<"}, + {">", ">"}, + }); + } +} diff --git a/src/main/java/com/samskivert/mustache/formats/NoEscaping.java b/src/main/java/com/samskivert/mustache/formats/NoEscaping.java new file mode 100644 index 0000000..5edcc37 --- /dev/null +++ b/src/main/java/com/samskivert/mustache/formats/NoEscaping.java @@ -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; + } +} diff --git a/src/main/java/com/samskivert/mustache/formats/SimpleEscaping.java b/src/main/java/com/samskivert/mustache/formats/SimpleEscaping.java new file mode 100644 index 0000000..b70b66b --- /dev/null +++ b/src/main/java/com/samskivert/mustache/formats/SimpleEscaping.java @@ -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; + } +} diff --git a/src/test/java/com/samskivert/mustache/MustacheTest.java b/src/test/java/com/samskivert/mustache/MustacheTest.java index ef24da1..054136f 100644 --- a/src/test/java/com/samskivert/mustache/MustacheTest.java +++ b/src/test/java/com/samskivert/mustache/MustacheTest.java @@ -13,6 +13,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import com.samskivert.mustache.formats.SimpleEscaping; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -295,6 +296,16 @@ public class MustacheTest execute(context("a", ""))); } + @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 () { assertEquals("{bob}", Mustache.compiler().compile("{bob}").execute(context())); assertEquals("bar", Mustache.compiler().compile("{{bob}bob}}").execute(