Basic implementation and some tests.
This commit is contained in:
@@ -3,15 +3,23 @@
|
|||||||
|
|
||||||
package com.samskivert.mustache;
|
package com.samskivert.mustache;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.lang.reflect.Array;
|
import java.lang.reflect.Array;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides <a href="http://mustache.github.com/">Mustache</a> templating services.
|
* Provides <a href="http://mustache.github.com/">Mustache</a> templating services.
|
||||||
|
* <p> Limitations:
|
||||||
|
* <ul><li> Only one or two character delimiters are supported when using {{=ab cd=}} to change
|
||||||
|
* delimiters.
|
||||||
|
* <li> {{< include}} is not supported. We specifically do not want the complexity of handling the
|
||||||
|
* automatic loading of dependent templates. </ul>
|
||||||
*/
|
*/
|
||||||
public class Mustache
|
public class Mustache
|
||||||
{
|
{
|
||||||
@@ -26,50 +34,273 @@ public class Mustache
|
|||||||
/**
|
/**
|
||||||
* Compiles the supplied template into a repeatedly executable intermediate form.
|
* Compiles the supplied template into a repeatedly executable intermediate form.
|
||||||
*/
|
*/
|
||||||
public static Template compile (Reader template)
|
public static Template compile (Reader source)
|
||||||
{
|
{
|
||||||
return null; // TODO
|
// a hand-rolled parser; whee!
|
||||||
|
Accumulator accum = new Accumulator();
|
||||||
|
char start1 = '{', start2 = '{', end1 = '}', end2 = '}';
|
||||||
|
int state = TEXT, startPos = 0, endPos = 0;
|
||||||
|
StringBuilder text = new StringBuilder();
|
||||||
|
int line = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
char c;
|
||||||
|
try {
|
||||||
|
int v = source.read();
|
||||||
|
if (v == -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
c = (char)v;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new MustacheException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '\n') {
|
||||||
|
line++;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case TEXT:
|
||||||
|
if (c == start1) {
|
||||||
|
if (start2 == -1) {
|
||||||
|
accum.addTextSegment(text);
|
||||||
|
state = TAG;
|
||||||
|
} else {
|
||||||
|
state = MATCHING_START;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text.append(c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MATCHING_START:
|
||||||
|
if (c == start2) {
|
||||||
|
accum.addTextSegment(text);
|
||||||
|
state = TAG;
|
||||||
|
} else {
|
||||||
|
text.append(start1);
|
||||||
|
if (c != start1) {
|
||||||
|
state = TEXT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TAG:
|
||||||
|
if (c == end1) {
|
||||||
|
if (end2 == -1) {
|
||||||
|
if (text.charAt(0) == '=') {
|
||||||
|
// TODO: change delimiters
|
||||||
|
} else {
|
||||||
|
accum = accum.addTagSegment(text, line);
|
||||||
|
}
|
||||||
|
state = TEXT;
|
||||||
|
} else {
|
||||||
|
state = MATCHING_END;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text.append(c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MATCHING_END:
|
||||||
|
if (c == end2) {
|
||||||
|
if (text.charAt(0) == '=') {
|
||||||
|
// TODO: change delimiters
|
||||||
|
} else {
|
||||||
|
accum = accum.addTagSegment(text, line);
|
||||||
|
}
|
||||||
|
state = TEXT;
|
||||||
|
} else {
|
||||||
|
text.append(end1);
|
||||||
|
if (c != end1) {
|
||||||
|
state = TAG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accumulate any trailing text
|
||||||
|
switch (state) {
|
||||||
|
case TEXT:
|
||||||
|
accum.addTextSegment(text);
|
||||||
|
break;
|
||||||
|
case MATCHING_START:
|
||||||
|
text.append(start1);
|
||||||
|
accum.addTextSegment(text);
|
||||||
|
break;
|
||||||
|
case MATCHING_END:
|
||||||
|
text.append(end1);
|
||||||
|
accum.addTextSegment(text);
|
||||||
|
break;
|
||||||
|
case TAG:
|
||||||
|
throw new MustacheException("Template ended while parsing a tag TODO");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Template(accum.finish());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mustache () {} // no instantiateski
|
private Mustache () {} // no instantiateski
|
||||||
|
|
||||||
|
protected static String escapeHTML (String text)
|
||||||
|
{
|
||||||
|
for (int ii = 0; ii < ATTR_ESCAPES.length; ii++) {
|
||||||
|
text = text.replace(ATTR_ESCAPES[ii][0], ATTR_ESCAPES[ii][1]);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final int TEXT = 0;
|
||||||
|
protected static final int MATCHING_START = 1;
|
||||||
|
protected static final int MATCHING_END = 2;
|
||||||
|
protected static final int TAG = 3;
|
||||||
|
|
||||||
|
protected static class Accumulator {
|
||||||
|
public void addTextSegment (StringBuilder text) {
|
||||||
|
if (text.length() > 0) {
|
||||||
|
_segs.add(new StringSegment(text.toString()));
|
||||||
|
text.setLength(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Accumulator addTagSegment (StringBuilder accum, final int line) {
|
||||||
|
final Accumulator outer = this;
|
||||||
|
String tag = accum.toString().trim();
|
||||||
|
final String tag1 = tag.substring(1).trim();
|
||||||
|
accum.setLength(0);
|
||||||
|
|
||||||
|
switch (tag.charAt(0)) {
|
||||||
|
case '#':
|
||||||
|
requireNoNewlines(tag, line);
|
||||||
|
return new Accumulator() {
|
||||||
|
public Template.Segment[] finish () {
|
||||||
|
throw new MustacheException("Section missing close tag " +
|
||||||
|
"[line=" + line + ", name=" + tag1 + "]");
|
||||||
|
}
|
||||||
|
protected Accumulator addCloseSectionSegment (String itag, int line) {
|
||||||
|
requireSameName(tag1, itag, line);
|
||||||
|
outer._segs.add(new SectionSegment(itag, super.finish()));
|
||||||
|
return outer;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case '^':
|
||||||
|
requireNoNewlines(tag, line);
|
||||||
|
return new Accumulator() {
|
||||||
|
public Template.Segment[] finish () {
|
||||||
|
throw new MustacheException("Inverted section missing close tag " +
|
||||||
|
"[line=" + line + ", name=" + tag1 + "]");
|
||||||
|
}
|
||||||
|
protected Accumulator addCloseSectionSegment (String itag, int line) {
|
||||||
|
requireSameName(tag1, itag, line);
|
||||||
|
outer._segs.add(new InvertedSectionSegment(itag, super.finish()));
|
||||||
|
return outer;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case '/':
|
||||||
|
requireNoNewlines(tag, line);
|
||||||
|
return addCloseSectionSegment(tag1, line);
|
||||||
|
|
||||||
|
case '!':
|
||||||
|
// comment!, ignore
|
||||||
|
return this;
|
||||||
|
|
||||||
|
case '&':
|
||||||
|
requireNoNewlines(tag, line);
|
||||||
|
_segs.add(new VariableSegment(tag1, false));
|
||||||
|
return this;
|
||||||
|
|
||||||
|
default:
|
||||||
|
requireNoNewlines(tag, line);
|
||||||
|
_segs.add(new VariableSegment(tag, true));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Template.Segment[] finish () {
|
||||||
|
return _segs.toArray(new Template.Segment[_segs.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Accumulator addCloseSectionSegment (String tag, int line) {
|
||||||
|
throw new MustacheException("Section close tag with no open tag " +
|
||||||
|
"[line=" + line + ", name=" + 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 + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void requireSameName (String name1, String name2, int line)
|
||||||
|
{
|
||||||
|
if (!name1.equals(name2)) {
|
||||||
|
throw new MustacheException(
|
||||||
|
"Section close tag with mismatched open tag " +
|
||||||
|
"[line=" + line + ", expected=" + name1 + ", got=" + name2 + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final List<Template.Segment> _segs = new ArrayList<Template.Segment>();
|
||||||
|
}
|
||||||
|
|
||||||
/** A simple segment that reproduces a string. */
|
/** A simple segment that reproduces a string. */
|
||||||
protected static class StringSegment extends Template.Segment {
|
protected static class StringSegment extends Template.Segment {
|
||||||
public StringSegment (String text) {
|
public StringSegment (String text) {
|
||||||
_text = text;
|
_text = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void execute (Object ctx, Writer out) {
|
@Override public void execute (Object ctx, Writer out) {
|
||||||
write(out, _text);
|
write(out, _text);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final String _text;
|
protected final String _text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A segment that substitutes the contents of a variable. */
|
/** A helper class for named segments. */
|
||||||
protected static class VariableSegment extends Template.Segment {
|
protected static abstract class NamedSegment extends Template.Segment {
|
||||||
public VariableSegment (String name) {
|
protected NamedSegment (String name) {
|
||||||
_name = name;
|
_name = name;
|
||||||
}
|
}
|
||||||
|
protected final String _name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A segment that substitutes the contents of a variable. */
|
||||||
|
protected static class VariableSegment extends NamedSegment {
|
||||||
|
public VariableSegment (String name, boolean escapeHTML) {
|
||||||
|
super(name);
|
||||||
|
_escapeHTML = escapeHTML;
|
||||||
|
}
|
||||||
@Override public void execute (Object ctx, Writer out) {
|
@Override public void execute (Object ctx, Writer out) {
|
||||||
Object value = getValue(ctx, _name);
|
Object value = getValue(ctx, _name);
|
||||||
// TODO: configurable behavior on missing values
|
// TODO: configurable behavior on missing values
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
write(out, String.valueOf(value));
|
String text = String.valueOf(value);
|
||||||
|
write(out, _escapeHTML ? escapeHTML(text) : text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
protected boolean _escapeHTML;
|
||||||
|
}
|
||||||
|
|
||||||
protected final String _name;
|
/** A helper class for compound segments. */
|
||||||
|
protected static abstract class CompoundSegment extends NamedSegment {
|
||||||
|
protected CompoundSegment (String name, Template.Segment[] segs) {
|
||||||
|
super(name);
|
||||||
|
_segs = segs;
|
||||||
|
}
|
||||||
|
protected void executeSegs (Object ctx, Writer out) {
|
||||||
|
for (Template.Segment seg : _segs) {
|
||||||
|
seg.execute(ctx, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected final Template.Segment[] _segs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A segment that represents a section. */
|
/** A segment that represents a section. */
|
||||||
protected static class SectionSegment extends Template.Segment {
|
protected static class SectionSegment extends CompoundSegment {
|
||||||
public SectionSegment (String name, Template.Segment[] segs) {
|
public SectionSegment (String name, Template.Segment[] segs) {
|
||||||
_name = name;
|
super(name, segs);
|
||||||
_segs = segs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void execute (Object ctx, Writer out) {
|
@Override public void execute (Object ctx, Writer out) {
|
||||||
Object value = getValue(ctx, _name);
|
Object value = getValue(ctx, _name);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
@@ -97,14 +328,47 @@ public class Mustache
|
|||||||
executeSegs(value, out);
|
executeSegs(value, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void executeSegs (Object ctx, Writer out) {
|
/** A segment that represents an inverted section. */
|
||||||
for (Template.Segment seg : _segs) {
|
protected static class InvertedSectionSegment extends CompoundSegment {
|
||||||
seg.execute(ctx, out);
|
public InvertedSectionSegment (String name, Template.Segment[] segs) {
|
||||||
|
super(name, segs);
|
||||||
|
}
|
||||||
|
@Override public void execute (Object ctx, Writer out) {
|
||||||
|
Object value = getValue(ctx, _name);
|
||||||
|
if (value == null) {
|
||||||
|
executeSegs(ctx, out); // TODO: configurable behavior on missing values
|
||||||
|
}
|
||||||
|
if (value instanceof Iterable<?>) {
|
||||||
|
Iterable<?> iable = (Iterable<?>)value;
|
||||||
|
if (!iable.iterator().hasNext()) {
|
||||||
|
executeSegs(ctx, out);
|
||||||
|
}
|
||||||
|
} else if (value instanceof Boolean) {
|
||||||
|
if (!(Boolean)value) {
|
||||||
|
executeSegs(ctx, out);
|
||||||
|
}
|
||||||
|
} else if (value.getClass().isArray()) {
|
||||||
|
if (Array.getLength(value) == 0) {
|
||||||
|
executeSegs(ctx, out);
|
||||||
|
}
|
||||||
|
} else if (value instanceof Iterator<?>) {
|
||||||
|
Iterator<?> iter = (Iterator<?>)value;
|
||||||
|
if (!iter.hasNext()) {
|
||||||
|
executeSegs(ctx, out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String _name;
|
|
||||||
protected Template.Segment[] _segs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Map of strings that must be replaced inside html attributes and their replacements. (They
|
||||||
|
* need to be applied in order so amps are not double escaped.) */
|
||||||
|
protected static final String[][] ATTR_ESCAPES = {
|
||||||
|
{ "&", "&" },
|
||||||
|
{ "'", "'" },
|
||||||
|
{ "\"", """ },
|
||||||
|
{ "<", "<" },
|
||||||
|
{ ">", ">" },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.samskivert.mustache;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.*;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Various unit tests.
|
||||||
|
*/
|
||||||
|
public class MustacheTest
|
||||||
|
{
|
||||||
|
@Test public void testSimpleVariable () {
|
||||||
|
test("bar", "{{foo}}", "foo", "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testOneShotSection () {
|
||||||
|
test("baz", "{{#foo}}{{bar}}{{/foo}}", "foo", context("bar", "baz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testListSection () {
|
||||||
|
test("bazbif", "{{#foo}}{{bar}}{{/foo}}", "foo",
|
||||||
|
Arrays.asList(context("bar", "baz"), context("bar", "bif")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testArraySection () {
|
||||||
|
test("bazbif", "{{#foo}}{{bar}}{{/foo}}", "foo",
|
||||||
|
new Object[] { context("bar", "baz"), context("bar", "bif") });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testIteratorSection () {
|
||||||
|
test("bazbif", "{{#foo}}{{bar}}{{/foo}}", "foo",
|
||||||
|
Arrays.asList(context("bar", "baz"), context("bar", "bif")).iterator());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testEmptyListSection () {
|
||||||
|
test("", "{{#foo}}{{bar}}{{/foo}}", "foo", Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testEmptyArraySection () {
|
||||||
|
test("", "{{#foo}}{{bar}}{{/foo}}", "foo", new Object[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testEmptyIteratorSection () {
|
||||||
|
test("", "{{#foo}}{{bar}}{{/foo}}", "foo", Collections.emptyList().iterator());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testFalseSection () {
|
||||||
|
test("", "{{#foo}}{{bar}}{{/foo}}", "foo", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testNestedListSection () {
|
||||||
|
test("1234", "{{#a}}{{#b}}{{c}}{{/b}}{{#d}}{{e}}{{/d}}{{/a}}", "a",
|
||||||
|
new Object[] { context("b", new Object[] { context("c", "1"), context("c", "2") }),
|
||||||
|
context("d", new Object[] { context("e", "3"), context("e", "4") }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testComment () {
|
||||||
|
test("foobar", "foo{{! nothing to see here}}bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void test (String expected, String template, Object... data)
|
||||||
|
{
|
||||||
|
assertEquals(expected, Mustache.compile(template).execute(context(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Object context (Object... data)
|
||||||
|
{
|
||||||
|
Map<String, Object> ctx = new HashMap<String, Object>();
|
||||||
|
for (int ii = 0; ii < data.length; ii += 2) {
|
||||||
|
ctx.put(data[ii].toString(), data[ii+1]);
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user