Added support for nesting contexts, which allows us to resolve variables in
parent contexts if they're not available in our current context.
This commit is contained in:
@@ -18,7 +18,7 @@ import java.util.Map;
|
|||||||
* <p> Basic usage: <pre>{@code
|
* <p> Basic usage: <pre>{@code
|
||||||
* String source = "Hello {{arg}}!";
|
* String source = "Hello {{arg}}!";
|
||||||
* Template tmpl = Mustache.compiler().compile(source);
|
* Template tmpl = Mustache.compiler().compile(source);
|
||||||
* Map<String, Object> context = new HashMap<String, Context>();
|
* Map<String, Object> context = new HashMap<String, Object>();
|
||||||
* context.put("arg", "world");
|
* context.put("arg", "world");
|
||||||
* tmpl.execute(context); // returns "Hello world!" }</pre>
|
* tmpl.execute(context); // returns "Hello world!" }</pre>
|
||||||
* <p> Limitations:
|
* <p> Limitations:
|
||||||
@@ -315,7 +315,7 @@ public class Mustache
|
|||||||
public StringSegment (String text) {
|
public StringSegment (String text) {
|
||||||
_text = text;
|
_text = text;
|
||||||
}
|
}
|
||||||
@Override public void execute (Template tmpl, Object ctx, Writer out) {
|
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
write(out, _text);
|
write(out, _text);
|
||||||
}
|
}
|
||||||
protected final String _text;
|
protected final String _text;
|
||||||
@@ -335,7 +335,7 @@ public class Mustache
|
|||||||
super(name);
|
super(name);
|
||||||
_escapeHTML = escapeHTML;
|
_escapeHTML = escapeHTML;
|
||||||
}
|
}
|
||||||
@Override public void execute (Template tmpl, Object ctx, Writer out) {
|
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
Object value = tmpl.getValue(ctx, _name);
|
Object value = tmpl.getValue(ctx, _name);
|
||||||
// TODO: configurable behavior on missing values
|
// TODO: configurable behavior on missing values
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
@@ -352,7 +352,7 @@ public class Mustache
|
|||||||
super(name);
|
super(name);
|
||||||
_segs = segs;
|
_segs = segs;
|
||||||
}
|
}
|
||||||
protected void executeSegs (Template tmpl, Object ctx, Writer out) {
|
protected void executeSegs (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
for (Template.Segment seg : _segs) {
|
for (Template.Segment seg : _segs) {
|
||||||
seg.execute(tmpl, ctx, out);
|
seg.execute(tmpl, ctx, out);
|
||||||
}
|
}
|
||||||
@@ -365,7 +365,7 @@ public class Mustache
|
|||||||
public SectionSegment (String name, Template.Segment[] segs) {
|
public SectionSegment (String name, Template.Segment[] segs) {
|
||||||
super(name, segs);
|
super(name, segs);
|
||||||
}
|
}
|
||||||
@Override public void execute (Template tmpl, Object ctx, Writer out) {
|
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
Object value = tmpl.getValue(ctx, _name);
|
Object value = tmpl.getValue(ctx, _name);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return; // TODO: configurable behavior on missing values
|
return; // TODO: configurable behavior on missing values
|
||||||
@@ -373,7 +373,7 @@ public class Mustache
|
|||||||
if (value instanceof Iterable<?>) {
|
if (value instanceof Iterable<?>) {
|
||||||
Iterable<?> iable = (Iterable<?>)value;
|
Iterable<?> iable = (Iterable<?>)value;
|
||||||
for (Object elem : iable) {
|
for (Object elem : iable) {
|
||||||
executeSegs(tmpl, elem, out);
|
executeSegs(tmpl, ctx.nest(elem), out);
|
||||||
}
|
}
|
||||||
} else if (value instanceof Boolean) {
|
} else if (value instanceof Boolean) {
|
||||||
if ((Boolean)value) {
|
if ((Boolean)value) {
|
||||||
@@ -381,15 +381,15 @@ 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, Array.get(value, ii), out);
|
executeSegs(tmpl, ctx.nest(Array.get(value, ii)), out);
|
||||||
}
|
}
|
||||||
} else if (value instanceof Iterator<?>) {
|
} else if (value instanceof Iterator<?>) {
|
||||||
Iterator<?> iter = (Iterator<?>)value;
|
Iterator<?> iter = (Iterator<?>)value;
|
||||||
while (iter.hasNext()) {
|
while (iter.hasNext()) {
|
||||||
executeSegs(tmpl, iter.next(), out);
|
executeSegs(tmpl, ctx.nest(iter.next()), out);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
executeSegs(tmpl, value, out);
|
executeSegs(tmpl, ctx.nest(value), out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,7 +399,7 @@ public class Mustache
|
|||||||
public InvertedSectionSegment (String name, Template.Segment[] segs) {
|
public InvertedSectionSegment (String name, Template.Segment[] segs) {
|
||||||
super(name, segs);
|
super(name, segs);
|
||||||
}
|
}
|
||||||
@Override public void execute (Template tmpl, Object ctx, Writer out) {
|
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
Object value = tmpl.getValue(ctx, _name);
|
Object value = tmpl.getValue(ctx, _name);
|
||||||
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
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import java.io.StringWriter;
|
|||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@@ -44,8 +42,9 @@ 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);
|
||||||
for (Segment seg : _segs) {
|
for (Segment seg : _segs) {
|
||||||
seg.execute(this, context, out);
|
seg.execute(this, ctx, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +68,22 @@ 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.
|
||||||
*/
|
*/
|
||||||
protected Object getValue (Object ctx, String name)
|
protected Object getValue (Context ctx, String name)
|
||||||
{
|
{
|
||||||
if (ctx == null) {
|
while (ctx != null) {
|
||||||
|
Object value = getValueIn(ctx.data, name);
|
||||||
|
if (value != null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
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 + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Object getValueIn (Object data, String name)
|
||||||
|
{
|
||||||
|
if (data == null) {
|
||||||
throw new NullPointerException("Null context for variable '" + name + "'");
|
throw new NullPointerException("Null context for variable '" + name + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,26 +91,34 @@ public class Template
|
|||||||
// resolve the subsequent component and so forth
|
// resolve the subsequent component and so forth
|
||||||
if (name.indexOf(".") != -1) {
|
if (name.indexOf(".") != -1) {
|
||||||
for (String comp : name.split("\\.")) {
|
for (String comp : name.split("\\.")) {
|
||||||
ctx = getValue(ctx, comp);
|
// 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 ctx;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
Key key = new Key(ctx.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) {
|
||||||
try {
|
try {
|
||||||
return fetcher.get(ctx, name);
|
return fetcher.get(data, name);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// zoiks! non-monomorphic call site, update the cache and try again
|
// zoiks! non-monomorphic call site, update the cache and try again
|
||||||
_fcache.put(key, fetcher = createFetcher(key));
|
fetcher = createFetcher(key);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fetcher = createFetcher(key);
|
fetcher = createFetcher(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we were unable to create a fetcher, just return null and our caller can either try
|
||||||
|
// the parent context, or do le freak out
|
||||||
|
if (fetcher == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Object value = fetcher.get(ctx, name);
|
Object value = fetcher.get(data, name);
|
||||||
_fcache.put(key, fetcher);
|
_fcache.put(key, fetcher);
|
||||||
return value;
|
return value;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -138,8 +158,7 @@ public class Template
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new MustacheException("No method or field with appropriate name and not Map " +
|
return null;
|
||||||
"[var=" + key.name + ", ctx=" + key.cclass.getName() + "]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static Method getMethod (Class<?> clazz, String name)
|
protected static Method getMethod (Class<?> clazz, String name)
|
||||||
@@ -196,10 +215,25 @@ public class Template
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static class Context
|
||||||
|
{
|
||||||
|
public final Object data;
|
||||||
|
public final Context parent;
|
||||||
|
|
||||||
|
public Context (Object data, Context parent) {
|
||||||
|
this.data = data;
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Context nest (Object data) {
|
||||||
|
return new Context(data, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** A template is broken into segments. */
|
/** A template is broken into segments. */
|
||||||
protected static abstract class Segment
|
protected static abstract class Segment
|
||||||
{
|
{
|
||||||
abstract void execute (Template tmpl, Object ctx, Writer out);
|
abstract void execute (Template tmpl, Context ctx, Writer out);
|
||||||
|
|
||||||
protected static void write (Writer out, String data) {
|
protected static void write (Writer out, String data) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -104,9 +104,8 @@ public class MustacheTest
|
|||||||
|
|
||||||
@Test public void testNestedListSection () {
|
@Test public void testNestedListSection () {
|
||||||
test("1234", "{{#a}}{{#b}}{{c}}{{/b}}{{#d}}{{e}}{{/d}}{{/a}}",
|
test("1234", "{{#a}}{{#b}}{{c}}{{/b}}{{#d}}{{e}}{{/d}}{{/a}}",
|
||||||
context("a", new Object[] {
|
context("a", context("b", new Object[] { context("c", "1"), context("c", "2") },
|
||||||
context("b", new Object[] { context("c", "1"), context("c", "2") }),
|
"d", new Object[] { context("e", "3"), context("e", "4") })));
|
||||||
context("d", new Object[] { context("e", "3"), context("e", "4") }) }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void testComment () {
|
@Test public void testComment () {
|
||||||
@@ -166,6 +165,19 @@ public class MustacheTest
|
|||||||
"endlist", tmpl, context("items", Collections.emptyList()));
|
"endlist", tmpl, context("items", Collections.emptyList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test public void testNestedContexts () {
|
||||||
|
test("foo((foobar)(foobaz))", "{{name}}({{#things}}({{name}}{{thing_name}}){{/things}})",
|
||||||
|
context("name", "foo",
|
||||||
|
"things", Arrays.asList(context("thing_name", "bar"),
|
||||||
|
context("thing_name", "baz"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testShadowedContext () {
|
||||||
|
test("foo((bar)(baz))", "{{name}}({{#things}}({{name}}){{/things}})",
|
||||||
|
context("name", "foo",
|
||||||
|
"things", Arrays.asList(context("name", "bar"), context("name", "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));
|
||||||
|
|||||||
Reference in New Issue
Block a user