diff --git a/src/main/java/com/hubspot/jinjava/tree/TreeParser.java b/src/main/java/com/hubspot/jinjava/tree/TreeParser.java index bec51dc0e..6ff286eda 100644 --- a/src/main/java/com/hubspot/jinjava/tree/TreeParser.java +++ b/src/main/java/com/hubspot/jinjava/tree/TreeParser.java @@ -57,6 +57,7 @@ public Node buildTree() { while (scanner.hasNext()) { Node node = nextNode(); + if (node != null) { parent.getChildren().add(node); } @@ -64,7 +65,9 @@ public Node buildTree() { if (parent != root) { interpreter.addError(TemplateError.fromException( - new MissingEndTagException(((TagNode) parent).getEndName(), parent.getMaster().getImage(), parent.getLineNumber()))); + new MissingEndTagException(((TagNode) parent).getEndName(), + parent.getMaster().getImage(), + parent.getLineNumber()))); } return root; @@ -73,29 +76,37 @@ public Node buildTree() { /** * @return null if EOF or error */ + private Node nextNode() { Token token = scanner.next(); switch (token.getType()) { - case TOKEN_FIXED: - return text((TextToken) token); + case TOKEN_FIXED: + return text((TextToken) token); - case TOKEN_EXPR_START: - return expression((ExpressionToken) token); + case TOKEN_EXPR_START: + return expression((ExpressionToken) token); - case TOKEN_TAG: - return tag((TagToken) token); + case TOKEN_TAG: + return tag((TagToken) token); - case TOKEN_NOTE: - break; + case TOKEN_NOTE: + break; - default: - interpreter.addError(TemplateError.fromException(new UnexpectedTokenException(token.getImage(), token.getLineNumber()))); + default: + interpreter.addError(TemplateError.fromException(new UnexpectedTokenException(token.getImage(), + token.getLineNumber()))); } - return null; } + private Node getLastSibling() { + if (parent == null || parent.getChildren().isEmpty()) { + return null; + } + return parent.getChildren().getLast(); + } + private Node text(TextToken textToken) { if (interpreter.getConfig().isLstripBlocks()) { if (scanner.hasNext() && scanner.peek().getType() == TOKEN_TAG) { @@ -103,6 +114,22 @@ private Node text(TextToken textToken) { } } + final Node lastSibling = getLastSibling(); + + // if last sibling was a tag and has rightTrimAfterEnd, strip whitespace + if (lastSibling != null + && lastSibling instanceof TagNode + && lastSibling.getMaster().isRightTrimAfterEnd()) { + textToken.setLeftTrim(true); + } + + // for first TextNode child of TagNode where rightTrim is enabled, mark it for left trim + if (parent instanceof TagNode + && lastSibling == null + && parent.getMaster().isRightTrim()) { + textToken.setLeftTrim(true); + } + TextNode n = new TextNode(textToken); n.setParent(parent); return n; @@ -124,6 +151,14 @@ private Node tag(TagToken tagToken) { if (tag instanceof EndTag) { endTag(tag, tagToken); return null; + } else { + // if a tag has left trim, mark the last sibling to trim right whitespace + if (tagToken.isLeftTrim()) { + final Node lastSibling = getLastSibling(); + if (lastSibling != null && lastSibling instanceof TextNode) { + lastSibling.getMaster().setRightTrim(true); + } + } } TagNode node = new TagNode(tag, tagToken); @@ -139,6 +174,18 @@ private Node tag(TagToken tagToken) { } private void endTag(Tag tag, TagToken tagToken) { + + final Node lastSibling = getLastSibling(); + + if (parent instanceof TagNode + && tagToken.isLeftTrim() + && lastSibling != null + && lastSibling instanceof TextNode) { + lastSibling.getMaster().setRightTrim(true); + } + + parent.getMaster().setRightTrimAfterEnd(tagToken.isRightTrim()); + while (!(parent instanceof RootNode)) { TagNode parentTag = (TagNode) parent; parent = parent.getParent(); @@ -147,9 +194,10 @@ private void endTag(Tag tag, TagToken tagToken) { break; } else { interpreter.addError(TemplateError.fromException( - new TemplateSyntaxException(tagToken.getImage(), "Mismatched end tag, expected: " + parentTag.getEndName(), tagToken.getLineNumber()))); + new TemplateSyntaxException(tagToken.getImage(), + "Mismatched end tag, expected: " + parentTag.getEndName(), + tagToken.getLineNumber()))); } } } - } diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/TextToken.java b/src/main/java/com/hubspot/jinjava/tree/parse/TextToken.java index 826ec0ac3..f2f64dac3 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/TextToken.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/TextToken.java @@ -46,6 +46,14 @@ public String trim() { } public String output() { + + if (isLeftTrim() && isRightTrim()) { + return trim(); + } else if (isLeftTrim()) { + return StringUtils.stripStart(content, null); + } else if (isRightTrim()) { + return StringUtils.stripEnd(content, null); + } return content; } diff --git a/src/main/java/com/hubspot/jinjava/tree/parse/Token.java b/src/main/java/com/hubspot/jinjava/tree/parse/Token.java index cf889eaa1..a8c4a7796 100644 --- a/src/main/java/com/hubspot/jinjava/tree/parse/Token.java +++ b/src/main/java/com/hubspot/jinjava/tree/parse/Token.java @@ -36,6 +36,7 @@ public abstract class Token implements Serializable { private boolean leftTrim; private boolean rightTrim; + private boolean rightTrimAfterEnd; public Token(String image, int lineNumber) { this.image = image; @@ -59,6 +60,10 @@ public boolean isRightTrim() { return rightTrim; } + public boolean isRightTrimAfterEnd() { + return rightTrimAfterEnd; + } + public void setLeftTrim(boolean leftTrim) { this.leftTrim = leftTrim; } @@ -67,6 +72,10 @@ public void setRightTrim(boolean rightTrim) { this.rightTrim = rightTrim; } + public void setRightTrimAfterEnd(boolean rightTrimAfterEnd) { + this.rightTrimAfterEnd = rightTrimAfterEnd; + } + @Override public String toString() { return image; diff --git a/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java b/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java index b713d8d68..bf082faf7 100644 --- a/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java +++ b/src/test/java/com/hubspot/jinjava/tree/TreeParserTest.java @@ -28,6 +28,55 @@ public void parseHtmlWithCommentLines() { assertThat(interpreter.getErrors()).isEmpty(); } + @Test + public void itStripsRightWhiteSpace() throws Exception { + String expression = "{% for foo in [1,2,3] -%} \n .{{ foo }}\n{% endfor %}"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo(".1\n.2\n.3\n"); + } + + @Test + public void itStripsLeftWhiteSpace() throws Exception { + String expression = "{% for foo in [1,2,3] %}\n{{ foo }}. \n {%- endfor %}"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo("\n1.\n2.\n3."); + } + + @Test + public void itStripsLeftAndRightWhiteSpace() throws Exception { + String expression = "{% for foo in [1,2,3] -%} \n .{{ foo }}. \n {%- endfor %}"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo(".1..2..3."); + } + + @Test + public void itPreservesInnerWhiteSpace() throws Exception { + String expression = "{% for foo in [1,2,3] -%}\nL{% if true %}\n{{ foo }}\n{% endif %}R\n{%- endfor %}"; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo("L\n1\nRL\n2\nRL\n3\nR"); + } + + @Test + public void itStripsLeftWhiteSpaceBeforeTag() throws Exception { + String expression = ".\n {%- for foo in [1,2,3] %} {{ foo }} {% endfor %} \n."; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo(". 1 2 3 \n."); + } + + @Test + public void itStripsRightWhiteSpaceAfterTag() throws Exception { + String expression = ".\n {% for foo in [1,2,3] %} {{ foo }} {% endfor -%} \n."; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo(".\n 1 2 3 ."); + } + + @Test + public void itStripsAllOuterWhiteSpace() throws Exception { + String expression = ".\n {%- for foo in [1,2,3] -%} {{ foo }} {%- endfor -%} \n."; + final Node tree = new TreeParser(interpreter, expression).buildTree(); + assertThat(interpreter.render(tree)).isEqualTo(".123."); + } + @Test public void trimAndLstripBlocks() { interpreter = new Jinjava(JinjavaConfig.newBuilder().withLstripBlocks(true).withTrimBlocks(true).build()).newInterpreter();