Basic implementation and some tests.

This commit is contained in:
Michael Bayne
2010-10-21 18:08:07 +00:00
parent 5640791b4b
commit 3eea40d1d4
2 changed files with 364 additions and 19 deletions
@@ -3,15 +3,23 @@
package com.samskivert.mustache;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* 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
{
@@ -26,50 +34,273 @@ public class Mustache
/**
* 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
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. */
protected static class StringSegment extends Template.Segment {
public StringSegment (String text) {
_text = text;
}
@Override public void execute (Object ctx, Writer out) {
write(out, _text);
}
protected final String _text;
}
/** A segment that substitutes the contents of a variable. */
protected static class VariableSegment extends Template.Segment {
public VariableSegment (String name) {
/** A helper class for named segments. */
protected static abstract class NamedSegment extends Template.Segment {
protected NamedSegment (String 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) {
Object value = getValue(ctx, _name);
// TODO: configurable behavior on missing values
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. */
protected static class SectionSegment extends Template.Segment {
protected static class SectionSegment extends CompoundSegment {
public SectionSegment (String name, Template.Segment[] segs) {
_name = name;
_segs = segs;
super(name, segs);
}
@Override public void execute (Object ctx, Writer out) {
Object value = getValue(ctx, _name);
if (value == null) {
@@ -97,14 +328,47 @@ public class Mustache
executeSegs(value, out);
}
}
}
protected void executeSegs (Object ctx, Writer out) {
for (Template.Segment seg : _segs) {
seg.execute(ctx, out);
/** A segment that represents an inverted section. */
protected static class InvertedSectionSegment extends CompoundSegment {
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 = {
{ "&", "&amp;" },
{ "'", "&apos;" },
{ "\"", "&quot;" },
{ "<", "&lt;" },
{ ">", "&gt;" },
};
}
@@ -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;
}
}