Inheritance support #120
Turn on inheritance tests. Some whitespace issues. Recursion working but multi standalone broken Make inheritance pass spec tests
This commit is contained in:
@@ -10,7 +10,9 @@ import java.io.StringReader;
|
|||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
@@ -379,6 +381,23 @@ public class Mustache {
|
|||||||
*/
|
*/
|
||||||
boolean visitInclude (String name);
|
boolean visitInclude (String name);
|
||||||
|
|
||||||
|
/** Visits a parent partial tag. For backward compatibility by default
|
||||||
|
* <code>false</code> is returned.
|
||||||
|
* @param name the name of the parent partial template specified by the tag.
|
||||||
|
* @return true if the template should be resolved and visited, false to skip it.
|
||||||
|
*/
|
||||||
|
default boolean visitParent (String name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Visits a block tag. For backward compatibility by default is skipped.
|
||||||
|
* @param name the name of the block.
|
||||||
|
* @return true if the contents of the block should be visited, false to skip.
|
||||||
|
*/
|
||||||
|
default boolean visitBlock (String name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/** Visits a section tag.
|
/** Visits a section tag.
|
||||||
* @param name the name of the section.
|
* @param name the name of the section.
|
||||||
* @return true if the contents of the section should be visited, false to skip.
|
* @return true if the contents of the section should be visited, false to skip.
|
||||||
@@ -416,7 +435,6 @@ public class Mustache {
|
|||||||
// trim() modifies segs! Its return is not a copy!
|
// trim() modifies segs! Its return is not a copy!
|
||||||
// now that we have all of our segments, we make a pass through them to trim whitespace
|
// now that we have all of our segments, we make a pass through them to trim whitespace
|
||||||
// from section tags which stand alone on their lines
|
// from section tags which stand alone on their lines
|
||||||
assert segs != null;
|
|
||||||
for (int ii = 0, ll = segs.length; ii < ll; ii++) {
|
for (int ii = 0, ll = segs.length; ii < ll; ii++) {
|
||||||
Template.Segment seg = segs[ii];
|
Template.Segment seg = segs[ii];
|
||||||
Template.Segment pseg = (ii > 0 ) ? segs[ii-1] : null;
|
Template.Segment pseg = (ii > 0 ) ? segs[ii-1] : null;
|
||||||
@@ -427,20 +445,30 @@ public class Mustache {
|
|||||||
boolean prevBlank = ((pseg == null && top) || (prev != null && prev.trailsBlank()));
|
boolean prevBlank = ((pseg == null && top) || (prev != null && prev.trailsBlank()));
|
||||||
boolean nextBlank = ((nseg == null && top) || (next != null && next.leadsBlank()));
|
boolean nextBlank = ((nseg == null && top) || (next != null && next.leadsBlank()));
|
||||||
// potentially trim around the open and close tags of a block segment
|
// potentially trim around the open and close tags of a block segment
|
||||||
if (seg instanceof BlockSegment) {
|
if (seg instanceof StandaloneSection) {
|
||||||
BlockSegment block = (BlockSegment)seg;
|
StandaloneSection sect = (StandaloneSection)seg;
|
||||||
if (prevBlank && block.firstLeadsBlank()) {
|
String indent = "";
|
||||||
if (pseg != null) segs[ii-1] = prev.trimTrailBlank();
|
if (prevBlank && sect.firstLeadsBlank()) {
|
||||||
block.trimFirstBlank();
|
if (prev != null) {
|
||||||
block._standaloneStart = true;
|
// capture the indent before we trim
|
||||||
|
indent = prev.indent();
|
||||||
|
segs[ii-1] = prev.trimTrailBlank();
|
||||||
|
}
|
||||||
|
sect.trimFirstBlank();
|
||||||
|
sect.standaloneStart(true);
|
||||||
}
|
}
|
||||||
if (nextBlank && block.lastTrailsBlank()) {
|
if (nextBlank && sect.lastTrailsBlank()) {
|
||||||
block.trimLastBlank();
|
sect.trimLastBlank();
|
||||||
if (nseg != null) segs[ii+1] = next.trimLeadBlank();
|
if (next != null) segs[ii+1] = next.trimLeadBlank();
|
||||||
block._standaloneEnd = true;
|
sect.standaloneEnd(true);
|
||||||
|
}
|
||||||
|
if (sect instanceof ParentTemplateSegment && ! indent.equals("")) {
|
||||||
|
ParentTemplateSegment pt = (ParentTemplateSegment) sect;
|
||||||
|
segs[ii] = pt.indent(indent, pseg == null, nseg == null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we have to indent partials if there is space before they are also standalone...
|
|
||||||
|
// we have to indent partials if there is space before and they are also standalone.
|
||||||
else if (seg instanceof IncludedTemplateSegment) {
|
else if (seg instanceof IncludedTemplateSegment) {
|
||||||
IncludedTemplateSegment include = (IncludedTemplateSegment) seg;
|
IncludedTemplateSegment include = (IncludedTemplateSegment) seg;
|
||||||
if (prev != null && prevBlank && nextBlank) {
|
if (prev != null && prevBlank && nextBlank) {
|
||||||
@@ -450,8 +478,11 @@ public class Mustache {
|
|||||||
include = include.indent(indent, pseg == null,nseg == null);
|
include = include.indent(indent, pseg == null,nseg == null);
|
||||||
segs[ii] = include;
|
segs[ii] = include;
|
||||||
}
|
}
|
||||||
//segs[ii-1] = prev.trimTrailBlank();
|
/*
|
||||||
/* We trim the end because partials follow standalone just like blocks */
|
* We trim the end because partials follow standalone just like blocks.
|
||||||
|
* HOWEVER we do NOT trim the previous StringSegment as it provides the partial indentation.
|
||||||
|
* See indentSegs.
|
||||||
|
*/
|
||||||
if (next != null) {
|
if (next != null) {
|
||||||
segs[ii+1] = next.trimLeadBlank();
|
segs[ii+1] = next.trimLeadBlank();
|
||||||
}
|
}
|
||||||
@@ -491,8 +522,8 @@ public class Mustache {
|
|||||||
Template.Segment pseg = (i > 0) ? _segs[i-1] : null;
|
Template.Segment pseg = (i > 0) ? _segs[i-1] : null;
|
||||||
Template.Segment nseg = (i < length - 1) ? _segs[i+1] : null;
|
Template.Segment nseg = (i < length - 1) ? _segs[i+1] : null;
|
||||||
Template.Segment copy;
|
Template.Segment copy;
|
||||||
if (seg instanceof BlockSegment) {
|
if (seg instanceof AbstractSectionSegment) {
|
||||||
BlockSegment bs = (BlockSegment) seg;
|
AbstractSectionSegment bs = (AbstractSectionSegment) seg;
|
||||||
boolean first;
|
boolean first;
|
||||||
boolean last;
|
boolean last;
|
||||||
if (pseg == null) {
|
if (pseg == null) {
|
||||||
@@ -536,8 +567,8 @@ public class Mustache {
|
|||||||
if (nseg == null) {
|
if (nseg == null) {
|
||||||
last = _last;
|
last = _last;
|
||||||
}
|
}
|
||||||
else if (nseg instanceof BlockSegment) {
|
else if (nseg instanceof AbstractSectionSegment) {
|
||||||
BlockSegment bs = (BlockSegment) nseg;
|
AbstractSectionSegment bs = (AbstractSectionSegment) nseg;
|
||||||
last = ! bs.isStandaloneStart();
|
last = ! bs.isStandaloneStart();
|
||||||
}
|
}
|
||||||
else if (nseg.isStandalone()) {
|
else if (nseg.isStandalone()) {
|
||||||
@@ -554,8 +585,8 @@ public class Mustache {
|
|||||||
* partial tag.
|
* partial tag.
|
||||||
* [ WS ]{{> partial }}[\n]
|
* [ WS ]{{> partial }}[\n]
|
||||||
*
|
*
|
||||||
* That is partial tags do not have the trailing blank removed during the trim
|
* That is partial tags do not have the trailing blank removed during the trim process.
|
||||||
* process. This avoids needlessley creating StringSegment tags.
|
* This avoids needlessley creating StringSegment tags.
|
||||||
*/
|
*/
|
||||||
if (seg.isStandalone()) {
|
if (seg.isStandalone()) {
|
||||||
boolean last;
|
boolean last;
|
||||||
@@ -590,6 +621,34 @@ public class Mustache {
|
|||||||
return _segs;
|
return _segs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Template.Segment[] replaceBlockSegs(Template.Segment[] _segs, Map<String, BlockSegment> blocks) {
|
||||||
|
if (blocks.isEmpty()) {
|
||||||
|
return _segs;
|
||||||
|
}
|
||||||
|
int length = _segs.length;
|
||||||
|
Template.Segment[] copySegs = new Template.Segment[length];
|
||||||
|
boolean changed = false;
|
||||||
|
for (int i = 0; i < _segs.length; i++) {
|
||||||
|
Template.Segment seg = _segs[i];
|
||||||
|
Template.Segment copy;
|
||||||
|
if (seg instanceof BlockReplaceable) {
|
||||||
|
BlockReplaceable br = (BlockReplaceable) seg;
|
||||||
|
copy = br.replaceBlocks(blocks);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
copy = seg;
|
||||||
|
}
|
||||||
|
if (copy != seg) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
copySegs[i] = copy;
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
return copySegs;
|
||||||
|
}
|
||||||
|
return _segs;
|
||||||
|
}
|
||||||
|
|
||||||
protected static void restoreStartTag (StringBuilder text, Delims starts) {
|
protected static void restoreStartTag (StringBuilder text, Delims starts) {
|
||||||
text.insert(0, starts.start1);
|
text.insert(0, starts.start1);
|
||||||
if (starts.start2 != NO_CHAR) {
|
if (starts.start2 != NO_CHAR) {
|
||||||
@@ -854,9 +913,34 @@ public class Mustache {
|
|||||||
};
|
};
|
||||||
|
|
||||||
case '>':
|
case '>':
|
||||||
_segs.add(new IncludedTemplateSegment(_comp, tag1));
|
_segs.add(new IncludedTemplateSegment(_comp, tag1, tagLine));
|
||||||
return this;
|
return this;
|
||||||
|
case '<':
|
||||||
|
requireNoNewlines(tag, tagLine);
|
||||||
|
return new Accumulator(_comp, false) {
|
||||||
|
@Override public Template.Segment[] finish () {
|
||||||
|
throw new MustacheParseException(
|
||||||
|
"Parent missing close tag '" + tag1 + "'", tagLine);
|
||||||
|
}
|
||||||
|
@Override protected Accumulator addCloseSectionSegment (String itag, int line) {
|
||||||
|
requireSameName(tag1, itag, line);
|
||||||
|
outer._segs.add(new ParentTemplateSegment(_comp, itag, super.finish(), tagLine));
|
||||||
|
return outer;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case '$':
|
||||||
|
requireNoNewlines(tag, tagLine);
|
||||||
|
return new Accumulator(_comp, false) {
|
||||||
|
@Override public Template.Segment[] finish () {
|
||||||
|
throw new MustacheParseException(
|
||||||
|
"Block missing close tag '" + tag1 + "'", tagLine);
|
||||||
|
}
|
||||||
|
@Override protected Accumulator addCloseSectionSegment (String itag, int line) {
|
||||||
|
requireSameName(tag1, itag, line);
|
||||||
|
outer._segs.add(new BlockSegment(_comp, itag, super.finish(), tagLine));
|
||||||
|
return outer;
|
||||||
|
}
|
||||||
|
};
|
||||||
case '^':
|
case '^':
|
||||||
requireNoNewlines(tag, tagLine);
|
requireNoNewlines(tag, tagLine);
|
||||||
return new Accumulator(_comp, false) {
|
return new Accumulator(_comp, false) {
|
||||||
@@ -1006,6 +1090,16 @@ public class Mustache {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isBlankWithNoNewline() {
|
||||||
|
String text = _text;
|
||||||
|
int len = text.length();
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
char c = text.charAt(i);
|
||||||
|
if (c == '\n' || ! Character.isWhitespace(c)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static int blankPos (String text, boolean leading, boolean first) {
|
private static int blankPos (String text, boolean leading, boolean first) {
|
||||||
int len = text.length();
|
int len = text.length();
|
||||||
for (int ii = leading ? 0 : len-1, ll = leading ? len : -1, dd = leading ? 1 : -1;
|
for (int ii = leading ? 0 : len-1, ll = leading ? len : -1, dd = leading ? 1 : -1;
|
||||||
@@ -1024,30 +1118,19 @@ public class Mustache {
|
|||||||
protected final boolean _first;
|
protected final boolean _first;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A segment that loads and executes a sub-template. */
|
/** An abstract segment that is a template include. */
|
||||||
protected static class IncludedTemplateSegment extends Template.Segment {
|
protected static abstract class AbstractPartialSegment extends NamedSegment {
|
||||||
public IncludedTemplateSegment (Compiler compiler, String name) {
|
protected AbstractPartialSegment (Compiler compiler, String name, int line, String indent) {
|
||||||
this(compiler, name, "");
|
super(name, line);
|
||||||
}
|
|
||||||
private IncludedTemplateSegment (Compiler compiler, String name, String indent) {
|
|
||||||
_comp = compiler;
|
_comp = compiler;
|
||||||
_name = name;
|
|
||||||
_indent = indent;
|
_indent = indent;
|
||||||
}
|
}
|
||||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
@Override public final void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
// we must take care to preserve our context rather than creating a new one, which
|
// we must take care to preserve our context rather than creating a new one, which
|
||||||
// would happen if we just called execute() with ctx.data
|
// would happen if we just called execute() with ctx.data
|
||||||
getTemplate().executeSegs(ctx, out);
|
getTemplate().executeSegs(ctx, out);
|
||||||
}
|
}
|
||||||
@Override public void decompile (Delims delims, StringBuilder into) {
|
protected final Template getTemplate () {
|
||||||
delims.addTag('>', _name, into);
|
|
||||||
}
|
|
||||||
@Override public void visit (Visitor visitor) {
|
|
||||||
if (visitor.visitInclude(_name)) {
|
|
||||||
getTemplate().visit(visitor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
protected Template getTemplate () {
|
|
||||||
// we compile our template lazily to avoid infinie recursion if a template includes
|
// we compile our template lazily to avoid infinie recursion if a template includes
|
||||||
// itself (see issue #13)
|
// itself (see issue #13)
|
||||||
Template t = _template;
|
Template t = _template;
|
||||||
@@ -1057,7 +1140,7 @@ public class Mustache {
|
|||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
if ((t = _template) == null) {
|
if ((t = _template) == null) {
|
||||||
_template = t = _comp.loadTemplate(_name).indent(_indent);
|
_template = t = _loadTemplate();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@@ -1065,31 +1148,158 @@ public class Mustache {
|
|||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
protected IncludedTemplateSegment indent (String indent, boolean first, boolean last) {
|
|
||||||
|
protected Template _loadTemplate() {
|
||||||
|
return _comp.loadTemplate(_name).indent(_indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public abstract boolean isStandalone();
|
||||||
|
|
||||||
|
protected final Compiler _comp;
|
||||||
|
protected final String _indent;
|
||||||
|
private final Lock lock = new ReentrantLock();
|
||||||
|
private volatile Template _template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A segment that loads and executes a sub-template by spec called a partial. */
|
||||||
|
protected static class IncludedTemplateSegment extends AbstractPartialSegment {
|
||||||
|
public IncludedTemplateSegment (Compiler compiler, String name, int line) {
|
||||||
|
this(compiler, name, line,"");
|
||||||
|
}
|
||||||
|
private IncludedTemplateSegment (Compiler compiler, String name, int line, String indent) {
|
||||||
|
super(compiler, name, line, indent);
|
||||||
|
}
|
||||||
|
@Override public void decompile (Delims delims, StringBuilder into) {
|
||||||
|
delims.addTag('<', _name, into);
|
||||||
|
}
|
||||||
|
@Override public void visit (Visitor visitor) {
|
||||||
|
if (visitor.visitInclude(_name)) {
|
||||||
|
getTemplate().visit(visitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override protected IncludedTemplateSegment indent(String indent, boolean first, boolean last) {
|
||||||
// Indent this partial based on the spacing provided.
|
// Indent this partial based on the spacing provided.
|
||||||
// per the spec however much the partial reference is indendented (leading whitespace)
|
// per the spec however much the partial reference is indendented (leading whitespace)
|
||||||
// is how much the partial content should be indented.
|
// is how much the partial content should be indented.
|
||||||
if (indent.equals("") || ! _standalone) {
|
if (indent.equals("") || ! _standalone) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
IncludedTemplateSegment is = new IncludedTemplateSegment(_comp, _name, indent + this._indent );
|
IncludedTemplateSegment is = new IncludedTemplateSegment(_comp, _name, _line, indent + this._indent );
|
||||||
is._standalone = _standalone;
|
is._standalone = _standalone;
|
||||||
return is;
|
return is;
|
||||||
}
|
}
|
||||||
@Override boolean isStandalone () { return _standalone; }
|
|
||||||
|
|
||||||
@Override
|
@Override public String toString () {
|
||||||
public String toString () {
|
|
||||||
return "Include(name=" + _name + ", indent=" + _indent + ", standalone=" + _standalone
|
return "Include(name=" + _name + ", indent=" + _indent + ", standalone=" + _standalone
|
||||||
+ ")";
|
+ ")";
|
||||||
}
|
}
|
||||||
|
@Override public boolean isStandalone() { return _standalone; }
|
||||||
|
protected boolean _standalone;
|
||||||
|
}
|
||||||
|
|
||||||
protected final Compiler _comp;
|
/** A segment that loads and executes a parent template by spec called inheritance. */
|
||||||
protected final String _name;
|
protected static class ParentTemplateSegment extends AbstractPartialSegment implements StandaloneSection {
|
||||||
private final String _indent;
|
public ParentTemplateSegment (Compiler compiler, String name, Template.Segment[] segs, int line) {
|
||||||
private final Lock lock = new ReentrantLock();
|
this(compiler, name, segs, line, "");
|
||||||
private volatile Template _template;
|
}
|
||||||
protected boolean _standalone = false;
|
private ParentTemplateSegment (Compiler compiler, String name, Template.Segment[] segs, int line, String indent) {
|
||||||
|
super(compiler, name, line, indent);
|
||||||
|
// Notice we consider the contents inside a parent to be at "top" = true.
|
||||||
|
// Furthermore to correctly trim we remove non blocks.
|
||||||
|
this._segs = trim(removeNonBlocks(segs), true);
|
||||||
|
this._blocks = new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
private ParentTemplateSegment (ParentTemplateSegment original, Template.Segment[] segs, String indent, Map<String, BlockSegment> blocks) {
|
||||||
|
super(original._comp, original._name, original._line, indent + original._indent);
|
||||||
|
this._segs = segs;
|
||||||
|
this._standaloneStart = original._standaloneStart;
|
||||||
|
this._standaloneEnd = original._standaloneEnd;
|
||||||
|
Map<String, BlockSegment> newBlocks = new LinkedHashMap<>();
|
||||||
|
newBlocks.putAll(original._blocks);
|
||||||
|
newBlocks.putAll(blocks);
|
||||||
|
this._blocks = newBlocks;
|
||||||
|
}
|
||||||
|
private static Template.Segment[] removeNonBlocks(Template.Segment[] segs) {
|
||||||
|
// the content inside a parent call
|
||||||
|
// e.g. {{<parent}}...{{/parent}}
|
||||||
|
// is ignored other than block segments: {{$block}}{{/block}}
|
||||||
|
List<Template.Segment> copy = new ArrayList<>();
|
||||||
|
for (Template.Segment seg : segs) {
|
||||||
|
if (seg instanceof BlockSegment) {
|
||||||
|
copy.add(seg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copy.toArray(new Template.Segment[] {});
|
||||||
|
}
|
||||||
|
@Override public void decompile (Delims delims, StringBuilder into) {
|
||||||
|
delims.addTag('<', _name, into);
|
||||||
|
}
|
||||||
|
@Override public void visit (Visitor visitor) {
|
||||||
|
if (visitor.visitParent(_name)) {
|
||||||
|
getTemplate().visit(visitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override protected ParentTemplateSegment indent(String indent, boolean first, boolean last) {
|
||||||
|
// Indent this partial based on the spacing provided.
|
||||||
|
// per the spec however much the partial reference is indendented (leading whitespace minus \n)
|
||||||
|
// is how much the partial content should be indented.
|
||||||
|
if (indent.equals("") || ! _standaloneStart) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new ParentTemplateSegment(this, this._segs, indent, Collections.emptyMap());
|
||||||
|
}
|
||||||
|
@Override protected Template _loadTemplate () {
|
||||||
|
Map<String, BlockSegment> blocks = new LinkedHashMap<>();
|
||||||
|
// While we might capture other segments we only care about blocks.
|
||||||
|
// The reason we are doing this now instead of at constructor time is
|
||||||
|
// that indentation and trim might have changed the segments (not sure on this).
|
||||||
|
for (Template.Segment seg : _segs) {
|
||||||
|
if (seg instanceof BlockSegment) {
|
||||||
|
BlockSegment bs = (BlockSegment) seg;
|
||||||
|
blocks.put(bs._name, bs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blocks.putAll(_blocks);
|
||||||
|
return super._loadTemplate().replaceBlocks(blocks);
|
||||||
|
}
|
||||||
|
// Parents have an unusual condition
|
||||||
|
// where if they are empty the end tag still
|
||||||
|
// owns the following newline.
|
||||||
|
// Thus lastTrails and trimLast need custom behavior.
|
||||||
|
@Override public boolean lastTrailsBlank () {
|
||||||
|
Template.Segment[] _segs = _segs();
|
||||||
|
int lastIdx = _segs.length-1;
|
||||||
|
|
||||||
|
if (lastIdx < 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(_segs[lastIdx] instanceof StringSegment)) return false;
|
||||||
|
return ((StringSegment)_segs[lastIdx]).trailsBlank();
|
||||||
|
}
|
||||||
|
@Override public void trimLastBlank () {
|
||||||
|
Template.Segment[] _segs = _segs();
|
||||||
|
int idx = _segs.length-1;
|
||||||
|
if (idx < 0) return;
|
||||||
|
_segs[idx] = ((StringSegment)_segs[idx]).trimTrailBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ParentTemplateSegment replaceBlocks(Map<String, BlockSegment> blocks) {
|
||||||
|
return new ParentTemplateSegment(this, _segs, "", blocks);
|
||||||
|
}
|
||||||
|
@Override public Template.Segment[] _segs() { return _segs; }
|
||||||
|
@Override public boolean isStandalone() { return _standaloneEnd; }
|
||||||
|
@Override public boolean isStandaloneStart() { return _standaloneStart; }
|
||||||
|
@Override public boolean isStandaloneEnd() { return _standaloneEnd; }
|
||||||
|
@Override public void standaloneStart(boolean standaloneStart) { this._standaloneStart = standaloneStart; }
|
||||||
|
@Override public void standaloneEnd(boolean standaloneEnd) { this._standaloneEnd = standaloneEnd; }
|
||||||
|
@Override public String toString() {
|
||||||
|
return "Parent(name=" + _name + ", indent=" + _indent + ", standaloneStart=" + _standaloneStart
|
||||||
|
+ ")";
|
||||||
|
}
|
||||||
|
protected final Template.Segment[] _segs;
|
||||||
|
protected boolean _standaloneStart = false;
|
||||||
|
protected boolean _standaloneEnd = false;
|
||||||
|
protected final Map<String, BlockSegment> _blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A helper class for named segments. */
|
/** A helper class for named segments. */
|
||||||
@@ -1140,32 +1350,50 @@ public class Mustache {
|
|||||||
protected final Escaper _escaper;
|
protected final Escaper _escaper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A helper class for block segments. */
|
protected interface StandaloneSection extends BlockReplaceable {
|
||||||
protected static abstract class BlockSegment extends NamedSegment {
|
default boolean firstLeadsBlank () {
|
||||||
public boolean firstLeadsBlank () {
|
Template.Segment[] _segs = _segs();
|
||||||
if (_segs.length == 0 || !(_segs[0] instanceof StringSegment)) return false;
|
if (_segs.length == 0 || !(_segs[0] instanceof StringSegment)) return false;
|
||||||
return ((StringSegment)_segs[0]).leadsBlank();
|
return ((StringSegment)_segs[0]).leadsBlank();
|
||||||
}
|
}
|
||||||
public void trimFirstBlank () {
|
default void trimFirstBlank () {
|
||||||
|
Template.Segment[] _segs = _segs();
|
||||||
_segs[0] = ((StringSegment)_segs[0]).trimLeadBlank();
|
_segs[0] = ((StringSegment)_segs[0]).trimLeadBlank();
|
||||||
}
|
}
|
||||||
|
default boolean lastTrailsBlank () {
|
||||||
public boolean lastTrailsBlank () {
|
Template.Segment[] _segs = _segs();
|
||||||
int lastIdx = _segs.length-1;
|
int lastIdx = _segs.length-1;
|
||||||
if (_segs.length == 0 || !(_segs[lastIdx] instanceof StringSegment)) return false;
|
if (_segs.length == 0 || !(_segs[lastIdx] instanceof StringSegment)) return false;
|
||||||
return ((StringSegment)_segs[lastIdx]).trailsBlank();
|
return ((StringSegment)_segs[lastIdx]).trailsBlank();
|
||||||
}
|
}
|
||||||
public void trimLastBlank () {
|
default void trimLastBlank () {
|
||||||
|
Template.Segment[] _segs = _segs();
|
||||||
int idx = _segs.length-1;
|
int idx = _segs.length-1;
|
||||||
_segs[idx] = ((StringSegment)_segs[idx]).trimTrailBlank();
|
_segs[idx] = ((StringSegment)_segs[idx]).trimTrailBlank();
|
||||||
}
|
}
|
||||||
|
boolean isStandaloneEnd ();
|
||||||
|
boolean isStandaloneStart ();
|
||||||
|
void standaloneStart (boolean standaloneStart);
|
||||||
|
void standaloneEnd (boolean standaloneEnd);
|
||||||
|
|
||||||
protected BlockSegment (String name, Template.Segment[] segs, int line) {
|
Template.Segment[] _segs();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected interface BlockReplaceable {
|
||||||
|
public Template.Segment replaceBlocks(Map<String, BlockSegment> blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A helper class for section-like segments. */
|
||||||
|
protected static abstract class AbstractSectionSegment extends NamedSegment implements StandaloneSection {
|
||||||
|
|
||||||
|
protected AbstractSectionSegment (Compiler compiler, String name, Template.Segment[] segs, int line) {
|
||||||
super(name, line);
|
super(name, line);
|
||||||
|
_comp = compiler;
|
||||||
_segs = trim(segs, false);
|
_segs = trim(segs, false);
|
||||||
}
|
}
|
||||||
protected BlockSegment (BlockSegment original, Template.Segment[] segs) {
|
protected AbstractSectionSegment (AbstractSectionSegment original, Template.Segment[] segs) {
|
||||||
super(original._name, original._line);
|
super(original._name, original._line);
|
||||||
|
_comp = original._comp;
|
||||||
// this call assumes the segments are already trimmed
|
// this call assumes the segments are already trimmed
|
||||||
_segs = segs;
|
_segs = segs;
|
||||||
}
|
}
|
||||||
@@ -1176,30 +1404,29 @@ public class Mustache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract BlockSegment indent (String indent, boolean first, boolean last);
|
protected abstract AbstractSectionSegment indent (String indent, boolean first, boolean last);
|
||||||
|
|
||||||
@Override
|
@Override public boolean isStandalone() { return _standaloneEnd; }
|
||||||
boolean isStandalone () {
|
@Override public boolean isStandaloneStart() { return _standaloneStart; }
|
||||||
return _standaloneEnd;
|
@Override public boolean isStandaloneEnd() { return _standaloneEnd; }
|
||||||
}
|
@Override public void standaloneStart(boolean standaloneStart) { this._standaloneStart = standaloneStart; }
|
||||||
boolean isStandaloneStart () {
|
@Override public void standaloneEnd(boolean standaloneEnd) { this._standaloneEnd = standaloneEnd; }
|
||||||
return _standaloneStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Override public Template.Segment[] _segs() { return _segs; }
|
||||||
|
|
||||||
|
protected final Compiler _comp;
|
||||||
protected final Template.Segment[] _segs;
|
protected final Template.Segment[] _segs;
|
||||||
protected boolean _standaloneStart = false;
|
protected boolean _standaloneStart = false;
|
||||||
protected boolean _standaloneEnd = false;
|
protected boolean _standaloneEnd = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A segment that represents a section. */
|
/** A segment that represents a section. */
|
||||||
protected static class SectionSegment extends BlockSegment {
|
protected static class SectionSegment extends AbstractSectionSegment {
|
||||||
public SectionSegment (Compiler compiler, String name, Template.Segment[] segs, int line) {
|
public SectionSegment (Compiler compiler, String name, Template.Segment[] segs, int line) {
|
||||||
super(name, segs, line);
|
super(compiler, name, segs, line);
|
||||||
_comp = compiler;
|
|
||||||
}
|
}
|
||||||
protected SectionSegment (SectionSegment original, Template.Segment[] segs) {
|
protected SectionSegment (SectionSegment original, Template.Segment[] segs) {
|
||||||
super(original, segs);
|
super(original, segs);
|
||||||
_comp = original._comp;
|
|
||||||
}
|
}
|
||||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
Object value = tmpl.getSectionValue(ctx, _name, _line); // won't return null
|
Object value = tmpl.getSectionValue(ctx, _name, _line); // won't return null
|
||||||
@@ -1240,27 +1467,75 @@ public class Mustache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override protected SectionSegment indent (String indent, boolean first, boolean last) {
|
@Override protected SectionSegment indent (String indent, boolean first, boolean last) {
|
||||||
// If the end tag is standalone we do NOT add the indent on the end
|
|
||||||
// (e.g. the newline following the end tag).
|
|
||||||
// This is because the block always owns the newlines within the block but
|
|
||||||
// it only owns the last newline following the end tag if and only if
|
|
||||||
// the closing tag is standalone.
|
|
||||||
Template.Segment[] segs = indentSegs(_segs, indent, first, last);
|
Template.Segment[] segs = indentSegs(_segs, indent, first, last);
|
||||||
if (segs == _segs) {
|
if (segs == _segs) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
return new SectionSegment(this, segs);
|
return new SectionSegment(this, segs);
|
||||||
}
|
}
|
||||||
|
@Override public SectionSegment replaceBlocks(Map<String, BlockSegment> blocks) {
|
||||||
|
Template.Segment[] segs = replaceBlockSegs(_segs, blocks);
|
||||||
|
if (segs == _segs) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new SectionSegment(this, segs);
|
||||||
|
}
|
||||||
@Override public String toString () {
|
@Override public String toString () {
|
||||||
return "Section(" + _name + ":" + _line + "): " + Arrays.toString(_segs);
|
return "Section(" + _name + ":" + _line + "): " + Arrays.toString(_segs);
|
||||||
}
|
}
|
||||||
protected final Compiler _comp;
|
}
|
||||||
|
|
||||||
|
/** A parent partial parameter using $ as the sigil. */
|
||||||
|
protected static class BlockSegment extends AbstractSectionSegment {
|
||||||
|
public BlockSegment (Compiler compiler, String name, Template.Segment[] segs, int line) {
|
||||||
|
super(compiler, name, segs, line);
|
||||||
|
}
|
||||||
|
protected BlockSegment (BlockSegment original, Template.Segment[] segs) {
|
||||||
|
super(original, segs);
|
||||||
|
}
|
||||||
|
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||||
|
executeSegs(tmpl, ctx, out);
|
||||||
|
}
|
||||||
|
@Override public void decompile (Delims delims, StringBuilder into) {
|
||||||
|
delims.addTag('$', _name, into);
|
||||||
|
for (Template.Segment seg : _segs) seg.decompile(delims, into);
|
||||||
|
delims.addTag('/', _name, into);
|
||||||
|
}
|
||||||
|
@Override public void visit (Visitor visitor) {
|
||||||
|
if (visitor.visitBlock(_name)) {
|
||||||
|
for (Template.Segment seg : _segs) {
|
||||||
|
seg.visit(visitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override protected BlockSegment indent (String indent, boolean first, boolean last) {
|
||||||
|
// Current indenting block segments is not defined by spec but might eventually
|
||||||
|
Template.Segment[] segs = indentSegs(_segs, indent, first, last);
|
||||||
|
if (segs == _segs) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new BlockSegment(this, segs);
|
||||||
|
}
|
||||||
|
@Override public BlockSegment replaceBlocks (Map<String, BlockSegment> blocks) {
|
||||||
|
BlockSegment bs = blocks.get(_name);
|
||||||
|
if (bs == null) {
|
||||||
|
Template.Segment[] segs = replaceBlockSegs(_segs, blocks);
|
||||||
|
if (segs == _segs) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new BlockSegment(this, segs);
|
||||||
|
}
|
||||||
|
return new BlockSegment(this, bs._segs);
|
||||||
|
}
|
||||||
|
@Override public String toString () {
|
||||||
|
return "Block(" + _name + ":" + _line + "): " + Arrays.toString(_segs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A segment that represents an inverted section. */
|
/** A segment that represents an inverted section. */
|
||||||
protected static class InvertedSegment extends BlockSegment {
|
protected static class InvertedSegment extends AbstractSectionSegment {
|
||||||
public InvertedSegment (Compiler compiler, String name, Template.Segment[] segs, int line) {
|
public InvertedSegment (Compiler compiler, String name, Template.Segment[] segs, int line) {
|
||||||
super(name, segs, line);
|
super(compiler, name, segs, line);
|
||||||
_comp = compiler;
|
_comp = compiler;
|
||||||
}
|
}
|
||||||
protected InvertedSegment (InvertedSegment original, Template.Segment[] segs) {
|
protected InvertedSegment (InvertedSegment original, Template.Segment[] segs) {
|
||||||
@@ -1300,14 +1575,20 @@ public class Mustache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override
|
@Override protected InvertedSegment indent (String indent, boolean first, boolean last) {
|
||||||
protected InvertedSegment indent (String indent, boolean first, boolean last) {
|
|
||||||
Template.Segment[] segs = indentSegs(_segs, indent, first, last);
|
Template.Segment[] segs = indentSegs(_segs, indent, first, last);
|
||||||
if (segs == _segs) {
|
if (segs == _segs) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
return new InvertedSegment(this, segs);
|
return new InvertedSegment(this, segs);
|
||||||
}
|
}
|
||||||
|
@Override public InvertedSegment replaceBlocks (Map<String, BlockSegment> blocks) {
|
||||||
|
Template.Segment[] segs = replaceBlockSegs(_segs, blocks);
|
||||||
|
if (segs == _segs) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new InvertedSegment(this, segs);
|
||||||
|
}
|
||||||
@Override public String toString () {
|
@Override public String toString () {
|
||||||
return "Inverted(" + _name + ":" + _line + "): " + Arrays.toString(_segs);
|
return "Inverted(" + _name + ":" + _line + "): " + Arrays.toString(_segs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import java.util.Collections;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.samskivert.mustache.Mustache.BlockSegment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a compiled template. Templates are executed with a <em>context</em> to generate
|
* Represents a compiled template. Templates are executed with a <em>context</em> to generate
|
||||||
* output. The context can be any tree of objects. Variables are resolved against the context.
|
* output. The context can be any tree of objects. Variables are resolved against the context.
|
||||||
@@ -165,7 +167,7 @@ public class Template {
|
|||||||
_fcache = compiler.collector.createFetcherCache();
|
_fcache = compiler.collector.createFetcherCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Template indent(String indent) {
|
protected Template indent (String indent) {
|
||||||
// What we want to do here is rebuild this partial template but indented.
|
// What we want to do here is rebuild this partial template but indented.
|
||||||
// If identing does not change anything we return the original template.
|
// If identing does not change anything we return the original template.
|
||||||
if (indent.equals("")) {
|
if (indent.equals("")) {
|
||||||
@@ -178,6 +180,17 @@ public class Template {
|
|||||||
return new Template(copySegs, _compiler);
|
return new Template(copySegs, _compiler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Template replaceBlocks (Map<String, BlockSegment> blocks) {
|
||||||
|
if (blocks.isEmpty()) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
Segment[] copySegs = Mustache.replaceBlockSegs(_segs, blocks);
|
||||||
|
if (copySegs == _segs) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new Template(copySegs, _compiler);
|
||||||
|
}
|
||||||
|
|
||||||
protected void executeSegs (Context ctx, Writer out) throws MustacheException {
|
protected void executeSegs (Context ctx, Writer out) throws MustacheException {
|
||||||
for (Segment seg : _segs) {
|
for (Segment seg : _segs) {
|
||||||
seg.execute(this, ctx, out);
|
seg.execute(this, ctx, out);
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ public class PartialThreadSafeTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPartialThreadSafe() throws Exception {
|
public void testPartialThreadSafe() throws Exception {
|
||||||
long t = System.currentTimeMillis();
|
|
||||||
AtomicInteger loadCount = new AtomicInteger();
|
AtomicInteger loadCount = new AtomicInteger();
|
||||||
TemplateLoader loader = new TemplateLoader() {
|
TemplateLoader loader = new TemplateLoader() {
|
||||||
@Override
|
@Override
|
||||||
@@ -64,7 +63,5 @@ public class PartialThreadSafeTest {
|
|||||||
}
|
}
|
||||||
assertTrue(q.isEmpty());
|
assertTrue(q.isEmpty());
|
||||||
assertEquals(1, loadCount.get());
|
assertEquals(1, loadCount.get());
|
||||||
System.out.println(loadCount);
|
|
||||||
System.out.println(System.currentTimeMillis() - t);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ public class CustomSpecTest extends SpecTest {
|
|||||||
@Parameters(name = "{1}")
|
@Parameters(name = "{1}")
|
||||||
public static Collection<Object[]> data () {
|
public static Collection<Object[]> data () {
|
||||||
String[] groups = new String[] {
|
String[] groups = new String[] {
|
||||||
"partials"
|
"sections",
|
||||||
|
"partials",
|
||||||
|
"~inheritance"
|
||||||
};
|
};
|
||||||
return SpecTest.data("/custom/specs/", groups);
|
return SpecTest.data("/custom/specs/", groups);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ public class OfficialSpecTest extends SpecTest {
|
|||||||
"interpolation",
|
"interpolation",
|
||||||
"inverted",
|
"inverted",
|
||||||
"sections",
|
"sections",
|
||||||
"partials"
|
"partials",
|
||||||
|
"~inheritance"
|
||||||
};
|
};
|
||||||
return SpecTest.data("/specs/specs/", groups);
|
return SpecTest.data("/specs/specs/", groups);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
package com.samskivert.mustache.specs;
|
package com.samskivert.mustache.specs;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,6 +21,7 @@ public class Spec
|
|||||||
this.map = map;
|
this.map = map;
|
||||||
@SuppressWarnings("unchecked") Map<String, String> partials =
|
@SuppressWarnings("unchecked") Map<String, String> partials =
|
||||||
(Map<String, String>) map.get("partials");
|
(Map<String, String>) map.get("partials");
|
||||||
|
if (partials == null) partials = Collections.emptyMap();
|
||||||
this.partials = partials;
|
this.partials = partials;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +60,15 @@ public class Spec
|
|||||||
value.accept(getDescription());
|
value.accept(getDescription());
|
||||||
label.accept("template");
|
label.accept("template");
|
||||||
value.accept(getTemplate());
|
value.accept(getTemplate());
|
||||||
|
if (! partials.isEmpty()) {
|
||||||
|
label.accept("partials");
|
||||||
|
sb.append("\n");
|
||||||
|
for( Entry<String, String> e : partials.entrySet()) {
|
||||||
|
sb.append("\t").append(e.getKey()).append(":\n");
|
||||||
|
value.accept(e.getValue());
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
label.accept("expected");
|
label.accept("expected");
|
||||||
value.accept(getExpectedOutput());
|
value.accept(getExpectedOutput());
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public abstract class SpecTest {
|
|||||||
// specifications, but we throw an exception (and rightfully so IMO; this is not a
|
// specifications, but we throw an exception (and rightfully so IMO; this is not a
|
||||||
// place where silent failure is helpful), so just ignore those test failures
|
// place where silent failure is helpful), so just ignore those test failures
|
||||||
if (!e.getMessage().contains("Invalid delimiter")) {
|
if (!e.getMessage().contains("Invalid delimiter")) {
|
||||||
|
e.printStackTrace();
|
||||||
Assert.fail(
|
Assert.fail(
|
||||||
desc + "\nExpected: " + uncrlf(spec.getExpectedOutput()) + "\nError: " + e);
|
desc + "\nExpected: " + uncrlf(spec.getExpectedOutput()) + "\nError: " + e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
overview: |
|
||||||
|
Section and End Section tags SHOULD be treated as standalone when
|
||||||
|
appropriate.
|
||||||
|
tests:
|
||||||
|
- name: Standalone Lines
|
||||||
|
desc: Standalone lines should be removed from the template.
|
||||||
|
data: { boolean: true }
|
||||||
|
template: |
|
||||||
|
| This Is
|
||||||
|
{{#boolean}}{{^missing}}|{{/missing}}{{/boolean}}
|
||||||
|
| A Line
|
||||||
|
expected: |
|
||||||
|
| This Is
|
||||||
|
|
|
||||||
|
| A Line
|
||||||
|
|
||||||
|
# eof
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
overview: |
|
||||||
|
Custom inheritance tests.
|
||||||
|
tests:
|
||||||
|
- name: Non block content should not impact standalone blocks
|
||||||
|
desc: Content inside a parent call that is not block tags should be ignored
|
||||||
|
data: { }
|
||||||
|
template: "{{<parent}}ignore{{$ballmer}}\npeaked\n\n:(\n{{/ballmer}}{{/parent}}"
|
||||||
|
partials:
|
||||||
|
parent: "{{$ballmer}}peaking{{/ballmer}}"
|
||||||
|
expected: "peaked\n\n:(\n"
|
||||||
|
|
||||||
|
# eof
|
||||||
Reference in New Issue
Block a user