Added a "standards mode" based on patches from mrdon. In standards mode, we do

not search parent contexts for name bindings, nor do we support compound keys.
Also added support for {{.}} which has the same semantics as {{this}}. I must
have missed this when reading the Mustache spec.
This commit is contained in:
Michael Bayne
2011-02-28 11:52:40 -08:00
parent c271e7df12
commit f4cfa40c2b
3 changed files with 85 additions and 34 deletions
@@ -34,6 +34,9 @@ public class Mustache
/** Whether or not HTML entities are escaped by default. */ /** Whether or not HTML entities are escaped by default. */
public final boolean escapeHTML; public final boolean escapeHTML;
/** Whether or not standards mode is enabled. */
public final boolean standardsMode;
/** Compiles the supplied template into a repeatedly executable intermediate form. */ /** Compiles the supplied template into a repeatedly executable intermediate form. */
public Template compile (String template) public Template compile (String template)
{ {
@@ -48,20 +51,28 @@ 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); return new Compiler(escapeHTML, this.standardsMode);
} }
protected Compiler (boolean escapeHTML) { /** Returns a compiler that either does or does not use standards mode. Standards mode
* 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);
}
protected Compiler (boolean escapeHTML, boolean standardsMode) {
this.escapeHTML = escapeHTML; this.escapeHTML = escapeHTML;
this.standardsMode = standardsMode;
} }
} }
/** /**
* Returns a compiler that escapes HTML by default. * 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); return new Compiler(true, false);
} }
/** /**
@@ -185,7 +196,7 @@ public class Mustache
throw new MustacheException("Template ended while parsing a tag TODO"); throw new MustacheException("Template ended while parsing a tag TODO");
} }
return new Template(accum.finish()); return new Template(accum.finish(), compiler);
} }
private Mustache () {} // no instantiateski private Mustache () {} // no instantiateski
@@ -356,10 +367,12 @@ public class Mustache
@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.getValue(ctx, _name, _line); Object value = tmpl.getValue(ctx, _name, _line);
// TODO: configurable behavior on missing values // TODO: configurable behavior on missing values
if (value != null) { if (value == null) {
String text = String.valueOf(value); throw new MustacheException(
write(out, _escapeHTML ? escapeHTML(text) : text); "No key, method or field with name '" + _name + "' on line " + _line);
} }
String text = String.valueOf(value);
write(out, _escapeHTML ? escapeHTML(text) : text);
} }
protected boolean _escapeHTML; protected boolean _escapeHTML;
} }
@@ -425,8 +438,7 @@ public class Mustache
Object value = tmpl.getValue(ctx, _name, _line); Object value = tmpl.getValue(ctx, _name, _line);
if (value == null) { if (value == null) {
executeSegs(tmpl, ctx, out); // TODO: configurable behavior on missing values executeSegs(tmpl, ctx, out); // TODO: configurable behavior on missing values
} } else if (value instanceof Iterable<?>) {
if (value instanceof Iterable<?>) {
Iterable<?> iable = (Iterable<?>)value; Iterable<?> iable = (Iterable<?>)value;
if (!iable.iterator().hasNext()) { if (!iable.iterator().hasNext()) {
executeSegs(tmpl, ctx, out); executeSegs(tmpl, ctx, out);
@@ -444,7 +456,7 @@ public class Mustache
if (!iter.hasNext()) { if (!iter.hasNext()) {
executeSegs(tmpl, ctx, out); executeSegs(tmpl, ctx, out);
} }
} } // TODO: fail?
} }
} }
@@ -60,9 +60,10 @@ public class Template
return out.toString(); return out.toString();
} }
protected Template (Segment[] segs) protected Template (Segment[] segs, Mustache.Compiler compiler)
{ {
_segs = segs; _segs = segs;
_compiler = compiler;
} }
/** /**
@@ -71,29 +72,33 @@ public class Template
* *
* @param ctx the context in which to look up the variable. * @param ctx the context in which to look up the variable.
* @param name the name of the variable to be resolved, which must be an interned string. * @param name the name of the variable to be resolved, which must be an interned string.
*
* @return the value associated with the supplied name or null if no value could be resolved.
*/ */
protected Object getValue (Context ctx, String name, int line) protected Object getValue (Context ctx, String name, int line)
{ {
// if we're dealing with a compound key, resolve each component and use the result to if (!_compiler.standardsMode) {
// resolve the subsequent component and so forth // if we're dealing with a compound key, resolve each component and use the result to
if (name.indexOf(".") != -1) { // resolve the subsequent component and so forth
String[] comps = name.split("\\."); if (name.indexOf(".") != -1) {
// we want to allow the first component of a compound key to be located in a parent String[] comps = name.split("\\.");
// context, but once we're selecting sub-components, they must only be resolved in the // we want to allow the first component of a compound key to be located in a parent
// object that represents that component // context, but once we're selecting sub-components, they must only be resolved in the
Object data = getValue(ctx, comps[0].intern(), line); // object that represents that component
for (int ii = 1; ii < comps.length; ii++) { Object data = getValue(ctx, comps[0].intern(), line);
// generate more helpful error message for (int ii = 1; ii < comps.length; ii++) {
if (data == null) { // generate more helpful error message
throw new NullPointerException( if (data == null) {
"Null context for compound variable '" + name + "' on line " + line + throw new NullPointerException(
". '" + comps[ii - 1] + "' resolved to null."); "Null context for compound variable '" + name + "' on line " + line +
". '" + comps[ii - 1] + "' resolved to null.");
}
// once we step into a composite key, we drop the ability to query our parent
// contexts; that would be weird and confusing
data = getValueIn(data, comps[ii].intern(), line);
} }
// once we step into a composite key, we drop the ability to query our parent return data;
// contexts; that would be weird and confusing
data = getValueIn(data, comps[ii].intern(), line);
} }
return data;
} }
// handle our special variables // handle our special variables
@@ -105,6 +110,11 @@ public class Template
return ctx.index; return ctx.index;
} }
// if we're in standards mode, we don't search our parent contexts
if (_compiler.standardsMode) {
return getValueIn(ctx.data, name, line);
}
while (ctx != null) { while (ctx != null) {
Object value = getValueIn(ctx.data, name, line); Object value = getValueIn(ctx.data, name, line);
if (value != null) { if (value != null) {
@@ -112,9 +122,8 @@ public class Template
} }
ctx = ctx.parent; ctx = ctx.parent;
} }
// we've popped all the way off the top of our stack of contexts, so fail // we've popped off the top of our stack of contexts, so return null
throw new MustacheException( return null;
"No key, method or field with name '" + name + "' on line " + line);
} }
protected Object getValueIn (Object data, String name, int line) protected Object getValueIn (Object data, String name, int line)
@@ -154,12 +163,14 @@ public class Template
} }
protected final Segment[] _segs; protected final Segment[] _segs;
protected final Mustache.Compiler _compiler;
protected final Map<Key, VariableFetcher> _fcache = protected final Map<Key, VariableFetcher> _fcache =
new ConcurrentHashMap<Key, VariableFetcher>(); new ConcurrentHashMap<Key, VariableFetcher>();
protected static VariableFetcher createFetcher (Key key) protected static VariableFetcher createFetcher (Key key)
{ {
if (key.name == THIS_NAME) { // support both .name and this.name to fetch members
if (key.name == DOT_NAME || key.name == THIS_NAME) {
return THIS_FETCHER; return THIS_FETCHER;
} }
@@ -314,6 +325,7 @@ public class Template
} }
}; };
protected static final String DOT_NAME = ".".intern();
protected static final String THIS_NAME = "this".intern(); protected static final String THIS_NAME = "this".intern();
protected static final String FIRST_NAME = "-first".intern(); protected static final String FIRST_NAME = "-first".intern();
protected static final String LAST_NAME = "-last".intern(); protected static final String LAST_NAME = "-last".intern();
@@ -203,6 +203,33 @@ public class MustacheTest
} }
} }
@Test public void testStandardsModeWithNullValuesInLoop () {
String tmpl = "first line\n{{#nonexistent}}foo{{/nonexistent}}\nsecond line";
String result = Mustache.compiler().standardsMode(true).compile(tmpl).execute(new Object());
assertEquals("first line\nsecond line", result);
}
@Test public void testStandardsModeWithNullValuesInInverseLoop () {
String tmpl = "first line\n{{^nonexistent}}foo{{/nonexistent}} \nsecond line";
String result = Mustache.compiler().standardsMode(true).compile(tmpl).execute(new Object());
assertEquals("first line\nfoo \nsecond line", result);
}
@Test public void testStandardsModeWithDotValue () {
String tmpl = "{{#foo}}:{{.}}:{{/foo}}";
String result = Mustache.compiler().standardsMode(true).compile(tmpl).
execute(Collections.singletonMap("foo", "bar"));
assertEquals(":bar:", result);
}
@Test(expected = MustacheException.class)
public void testStandardsModeWithNoParentContextSearching () {
String tmpl = "{{#parent}}foo{{parentProperty}}bar{{/parent}}";
String result = Mustache.compiler().standardsMode(true).compile(tmpl).
execute(context("parent", new Object(),
"parentProperty", "bar"));
}
protected void test (String expected, String template, Object ctx) protected void test (String expected, String template, Object ctx)
{ {
assertEquals(expected, Mustache.compiler().compile(template).execute(ctx)); assertEquals(expected, Mustache.compiler().compile(template).execute(ctx));