Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix all whitespace specs #215

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
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