//
// JMustache - A Java implementation of the Mustache templating language
// http://github.com/samskivert/jmustache/blob/master/LICENSE
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.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides <a href="http://mustache.github.com/">Mustache</a> templating services.
* <p> Basic usage: <pre>{@code
* String source = "Hello {{arg}}!";
* Template tmpl = Mustache.compiler().compile(source);
* Map<String, Object> context = new HashMap<String, Object>();
* context.put("arg", "world");
* tmpl.execute(context); // returns "Hello world!" }</pre>
* <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 static Logger log = LoggerFactory.getLogger(Mustache.class);
/** An interface to the Mustache compilation process. See {@link Mustache}. */
public static class Compiler {
/** Whether or not HTML entities are escaped by default. */
public final boolean stripSpan;
/** Compiles the supplied template into a repeatedly executable intermediate form. */
public Template compile (String template)
{
return compile(new StringReader(template));
}
/** Compiles the supplied template into a repeatedly executable intermediate form. */
public Template compile (Reader source)
{
return Mustache.compile(source, this);
}
/** Returns a compiler that either does or does not escape HTML by default. */
public Compiler stripSpan (boolean stripSpan) {
return new Compiler(stripSpan);
}
protected Compiler (boolean stripSpan) {
this.stripSpan = stripSpan;
}
}
/**
* Returns a compiler that escapes HTML by default.
*/
public static Compiler compiler ()
{
return new Compiler(true);
}
/**
* Compiles the supplied template into a repeatedly executable intermediate form.
*/
protected static Template compile (Reader source, Compiler compiler)
{
// a hand-rolled parser; whee!
Accumulator accum = new Accumulator(compiler);
char start1 = '{', start2 = '{', end1 = '}', end2 = '}';
int state = TEXT;
StringBuilder text = new StringBuilder();
int line = 1;
boolean skipNewline = false;
boolean skippedExtraBracket = false;
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++;
// 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 {
skipNewline = false;
}
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) {
text.append(c);
state = TEXT;
}
}
break;
case TAG:
if (c == end1) {
if (!skippedExtraBracket && text.charAt(0) == '{') {
// This tag requires an extra closing '}', we need to skip it
skippedExtraBracket = true;
} else if (end2 == -1) {
if (text.charAt(0) == '=') {
// TODO: change delimiters
} else {
if (sanityCheckTag(text, line, start1, start2)) {
accum = accum.addTagSegment(text, line);
} else {
text.setLength(0);
}
skipNewline = accum.skipNewline();
skippedExtraBracket = false;
}
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 {
if (sanityCheckTag(text, line, start1, start2)) {
accum = accum.addTagSegment(text, line);
} else {
text.setLength(0);
}
skipNewline = accum.skipNewline();
skippedExtraBracket = false;
}
state = TEXT;
} else {
text.append(end1);
if (c != end1) {
text.append(c);
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:
log.error("Template ended while parsing a tag [line=" + line + ", tag="+ text + "]");
text.append(end1);
accum.addTextSegment(text);
break;
}
return new Template(accum.finish());
}
private Mustache () {} // no instantiateski
protected static boolean 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)) {
log.error("Tag contains start tag delimiter, probably missing close delimiter " +
"[line=" + line + ", tag=" + accum + "]");
return false;
}
}
}
return true;
}
protected static final Pattern spanPattern = Pattern.compile("^<span.+?>(.*)</span>");
protected static String stripSpan (String text)
{
// Changed for anki, stripping the wrapped field span when using {{{
Matcher m = spanPattern.matcher(text);
if (m.find()) {
text = m.group(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 Accumulator (Compiler compiler) {
_compiler = compiler;
}
public boolean skipNewline () {
// return true if we just added a compound segment which means we're immediately
// following the close section tag
return (_segs.size() > 0 && _segs.get(_segs.size()-1) instanceof CompoundSegment);
}
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 tagLine) {
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, tagLine);
return new Accumulator(_compiler) {
@Override public boolean skipNewline () {
// if we just opened this section, we want to skip a newline
return (_segs.size() == 0) || super.skipNewline();
}
@Override public Template.Segment[] finish () {
throw new MustacheException("Section missing close tag " +
"[line=" + tagLine + ", tag=" + tag1 + "]");
}
@Override protected Accumulator addCloseSectionSegment (String itag, int line) {
requireSameName(tag1, itag, line);
outer._segs.add(new SectionSegment(itag, super.finish(), tagLine));
return outer;
}
};
case '^':
requireNoNewlines(tag, tagLine);
return new Accumulator(_compiler) {
@Override public boolean skipNewline () {
// if we just opened this section, we want to skip a newline
return (_segs.size() == 0) || super.skipNewline();
}
@Override public Template.Segment[] finish () {
throw new MustacheException("Inverted section missing close tag " +
"[line=" + tagLine + ", tag=" + tag1 + "]");
}
@Override protected Accumulator addCloseSectionSegment (String itag, int line) {
requireSameName(tag1, itag, line);
outer._segs.add(new InvertedSectionSegment(itag, super.finish(), tagLine));
return outer;
}
};
case '/':
requireNoNewlines(tag, tagLine);
return addCloseSectionSegment(tag1, tagLine);
case '!':
// comment!, ignore
return this;
case '{':
requireNoNewlines(tag1, tagLine);
_segs.add(new VariableSegment(tag1, _compiler.stripSpan, tagLine));
return this;
default:
requireNoNewlines(tag, tagLine);
_segs.add(new VariableSegment(tag, false, tagLine));
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 + ", 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 newline " +
"[line=" + line + ", tag=" + 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 Compiler _compiler;
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 (Template tmpl, Template.Context ctx, Writer out) {
write(out, _text);
}
protected final String _text;
}
/** A helper class for named segments. */
protected static abstract class NamedSegment extends Template.Segment {
protected NamedSegment (String name, int line) {
_name = name.intern();
_line = line;
}
protected final String _name;
protected final int _line;
}
/** A segment that substitutes the contents of a variable. */
protected static class VariableSegment extends NamedSegment {
public VariableSegment (String name, boolean stripSpan, int line) {
super(name, line);
_stripSpan = stripSpan;
}
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
Object value = tmpl.getValue(ctx, _name, _line);
// TODO: configurable behavior on missing values
if (value != null) {
String text = String.valueOf(value);
write(out, _stripSpan ? stripSpan(text) : text);
}
}
protected boolean _stripSpan;
}
/** A helper class for compound segments. */
protected static abstract class CompoundSegment extends NamedSegment {
protected CompoundSegment (String name, Template.Segment[] segs, int line) {
super(name, line);
_segs = segs;
}
protected void executeSegs (Template tmpl, Template.Context ctx, Writer out) {
for (Template.Segment seg : _segs) {
seg.execute(tmpl, ctx, out);
}
}
protected final Template.Segment[] _segs;
}
/** A segment that represents a section. */
protected static class SectionSegment extends CompoundSegment {
public SectionSegment (String name, Template.Segment[] segs, int line) {
super(name, segs, line);
}
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
Object value = tmpl.getValue(ctx, _name, _line);
if (value == null) {
return; // TODO: configurable behavior on missing values
}
if (value instanceof Iterable<?>) {
value = ((Iterable<?>)value).iterator();
}
if (value instanceof Iterator<?>) {
Template.Mode mode = null;
int index = 0;
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, ++index, mode), out);
}
} else if (value instanceof Boolean) {
if ((Boolean)value) {
executeSegs(tmpl, ctx, out);
}
} else if (value instanceof String) {
if (((String) value).length() > 0) {
executeSegs(tmpl, ctx, out);
}
} else if (value.getClass().isArray()) {
for (int ii = 0, ll = Array.getLength(value); ii < ll; ii++) {
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), ii+1, mode), out);
}
} else {
executeSegs(tmpl, ctx.nest(value, 0, Template.Mode.OTHER), out);
}
}
}
/** A segment that represents an inverted section. */
protected static class InvertedSectionSegment extends CompoundSegment {
public InvertedSectionSegment (String name, Template.Segment[] segs, int line) {
super(name, segs, line);
}
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
Object value = tmpl.getValue(ctx, _name, _line);
if (value == null) {
executeSegs(tmpl, ctx, out); // TODO: configurable behavior on missing values
}
if (value instanceof Iterable<?>) {
Iterable<?> iable = (Iterable<?>)value;
if (!iable.iterator().hasNext()) {
executeSegs(tmpl, ctx, out);
}
} else if (value instanceof Boolean) {
if (!(Boolean)value) {
executeSegs(tmpl, ctx, out);
}
} else if (value instanceof String) {
if (((String) value).length() == 0) {
executeSegs(tmpl, ctx, out);
}
} else if (value.getClass().isArray()) {
if (Array.getLength(value) == 0) {
executeSegs(tmpl, ctx, out);
}
} else if (value instanceof Iterator<?>) {
Iterator<?> iter = (Iterator<?>)value;
if (!iter.hasNext()) {
executeSegs(tmpl, ctx, out);
}
}
}
}
/** 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 = {
{ "&", "&" },
{ "'", "'" },
{ "\"", """ },
{ "<", "<" },
{ ">", ">" },
};
}