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. */
|
/** 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));
|
||||||
|
|||||||
Reference in New Issue
Block a user