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,11 +367,13 @@ 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) {
throw new MustacheException(
"No key, method or field with name '" + _name + "' on line " + _line);
}
String text = String.valueOf(value); String text = String.valueOf(value);
write(out, _escapeHTML ? escapeHTML(text) : text); 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,9 +72,12 @@ 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 (!_compiler.standardsMode) {
// if we're dealing with a compound key, resolve each component and use the result to // if we're dealing with a compound key, resolve each component and use the result to
// resolve the subsequent component and so forth // resolve the subsequent component and so forth
if (name.indexOf(".") != -1) { if (name.indexOf(".") != -1) {
@@ -95,6 +99,7 @@ public class Template
} }
return data; return data;
} }
}
// handle our special variables // handle our special variables
if (name == FIRST_NAME) { if (name == FIRST_NAME) {
@@ -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));