From 979b3b887c8edb8ef0428f3df4c6035090b7139a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=B6nhusen?= Date: Wed, 15 Nov 2023 11:24:40 +0100 Subject: [PATCH] Add indentation to partials --- .../com/samskivert/mustache/Mustache.java | 73 +++++++++++++-- .../com/samskivert/mustache/SharedTests.java | 91 +++++++++++++++++++ .../samskivert/mustache/specs/SpecRunner.java | 2 +- 3 files changed, 156 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 2b9d078..f17ed8f 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -84,7 +84,11 @@ public class Mustache { /** Compiles the supplied template into a repeatedly executable intermediate form. */ public Template compile (Reader source) { - return Mustache.compile(source, this); + return compile(source, ""); + } + /** Compiles the supplied template into a repeatedly executable intermediate form. */ + public Template compile (Reader source, String indent) { + return Mustache.compile(source, this, indent); } /** Returns a compiler that either does or does not escape HTML by default. Note: this @@ -214,11 +218,11 @@ public class Mustache { * @throw MustacheException if the template could not be loaded (due to I/O exception) or * compiled (due to syntax error, etc.). */ - public Template loadTemplate (String name) throws MustacheException { + public Template loadTemplate (String name, String indent) throws MustacheException { Reader tin = null; try { tin = loader.getTemplate(name); - return compile(tin); + return compile(tin, indent); } catch (Exception e) { if (e instanceof RuntimeException) { throw (RuntimeException)e; @@ -385,13 +389,55 @@ public class Mustache { /** * Compiles the supplied template into a repeatedly executable intermediate form. */ - protected static Template compile (Reader source, Compiler compiler) { + protected static Template compile (Reader source, Compiler compiler, String indent) { Accumulator accum = new Parser(compiler).parse(source); - return new Template(trim(accum.finish(), true), compiler); + return new Template(trim(indent(accum.finish(), indent, false), true), compiler); } private Mustache () {} // no instantiateski + protected static Template.Segment[] indent(Template.Segment[] segs, String indent, boolean inline) { + for (int ii = 0, ll = segs.length; ii < ll ; ii++) { + Template.Segment seg = segs[ii]; + Template.Segment pseg = (ii > 0) ? segs[ii - 1] : null; + Template.Segment nseg = (ii < ll - 1) ? segs[ii + 1] : null; + StringSegment prev = (pseg instanceof StringSegment) ? (StringSegment)pseg : null; + boolean prevBlank = prev != null && prev.trailsBlank(); + // add indent after each new line when the next line is not empty + if (seg instanceof StringSegment && !indent.isEmpty()) { + final StringSegment segment = ((StringSegment) seg); + // add indent to every line that is not blank + String indentedText = segment._text.replaceAll("\n(?!\r?\n)", "\n" + indent); + // if the last line ist just the indent, remove it + if (nseg == null && indentedText.endsWith("\n" + indent)) { + indentedText = indentedText.substring(0, indentedText.length() - indent.length()); + } + // if this is the first segement in a block, add indent at the beginning + if (pseg == null && !inline) { + indentedText = indent + indentedText; + } + segs[ii] = new StringSegment(indentedText, false); + } else if (seg instanceof BlockSegment) { + BlockSegment block = (BlockSegment)seg; + final boolean inlineBlock = !block.firstLeadsBlank() || !prevBlank; + block._segs = indent(block._segs, indent, inlineBlock); + } else if (seg instanceof IncludedTemplateSegment) { + final IncludedTemplateSegment segment = (IncludedTemplateSegment) seg; + if (prev != null && prev.trailsBlank()) { + segment._indent = prev.getTrailBlank(); + } + } + } + // add indent to the first line if it starts with a variable + if (!indent.isEmpty() && segs.length > 0 && !inline && (segs[0] instanceof VariableSegment)) { + Template.Segment[] oldSegs = segs; + segs = new Template.Segment[segs.length + 1]; + segs[0] = new StringSegment(indent, false); + System.arraycopy(oldSegs, 0, segs, 1, oldSegs.length); + } + return segs; + } + protected static Template.Segment[] trim (Template.Segment[] segs, boolean top) { // 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 @@ -415,9 +461,10 @@ public class Mustache { block.trimLastBlank(); if (nseg != null) segs[ii+1] = next.trimLeadBlank(); } + trim(block._segs, false); } // potentially trim around non-printing (comments/delims) segments - else if (seg instanceof FauxSegment) { + else if (seg instanceof FauxSegment || seg instanceof IncludedTemplateSegment) { if (prevBlank && nextBlank) { if (pseg != null) segs[ii-1] = prev.trimTrailBlank(); if (nseg != null) segs[ii+1] = next.trimLeadBlank(); @@ -775,6 +822,13 @@ public class Mustache { _trailBlank = trailBlank; } + public String getTrailBlank() { + if (!trailsBlank()) { + return ""; + } + return _text.substring(_trailBlank); + } + public boolean leadsBlank () { return _leadBlank != -1; } public boolean trailsBlank () { return _trailBlank != -1; } @@ -821,6 +875,7 @@ public class Mustache { /** A segment that loads and executes a sub-template. */ protected static class IncludedTemplateSegment extends Template.Segment { + private String _indent = ""; public IncludedTemplateSegment (Compiler compiler, String name) { _comp = compiler; _name = name; @@ -842,7 +897,7 @@ public class Mustache { // we compile our template lazily to avoid infinie recursion if a template includes // itself (see issue #13) if (_template == null) { - _template = _comp.loadTemplate(_name); + _template = _comp.loadTemplate(_name, _indent); } return _template; } @@ -913,7 +968,7 @@ public class Mustache { protected BlockSegment (String name, Template.Segment[] segs, int line) { super(name, line); - _segs = trim(segs, false); + _segs = segs; } protected void executeSegs (Template tmpl, Template.Context ctx, Writer out) { for (Template.Segment seg : _segs) { @@ -921,7 +976,7 @@ public class Mustache { } } - protected final Template.Segment[] _segs; + protected Template.Segment[] _segs; } /** A segment that represents a section. */ diff --git a/src/test/java/com/samskivert/mustache/SharedTests.java b/src/test/java/com/samskivert/mustache/SharedTests.java index 33f1b17..0ce962c 100644 --- a/src/test/java/com/samskivert/mustache/SharedTests.java +++ b/src/test/java/com/samskivert/mustache/SharedTests.java @@ -228,6 +228,97 @@ public abstract class SharedTests extends GWTTestCase }), "foo inside:foo nonfoo foo", "{{bar}} {{>foo}} {{>baz}} {{bar}}", context("bar", "foo")); } + @Test public void testPartialIndent () { + test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { + public Reader getTemplate (String name) { + return new StringReader("|\n{{{content}}}\n|\n"); + } + }), "\\\n |\n <\n->\n |\n/\n", "\\\n {{>partial}}\n/\n", context("content", "<\n->")); + } + + @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()); + } + + @Test public void testNestedPartialBlankLines () { + test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { + public Reader getTemplate (String name) { + if (name.equals("partial")) { + return new StringReader("1\n\t{{>nest}}\n1\n"); + } else { + 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()); + } + + @Test public void testNestedPartialBlankLinesCRLF () { + test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { + public Reader getTemplate (String name) { + if (name.equals("partial")) { + return new StringReader("1\r\n\t{{>nest}}\r\n1\r\n"); + } else { + 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()); + } + + @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 testPartialIndentWithVariableAtTheStart () { + test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { + public Reader getTemplate (String name) { + return new StringReader("{{{content}}}\n|\n"); + } + }), "\\\n <\n->\n |\n/\n", "\\\n {{>partial}}\n/\n", context("content", "<\n->")); + } + + @Test public void testPartialIndentWithBlock () { + test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { + public Reader getTemplate (String name) { + return new StringReader("|\n{{#show}}\n{{{content}}}{{/show}}\n|\n"); + } + }), "\\\n |\n <\n->\n |\n/\n", "\\\n {{>partial}}\n/\n", context("show", true, "content", "<\n->")); + } + @Test public void testPartialIndentInBlock () { + test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { + public Reader getTemplate (String name) { + return new StringReader("content"); + } + }), " content", "{{#show}}\n {{>partial}}\n{{/show}}\n", context("show", true)); + } + + @Test public void testPartialIndentWithBlockAtStart () { + test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { + public Reader getTemplate (String name) { + return new StringReader("{{#show}}\n{{{content}}}{{/show}}\n|\n"); + } + }), "\\\n <\n->\n |\n/\n", "\\\n {{>partial}}\n/\n", context("show", true, "content", "<\n->")); + } + + @Test public void testPartialIndentWithInlineBlock () { + test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { + public Reader getTemplate (String name) { + return new StringReader("line {{#show}}content{{/show}}\n"); + } + }), "\\\n line content\n/\n", "\\\n {{>partial}}\n/\n", context("show", true)); + } + @Test public void testPartialPlusNestedContext () { test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { public Reader getTemplate (String name) { diff --git a/src/test/java/com/samskivert/mustache/specs/SpecRunner.java b/src/test/java/com/samskivert/mustache/specs/SpecRunner.java index 3a47234..b0315f3 100644 --- a/src/test/java/com/samskivert/mustache/specs/SpecRunner.java +++ b/src/test/java/com/samskivert/mustache/specs/SpecRunner.java @@ -53,7 +53,7 @@ public class SpecRunner extends BlockJUnit4ClassRunner "interpolation", "inverted", "sections", - // "partials" + "partials" }; Method m = getTestClassMethod("test", Spec.class); List tests = new ArrayList();