From 0b9b01dab6840bee76ed23b28da6b1e5d7c95d45 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 22 Oct 2010 05:02:09 +0000 Subject: [PATCH] Fixed some bugs with newline skipping. Improved error handling when a tag is not closed properly and another tag follows it on the same line. Added support for -first and -last special variables. Fixed problem with resolving outermost component of a compound key in parent contexts. --- .../com/samskivert/mustache/Mustache.java | 50 ++++++++++++---- .../com/samskivert/mustache/Template.java | 59 ++++++++++++++----- .../com/samskivert/mustache/MustacheTest.java | 10 ++++ 3 files changed, 90 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 40844af..37599f7 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -94,6 +94,7 @@ public class Mustache // if we just parsed an open section or close section task, we'll skip the first // newline character following it, if desired; TODO: handle CR, sigh if (skipNewline) { + skipNewline = false; continue; } } else { @@ -133,6 +134,7 @@ public class Mustache if (text.charAt(0) == '=') { // TODO: change delimiters } else { + sanityCheckTag(text, line, start1, start2); accum = accum.addTagSegment(text, line); skipNewline = accum.skipNewline(); } @@ -150,6 +152,7 @@ public class Mustache if (text.charAt(0) == '=') { // TODO: change delimiters } else { + sanityCheckTag(text, line, start1, start2); accum = accum.addTagSegment(text, line); skipNewline = accum.skipNewline(); } @@ -187,6 +190,19 @@ public class Mustache private Mustache () {} // no instantiateski + protected static void sanityCheckTag (StringBuilder accum, int line, char start1, char start2) + { + for (int ii = 0, ll = accum.length(); ii < ll; ii++) { + if (accum.charAt(ii) == start1) { + if (start2 == -1 || (ii < ll-1 && accum.charAt(ii+1) == start2)) { + throw new MustacheException( + "Tag contains start tag delimiter, probably missing close delimiter " + + "[line=" + line + ", tag=" + accum + "]"); + } + } + } + } + protected static String escapeHTML (String text) { for (int ii = 0; ii < ATTR_ESCAPES.length; ii++) { @@ -234,7 +250,7 @@ public class Mustache } @Override public Template.Segment[] finish () { throw new MustacheException("Section missing close tag " + - "[line=" + line + ", name=" + tag1 + "]"); + "[line=" + line + ", tag=" + tag1 + "]"); } @Override protected Accumulator addCloseSectionSegment (String itag, int line) { requireSameName(tag1, itag, line); @@ -252,7 +268,7 @@ public class Mustache } @Override public Template.Segment[] finish () { throw new MustacheException("Inverted section missing close tag " + - "[line=" + line + ", name=" + tag1 + "]"); + "[line=" + line + ", tag=" + tag1 + "]"); } @Override protected Accumulator addCloseSectionSegment (String itag, int line) { requireSameName(tag1, itag, line); @@ -287,13 +303,13 @@ public class Mustache protected Accumulator addCloseSectionSegment (String tag, int line) { throw new MustacheException("Section close tag with no open tag " + - "[line=" + line + ", name=" + tag + "]"); + "[line=" + line + ", tag=" + tag + "]"); } protected static void requireNoNewlines (String tag, int line) { if (tag.indexOf("\n") != -1 || tag.indexOf("\r") != -1) { throw new MustacheException("Invalid tag name: contains newlne " + - "[line=" + line + ", name=" + tag + "]"); + "[line=" + line + ", tag=" + tag + "]"); } } @@ -324,7 +340,7 @@ public class Mustache /** A helper class for named segments. */ protected static abstract class NamedSegment extends Template.Segment { protected NamedSegment (String name) { - _name = name; + _name = name.intern(); } protected final String _name; } @@ -371,9 +387,12 @@ public class Mustache return; // TODO: configurable behavior on missing values } if (value instanceof Iterable) { - Iterable iable = (Iterable)value; - for (Object elem : iable) { - executeSegs(tmpl, ctx.nest(elem), out); + Template.Mode mode = null; + for (Iterator iter = ((Iterable)value).iterator(); iter.hasNext(); ) { + Object elem = iter.next(); + mode = (mode == null) ? Template.Mode.FIRST : + (iter.hasNext() ? Template.Mode.OTHER : Template.Mode.LAST); + executeSegs(tmpl, ctx.nest(elem, mode), out); } } else if (value instanceof Boolean) { if ((Boolean)value) { @@ -381,15 +400,20 @@ public class Mustache } } else if (value.getClass().isArray()) { for (int ii = 0, ll = Array.getLength(value); ii < ll; ii++) { - executeSegs(tmpl, ctx.nest(Array.get(value, ii)), out); + Template.Mode mode = (ii == 0) ? Template.Mode.FIRST : + ((ii == ll-1) ? Template.Mode.LAST : Template.Mode.OTHER); + executeSegs(tmpl, ctx.nest(Array.get(value, ii), mode), out); } } else if (value instanceof Iterator) { - Iterator iter = (Iterator)value; - while (iter.hasNext()) { - executeSegs(tmpl, ctx.nest(iter.next()), out); + Template.Mode mode = null; + for (Iterator iter = (Iterator)value; iter.hasNext(); ) { + Object elem = iter.next(); + mode = (mode == null) ? Template.Mode.FIRST : + (iter.hasNext() ? Template.Mode.OTHER : Template.Mode.LAST); + executeSegs(tmpl, ctx.nest(elem, mode), out); } } else { - executeSegs(tmpl, ctx.nest(value), out); + executeSegs(tmpl, ctx.nest(value, Template.Mode.OTHER), out); } } } diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index 6277337..dcc4e93 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -42,7 +42,7 @@ public class Template */ public void execute (Object context, Writer out) throws MustacheException { - Context ctx = new Context(context, null); + Context ctx = new Context(context, null, Mode.OTHER); for (Segment seg : _segs) { seg.execute(this, ctx, out); } @@ -67,9 +67,41 @@ public class Template /** * Called by executing segments to obtain the value of the specified variable in the supplied * context. + * + * @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. */ protected Object getValue (Context ctx, String name) { + // 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()); + 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 + "'. " + + "The component previous to '" + comps[ii] + "' 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()); + } + return data; + } + + // handle our special variables + if (name == FIRST_NAME) { + return ctx.mode == Mode.FIRST; + } else if (name == LAST_NAME) { + return ctx.mode == Mode.LAST; + } + while (ctx != null) { Object value = getValueIn(ctx.data, name); if (value != null) { @@ -87,17 +119,6 @@ public class Template throw new NullPointerException("Null context for variable '" + name + "'"); } - // if we're dealing with a composite key, resolve each component and use the result to - // resolve the subsequent component and so forth - if (name.indexOf(".") != -1) { - for (String comp : name.split("\\.")) { - // 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, comp); - } - return data; - } - Key key = new Key(data.getClass(), name); VariableFetcher fetcher = _fcache.get(key); if (fetcher != null) { @@ -215,18 +236,22 @@ public class Template return null; } + protected static enum Mode { FIRST, OTHER, LAST }; + protected static class Context { public final Object data; public final Context parent; + public final Mode mode; - public Context (Object data, Context parent) { + public Context (Object data, Context parent, Mode mode) { this.data = data; this.parent = parent; + this.mode = mode; } - public Context nest (Object data) { - return new Context(data, this); + public Context nest (Object data, Mode mode) { + return new Context(data, this, mode); } } @@ -252,7 +277,7 @@ public class Template public Key (Class cclass, String name) { this.cclass = cclass; - this.name = name.intern(); + this.name = name; } @Override public int hashCode () { @@ -282,4 +307,6 @@ public class Template }; 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 895cd36..30a41b8 100644 --- a/src/test/java/com/samskivert/mustache/MustacheTest.java +++ b/src/test/java/com/samskivert/mustache/MustacheTest.java @@ -178,6 +178,16 @@ public class MustacheTest "things", Arrays.asList(context("name", "bar"), context("name", "baz")))); } + @Test public void testFirst () { + test("foo|bar|baz", "{{#things}}{{^-first}}|{{/-first}}{{this}}{{/things}}", + context("things", Arrays.asList("foo", "bar", "baz"))); + } + + @Test public void testLast () { + test("foo|bar|baz", "{{#things}}{{this}}{{^-last}}|{{/-last}}{{/things}}", + context("things", Arrays.asList("foo", "bar", "baz"))); + } + protected void test (String expected, String template, Object ctx) { assertEquals(expected, Mustache.compiler().compile(template).execute(ctx));