diff --git a/README.md b/README.md index 2c1ab3c..befab26 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,8 @@ Sections behave as you would expect: * Array, `Iterator`, or `Iterable` values repeatedly execute the section with each element used as the context for each iteration. Empty collections result in zero instances of the section being included in the template. - * An unresolvable or null value is treated as false (by default, see _Default Values_ for more - details). + * An unresolvable or null value is treated as false. This behavior can be changed by using + `strictSections()`. See _Default Values_ for more details. * Any other object results in a single execution of the section with that object as a context. See the code in @@ -206,12 +206,16 @@ throws an exception. Mustache.compiler().nullValue("what").compile(tmpl).execute(map); // throws MustacheException when executing the template because doesNotExist cannot be resolved -Note that section behavior deviates from the above specification (for historical reasons and -because it's kind of useful). By default, a section that is not resolvable or resolves to null will -be omitted (and conversely, an inverse section that is not resolvable or resolves to null will be -included). If you use `defaultValue()`, this behavior is preserved. If you use `nullValue()`, -sections that refer to an unresolvable variable will now throw an exception (sections that refer to -a resolvable, but null-valued variable, will behave as before). +### Sections + +Sections are not affected by the `nullValue()` or `defaultValue()` settings. Their behavior is +governed by a separate configuration: `strictSections()`. + +By default, a section that is not resolvable or which resolves to `null` will be omitted (and +conversely, an inverse section that is not resolvable or resolves to `null` will be included). If +you use `strictSections(true)`, sections that refer to an unresolvable value will always throw an +exception. Sections that refer to a resolvable but `null` value never throw an exception, +regardless of the `strictSections()` setting. Extensions ========== diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 330d014..fb36961 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -34,6 +34,11 @@ public class Mustache /** Whether or not standards mode is enabled. */ public final boolean standardsMode; + /** Whether or not to throw an exception when a section resolves to a missing value. If + * false, the section is simply omitted (or included in the case of inverse sections). If + * true, a {@code MustacheException} is thrown. */ + public final boolean strictSections; + /** A value to use when a variable resolves to null. If this value is null (which is the * default null value), an exception will be thrown. If {@link #missingIsNull} is also * true, this value will be used when a variable cannot be resolved. @@ -92,16 +97,27 @@ public class Mustache * disables the non-standard JMustache extensions like looking up missing names in a parent * context. */ public Compiler standardsMode (boolean standardsMode) { - return new Compiler(standardsMode, this.nullValue, this.missingIsNull, - this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, - this.escaper, this.loader, this.collector, this.delims); + return new Compiler(standardsMode, this.strictSections, this.nullValue, + this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, + this.formatter, this.escaper, this.loader, this.collector, + this.delims); + } + + /** Returns a compiler that throws an exception when a section references a missing value + * ({@code true}) or treats a missing value as {@code false} ({@code false}, the default). + */ + public Compiler strictSections (boolean strictSections) { + return new Compiler(this.standardsMode, strictSections, this.nullValue, + this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, + this.formatter, this.escaper, this.loader, this.collector, + this.delims); } /** Returns a compiler that will use the given value for any variable that is missing, or * otherwise resolves to null. This is like {@link #nullValue} except that it returns the * supplied default for missing keys and existing keys that return null values. */ public Compiler defaultValue (String defaultValue) { - return new Compiler(this.standardsMode, defaultValue, true, + return new Compiler(this.standardsMode, this.strictSections, defaultValue, true, this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, this.escaper, this.loader, this.collector, this.delims); } @@ -118,60 +134,62 @@ public class Mustache * which maps to {@code null}, then {@code nullValue} is used. * */ public Compiler nullValue (String nullValue) { - return new Compiler(this.standardsMode, nullValue, false, + return new Compiler(this.standardsMode, this.strictSections, nullValue, false, this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, this.escaper, this.loader, this.collector, this.delims); } /** Returns a compiler that will treat empty string as a false value if parameter is true. */ public Compiler emptyStringIsFalse (boolean emptyStringIsFalse) { - return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, - emptyStringIsFalse, this.zeroIsFalse, this.formatter, - this.escaper, this.loader, this.collector, this.delims); + return new Compiler(this.standardsMode, this.strictSections, this.nullValue, + this.missingIsNull, emptyStringIsFalse, this.zeroIsFalse, + this.formatter, this.escaper, this.loader, this.collector, + this.delims); } /** Returns a compiler that will treat zero as a false value if parameter is true. */ public Compiler zeroIsFalse (boolean zeroIsFalse) { - return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, - this.emptyStringIsFalse, zeroIsFalse, this.formatter, - this.escaper, this.loader, this.collector, this.delims); + return new Compiler(this.standardsMode, this.strictSections, this.nullValue, + this.missingIsNull, this.emptyStringIsFalse, zeroIsFalse, + this.formatter, this.escaper, this.loader, this.collector, + this.delims); } /** Configures the {@link Formatter} used to turn objects into strings. */ public Compiler withFormatter (Formatter formatter) { - return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, - this.emptyStringIsFalse, this.zeroIsFalse, formatter, - this.escaper, this.loader, this.collector, this.delims); + return new Compiler(this.standardsMode, this.strictSections, this.nullValue, + this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, + formatter, this.escaper, this.loader, this.collector, this.delims); } /** Configures the {@link Escaper} used to escape substituted text. */ public Compiler withEscaper (Escaper escaper) { - return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, - this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, - escaper, this.loader, this.collector, this.delims); + return new Compiler(this.standardsMode, this.strictSections, this.nullValue, + this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, + this.formatter, escaper, this.loader, this.collector, this.delims); } /** Returns a compiler configured to use the supplied template loader to handle partials. */ public Compiler withLoader (TemplateLoader loader) { - return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, - this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, - this.escaper, loader, this.collector, this.delims); + return new Compiler(this.standardsMode, this.strictSections, this.nullValue, + this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, + this.formatter, this.escaper, loader, this.collector, this.delims); } /** Returns a compiler configured to use the supplied collector. */ public Compiler withCollector (Collector collector) { - return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, - this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, - this.escaper, this.loader, collector, this.delims); + return new Compiler(this.standardsMode, this.strictSections, this.nullValue, + this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, + this.formatter, this.escaper, this.loader, collector, this.delims); } /** Returns a compiler configured to use the supplied delims as default delimiters. * @param delims a string of the form {@code AB CD} or {@code A D} where A and B are * opening delims and C and D are closing delims. */ public Compiler withDelims (String delims) { - return new Compiler(this.standardsMode, this.nullValue, this.missingIsNull, - this.emptyStringIsFalse, this.zeroIsFalse, this.formatter, - this.escaper, this.loader, this.collector, + return new Compiler(this.standardsMode, this.strictSections, this.nullValue, + this.missingIsNull, this.emptyStringIsFalse, this.zeroIsFalse, + this.formatter, this.escaper, this.loader, this.collector, new Delims().updateDelims(delims)); } @@ -189,11 +207,12 @@ public class Mustache (zeroIsFalse && (value instanceof Number) && ((Number)value).longValue() == 0); } - protected Compiler (boolean standardsMode, String nullValue, boolean missingIsNull, - boolean emptyStringIsFalse, boolean zeroIsFalse, Formatter formatter, - Escaper escaper, TemplateLoader loader, Collector collector, - Delims delims) { + protected Compiler (boolean standardsMode, boolean strictSections, String nullValue, + boolean missingIsNull, boolean emptyStringIsFalse, boolean zeroIsFalse, + Formatter formatter, Escaper escaper, TemplateLoader loader, + Collector collector, Delims delims) { this.standardsMode = standardsMode; + this.strictSections = strictSections; this.nullValue = nullValue; this.missingIsNull = missingIsNull; this.emptyStringIsFalse = emptyStringIsFalse; @@ -281,9 +300,10 @@ public class Mustache * Returns a compiler that escapes HTML by default and does not use standards mode. */ public static Compiler compiler () { - return new Compiler(/*standardsMode=*/false, /*nullValue=*/null, /*missingIsNull=*/true, - /*emptyStringIsFalse=*/false, /*zeroIsFalse=*/false, DEFAULT_FORMATTER, - Escapers.HTML, FAILING_LOADER, new DefaultCollector(), new Delims()); + return new Compiler(/*standardsMode=*/false, /*strictSections=*/false, /*nullValue=*/null, + /*missingIsNull=*/false, /*emptyStringIsFalse=*/false, + /*zeroIsFalse=*/false, DEFAULT_FORMATTER, Escapers.HTML, FAILING_LOADER, + new DefaultCollector(), new Delims()); } /** diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index 5c96424..062a3ed 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -207,14 +207,11 @@ public class Template /** * Returns the value of the specified variable, noting that it is intended to be used as the - * contents for a segment. Presently this does not do anything special, but eventually this - * will be the means by which we enact configured behavior for sections that reference null or - * missing variables. Right now, all such variables result in a length 0 section. + * contents for a section. */ protected Object getSectionValue (Context ctx, String name, int line) { - // TODO: configurable behavior on missing values - Object value = getValue(ctx, name, line, _compiler.missingIsNull); - // TODO: configurable behavior on null values + Object value = getValue(ctx, name, line, !_compiler.strictSections); + // TODO: configurable behavior on null values? return (value == null) ? Collections.emptyList() : value; } diff --git a/src/test/java/com/samskivert/mustache/MustacheTest.java b/src/test/java/com/samskivert/mustache/MustacheTest.java index cf50f30..e144f37 100644 --- a/src/test/java/com/samskivert/mustache/MustacheTest.java +++ b/src/test/java/com/samskivert/mustache/MustacheTest.java @@ -161,20 +161,47 @@ public class MustacheTest test("", "{{#foo}}{{bar}}{{/foo}}", new Object() { Object foo = null; }); - } - - @Test public void testNullSectionWithDefaultValue () { test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() { Object foo = null; }); - } - - @Test public void testNullSectionWithNullValue () { test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() { Object foo = null; }); } + @Test public void testMissingNonStrictSection () { + test("", "{{#foo}}{{bar}}{{/foo}}", new Object() { + // no foo; section omitted due to non-strict-sections + }); + test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() { + // no foo; no exception because nullValue does change section strictness + }); + test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() { + // no foo; no exception because defaultValue does change section strictness + }); + } + + @Test(expected=MustacheException.class) + public void testMissingStrictSection () { + test(Mustache.compiler().strictSections(true), "", "{{#foo}}{{bar}}{{/foo}}", new Object() { + // no foo; should throw exception due to strict-sections + }); + } + + @Test(expected=MustacheException.class) + public void testMissingStrictSectionNullValue () { + // missing strict-sections always throw regardless of nullValue() + test(Mustache.compiler().strictSections(true).nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", + new Object()); + } + + @Test(expected=MustacheException.class) + public void testMissingStrictSectionDefaultValue () { + // missing strict-sections always throw regardless of defaultValue() + test(Mustache.compiler().strictSections(true).defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", + new Object()); + } + @Test public void testSectionWithNonFalseyEmptyString () { test(Mustache.compiler(), "test", "{{#foo}}test{{/foo}}", new Object() { String foo = ""; @@ -223,25 +250,6 @@ public class MustacheTest }); } - @Test public void testMissingSection () { - test("", "{{#foo}}{{bar}}{{/foo}}", new Object() { - // no foo - }); - } - - @Test public void testMissingSectionWithDefaultValue () { - test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() { - // no foo - }); - } - - @Test(expected=MustacheException.class) - public void testMissingSectionWithNullValue () { - test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() { - // no foo - }); - } - @Test public void testComment () { test("foobar", "foo{{! nothing to see here}}bar", new Object()); } @@ -558,15 +566,15 @@ public class MustacheTest } @Test public void testStandardsModeWithNullValuesInLoop () { - String tmpl = "first line\n{{#nonexistent}}foo\n{{/nonexistent}}\nsecond line"; - String result = Mustache.compiler().standardsMode(true).compile(tmpl).execute(new Object()); - check("first line\nsecond line", result); + test("first line\nsecond line", + "first line\n{{#nullvalue}}foo\n{{/nullvalue}}\nsecond line", + context("nullvalue", null)); } @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()); - check("first line\nfoo \nsecond line", result); + test("first line\nfoo \nsecond line", + "first line\n{{^nullvalue}}foo{{/nullvalue}} \nsecond line", + context("nullvalue", null)); } @Test public void testStandardsModeWithDotValue () {