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.
This commit is contained in:
Michael Bayne
2010-10-22 05:02:09 +00:00
parent 5932123d79
commit 0b9b01dab6
3 changed files with 90 additions and 29 deletions
@@ -94,6 +94,7 @@ public class Mustache
// if we just parsed an open section or close section task, we'll skip the first // 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 // newline character following it, if desired; TODO: handle CR, sigh
if (skipNewline) { if (skipNewline) {
skipNewline = false;
continue; continue;
} }
} else { } else {
@@ -133,6 +134,7 @@ public class Mustache
if (text.charAt(0) == '=') { if (text.charAt(0) == '=') {
// TODO: change delimiters // TODO: change delimiters
} else { } else {
sanityCheckTag(text, line, start1, start2);
accum = accum.addTagSegment(text, line); accum = accum.addTagSegment(text, line);
skipNewline = accum.skipNewline(); skipNewline = accum.skipNewline();
} }
@@ -150,6 +152,7 @@ public class Mustache
if (text.charAt(0) == '=') { if (text.charAt(0) == '=') {
// TODO: change delimiters // TODO: change delimiters
} else { } else {
sanityCheckTag(text, line, start1, start2);
accum = accum.addTagSegment(text, line); accum = accum.addTagSegment(text, line);
skipNewline = accum.skipNewline(); skipNewline = accum.skipNewline();
} }
@@ -187,6 +190,19 @@ public class Mustache
private Mustache () {} // no instantiateski 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) protected static String escapeHTML (String text)
{ {
for (int ii = 0; ii < ATTR_ESCAPES.length; ii++) { for (int ii = 0; ii < ATTR_ESCAPES.length; ii++) {
@@ -234,7 +250,7 @@ public class Mustache
} }
@Override public Template.Segment[] finish () { @Override public Template.Segment[] finish () {
throw new MustacheException("Section missing close tag " + throw new MustacheException("Section missing close tag " +
"[line=" + line + ", name=" + tag1 + "]"); "[line=" + line + ", tag=" + tag1 + "]");
} }
@Override protected Accumulator addCloseSectionSegment (String itag, int line) { @Override protected Accumulator addCloseSectionSegment (String itag, int line) {
requireSameName(tag1, itag, line); requireSameName(tag1, itag, line);
@@ -252,7 +268,7 @@ public class Mustache
} }
@Override public Template.Segment[] finish () { @Override public Template.Segment[] finish () {
throw new MustacheException("Inverted section missing close tag " + throw new MustacheException("Inverted section missing close tag " +
"[line=" + line + ", name=" + tag1 + "]"); "[line=" + line + ", tag=" + tag1 + "]");
} }
@Override protected Accumulator addCloseSectionSegment (String itag, int line) { @Override protected Accumulator addCloseSectionSegment (String itag, int line) {
requireSameName(tag1, itag, line); requireSameName(tag1, itag, line);
@@ -287,13 +303,13 @@ public class Mustache
protected Accumulator addCloseSectionSegment (String tag, int line) { protected Accumulator addCloseSectionSegment (String tag, int line) {
throw new MustacheException("Section close tag with no open tag " + 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) { protected static void requireNoNewlines (String tag, int line) {
if (tag.indexOf("\n") != -1 || tag.indexOf("\r") != -1) { if (tag.indexOf("\n") != -1 || tag.indexOf("\r") != -1) {
throw new MustacheException("Invalid tag name: contains newlne " + 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. */ /** A helper class for named segments. */
protected static abstract class NamedSegment extends Template.Segment { protected static abstract class NamedSegment extends Template.Segment {
protected NamedSegment (String name) { protected NamedSegment (String name) {
_name = name; _name = name.intern();
} }
protected final String _name; protected final String _name;
} }
@@ -371,9 +387,12 @@ public class Mustache
return; // TODO: configurable behavior on missing values return; // TODO: configurable behavior on missing values
} }
if (value instanceof Iterable<?>) { if (value instanceof Iterable<?>) {
Iterable<?> iable = (Iterable<?>)value; Template.Mode mode = null;
for (Object elem : iable) { for (Iterator<?> iter = ((Iterable<?>)value).iterator(); iter.hasNext(); ) {
executeSegs(tmpl, ctx.nest(elem), out); 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) { } else if (value instanceof Boolean) {
if ((Boolean)value) { if ((Boolean)value) {
@@ -381,15 +400,20 @@ public class Mustache
} }
} else if (value.getClass().isArray()) { } else if (value.getClass().isArray()) {
for (int ii = 0, ll = Array.getLength(value); ii < ll; ii++) { 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<?>) { } else if (value instanceof Iterator<?>) {
Iterator<?> iter = (Iterator<?>)value; Template.Mode mode = null;
while (iter.hasNext()) { for (Iterator<?> iter = (Iterator<?>)value; iter.hasNext(); ) {
executeSegs(tmpl, ctx.nest(iter.next()), out); 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 { } else {
executeSegs(tmpl, ctx.nest(value), out); executeSegs(tmpl, ctx.nest(value, Template.Mode.OTHER), out);
} }
} }
} }
@@ -42,7 +42,7 @@ public class Template
*/ */
public void execute (Object context, Writer out) throws MustacheException 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) { for (Segment seg : _segs) {
seg.execute(this, ctx, out); 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 * Called by executing segments to obtain the value of the specified variable in the supplied
* context. * 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) 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) { while (ctx != null) {
Object value = getValueIn(ctx.data, name); Object value = getValueIn(ctx.data, name);
if (value != null) { if (value != null) {
@@ -87,17 +119,6 @@ public class Template
throw new NullPointerException("Null context for variable '" + name + "'"); 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); Key key = new Key(data.getClass(), name);
VariableFetcher fetcher = _fcache.get(key); VariableFetcher fetcher = _fcache.get(key);
if (fetcher != null) { if (fetcher != null) {
@@ -215,18 +236,22 @@ public class Template
return null; return null;
} }
protected static enum Mode { FIRST, OTHER, LAST };
protected static class Context protected static class Context
{ {
public final Object data; public final Object data;
public final Context parent; 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.data = data;
this.parent = parent; this.parent = parent;
this.mode = mode;
} }
public Context nest (Object data) { public Context nest (Object data, Mode mode) {
return new Context(data, this); return new Context(data, this, mode);
} }
} }
@@ -252,7 +277,7 @@ public class Template
public Key (Class<?> cclass, String name) { public Key (Class<?> cclass, String name) {
this.cclass = cclass; this.cclass = cclass;
this.name = name.intern(); this.name = name;
} }
@Override public int hashCode () { @Override public int hashCode () {
@@ -282,4 +307,6 @@ public class Template
}; };
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 LAST_NAME = "-last".intern();
} }
@@ -178,6 +178,16 @@ public class MustacheTest
"things", Arrays.asList(context("name", "bar"), context("name", "baz")))); "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) 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));