Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -259,6 +259,10 @@ public Mustache compilePartial(String s) {
}
}

protected MustacheParser createParser() {
return new MustacheParser(this);
}

protected Function<String, Mustache> getMustacheCacheFunction() {
return mc::compile;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
116 changes: 107 additions & 9 deletions compiler/src/main/java/com/github/mustachejava/MustacheParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@
public class MustacheParser {
public static final String DEFAULT_SM = "{{";
public static final String DEFAULT_EM = "}}";

/**
* For legacy reasons we keep the non-spec conform parsing of whitespace unless this flag is true.
*/
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) {
Expand Down Expand Up @@ -74,7 +85,7 @@ protected Mustache compile(final Reader reader, String tag, final AtomicInteger
// Increment the line
if (c == '\n') {
currentLine.incrementAndGet();
if (!iterable || (iterable && !onlywhitespace)) {
if (specConformWhitespace || !iterable || (iterable && !onlywhitespace)) {
if (sawCR) out.append("\r");
out.append("\n");
}
Expand Down Expand Up @@ -140,13 +151,19 @@ protected Mustache compile(final Reader reader, String tag, final AtomicInteger
case '$': {
boolean oldStartOfLine = startOfLine;
startOfLine = startOfLine & onlywhitespace;

boolean nextStartOfLine = specConformWhitespace && trimNewline(startOfLine, br);

int line = currentLine.get();
final Mustache mustache = compile(br, variable, currentLine, file, sm, em, startOfLine);
int lines = currentLine.get() - line;
if (!onlywhitespace || lines == 0) {

if ((specConformWhitespace && !nextStartOfLine) ||
(!specConformWhitespace && (!onlywhitespace || lines == 0))) {
write(mv, out, file, currentLine.intValue(), oldStartOfLine);
}
out = new StringBuilder();

switch (ch) {
case '#':
mv.iterable(new TemplateContext(sm, em, file, line, startOfLine), variable, mustache);
Expand All @@ -164,14 +181,21 @@ protected Mustache compile(final Reader reader, String tag, final AtomicInteger
mv.checkName(new TemplateContext(sm, em, file, line, startOfLine), variable, mustache);
break;
}

startOfLine = nextStartOfLine;
iterable = lines != 0;
break;
}
case '/': {
// Tag end
if (!startOfLine || !onlywhitespace) {
if (specConformWhitespace) {
if (!trimNewline(onlywhitespace & startOfLine, br)) {
write(mv, out, file, currentLine.intValue(), startOfLine);
}
} else if (!startOfLine || !onlywhitespace) {
write(mv, out, file, currentLine.intValue(), startOfLine);
}

if (!variable.equals(tag)) {
TemplateContext tc = new TemplateContext(sm, em, file, currentLine.get(), startOfLine);
throw new MustacheException(
Expand All @@ -181,9 +205,13 @@ 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
startOfLine = specConformWhitespace && trimNewline(startOfLine, br);
break;
}
case '{': {
Expand All @@ -200,12 +228,16 @@ protected Mustache compile(final Reader reader, String tag, final AtomicInteger
}
}
mv.value(new TemplateContext(sm, em, file, currentLine.get(), false), name, false);

startOfLine = false;
break;
}
case '&': {
// Not escaped
out = write(mv, out, file, currentLine.intValue(), startOfLine);
mv.value(new TemplateContext(sm, em, file, currentLine.get(), false), variable, false);

startOfLine = false;
break;
}
case '%':
Expand All @@ -222,15 +254,30 @@ protected Mustache compile(final Reader reader, String tag, final AtomicInteger
args = variable.substring(index + 1);
}
mv.pragma(new TemplateContext(sm, em, file, currentLine.get(), startOfLine), pragma, args);

startOfLine = false;
break;
case '!':
// Comment
mv.comment(new TemplateContext(sm, em, file, currentLine.get(), startOfLine), variable);
out = write(mv, out, file, currentLine.intValue(), startOfLine);

if (specConformWhitespace) {
boolean sol = trimNewline(startOfLine & onlywhitespace, br);

if (!sol) {
write(mv, out, file, currentLine.intValue(), startOfLine);
}

startOfLine = sol;
} else {
write(mv, out, file, currentLine.intValue(), startOfLine);
startOfLine = false;
}
out = new StringBuilder();

break;
case '=':
// Change delimiters
out = write(mv, out, file, currentLine.intValue(), startOfLine);
String trimmed = command.substring(1).trim();
String[] split = trimmed.split("\\s+");
if (split.length != 2) {
Expand All @@ -239,6 +286,23 @@ protected Mustache compile(final Reader reader, String tag, final AtomicInteger
}
sm = split[0];
em = split[1];


if (specConformWhitespace) {
boolean sol = trimNewline(startOfLine & onlywhitespace, br);

if (!sol) {
write(mv, out, file, currentLine.intValue(), startOfLine);
}

startOfLine = sol;
} else {
write(mv, out, file, currentLine.intValue(), startOfLine);
startOfLine = false;
}
out = new StringBuilder();


break;
default: {
if (c == -1) {
Expand All @@ -248,11 +312,16 @@ protected Mustache compile(final Reader reader, String tag, final AtomicInteger
// Reference
out = write(mv, out, file, currentLine.intValue(), startOfLine);
mv.value(new TemplateContext(sm, em, file, currentLine.get(), false), command.trim(), true);
startOfLine = false;
break;
}
}
// Additional text is no longer at the start of the line
startOfLine = false;
if (!specConformWhitespace) {
// Additional text is no longer at the start of the line
// in spec-conform whitespace parsing we sometimes chop a whole line so we let the individual commands
// decide wether we are at the start of a line
startOfLine = false;
}
continue;
} else {
// Only one
Expand Down Expand Up @@ -297,4 +366,33 @@ private StringBuilder write(MustacheVisitor mv, StringBuilder out, String file,
return new StringBuilder();
}

/**
* Some statements such as partials are treated as "standalone".
* This means that if they are the only content on this line (except whitespace) then the following newline is
* chopped.
* For backwards compatibility we only do this if the parser is explicitly configured so.
* @param firstStmt If the statement that was just read was at the start of line with only whitespace preceding it
* @param br The reader
* @return true if trimming was allowed and a following new line was removed or the buffer was finished;
* @throws IOException
*/
private boolean trimNewline(boolean firstStmt, Reader br) throws IOException {
boolean trimmed = false;

if (firstStmt) {
br.mark(2);
int ca = br.read();
if (ca == '\r') {
ca = br.read();
}

if (ca == '\n' || ca == -1) {
trimmed = true;
} else {
br.reset();
}
}

return trimmed;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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<Object> 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) {
((IndentWriter) writer).flushIndent();
writer = ((IndentWriter) writer).inner;
}
}

super.execute(writer, value);
}
}
}
Loading