diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 72e3409..e6f055e 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -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? } } diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index 06c84c4..33229f9 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -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 _fcache = new ConcurrentHashMap(); 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(); diff --git a/src/test/java/com/samskivert/mustache/MustacheTest.java b/src/test/java/com/samskivert/mustache/MustacheTest.java index c3e9491..456f6ec 100644 --- a/src/test/java/com/samskivert/mustache/MustacheTest.java +++ b/src/test/java/com/samskivert/mustache/MustacheTest.java @@ -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));