diff --git a/compiler/src/main/java/com/github/mustachejava/DefaultMustacheFactory.java b/compiler/src/main/java/com/github/mustachejava/DefaultMustacheFactory.java index 9140c0536..20a9fd789 100644 --- a/compiler/src/main/java/com/github/mustachejava/DefaultMustacheFactory.java +++ b/compiler/src/main/java/com/github/mustachejava/DefaultMustacheFactory.java @@ -35,7 +35,7 @@ public class DefaultMustacheFactory implements MustacheFactory { /** * This parser should work with any MustacheFactory */ - protected final MustacheParser mc = new MustacheParser(this); + protected final MustacheParser mc = createParser(); /** * New templates that are generated at runtime are cached here. The template key @@ -259,6 +259,10 @@ public Mustache compilePartial(String s) { } } + protected MustacheParser createParser() { + return new MustacheParser(this); + } + protected Function getMustacheCacheFunction() { return mc::compile; } diff --git a/compiler/src/main/java/com/github/mustachejava/DefaultMustacheVisitor.java b/compiler/src/main/java/com/github/mustachejava/DefaultMustacheVisitor.java index 69b71cd37..786c4fa9e 100644 --- a/compiler/src/main/java/com/github/mustachejava/DefaultMustacheVisitor.java +++ b/compiler/src/main/java/com/github/mustachejava/DefaultMustacheVisitor.java @@ -64,7 +64,7 @@ public void checkName(TemplateContext templateContext, String variable, Mustache } @Override - public void partial(TemplateContext tc, final String variable) { + public void partial(TemplateContext tc, final String variable, String indent) { TemplateContext partialTC = new TemplateContext("{{", "}}", tc.file(), tc.line(), tc.startOfLine()); list.add(new PartialCode(partialTC, df, variable)); } diff --git a/compiler/src/main/java/com/github/mustachejava/DeferringMustacheFactory.java b/compiler/src/main/java/com/github/mustachejava/DeferringMustacheFactory.java index 945717d9b..0aec6379a 100644 --- a/compiler/src/main/java/com/github/mustachejava/DeferringMustacheFactory.java +++ b/compiler/src/main/java/com/github/mustachejava/DeferringMustacheFactory.java @@ -70,7 +70,7 @@ public MustacheVisitor createMustacheVisitor() { final AtomicLong id = new AtomicLong(0); return new DefaultMustacheVisitor(this) { @Override - public void partial(TemplateContext tc, final String variable) { + public void partial(TemplateContext tc, final String variable, final String indent) { TemplateContext partialTC = new TemplateContext("{{", "}}", tc.file(), tc.line(), tc.startOfLine()); final Long divid = id.incrementAndGet(); list.add(new PartialCode(partialTC, df, variable) { diff --git a/compiler/src/main/java/com/github/mustachejava/MustacheParser.java b/compiler/src/main/java/com/github/mustachejava/MustacheParser.java index 928132385..ed59dbd0e 100644 --- a/compiler/src/main/java/com/github/mustachejava/MustacheParser.java +++ b/compiler/src/main/java/com/github/mustachejava/MustacheParser.java @@ -16,10 +16,16 @@ public class MustacheParser { public static final String DEFAULT_SM = "{{"; public static final String DEFAULT_EM = "}}"; + private final boolean specConformWhitespace; private MustacheFactory mf; - protected MustacheParser(MustacheFactory mf) { + protected MustacheParser(MustacheFactory mf, boolean specConformWhitespace) { this.mf = mf; + this.specConformWhitespace = specConformWhitespace; + } + + protected MustacheParser(MustacheFactory mf) { + this(mf, false); } public Mustache compile(String file) { @@ -181,9 +187,20 @@ protected Mustache compile(final Reader reader, String tag, final AtomicInteger return mv.mustache(new TemplateContext(sm, em, file, 0, startOfLine)); } case '>': { + String indent = (onlywhitespace && startOfLine) ? out.toString() : ""; out = write(mv, out, file, currentLine.intValue(), startOfLine); startOfLine = startOfLine & onlywhitespace; - mv.partial(new TemplateContext(sm, em, file, currentLine.get(), startOfLine), variable); + mv.partial(new TemplateContext(sm, em, file, currentLine.get(), startOfLine), variable, indent); + + // a new line following a partial is dropped + if (specConformWhitespace && startOfLine) { + br.mark(2); + int ca = br.read(); + if (ca == '\r') { + ca = br.read(); + } + if (ca != '\n') br.reset(); + } break; } case '{': { diff --git a/compiler/src/main/java/com/github/mustachejava/MustacheVisitor.java b/compiler/src/main/java/com/github/mustachejava/MustacheVisitor.java index a8083b7d4..5866e5698 100644 --- a/compiler/src/main/java/com/github/mustachejava/MustacheVisitor.java +++ b/compiler/src/main/java/com/github/mustachejava/MustacheVisitor.java @@ -12,7 +12,7 @@ public interface MustacheVisitor { void notIterable(TemplateContext templateContext, String variable, Mustache mustache); - void partial(TemplateContext templateContext, String variable); + void partial(TemplateContext templateContext, String variable, String indent); void value(TemplateContext templateContext, String variable, boolean encoded); diff --git a/compiler/src/main/java/com/github/mustachejava/SpecMustacheFactory.java b/compiler/src/main/java/com/github/mustachejava/SpecMustacheFactory.java new file mode 100644 index 000000000..d4d9065cd --- /dev/null +++ b/compiler/src/main/java/com/github/mustachejava/SpecMustacheFactory.java @@ -0,0 +1,47 @@ +package com.github.mustachejava; + +import com.github.mustachejava.resolver.DefaultResolver; + +import java.io.File; + +/** + * This factory is similar to DefaultMustacheFactory but handles whitespace according to the mustache specification. + * Therefore the rendering is less performant than with the DefaultMustacheFactory. + */ +public class SpecMustacheFactory extends DefaultMustacheFactory { + @Override + public MustacheVisitor createMustacheVisitor() { + return new SpecMustacheVisitor(this); + } + + public SpecMustacheFactory() { + super(); + } + + public SpecMustacheFactory(MustacheResolver mustacheResolver) { + super(mustacheResolver); + } + + /** + * Use the classpath to resolve mustache templates. + * + * @param classpathResourceRoot the location in the resources where templates are stored + */ + public SpecMustacheFactory(String classpathResourceRoot) { + super(classpathResourceRoot); + } + + /** + * Use the file system to resolve mustache templates. + * + * @param fileRoot the root of the file system where templates are stored + */ + public SpecMustacheFactory(File fileRoot) { + super(fileRoot); + } + + @Override + protected MustacheParser createParser() { + return new MustacheParser(this, true); + } +} diff --git a/compiler/src/main/java/com/github/mustachejava/SpecMustacheVisitor.java b/compiler/src/main/java/com/github/mustachejava/SpecMustacheVisitor.java new file mode 100644 index 000000000..8736d2cee --- /dev/null +++ b/compiler/src/main/java/com/github/mustachejava/SpecMustacheVisitor.java @@ -0,0 +1,62 @@ +package com.github.mustachejava; + +import com.github.mustachejava.codes.PartialCode; +import com.github.mustachejava.codes.ValueCode; +import com.github.mustachejava.util.IndentWriter; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; + +public class SpecMustacheVisitor extends DefaultMustacheVisitor { + public SpecMustacheVisitor(DefaultMustacheFactory df) { + super(df); + } + + @Override + public void partial(TemplateContext tc, final String variable, String indent) { + TemplateContext partialTC = new TemplateContext("{{", "}}", tc.file(), tc.line(), tc.startOfLine()); + list.add(new SpecPartialCode(partialTC, df, variable, indent)); + } + + @Override + public void value(TemplateContext tc, final String variable, boolean encoded) { + list.add(new SpecValueCode(tc, df, variable, encoded)); + } + + static class SpecPartialCode extends PartialCode { + private final String indent; + + public SpecPartialCode(TemplateContext tc, DefaultMustacheFactory cf, String variable, String indent) { + super(tc, cf, variable); + this.indent = indent; + } + + @Override + protected Writer executePartial(Writer writer, final List scopes) { + partial.execute(new IndentWriter(writer, indent), scopes); + return writer; + } + } + + static class SpecValueCode extends ValueCode { + + public SpecValueCode(TemplateContext tc, DefaultMustacheFactory df, String variable, boolean encoded) { + super(tc, df, variable, encoded); + } + + @Override + protected void execute(Writer writer, final String value) throws IOException { + if (writer instanceof IndentWriter) { + IndentWriter iw = (IndentWriter) writer; + iw.flushIndent(); + writer = iw.inner; + while (writer instanceof IndentWriter) { + writer = ((IndentWriter) writer).inner; + } + } + + super.execute(writer, value); + } + } +} diff --git a/compiler/src/main/java/com/github/mustachejava/codes/PartialCode.java b/compiler/src/main/java/com/github/mustachejava/codes/PartialCode.java index a10861f69..b0dac3f9b 100644 --- a/compiler/src/main/java/com/github/mustachejava/codes/PartialCode.java +++ b/compiler/src/main/java/com/github/mustachejava/codes/PartialCode.java @@ -15,6 +15,7 @@ public class PartialCode extends DefaultCode { protected PartialCode(TemplateContext tc, DefaultMustacheFactory df, Mustache mustache, String type, String variable) { super(tc, df, mustache, variable, type); + // Use the name of the parent to get the name of the partial String file = tc.file(); int dotindex = file.lastIndexOf("."); @@ -67,7 +68,7 @@ public Writer execute(Writer writer, final List scopes) { } writer = depthLimitedWriter; } - Writer execute = partial.execute(writer, scopes); + Writer execute = executePartial(writer, scopes); if (isRecursive) { assert depthLimitedWriter != null; depthLimitedWriter.decr(); @@ -75,6 +76,10 @@ public Writer execute(Writer writer, final List scopes) { return appendText(execute); } + protected Writer executePartial(Writer writer, final List scopes) { + return partial.execute(writer, scopes); + } + @Override public synchronized void init() { filterText(); diff --git a/compiler/src/main/java/com/github/mustachejava/util/IndentWriter.java b/compiler/src/main/java/com/github/mustachejava/util/IndentWriter.java new file mode 100644 index 000000000..f44bb1b64 --- /dev/null +++ b/compiler/src/main/java/com/github/mustachejava/util/IndentWriter.java @@ -0,0 +1,57 @@ +package com.github.mustachejava.util; + +import java.io.IOException; +import java.io.Writer; + +public class IndentWriter extends Writer { + + public final Writer inner; + private final String indent; + private boolean prependIndent = false; + + public IndentWriter(Writer inner, String indent) { + this.inner = inner; + this.indent = indent; + } + + @Override + public void write(char[] chars, int off, int len) throws IOException { + int newOff = off; + for (int i = newOff; i < len; ++i) { + if (chars[i] == '\n') { + // write character up to newline + writeLine(chars, newOff, i + 1 - newOff); + this.prependIndent = true; + + newOff = i + 1; + } + } + writeLine(chars, newOff, len - (newOff - off)); + } + + public void flushIndent() throws IOException { + if (this.prependIndent) { + inner.append(indent); + this.prependIndent = false; + } + } + + private void writeLine(char[] chars, int off, int len) throws IOException { + if (len <= 0) { + return; + } + + this.flushIndent(); + inner.write(chars, off, len); + } + + @Override + public void flush() throws IOException { + inner.flush(); + } + + @Override + public void close() throws IOException { + inner.close(); + } +} diff --git a/compiler/src/test/java/com/github/mustachejava/FullSpecTest.java b/compiler/src/test/java/com/github/mustachejava/FullSpecTest.java new file mode 100644 index 000000000..46f0e225e --- /dev/null +++ b/compiler/src/test/java/com/github/mustachejava/FullSpecTest.java @@ -0,0 +1,56 @@ +package com.github.mustachejava; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.Reader; +import java.io.StringReader; + +public class FullSpecTest extends SpecTest { + @Override + @Test + @Ignore("not ready yet") + public void interpolations() { + } + + @Override + @Test + @Ignore("not ready yet") + public void sections() { + } + + @Override + @Test + @Ignore("not ready yet") + public void delimiters() { + } + + @Override + @Test + @Ignore("not ready yet") + public void inverted() { + } + + @Override + @Test + @Ignore("not ready yet") + public void lambdas() { + } + + @Override + protected DefaultMustacheFactory createMustacheFactory(final JsonNode test) { + return new SpecMustacheFactory("/spec/specs") { + @Override + public Reader getReader(String resourceName) { + JsonNode partial = test.get("partials").get(resourceName); + return new StringReader(partial == null ? "" : partial.asText()); + } + }; + } + + @Override + protected String transformOutput(String output) { + return output; + } +} diff --git a/compiler/src/test/java/com/github/mustachejava/InterpreterTest.java b/compiler/src/test/java/com/github/mustachejava/InterpreterTest.java index 4edfef430..264144471 100644 --- a/compiler/src/test/java/com/github/mustachejava/InterpreterTest.java +++ b/compiler/src/test/java/com/github/mustachejava/InterpreterTest.java @@ -684,7 +684,7 @@ public void testDynamicPartial() throws MustacheException, IOException { public MustacheVisitor createMustacheVisitor() { return new DefaultMustacheVisitor(this) { @Override - public void partial(TemplateContext tc, String variable) { + public void partial(TemplateContext tc, String variable, String indent) { if (variable.startsWith("+")) { // This is a dynamic partial rather than a static one TemplateContext partialTC = new TemplateContext("{{", "}}", tc.file(), tc.line(), tc.startOfLine()); @@ -713,7 +713,7 @@ public Writer execute(Writer writer, List scopes) { } }); } else { - super.partial(tc, variable); + super.partial(tc, variable, indent); } } }; @@ -1222,7 +1222,7 @@ public void testOverrideExtension() throws IOException { public MustacheVisitor createMustacheVisitor() { return new DefaultMustacheVisitor(this) { @Override - public void partial(TemplateContext tc, String variable) { + public void partial(TemplateContext tc, String variable, String indent) { TemplateContext partialTC = new TemplateContext("{{", "}}", tc.file(), tc.line(), tc.startOfLine()); list.add(new PartialCode(partialTC, df, variable) { @Override diff --git a/compiler/src/test/java/com/github/mustachejava/SpecTest.java b/compiler/src/test/java/com/github/mustachejava/SpecTest.java index 438a85dc3..7b602c67f 100644 --- a/compiler/src/test/java/com/github/mustachejava/SpecTest.java +++ b/compiler/src/test/java/com/github/mustachejava/SpecTest.java @@ -147,7 +147,7 @@ Function lambda() { StringWriter writer = new StringWriter(); compile.execute(writer, new Object[]{new ObjectMapper().readValue(data.toString(), Map.class), functionMap.get(file)}); String expected = test.get("expected").asText(); - if (writer.toString().replaceAll("\\s+", "").equals(expected.replaceAll("\\s+", ""))) { + if (transformOutput(writer.toString()).equals(transformOutput(expected))) { System.out.print(": success"); if (writer.toString().equals(expected)) { System.out.println("!"); @@ -174,6 +174,10 @@ Function lambda() { assertFalse(fail > 0); } + protected String transformOutput(String output) { + return output.replaceAll("\\s+", ""); + } + protected DefaultMustacheFactory createMustacheFactory(final JsonNode test) { return new DefaultMustacheFactory("/spec/specs") { @Override