diff --git a/src/main/java/com/samskivert/mustache/Escapers.java b/src/main/java/com/samskivert/mustache/Escapers.java index b8a8a1e..ba6e269 100644 --- a/src/main/java/com/samskivert/mustache/Escapers.java +++ b/src/main/java/com/samskivert/mustache/Escapers.java @@ -4,11 +4,17 @@ package com.samskivert.mustache; +import java.io.IOException; +import java.io.UncheckedIOException; + /** * Defines some standard {@link Mustache.Escaper}s. */ public class Escapers { + // TODO for 2.0 this class should be final and have a private constructor but that + // would break semver on the off chance someone did extend it. + /** Escapes HTML entities. */ public static final Mustache.Escaper HTML = simple(new String[][] { { "&", "&" }, @@ -30,6 +36,11 @@ public class Escapers /** Returns an escaper that replaces a list of text sequences with canned replacements. * @param repls a list of {@code (text, replacement)} pairs. */ public static Mustache.Escaper simple (final String[]... repls) { + String[] lookupTable = Lookup7bitEscaper.createTable(repls); + if (lookupTable != null) { + return new Lookup7bitEscaper(lookupTable); + } + // our lookup replacements are not 7 bit ascii. return new Mustache.Escaper() { @Override public String escape (String text) { for (String[] escape : repls) { @@ -39,4 +50,98 @@ public class Escapers } }; } + // This is based on benchmarking: https://github.com/jstachio/escape-benchmark + private static class Lookup7bitEscaper implements Mustache.Escaper { + /* + * This only works for replacing the lower 7 bit ascii + * characters + */ + private final String[] lookupTable; + + private Lookup7bitEscaper( + String[] lookupTable) { + super(); + this.lookupTable = lookupTable; + } + + static /* @Nullable */ String[] createTable (String[][] mappings) { + String[] table = new String[128]; + for (String[] entry : mappings) { + String key = entry[0]; + String value = entry[1]; + if (key.length() != 1) { + return null; + } + char k = key.charAt(0); + if (k > 127) { + return null; + } + table[k] = value; + } + return table; + } + + @Override + public void escape (Appendable a, CharSequence raw) throws IOException { + int end = raw.length(); + for (int i = 0, start = 0; i < end; i++) { + char c = raw.charAt(i); + String found = escapeChar(lookupTable, c); + /* + * While this could be done with one loop it appears through + * benchmarking that by having the first loop assume the string + * to be not changed creates a fast path for strings with no escaping needed. + */ + if (found != null) { + a.append(raw, 0, i); + a.append(found); + start = i = i + 1; + for (; i < end; i++) { + c = raw.charAt(i); + found = escapeChar(lookupTable, c); + if (found != null) { + a.append(raw, start, i); + a.append(found); + start = i + 1; + } + } + a.append(raw, start, end); + return; + } + } + a.append(raw); + } + + private static /* @Nullable */ String escapeChar (String[] lookupTable, char c) { + if (c > 127) { + return null; + } + return lookupTable[c]; + } + + @Override + public String escape (String raw) { + StringBuilder sb = new StringBuilder(raw.length()); + try { + escape(sb, raw); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return sb.toString(); + } + + @Override + public String toString () { + StringBuilder sb = new StringBuilder(); + sb.append("Escaper["); + for(char i = 0; i < lookupTable.length; i++) { + String value = lookupTable[i]; + if (value != null) { + sb.append("{'").append(i).append("', '").append(value).append("'},"); + } + } + sb.append("]"); + return sb.toString(); + } + } } diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 19bf531..e79931f 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -317,6 +317,17 @@ public class Mustache { default CharSequence escape (CharSequence raw) { return escape(raw.toString()); } + + /** + * Escapes the raw characters with escape sequeneces if needed and appends to the appendable. + * The default implementation calls {@link #escape(CharSequence)}. + * @param a the stream like to append to. + * @param raw input string. + * @throws IOException if an error happens while writing to the appendable. + */ + default void escape (Appendable a, CharSequence raw) throws IOException { + a.append(escape(raw)); + } } /** Handles loading partial templates. */ @@ -1317,7 +1328,7 @@ public class Mustache { "No key, method or field with name '" + _name + "' on line " + _line; throw new MustacheException.Context(msg, _name, _line); } - write(out, _escaper.escape(_formatter.format(value))); + escape(out, _formatter.format(value), _escaper); } @Override public void decompile (Delims delims, StringBuilder into) { delims.addTag(' ', _name, into); diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index 393ed72..acb5d4d 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -433,6 +433,14 @@ public class Template { throw new MustacheException(ioe); } } + + protected static void escape (Appendable out, CharSequence data, Mustache.Escaper escape) { + try { + escape.escape(out, data); + } catch (IOException ioe) { + throw new MustacheException(ioe); + } + } } /** Used to cache variable fetchers for a given context class, name combination. */