Skip to content

Commit

Permalink
Merge pull request samskivert#166 from jstachio/stream_escape
Browse files Browse the repository at this point in the history
Improve escaping performance
  • Loading branch information
samskivert authored Dec 22, 2023
2 parents a4cd801 + 96aac1a commit 67451f4
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 1 deletion.
105 changes: 105 additions & 0 deletions src/main/java/com/samskivert/mustache/Escapers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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[][] {
{ "&", "&" },
Expand All @@ -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) {
Expand All @@ -39,4 +50,98 @@ public static Mustache.Escaper simple (final String[]... repls) {
}
};
}
// 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();
}
}
}
13 changes: 12 additions & 1 deletion src/main/java/com/samskivert/mustache/Mustache.java
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,17 @@ public interface Escaper {
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. */
Expand Down Expand Up @@ -1317,7 +1328,7 @@ public VariableSegment (String name, int line, Formatter formatter, Escaper esca
"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);
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/samskivert/mustache/Template.java
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,14 @@ protected static void write (Writer out, CharSequence data) {
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. */
Expand Down

0 comments on commit 67451f4

Please sign in to comment.