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:
@@ -34,6 +34,9 @@ public class Mustache
|
||||
/** Whether or not HTML entities are escaped by default. */
|
||||
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. */
|
||||
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. */
|
||||
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.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 ()
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
return new Template(accum.finish());
|
||||
return new Template(accum.finish(), compiler);
|
||||
}
|
||||
|
||||
private Mustache () {} // no instantiateski
|
||||
@@ -356,10 +367,12 @@ public class Mustache
|
||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||
Object value = tmpl.getValue(ctx, _name, _line);
|
||||
// TODO: configurable behavior on missing values
|
||||
if (value != null) {
|
||||
String text = String.valueOf(value);
|
||||
write(out, _escapeHTML ? escapeHTML(text) : text);
|
||||
if (value == null) {
|
||||
throw new MustacheException(
|
||||
"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;
|
||||
}
|
||||
@@ -425,8 +438,7 @@ public class Mustache
|
||||
Object value = tmpl.getValue(ctx, _name, _line);
|
||||
if (value == null) {
|
||||
executeSegs(tmpl, ctx, out); // TODO: configurable behavior on missing values
|
||||
}
|
||||
if (value instanceof Iterable<?>) {
|
||||
} else if (value instanceof Iterable<?>) {
|
||||
Iterable<?> iable = (Iterable<?>)value;
|
||||
if (!iable.iterator().hasNext()) {
|
||||
executeSegs(tmpl, ctx, out);
|
||||
@@ -444,7 +456,7 @@ public class Mustache
|
||||
if (!iter.hasNext()) {
|
||||
executeSegs(tmpl, ctx, out);
|
||||
}
|
||||
}
|
||||
} // TODO: fail?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,9 +60,10 @@ public class Template
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
protected Template (Segment[] segs)
|
||||
protected Template (Segment[] segs, Mustache.Compiler compiler)
|
||||
{
|
||||
_segs = segs;
|
||||
_compiler = compiler;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,29 +72,33 @@ public class Template
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
// if we're dealing with a compound key, resolve each component and use the result to
|
||||
// resolve the subsequent component and so forth
|
||||
if (name.indexOf(".") != -1) {
|
||||
String[] comps = name.split("\\.");
|
||||
// we want to allow the first component of a compound key to be located in a parent
|
||||
// context, but once we're selecting sub-components, they must only be resolved in the
|
||||
// object that represents that component
|
||||
Object data = getValue(ctx, comps[0].intern(), line);
|
||||
for (int ii = 1; ii < comps.length; ii++) {
|
||||
// generate more helpful error message
|
||||
if (data == null) {
|
||||
throw new NullPointerException(
|
||||
"Null context for compound variable '" + name + "' on line " + line +
|
||||
". '" + comps[ii - 1] + "' resolved to null.");
|
||||
if (!_compiler.standardsMode) {
|
||||
// if we're dealing with a compound key, resolve each component and use the result to
|
||||
// resolve the subsequent component and so forth
|
||||
if (name.indexOf(".") != -1) {
|
||||
String[] comps = name.split("\\.");
|
||||
// we want to allow the first component of a compound key to be located in a parent
|
||||
// context, but once we're selecting sub-components, they must only be resolved in the
|
||||
// object that represents that component
|
||||
Object data = getValue(ctx, comps[0].intern(), line);
|
||||
for (int ii = 1; ii < comps.length; ii++) {
|
||||
// generate more helpful error message
|
||||
if (data == null) {
|
||||
throw new NullPointerException(
|
||||
"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
|
||||
// contexts; that would be weird and confusing
|
||||
data = getValueIn(data, comps[ii].intern(), line);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// handle our special variables
|
||||
@@ -105,6 +110,11 @@ public class Template
|
||||
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) {
|
||||
Object value = getValueIn(ctx.data, name, line);
|
||||
if (value != null) {
|
||||
@@ -112,9 +122,8 @@ public class Template
|
||||
}
|
||||
ctx = ctx.parent;
|
||||
}
|
||||
// we've popped all the way off the top of our stack of contexts, so fail
|
||||
throw new MustacheException(
|
||||
"No key, method or field with name '" + name + "' on line " + line);
|
||||
// we've popped off the top of our stack of contexts, so return null
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Object getValueIn (Object data, String name, int line)
|
||||
@@ -154,12 +163,14 @@ public class Template
|
||||
}
|
||||
|
||||
protected final Segment[] _segs;
|
||||
protected final Mustache.Compiler _compiler;
|
||||
protected final Map<Key, VariableFetcher> _fcache =
|
||||
new ConcurrentHashMap<Key, VariableFetcher>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 FIRST_NAME = "-first".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)
|
||||
{
|
||||
assertEquals(expected, Mustache.compiler().compile(template).execute(ctx));
|
||||
|
||||
Reference in New Issue
Block a user