diff --git a/pom.xml b/pom.xml
index 919b0b5..d2ae101 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,8 @@
11
UTF-8
+
+ 2023-01-01T00:00:00Z
@@ -60,7 +62,7 @@
org.yaml
snakeyaml
- 2.0
+ 2.2
test
@@ -70,7 +72,7 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.11.0
+ 3.12.1
${source.level}
${source.level}
@@ -159,7 +161,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.2.2
+ 3.2.5
**/*Test.java
@@ -168,7 +170,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 3.6.2
+ 3.6.3
true
public
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. */