Only skip newlines if a tag is the only thing on the line.
Otherwise you get undesirable newline skipping if you do things like, for
example:
{{#foos}}
{{bar}}{{^-last}}, {{/-last}}
{{/foos}}
where you want the newline after {{/-last}}, but it was getting skipped.
This commit is contained in:
@@ -276,8 +276,8 @@ By taking advantage of reflection and bean-property-style lookups, you can do ko
|
||||
Newline trimming
|
||||
----------------
|
||||
|
||||
Newlines immediately following the opening or closing section tag are trimmed.
|
||||
This allows for civilized templates, like:
|
||||
If the opening or closing section tag are the only thing on a line, any newline
|
||||
following the tag is trimmed. This allows for civilized templates, like:
|
||||
|
||||
Favorite foods:
|
||||
{{#people}}
|
||||
|
||||
@@ -183,7 +183,8 @@ public class Mustache
|
||||
Accumulator accum;
|
||||
|
||||
int state = TEXT;
|
||||
int line = 1;
|
||||
int line = 1, column = 0;
|
||||
int tagStartColumn = -1;
|
||||
boolean skipNewline = false;
|
||||
|
||||
public Parser (Compiler compiler) {
|
||||
@@ -206,14 +207,15 @@ public class Mustache
|
||||
}
|
||||
|
||||
if (c == '\n') {
|
||||
column = 0;
|
||||
line++;
|
||||
// if we just parsed an open section or close section task, we'll skip the first
|
||||
// newline character following it, if desired; TODO: handle CR, sigh
|
||||
// skip this newline character if we're configured to do so; TODO: handle CR
|
||||
if (skipNewline) {
|
||||
skipNewline = false;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
column++;
|
||||
skipNewline = false;
|
||||
}
|
||||
|
||||
@@ -244,6 +246,7 @@ public class Mustache
|
||||
case TEXT:
|
||||
if (c == delims.start1) {
|
||||
state = MATCHING_START;
|
||||
tagStartColumn = column;
|
||||
if (delims.start2 == NO_CHAR) {
|
||||
parseChar(NO_CHAR);
|
||||
}
|
||||
@@ -276,6 +279,7 @@ public class Mustache
|
||||
// plain text and start matching a new tag from this point
|
||||
restoreStartTag(text, delims);
|
||||
accum.addTextSegment(text);
|
||||
tagStartColumn = column;
|
||||
if (delims.start2 == NO_CHAR) {
|
||||
accum.addTextSegment(text);
|
||||
state = TAG;
|
||||
@@ -313,7 +317,7 @@ public class Mustache
|
||||
}
|
||||
// process the tag between the mustaches
|
||||
accum = accum.addTagSegment(text, line);
|
||||
skipNewline = accum.skipNewline();
|
||||
skipNewline = (tagStartColumn == 1) && accum.justOpenedOrClosedCompound();
|
||||
}
|
||||
state = TEXT;
|
||||
|
||||
@@ -377,10 +381,9 @@ public class Mustache
|
||||
_compiler = compiler;
|
||||
}
|
||||
|
||||
public boolean skipNewline () {
|
||||
// return true if we just added a compound segment which means we're immediately
|
||||
// following the close section tag
|
||||
return (_segs.size() > 0 && _segs.get(_segs.size()-1) instanceof CompoundSegment);
|
||||
public boolean justOpenedOrClosedCompound () {
|
||||
// return true if we just closed a compound segment; we'll handle just opened elsewhere
|
||||
return (!_segs.isEmpty() && _segs.get(_segs.size()-1) instanceof CompoundSegment);
|
||||
}
|
||||
|
||||
public void addTextSegment (StringBuilder text) {
|
||||
@@ -400,9 +403,9 @@ public class Mustache
|
||||
case '#':
|
||||
requireNoNewlines(tag, tagLine);
|
||||
return new Accumulator(_compiler) {
|
||||
@Override public boolean skipNewline () {
|
||||
// if we just opened this section, we want to skip a newline
|
||||
return (_segs.size() == 0) || super.skipNewline();
|
||||
@Override public boolean justOpenedOrClosedCompound () {
|
||||
// if we just opened this section, we'll have no segments
|
||||
return (_segs.isEmpty()) || super.justOpenedOrClosedCompound();
|
||||
}
|
||||
@Override public Template.Segment[] finish () {
|
||||
throw new MustacheParseException(
|
||||
@@ -422,9 +425,9 @@ public class Mustache
|
||||
case '^':
|
||||
requireNoNewlines(tag, tagLine);
|
||||
return new Accumulator(_compiler) {
|
||||
@Override public boolean skipNewline () {
|
||||
// if we just opened this section, we want to skip a newline
|
||||
return (_segs.size() == 0) || super.skipNewline();
|
||||
@Override public boolean justOpenedOrClosedCompound () {
|
||||
// if we just opened this section, we'll have no segments
|
||||
return (_segs.isEmpty()) || super.justOpenedOrClosedCompound();
|
||||
}
|
||||
@Override public Template.Segment[] finish () {
|
||||
throw new MustacheParseException(
|
||||
|
||||
@@ -306,6 +306,16 @@ public class MustacheTest
|
||||
"endlist", tmpl, context("items", Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Test public void testNewlineNonSkipping () {
|
||||
// only when a section tag is by itself on a line should we absorb the newline following it
|
||||
String tmpl = "thing?: {{#thing}}yes{{/thing}}{{^thing}}no{{/thing}}\n" +
|
||||
"that's nice";
|
||||
test("thing?: yes\n" +
|
||||
"that's nice", tmpl, context("thing", true));
|
||||
test("thing?: no\n" +
|
||||
"that's nice", tmpl, context("thing", false));
|
||||
}
|
||||
|
||||
@Test public void testNestedContexts () {
|
||||
test("foo((foobar)(foobaz))", "{{name}}({{#things}}({{name}}{{thing_name}}){{/things}})",
|
||||
context("name", "foo",
|
||||
@@ -352,7 +362,7 @@ public class MustacheTest
|
||||
}
|
||||
|
||||
@Test public void testStandardsModeWithNullValuesInLoop () {
|
||||
String tmpl = "first line\n{{#nonexistent}}foo{{/nonexistent}}\nsecond line";
|
||||
String tmpl = "first line\n{{#nonexistent}}foo\n{{/nonexistent}}\nsecond line";
|
||||
String result = Mustache.compiler().standardsMode(true).compile(tmpl).execute(new Object());
|
||||
assertEquals("first line\nsecond line", result);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user