diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index e64d663..59294cf 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -10,7 +10,9 @@ import java.io.StringReader; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; @@ -379,6 +381,23 @@ public class Mustache { */ boolean visitInclude (String name); + /** Visits a parent partial tag. For backward compatibility by default + * false 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. * @param name the name of the section. * @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! // 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 - assert segs != null; for (int ii = 0, ll = segs.length; ii < ll; ii++) { Template.Segment seg = segs[ii]; 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 nextBlank = ((nseg == null && top) || (next != null && next.leadsBlank())); // potentially trim around the open and close tags of a block segment - if (seg instanceof BlockSegment) { - BlockSegment block = (BlockSegment)seg; - if (prevBlank && block.firstLeadsBlank()) { - if (pseg != null) segs[ii-1] = prev.trimTrailBlank(); - block.trimFirstBlank(); - block._standaloneStart = true; + if (seg instanceof StandaloneSection) { + StandaloneSection sect = (StandaloneSection)seg; + String indent = ""; + if (prevBlank && sect.firstLeadsBlank()) { + if (prev != null) { + // capture the indent before we trim + indent = prev.indent(); + segs[ii-1] = prev.trimTrailBlank(); + } + sect.trimFirstBlank(); + sect.standaloneStart(true); } - if (nextBlank && block.lastTrailsBlank()) { - block.trimLastBlank(); - if (nseg != null) segs[ii+1] = next.trimLeadBlank(); - block._standaloneEnd = true; + if (nextBlank && sect.lastTrailsBlank()) { + sect.trimLastBlank(); + if (next != null) segs[ii+1] = next.trimLeadBlank(); + 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) { IncludedTemplateSegment include = (IncludedTemplateSegment) seg; if (prev != null && prevBlank && nextBlank) { @@ -450,8 +478,11 @@ public class Mustache { include = include.indent(indent, pseg == null,nseg == null); 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) { segs[ii+1] = next.trimLeadBlank(); } @@ -491,8 +522,8 @@ public class Mustache { Template.Segment pseg = (i > 0) ? _segs[i-1] : null; Template.Segment nseg = (i < length - 1) ? _segs[i+1] : null; Template.Segment copy; - if (seg instanceof BlockSegment) { - BlockSegment bs = (BlockSegment) seg; + if (seg instanceof AbstractSectionSegment) { + AbstractSectionSegment bs = (AbstractSectionSegment) seg; boolean first; boolean last; if (pseg == null) { @@ -536,8 +567,8 @@ public class Mustache { if (nseg == null) { last = _last; } - else if (nseg instanceof BlockSegment) { - BlockSegment bs = (BlockSegment) nseg; + else if (nseg instanceof AbstractSectionSegment) { + AbstractSectionSegment bs = (AbstractSectionSegment) nseg; last = ! bs.isStandaloneStart(); } else if (nseg.isStandalone()) { @@ -554,8 +585,8 @@ public class Mustache { * partial tag. * [ WS ]{{> partial }}[\n] * - * That is partial tags do not have the trailing blank removed during the trim - * process. This avoids needlessley creating StringSegment tags. + * That is partial tags do not have the trailing blank removed during the trim process. + * This avoids needlessley creating StringSegment tags. */ if (seg.isStandalone()) { boolean last; @@ -590,6 +621,34 @@ public class Mustache { return _segs; } + static Template.Segment[] replaceBlockSegs(Template.Segment[] _segs, Map 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) { text.insert(0, starts.start1); if (starts.start2 != NO_CHAR) { @@ -854,9 +913,34 @@ public class Mustache { }; case '>': - _segs.add(new IncludedTemplateSegment(_comp, tag1)); + _segs.add(new IncludedTemplateSegment(_comp, tag1, tagLine)); 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 '^': requireNoNewlines(tag, tagLine); return new Accumulator(_comp, false) { @@ -1024,30 +1108,19 @@ public class Mustache { protected final boolean _first; } - /** A segment that loads and executes a sub-template. */ - protected static class IncludedTemplateSegment extends Template.Segment { - public IncludedTemplateSegment (Compiler compiler, String name) { - this(compiler, name, ""); - } - private IncludedTemplateSegment (Compiler compiler, String name, String indent) { + /** An abstract segment that is a template include. */ + protected static abstract class AbstractPartialSegment extends NamedSegment { + protected AbstractPartialSegment (Compiler compiler, String name, int line, String indent) { + super(name, line); _comp = compiler; - _name = name; _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 // would happen if we just called execute() with ctx.data getTemplate().executeSegs(ctx, out); } - @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); - } - } - protected Template getTemplate () { + protected final Template getTemplate () { // we compile our template lazily to avoid infinie recursion if a template includes // itself (see issue #13) Template t = _template; @@ -1057,7 +1130,7 @@ public class Mustache { lock.lock(); try { if ((t = _template) == null) { - _template = t = _comp.loadTemplate(_name).indent(_indent); + _template = t = _loadTemplate(); } } finally { lock.unlock(); @@ -1065,31 +1138,158 @@ public class Mustache { } 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. // per the spec however much the partial reference is indendented (leading whitespace) // is how much the partial content should be indented. if (indent.equals("") || ! _standalone) { return this; } - IncludedTemplateSegment is = new IncludedTemplateSegment(_comp, _name, indent + this._indent ); + IncludedTemplateSegment is = new IncludedTemplateSegment(_comp, _name, _line, indent + this._indent ); is._standalone = _standalone; return is; } - @Override boolean isStandalone () { return _standalone; } - @Override - public String toString () { + @Override public String toString () { return "Include(name=" + _name + ", indent=" + _indent + ", standalone=" + _standalone + ")"; } + @Override public boolean isStandalone() { return _standalone; } + protected boolean _standalone; + } - protected final Compiler _comp; - protected final String _name; - private final String _indent; - private final Lock lock = new ReentrantLock(); - private volatile Template _template; - protected boolean _standalone = false; + /** A segment that loads and executes a parent template by spec called inheritance. */ + protected static class ParentTemplateSegment extends AbstractPartialSegment implements StandaloneSection { + public ParentTemplateSegment (Compiler compiler, String name, Template.Segment[] segs, int line) { + this(compiler, name, segs, line, ""); + } + 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 blocks) { + super(original._comp, original._name, original._line, indent + original._indent); + this._segs = segs; + this._standaloneStart = original._standaloneStart; + this._standaloneEnd = original._standaloneEnd; + Map 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. {{ 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 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 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 _blocks; } /** A helper class for named segments. */ @@ -1140,32 +1340,50 @@ public class Mustache { protected final Escaper _escaper; } - /** A helper class for block segments. */ - protected static abstract class BlockSegment extends NamedSegment { - public boolean firstLeadsBlank () { + protected interface StandaloneSection extends BlockReplaceable { + default boolean firstLeadsBlank () { + Template.Segment[] _segs = _segs(); if (_segs.length == 0 || !(_segs[0] instanceof StringSegment)) return false; return ((StringSegment)_segs[0]).leadsBlank(); } - public void trimFirstBlank () { + default void trimFirstBlank () { + Template.Segment[] _segs = _segs(); _segs[0] = ((StringSegment)_segs[0]).trimLeadBlank(); } - - public boolean lastTrailsBlank () { + default boolean lastTrailsBlank () { + Template.Segment[] _segs = _segs(); int lastIdx = _segs.length-1; if (_segs.length == 0 || !(_segs[lastIdx] instanceof StringSegment)) return false; return ((StringSegment)_segs[lastIdx]).trailsBlank(); } - public void trimLastBlank () { + default void trimLastBlank () { + Template.Segment[] _segs = _segs(); int idx = _segs.length-1; _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 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); + _comp = compiler; _segs = trim(segs, false); } - protected BlockSegment (BlockSegment original, Template.Segment[] segs) { + protected AbstractSectionSegment (AbstractSectionSegment original, Template.Segment[] segs) { super(original._name, original._line); + _comp = original._comp; // this call assumes the segments are already trimmed _segs = segs; } @@ -1176,30 +1394,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 - boolean isStandalone () { - return _standaloneEnd; - } - boolean isStandaloneStart () { - return _standaloneStart; - } + @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 Template.Segment[] _segs() { return _segs; } + + protected final Compiler _comp; protected final Template.Segment[] _segs; protected boolean _standaloneStart = false; protected boolean _standaloneEnd = false; } /** 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) { - super(name, segs, line); - _comp = compiler; + super(compiler, name, segs, line); } protected SectionSegment (SectionSegment original, Template.Segment[] segs) { super(original, segs); - _comp = original._comp; } @Override public void execute (Template tmpl, Template.Context ctx, Writer out) { Object value = tmpl.getSectionValue(ctx, _name, _line); // won't return null @@ -1240,27 +1457,75 @@ public class Mustache { } } @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); if (segs == _segs) { return this; } return new SectionSegment(this, segs); } + @Override public SectionSegment replaceBlocks(Map blocks) { + Template.Segment[] segs = replaceBlockSegs(_segs, blocks); + if (segs == _segs) { + return this; + } + return new SectionSegment(this, segs); + } @Override public String toString () { 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 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. */ - protected static class InvertedSegment extends BlockSegment { + protected static class InvertedSegment extends AbstractSectionSegment { public InvertedSegment (Compiler compiler, String name, Template.Segment[] segs, int line) { - super(name, segs, line); + super(compiler, name, segs, line); _comp = compiler; } protected InvertedSegment (InvertedSegment original, Template.Segment[] segs) { @@ -1300,14 +1565,20 @@ public class Mustache { } } } - @Override - protected InvertedSegment indent (String indent, boolean first, boolean last) { + @Override protected InvertedSegment indent (String indent, boolean first, boolean last) { Template.Segment[] segs = indentSegs(_segs, indent, first, last); if (segs == _segs) { return this; } return new InvertedSegment(this, segs); } + @Override public InvertedSegment replaceBlocks (Map blocks) { + Template.Segment[] segs = replaceBlockSegs(_segs, blocks); + if (segs == _segs) { + return this; + } + return new InvertedSegment(this, segs); + } @Override public String toString () { return "Inverted(" + _name + ":" + _line + "): " + Arrays.toString(_segs); } diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index b4ed014..393ed72 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -11,6 +11,8 @@ import java.util.Collections; import java.util.Iterator; import java.util.Map; +import com.samskivert.mustache.Mustache.BlockSegment; + /** * Represents a compiled template. Templates are executed with a context to generate * 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(); } - protected Template indent(String indent) { + protected Template indent (String indent) { // 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 (indent.equals("")) { @@ -178,6 +180,17 @@ public class Template { return new Template(copySegs, _compiler); } + protected Template replaceBlocks (Map 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 { for (Segment seg : _segs) { seg.execute(this, ctx, out); diff --git a/src/test/java/com/samskivert/mustache/PartialThreadSafeTest.java b/src/test/java/com/samskivert/mustache/PartialThreadSafeTest.java index 577764c..2b25ee1 100644 --- a/src/test/java/com/samskivert/mustache/PartialThreadSafeTest.java +++ b/src/test/java/com/samskivert/mustache/PartialThreadSafeTest.java @@ -26,7 +26,6 @@ public class PartialThreadSafeTest { @Test public void testPartialThreadSafe() throws Exception { - long t = System.currentTimeMillis(); AtomicInteger loadCount = new AtomicInteger(); TemplateLoader loader = new TemplateLoader() { @Override @@ -64,7 +63,5 @@ public class PartialThreadSafeTest { } assertTrue(q.isEmpty()); assertEquals(1, loadCount.get()); - System.out.println(loadCount); - System.out.println(System.currentTimeMillis() - t); } } diff --git a/src/test/java/com/samskivert/mustache/specs/CustomSpecTest.java b/src/test/java/com/samskivert/mustache/specs/CustomSpecTest.java index b83052d..9e9bd7e 100644 --- a/src/test/java/com/samskivert/mustache/specs/CustomSpecTest.java +++ b/src/test/java/com/samskivert/mustache/specs/CustomSpecTest.java @@ -16,7 +16,9 @@ public class CustomSpecTest extends SpecTest { @Parameters(name = "{1}") public static Collection data () { String[] groups = new String[] { - "partials" + "sections", + "partials", + "~inheritance" }; return SpecTest.data("/custom/specs/", groups); } diff --git a/src/test/java/com/samskivert/mustache/specs/OfficialSpecTest.java b/src/test/java/com/samskivert/mustache/specs/OfficialSpecTest.java index 93b18d9..603ada0 100644 --- a/src/test/java/com/samskivert/mustache/specs/OfficialSpecTest.java +++ b/src/test/java/com/samskivert/mustache/specs/OfficialSpecTest.java @@ -21,7 +21,8 @@ public class OfficialSpecTest extends SpecTest { "interpolation", "inverted", "sections", - "partials" + "partials", + "~inheritance" }; return SpecTest.data("/specs/specs/", groups); } diff --git a/src/test/java/com/samskivert/mustache/specs/Spec.java b/src/test/java/com/samskivert/mustache/specs/Spec.java index c2315af..197ab31 100644 --- a/src/test/java/com/samskivert/mustache/specs/Spec.java +++ b/src/test/java/com/samskivert/mustache/specs/Spec.java @@ -4,7 +4,9 @@ package com.samskivert.mustache.specs; +import java.util.Collections; import java.util.Map; +import java.util.Map.Entry; import java.util.function.Consumer; /** @@ -19,6 +21,7 @@ public class Spec this.map = map; @SuppressWarnings("unchecked") Map partials = (Map) map.get("partials"); + if (partials == null) partials = Collections.emptyMap(); this.partials = partials; } @@ -57,6 +60,15 @@ public class Spec value.accept(getDescription()); label.accept("template"); value.accept(getTemplate()); + if (! partials.isEmpty()) { + label.accept("partials"); + sb.append("\n"); + for( Entry e : partials.entrySet()) { + sb.append("\t").append(e.getKey()).append(":\n"); + value.accept(e.getValue()); + } + sb.append("\n"); + } label.accept("expected"); value.accept(getExpectedOutput()); return sb.toString(); diff --git a/src/test/java/com/samskivert/mustache/specs/SpecTest.java b/src/test/java/com/samskivert/mustache/specs/SpecTest.java index ff818c3..f5c36d5 100644 --- a/src/test/java/com/samskivert/mustache/specs/SpecTest.java +++ b/src/test/java/com/samskivert/mustache/specs/SpecTest.java @@ -56,6 +56,7 @@ public abstract class SpecTest { // 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 if (!e.getMessage().contains("Invalid delimiter")) { + e.printStackTrace(); Assert.fail( desc + "\nExpected: " + uncrlf(spec.getExpectedOutput()) + "\nError: " + e); } diff --git a/src/test/resources/custom/specs/sections.yml b/src/test/resources/custom/specs/sections.yml new file mode 100644 index 0000000..f0752b1 --- /dev/null +++ b/src/test/resources/custom/specs/sections.yml @@ -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 \ No newline at end of file diff --git a/src/test/resources/custom/specs/~inheritance.yml b/src/test/resources/custom/specs/~inheritance.yml new file mode 100644 index 0000000..55b3154 --- /dev/null +++ b/src/test/resources/custom/specs/~inheritance.yml @@ -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: "{{