diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 13a6c54..fee43a6 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -430,25 +430,31 @@ protected static Template.Segment[] trim (Template.Segment[] segs, boolean top) if (prevBlank && block.firstLeadsBlank()) { if (pseg != null) segs[ii-1] = prev.trimTrailBlank(); block.trimFirstBlank(); + block._standaloneStart = true; } if (nextBlank && block.lastTrailsBlank()) { block.trimLastBlank(); if (nseg != null) segs[ii+1] = next.trimLeadBlank(); + block._standaloneEnd = true; } } // we have to indent partials if there is space before // they are also standalone... else if (seg instanceof IncludedTemplateSegment) { + IncludedTemplateSegment include = (IncludedTemplateSegment) seg; if (prev != null && prevBlank && nextBlank) { String indent = prev.indent(); + include._standalone = true; if (! indent.equals("")) { - segs[ii] = seg.indent(indent, nseg == null); + 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 */ - if (nseg != null) { + if (next != null) { segs[ii+1] = next.trimLeadBlank(); } } @@ -464,6 +470,59 @@ else if (seg instanceof FauxSegment) { return segs; } + static Template.Segment[] indentSegs(Template.Segment[] _segs, String indent) { + // unlike trim this method clones the segments if they have changed + // so the return value must be handled + // a simple identity check on the return can be used to determine + // if there is change + if (indent.equals("")) { + 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 pseg = (i > 0) ? _segs[i-1] : null; + Template.Segment nseg = (i < length - 1) ? _segs[i+1] : null; + /* + * If we are the first segment then we rely on the indentation + * already present before the partial tag. + * [ WS ]{{> partial }} + * + * That is partial tags do not have the trailing blank + * removed. + * + * This avoids needlessley creating StringSegment tags. + */ + boolean first = pseg == null ? false : seg.isStandalone() || pseg.isStandalone(); + boolean last; + if (nseg instanceof BlockSegment){ + BlockSegment bs = (BlockSegment) nseg; + last = bs.isStandaloneStart(); + } + else if (nseg == null || nseg.isStandalone()) { + last = false; + } + else { + last = true; + } + /* + * Recursively indent + */ + Template.Segment copy = seg.indent(indent, first, last); + 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) { @@ -801,15 +860,16 @@ protected static void requireSameName (String name1, String name2, int line) { /** A simple segment that reproduces a string. */ protected static class StringSegment extends Template.Segment { public StringSegment (String text, boolean first) { - this(text, blankPos(text, true, first), blankPos(text, false, first)); + this(text, blankPos(text, true, first), blankPos(text, false, first), first); } - public StringSegment (String text, int leadBlank, int trailBlank) { + public StringSegment (String text, int leadBlank, int trailBlank, boolean first) { assert leadBlank >= -1; assert trailBlank >= -1; _text = text; _leadBlank = leadBlank; _trailBlank = trailBlank; + _first = first; } public boolean leadsBlank () { return _leadBlank != -1; } @@ -818,11 +878,11 @@ public StringSegment (String text, int leadBlank, int trailBlank) { public StringSegment trimLeadBlank () { if (_leadBlank == -1) return this; int lpos = _leadBlank+1, newTrail = _trailBlank == -1 ? -1 : _trailBlank-lpos; - return new StringSegment(_text.substring(lpos), -1, newTrail); + return new StringSegment(_text.substring(lpos), -1, newTrail, _first); } public StringSegment trimTrailBlank () { return _trailBlank == -1 ? this : new StringSegment( - _text.substring(0, _trailBlank), _leadBlank, -1); + _text.substring(0, _trailBlank), _leadBlank, -1, _first); } /** @@ -836,12 +896,16 @@ String indent() { return _text.substring(_trailBlank); } - StringSegment indent(String indent, boolean last) { + StringSegment indent(String indent, boolean first, boolean last) { if (indent.equals("")) { return this; } - String reindent = reindent(_text, indent, last); - return new StringSegment(reindent , _leadBlank, _trailBlank); + String reindent = reindent(_text, indent, first, last); + return new StringSegment(reindent, _first); + } + + @Override boolean isStandalone() { + return false; } @Override public void execute (Template tmpl, Template.Context ctx, Writer out) { @@ -857,15 +921,18 @@ StringSegment indent(String indent, boolean last) { return "Text(" + _text.replace("\r", "\\r").replace("\n", "\\n") + ")" + _leadBlank + "/" + _trailBlank; } - + //we indent after every new line for partial indententation - private static String reindent(String input, String indent, boolean last) { + private static String reindent(String input, String indent, boolean first, boolean last) { int length = input.length(); - StringBuilder sb = new StringBuilder(length); + StringBuilder sb = new StringBuilder(indent.length() + length); + if (first) { + sb.append(indent); + } for (int i = 0; i < length; i++) { char c = input.charAt(i); sb.append(c); - if (c == '\n' && ! ( last && i == length - 1)) { + if (c == '\n' && ( last || i != length - 1) ) { sb.append(indent); } } @@ -887,6 +954,7 @@ private static int blankPos (String text, boolean leading, boolean first) { protected final String _text; protected final int _leadBlank, _trailBlank; + protected final boolean _first; } /** A segment that loads and executes a sub-template. */ @@ -920,20 +988,30 @@ protected Template getTemplate () { } return _template; } - protected IncludedTemplateSegment indent(String indent, boolean last) { + 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("")) { + if (indent.equals("") || ! _standalone) { return this; } - return new IncludedTemplateSegment(_comp, _name, indent + this._indent ); + IncludedTemplateSegment is = new IncludedTemplateSegment(_comp, _name, indent + this._indent ); + is._standalone = _standalone; + return is; } - + @Override boolean isStandalone() { return _standalone; } + + @Override + public String toString() { + return "Include(name=" + _name + ", indent=" + _indent + ", standalone=" + _standalone + + ")"; + } + protected final Compiler _comp; protected final String _name; private final String _indent; private Template _template; + protected boolean _standalone = false; } /** A helper class for named segments. */ @@ -970,9 +1048,13 @@ public VariableSegment (String name, int line, Formatter formatter, Escaper esca visitor.visitVariable(_name); } @Override - VariableSegment indent(String indent, boolean last) { + VariableSegment indent(String indent, boolean first, boolean last) { return this; } + @Override + boolean isStandalone() { + return false; + } @Override public String toString () { return "Var(" + _name + ":" + _line + ")"; } @@ -1004,15 +1086,31 @@ protected BlockSegment (String name, Template.Segment[] segs, int line) { super(name, line); _segs = trim(segs, false); } + protected BlockSegment (BlockSegment original, Template.Segment[] segs) { + super(original._name, original._line); + // this call assumes the segments are already trimmed + _segs = segs; + } + protected void executeSegs (Template tmpl, Template.Context ctx, Writer out) { for (Template.Segment seg : _segs) { seg.execute(tmpl, ctx, out); } } - - protected abstract BlockSegment indent(String indent, boolean last); + protected abstract BlockSegment indent (String indent, boolean first, boolean last); + + @Override + boolean isStandalone() { + return _standaloneEnd; + } + boolean isStandaloneStart() { + return _standaloneStart; + } + protected final Template.Segment[] _segs; + protected boolean _standaloneStart = false; + protected boolean _standaloneEnd = false; } /** A segment that represents a section. */ @@ -1021,6 +1119,10 @@ public SectionSegment (Compiler compiler, String name, Template.Segment[] segs, super(name, segs, line); _comp = compiler; } + 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 Iterator iter = _comp.collector.toIterator(value); @@ -1059,16 +1161,15 @@ public SectionSegment (Compiler compiler, String name, Template.Segment[] segs, } } } - @Override - protected SectionSegment indent(String indent, boolean last) { + @Override protected SectionSegment indent (String indent, boolean first, boolean last) { if (indent.equals("")) { return this; } - Template.Segment[] segs = Template.indentSegs(_segs, indent); + Template.Segment[] segs = indentSegs(_segs, indent); if (segs == _segs) { return this; } - return new SectionSegment(_comp, _name, segs, _line); + return new SectionSegment(this, segs); } @Override public String toString () { return "Section(" + _name + ":" + _line + "): " + Arrays.toString(_segs); @@ -1082,6 +1183,10 @@ public InvertedSegment (Compiler compiler, String name, Template.Segment[] segs, super(name, segs, line); _comp = compiler; } + protected InvertedSegment (InvertedSegment 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 Iterator iter = _comp.collector.toIterator(value); @@ -1116,15 +1221,15 @@ public InvertedSegment (Compiler compiler, String name, Template.Segment[] segs, } } @Override - protected InvertedSegment indent(String indent, boolean last) { + protected InvertedSegment indent (String indent, boolean first, boolean last) { if (indent.equals("")) { return this; } - Template.Segment[] segs = Template.indentSegs(_segs, indent); + Template.Segment[] segs = indentSegs(_segs, indent); if (segs == _segs) { return this; } - return new InvertedSegment(_comp, _name, segs, _line); + return new InvertedSegment(this, segs); } @Override public String toString () { return "Inverted(" + _name + ":" + _line + "): " + Arrays.toString(_segs); @@ -1136,7 +1241,8 @@ protected static class FauxSegment extends Template.Segment { @Override public void execute (Template tmpl, Template.Context ctx, Writer out) {} // nada @Override public void decompile (Delims delims, StringBuilder into) {} // nada @Override public void visit (Visitor visit) {} - @Override FauxSegment indent(String indent, boolean last) { return this; } + @Override FauxSegment indent (String indent, boolean first, boolean last) { return this; } + @Override boolean isStandalone() { return true; } @Override public String toString () { return "Faux"; } } diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index 3e30fc1..7e74f46 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -176,33 +176,12 @@ protected Template indent(String indent) { if (indent.equals("")) { return this; } - Segment[] copySegs = indentSegs(_segs, indent); + Segment[] copySegs = Mustache.indentSegs(_segs, indent); if (copySegs == _segs) { return this; } return new Template(copySegs, _compiler); } - - static Segment[] indentSegs(Segment[] _segs, String indent) { - if (indent.equals("")) { - return _segs; - } - int length = _segs.length; - Segment[] copySegs = new Segment[length]; - boolean changed = false; - for (int i = 0; i < _segs.length; i++) { - Segment seg = _segs[i]; - Segment copy = seg.indent(indent, i == (length - 1)); - if (copy != seg) { - changed = true; - } - copySegs[i] = copy; - } - if (changed) { - return copySegs; - } - return _segs; - } protected void executeSegs (Context ctx, Writer out) throws MustacheException { for (Segment seg : _segs) { @@ -422,8 +401,28 @@ protected static abstract class Segment { abstract void decompile (Mustache.Delims delims, StringBuilder into); abstract void visit (Mustache.Visitor visitor); - - abstract Segment indent(String indent, boolean last); + /** + * Recursively indent by the parameter indent. + * @param indent should be space characters that are not \n + * @param first append indent to the first line (regardless if it has a \n or not) + * @param last append indent on the last \n that has no text after it + * @return newly crated segment or the same segment if nothing changed. + */ + abstract Segment indent(String indent, boolean first, boolean last); + /** + * Whether or not the segment is standalone. + * For blocks this is based on the closing tag. + * Once Trim is called standalone tags are determined so + * that proper (re)indentation will work without reparsing + * the template. + * + * String and variable tags are never standalone. + * + * The definition of standalone is defined by the mustache spec. + * + * @return true if the tag is standalone + */ + abstract boolean isStandalone(); protected static void write (Writer out, CharSequence data) { try { diff --git a/src/test/java/com/samskivert/mustache/SharedTests.java b/src/test/java/com/samskivert/mustache/SharedTests.java index 1419236..e1763b7 100644 --- a/src/test/java/com/samskivert/mustache/SharedTests.java +++ b/src/test/java/com/samskivert/mustache/SharedTests.java @@ -4,16 +4,34 @@ package com.samskivert.mustache; +import static java.util.Map.entry; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.Writer; -import java.util.*; - +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; + +import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; + +import com.samskivert.mustache.specs.SpecTest; /** * Vestige from when JMustache supported both GWT and JVM. @@ -210,17 +228,25 @@ public abstract class SharedTests fail(); } catch (UnsupportedOperationException uoe) {} // expected } + + + Map partials = new LinkedHashMap<>(); + @SafeVarargs + protected final Mustache.TemplateLoader partials(Map.Entry ... entries) { + Map templates = new LinkedHashMap<>(); + for (Entry e : entries) { + templates.put(e.getKey(), e.getValue()); + } + partials = templates; + return name -> new StringReader(templates.get(name)); + } + @Test public void testPartial () { - test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { - public Reader getTemplate (String name) { - if (name.equals("foo")) { - return new StringReader("inside:{{bar}}"); - } else { - return new StringReader("nonfoo"); - } - } - }), "foo inside:foo nonfoo foo", "{{bar}} {{>foo}} {{>baz}} {{bar}}", context("bar", "foo")); + test(Mustache.compiler().withLoader( + partials(entry("foo", "inside:{{bar}}"), + entry("baz", "nonfoo"))) + , "foo inside:foo nonfoo foo", "{{bar}} {{>foo}} {{>baz}} {{bar}}", context("bar", "foo")); } @Test public void testPartialIndent () { @@ -231,15 +257,15 @@ public Reader getTemplate (String name) { }), "\\\n |\n <\n->\n |\n/\n", "\\\n {{>partial}}\n/\n", context("content", "<\n->")); } - /* @Test */ public void testPartialBlankLines () { + @Test public void testPartialBlankLines () { test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { public Reader getTemplate (String name) { return new StringReader("|\na\n\nb\n|\n"); } - }), "\\\n\t|\n\ta\n\n\tb\n\t|\n/\n", "\\\n\t{{>partial}}\n/\n", context()); + }), "\\\n\t|\n\ta\n\t\n\tb\n\t|\n/\n", "\\\n\t{{>partial}}\n/\n", context()); } - /* @Test */ public void testNestedPartialBlankLines () { + @Test public void testNestedPartialBlankLines () { test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { public Reader getTemplate (String name) { if (name.equals("partial")) { @@ -248,10 +274,10 @@ public Reader getTemplate (String name) { return new StringReader("2\na\n\nb\n2\n"); } } - }), "\\\n\t1\n\t\t2\n\t\ta\n\n\t\tb\n\t\t2\n\t1\n/\n", "\\\n\t{{>partial}}\n/\n", context()); + }), "\\\n\t1\n\t\t2\n\t\ta\n\t\t\n\t\tb\n\t\t2\n\t1\n/\n", "\\\n\t{{>partial}}\n/\n", context()); } - /* @Test */ public void testNestedPartialBlankLinesCRLF () { + @Test public void testNestedPartialBlankLinesCRLF () { test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { public Reader getTemplate (String name) { if (name.equals("partial")) { @@ -260,19 +286,14 @@ public Reader getTemplate (String name) { return new StringReader("2\r\na\r\n\r\nb\r\n2\r\n"); } } - }), "\\\r\n\t1\r\n\t\t2\r\n\t\ta\r\n\r\n\t\tb\r\n\t\t2\r\n\t1\r\n/\r\n", "\\\r\n\t{{>partial}}\r\n/\r\n", context()); + }), "\\\r\n\t1\r\n\t\t2\r\n\t\ta\r\n\t\t\r\n\t\tb\r\n\t\t2\r\n\t1\r\n/\r\n", "\\\r\n\t{{>partial}}\r\n/\r\n", context()); } - /* @Test */ public void testNestedPartialIndent () { - test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { - public Reader getTemplate (String name) { - if (name.equals("partial")) { - return new StringReader("1\n {{>nest}}\n1\n"); - } else { - return new StringReader("2\n{{{content}}}\n2\n"); - } - } - }), "|\n 1\n 2\n <\n->\n 2\n 1\n|\n", "|\n {{>partial}}\n|\n", context("content", "<\n->")); + @Test public void testNestedPartialIndent () { + Mustache.TemplateLoader loader = partials(entry("partial", "1\n {{>nest}}\n1\n"), entry("nest", "2\n{{{content}}}\n2\n")); + test(Mustache.compiler().withLoader(loader), + "|\n 1\n 2\n <\n->\n 2\n 1\n|\n", + "|\n {{>partial}}\n|\n", context("content", "<\n->")); } @Test public void testPartialIndentWithVariableAtTheStart () { @@ -826,12 +847,40 @@ public void execute (Template.Fragment frag, Writer out) { test(Mustache.compiler().withDelims("<% %>"), "bar", "<%foo%>", context("foo", "bar")); } - protected void test (Mustache.Compiler compiler, String expected, String template, Object ctx) { - check(expected, compiler.compile(template).execute(ctx)); + protected String name; + + @Rule public TestRule watcher = new TestWatcher() { + protected void starting(Description description) { + name = description.getDisplayName(); + } + }; + + protected void test(Mustache.Compiler compiler, String expected, String template, Object ctx) { + String actual = compiler.compile(template).execute(ctx); + if (! Objects.equals(expected, actual)) { + System.out.println(""); + System.out.println("----------------------------------------"); + System.out.println(""); + System.out.println("Failed: " + name); + System.out.println("Template : \"" + SpecTest.showWhitespace(template) + "\""); + if (! partials.isEmpty()) { + System.out.println("Partials : "); + for (Entry e : partials.entrySet()) { + System.out.println("\t" + e.getKey() + ": \"" + SpecTest.showWhitespace(e.getValue()) + "\""); + } + } + System.out.println("Expected : \"" + SpecTest.showWhitespace(expected) + "\""); + System.out.println("Result : \"" + SpecTest.showWhitespace(actual) + "\""); + System.out.println("----------------------------------------"); + System.out.println(""); + } + check(expected, actual); } protected void check (String expected, String output) { - assertEquals(uncrlf(expected), uncrlf(output)); + //assertEquals(uncrlf(expected), uncrlf(output)); + assertEquals(SpecTest.showWhitespace(expected), SpecTest.showWhitespace(output)); + } protected void test (String expected, String template, Object ctx) { diff --git a/src/test/java/com/samskivert/mustache/specs/CustomSpecTest.java b/src/test/java/com/samskivert/mustache/specs/CustomSpecTest.java new file mode 100644 index 0000000..4219bc6 --- /dev/null +++ b/src/test/java/com/samskivert/mustache/specs/CustomSpecTest.java @@ -0,0 +1,24 @@ +package com.samskivert.mustache.specs; + +import java.util.Collection; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class CustomSpecTest extends SpecTest { + + public CustomSpecTest(Spec spec, String name) { + super(spec, name); + } + + @Parameters(name = "{1}") + public static Collection data () { + String[] groups = new String[] { + "partials" + }; + 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 new file mode 100644 index 0000000..63dbe47 --- /dev/null +++ b/src/test/java/com/samskivert/mustache/specs/OfficialSpecTest.java @@ -0,0 +1,29 @@ +package com.samskivert.mustache.specs; + +import java.util.Collection; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class OfficialSpecTest extends SpecTest { + + public OfficialSpecTest(Spec spec, String name) { + super(spec, name); + } + + @Parameters(name = "{1}") + public static Collection data () { + String[] groups = new String[] { + "comments", + "delimiters", + "interpolation", + "inverted", + "sections", + "partials" + }; + 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 eb548fb..84517c8 100644 --- a/src/test/java/com/samskivert/mustache/specs/Spec.java +++ b/src/test/java/com/samskivert/mustache/specs/Spec.java @@ -5,6 +5,7 @@ package com.samskivert.mustache.specs; import java.util.Map; +import java.util.function.Consumer; /** * @author Yoryos Valotasios @@ -26,7 +27,7 @@ public String getName () { } public String getDescription () { - return (String) map.get("descr"); + return (String) map.get("desc"); } public String getTemplate () { @@ -44,4 +45,20 @@ public Object getData () { public String getPartial (String name) { return partials == null ? null : partials.get(name); } -} + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + Consumer value = s -> sb.append("\"").append(s).append("\"").append("\n"); + Consumer label = s -> sb.append("").append(s).append(": "); + label.accept("name"); + value.accept(getName()); + label.accept("desc"); + value.accept(getDescription()); + label.accept("template"); + value.accept(getTemplate()); + label.accept("expected"); + value.accept(getExpectedOutput()); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/test/java/com/samskivert/mustache/specs/SpecAwareTemplateLoader.java b/src/test/java/com/samskivert/mustache/specs/SpecAwareTemplateLoader.java index 2e01f13..f99c2bd 100644 --- a/src/test/java/com/samskivert/mustache/specs/SpecAwareTemplateLoader.java +++ b/src/test/java/com/samskivert/mustache/specs/SpecAwareTemplateLoader.java @@ -12,9 +12,10 @@ public class SpecAwareTemplateLoader implements Mustache.TemplateLoader { private static final String EMPTY_STRING = ""; - private Spec spec; + private final Spec spec; - public void setSpec (Spec spec) { + public SpecAwareTemplateLoader(Spec spec) { + super(); this.spec = spec; } diff --git a/src/test/java/com/samskivert/mustache/specs/SpecTest.java b/src/test/java/com/samskivert/mustache/specs/SpecTest.java index a8d0a7d..4723c39 100644 --- a/src/test/java/com/samskivert/mustache/specs/SpecTest.java +++ b/src/test/java/com/samskivert/mustache/specs/SpecTest.java @@ -7,19 +7,14 @@ import java.util.Objects; import org.junit.Assert; -import org.junit.BeforeClass; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.error.YAMLException; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Template; -@RunWith(Parameterized.class) -public class SpecTest { +public abstract class SpecTest { private static final Yaml yaml = new Yaml(); @@ -32,19 +27,11 @@ public SpecTest (Spec spec, String name) { this.name = name; } - private static Mustache.Compiler compiler; - private static SpecAwareTemplateLoader loader; - - @BeforeClass - public static void setUp () { - loader = new SpecAwareTemplateLoader(); - compiler = Mustache.compiler().defaultValue("").withLoader(loader); - } - @Test public void test () throws Exception { //System.out.println("Testing: " + name); - loader.setSpec(spec); + SpecAwareTemplateLoader loader = new SpecAwareTemplateLoader(spec); + Mustache.Compiler compiler = Mustache.compiler().defaultValue("").withLoader(loader); String tmpl = spec.getTemplate(); String desc = String.format("Template: '''%s'''\nData: '%s'\n", uncrlf(tmpl), uncrlf(spec.getData().toString())); @@ -75,7 +62,7 @@ public void test () throws Exception { } } - private static String showWhitespace (String s) { + public static String showWhitespace (String s) { s = s.replace("\r\n", "\u240D"); s = s.replace('\t', '\u21E5'); s = s.replace("\n", "\u21B5\n"); @@ -88,20 +75,12 @@ private static String uncrlf (String text) { return (text == null) ? null : text.replace("\r", "\\r").replace("\n", "\\n"); } - @Parameters(name = "{1}") - public static Collection data () { - String[] groups = new String[] { - "comments", - "delimiters", - "interpolation", - "inverted", - "sections", - "partials" - }; + + public static Collection data (String specPath, String[] groups) { List tuples = new ArrayList<>(); int i = 0; for (String g : groups) { - Iterable specs = getTestsForGroup(g); + Iterable specs = getTestsForGroup(specPath, g); for (Spec s : specs) { Object[] tuple = new Object[] {s, g + "-" + s.getName() + "-" + i++}; tuples.add(tuple); @@ -110,8 +89,13 @@ public static Collection data () { return tuples; } - private static Iterable getTestsForGroup (String name) { - String ymlPath = "/specs/specs/" + name + ".yml"; + private static Iterable getTestsForGroup (String specPath, String name) { + //String ymlPath = "/specs/specs/" + name + ".yml"; + String ymlPath = specPath + name + ".yml"; + return getTestsFromYaml(name, ymlPath); + } + + private static Iterable getTestsFromYaml(String name, String ymlPath) { try { @SuppressWarnings("unchecked") Map map = (Map) yaml.load(SpecTest.class.getResourceAsStream(ymlPath)); diff --git a/src/test/resources/custom/specs/partials.yml b/src/test/resources/custom/specs/partials.yml new file mode 100644 index 0000000..29494a1 --- /dev/null +++ b/src/test/resources/custom/specs/partials.yml @@ -0,0 +1,45 @@ +overview: | + Partial tags are used to expand an external template into the current + template. + + The tag's content MUST be a non-whitespace character sequence NOT containing + the current closing delimiter. + + This tag's content names the partial to inject. Set Delimiter tags MUST NOT + affect the parsing of a partial. The partial MUST be rendered against the + context stack local to the tag. If the named partial cannot be found, the + empty string SHOULD be used instead, as in interpolations. + + Partial tags SHOULD be treated as standalone when appropriate. If this tag + is used standalone, any whitespace preceding the tag should treated as + indentation, and prepended to each line of the partial before rendering. +tests: + + - name: Nested Partial Indent + desc: "Nested partials should including the parent partials indent" + data: { content: "<\n->" } + template: "|\n {{>partial}}\n|\n" + partials: + partial: "1\n {{>nest}}\n1\n" + nest: "2\n{{{content}}}\n2\n" + expected: "|\n 1\n 2\n <\n->\n 2\n 1\n|\n" + + - name: Standalone Indentation + desc: Each line of the partial should be indented before rendering. + data: { content: "<\n->" } + template: | + \ + {{>partial}} + / + partials: + partial: | + | + {{{content}}} + | + expected: | + \ + | + < + -> + | + /