Skip to content

Commit eca33f8

Browse files
authored
Merge pull request #1126 from HubSpot/legacy-override-for-whitespace-control
Re-add note/expression whitespace control behind legacy override
2 parents f3b8912 + eafc139 commit eca33f8

File tree

9 files changed

+294
-37
lines changed

9 files changed

+294
-37
lines changed

src/main/java/com/hubspot/jinjava/LegacyOverrides.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,30 @@
33
/**
44
* This class allows Jinjava to be configured to override legacy behaviour.
55
* LegacyOverrides.NONE signifies that none of the legacy functionality will be overridden.
6+
* LegacyOverrides.ALL signifies that all new functionality will be used; avoid legacy "bugs".
67
*/
78
public class LegacyOverrides {
89
public static final LegacyOverrides NONE = new LegacyOverrides.Builder().build();
10+
public static final LegacyOverrides ALL = new LegacyOverrides.Builder()
11+
.withEvaluateMapKeys(true)
12+
.withIterateOverMapKeys(true)
13+
.withUsePyishObjectMapper(true)
14+
.withUseSnakeCasePropertyNaming(true)
15+
.withWhitespaceRequiredWithinTokens(true)
16+
.withUseNaturalOperatorPrecedence(true)
17+
.withParseWhitespaceControlStrictly(true)
18+
.withAllowAdjacentTextNodes(true)
19+
.withUseTrimmingForNotesAndExpressions(true)
20+
.build();
921
private final boolean evaluateMapKeys;
1022
private final boolean iterateOverMapKeys;
1123
private final boolean usePyishObjectMapper;
1224
private final boolean useSnakeCasePropertyNaming;
1325
private final boolean whitespaceRequiredWithinTokens;
1426
private final boolean useNaturalOperatorPrecedence;
1527
private final boolean parseWhitespaceControlStrictly;
28+
private final boolean allowAdjacentTextNodes;
29+
private final boolean useTrimmingForNotesAndExpressions;
1630

1731
private LegacyOverrides(Builder builder) {
1832
evaluateMapKeys = builder.evaluateMapKeys;
@@ -22,6 +36,8 @@ private LegacyOverrides(Builder builder) {
2236
whitespaceRequiredWithinTokens = builder.whitespaceRequiredWithinTokens;
2337
useNaturalOperatorPrecedence = builder.useNaturalOperatorPrecedence;
2438
parseWhitespaceControlStrictly = builder.parseWhitespaceControlStrictly;
39+
allowAdjacentTextNodes = builder.allowAdjacentTextNodes;
40+
useTrimmingForNotesAndExpressions = builder.useTrimmingForNotesAndExpressions;
2541
}
2642

2743
public static Builder newBuilder() {
@@ -56,6 +72,14 @@ public boolean isParseWhitespaceControlStrictly() {
5672
return parseWhitespaceControlStrictly;
5773
}
5874

75+
public boolean isAllowAdjacentTextNodes() {
76+
return allowAdjacentTextNodes;
77+
}
78+
79+
public boolean isUseTrimmingForNotesAndExpressions() {
80+
return useTrimmingForNotesAndExpressions;
81+
}
82+
5983
public static class Builder {
6084
private boolean evaluateMapKeys = false;
6185
private boolean iterateOverMapKeys = false;
@@ -64,6 +88,8 @@ public static class Builder {
6488
private boolean whitespaceRequiredWithinTokens = false;
6589
private boolean useNaturalOperatorPrecedence = false;
6690
private boolean parseWhitespaceControlStrictly = false;
91+
private boolean allowAdjacentTextNodes = false;
92+
private boolean useTrimmingForNotesAndExpressions = false;
6793

6894
private Builder() {}
6995

@@ -83,6 +109,10 @@ public static Builder from(LegacyOverrides legacyOverrides) {
83109
.withUseNaturalOperatorPrecedence(legacyOverrides.useNaturalOperatorPrecedence)
84110
.withParseWhitespaceControlStrictly(
85111
legacyOverrides.parseWhitespaceControlStrictly
112+
)
113+
.withAllowAdjacentTextNodes(legacyOverrides.allowAdjacentTextNodes)
114+
.withUseTrimmingForNotesAndExpressions(
115+
legacyOverrides.useTrimmingForNotesAndExpressions
86116
);
87117
}
88118

@@ -126,5 +156,17 @@ public Builder withParseWhitespaceControlStrictly(
126156
this.parseWhitespaceControlStrictly = parseWhitespaceControlStrictly;
127157
return this;
128158
}
159+
160+
public Builder withAllowAdjacentTextNodes(boolean allowAdjacentTextNodes) {
161+
this.allowAdjacentTextNodes = allowAdjacentTextNodes;
162+
return this;
163+
}
164+
165+
public Builder withUseTrimmingForNotesAndExpressions(
166+
boolean useTrimmingForNotesAndExpressions
167+
) {
168+
this.useTrimmingForNotesAndExpressions = useTrimmingForNotesAndExpressions;
169+
return this;
170+
}
129171
}
130172
}

src/main/java/com/hubspot/jinjava/tree/TreeParser.java

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.google.common.collect.Iterators;
1919
import com.google.common.collect.PeekingIterator;
20+
import com.hubspot.jinjava.JinjavaConfig;
2021
import com.hubspot.jinjava.interpret.DisabledException;
2122
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
2223
import com.hubspot.jinjava.interpret.MissingEndTagException;
@@ -62,9 +63,15 @@ public Node buildTree() {
6263
Node node = nextNode();
6364

6465
if (node != null) {
65-
if (node instanceof TextNode && getLastSibling() instanceof TextNode) {
66+
if (
67+
node instanceof TextNode &&
68+
getLastSibling() instanceof TextNode &&
69+
!interpreter.getConfig().getLegacyOverrides().isAllowAdjacentTextNodes()
70+
) {
6671
// merge adjacent text nodes so whitespace control properly applies
67-
getLastSibling().getMaster().mergeImageAndContent(node.getMaster());
72+
((TextToken) getLastSibling().getMaster()).mergeImageAndContent(
73+
(TextToken) node.getMaster()
74+
);
6875
} else {
6976
parent.getChildren().add(node);
7077
}
@@ -96,6 +103,12 @@ public Node buildTree() {
96103

97104
private Node nextNode() {
98105
Token token = scanner.next();
106+
if (token.isLeftTrim() && isTrimmingEnabledForToken(token, interpreter.getConfig())) {
107+
final Node lastSibling = getLastSibling();
108+
if (lastSibling instanceof TextNode) {
109+
lastSibling.getMaster().setRightTrim(true);
110+
}
111+
}
99112

100113
if (token.getType() == symbols.getFixed()) {
101114
if (token instanceof UnclosedToken) {
@@ -170,7 +183,11 @@ private Node text(TextToken textToken) {
170183
final Node lastSibling = getLastSibling();
171184

172185
// if last sibling was a tag and has rightTrimAfterEnd, strip whitespace
173-
if (lastSibling instanceof TagNode && isRightTrim((TagNode) lastSibling)) {
186+
if (
187+
lastSibling != null &&
188+
isRightTrim(lastSibling) &&
189+
isTrimmingEnabledForToken(lastSibling.getMaster(), interpreter.getConfig())
190+
) {
174191
textToken.setLeftTrim(true);
175192
}
176193

@@ -186,18 +203,21 @@ private Node text(TextToken textToken) {
186203
return n;
187204
}
188205

189-
private boolean isRightTrim(TagNode lastSibling) {
190-
return (
191-
lastSibling.getEndName() == null ||
192-
(
193-
lastSibling.getTag() instanceof FlexibleTag &&
194-
!((FlexibleTag) lastSibling.getTag()).hasEndTag(
195-
(TagToken) lastSibling.getMaster()
196-
)
206+
private boolean isRightTrim(Node lastSibling) {
207+
if (lastSibling instanceof TagNode) {
208+
return (
209+
((TagNode) lastSibling).getEndName() == null ||
210+
(
211+
((TagNode) lastSibling).getTag() instanceof FlexibleTag &&
212+
!((FlexibleTag) ((TagNode) lastSibling).getTag()).hasEndTag(
213+
(TagToken) lastSibling.getMaster()
214+
)
215+
)
197216
)
198-
)
199-
? lastSibling.getMaster().isRightTrim()
200-
: lastSibling.getMaster().isRightTrimAfterEnd();
217+
? lastSibling.getMaster().isRightTrim()
218+
: lastSibling.getMaster().isRightTrimAfterEnd();
219+
}
220+
return lastSibling.getMaster().isRightTrim();
201221
}
202222

203223
private Node expression(ExpressionToken expressionToken) {
@@ -242,14 +262,6 @@ private Node tag(TagToken tagToken) {
242262
if (tag instanceof EndTag) {
243263
endTag(tag, tagToken);
244264
return null;
245-
} else {
246-
// if a tag has left trim, mark the last sibling to trim right whitespace
247-
if (tagToken.isLeftTrim()) {
248-
final Node lastSibling = getLastSibling();
249-
if (lastSibling instanceof TextNode) {
250-
lastSibling.getMaster().setRightTrim(true);
251-
}
252-
}
253265
}
254266

255267
TagNode node = new TagNode(tag, tagToken, symbols);
@@ -268,16 +280,6 @@ private Node tag(TagToken tagToken) {
268280
}
269281

270282
private void endTag(Tag tag, TagToken tagToken) {
271-
final Node lastSibling = getLastSibling();
272-
273-
if (
274-
parent instanceof TagNode &&
275-
tagToken.isLeftTrim() &&
276-
lastSibling instanceof TextNode
277-
) {
278-
lastSibling.getMaster().setRightTrim(true);
279-
}
280-
281283
if (parent.getMaster() != null) { // root node
282284
parent.getMaster().setRightTrimAfterEnd(tagToken.isRightTrim());
283285
}
@@ -318,4 +320,11 @@ private void endTag(Tag tag, TagToken tagToken) {
318320
);
319321
}
320322
}
323+
324+
private boolean isTrimmingEnabledForToken(Token token, JinjavaConfig jinjavaConfig) {
325+
if (token instanceof TagToken || token instanceof TextToken) {
326+
return true;
327+
}
328+
return jinjavaConfig.getLegacyOverrides().isUseTrimmingForNotesAndExpressions();
329+
}
321330
}

src/main/java/com/hubspot/jinjava/tree/output/OutputList.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
44
import com.hubspot.jinjava.interpret.OutputTooBigException;
55
import com.hubspot.jinjava.interpret.TemplateError;
6+
import com.hubspot.jinjava.tree.parse.TokenScannerSymbols;
67
import com.hubspot.jinjava.util.LengthLimitingStringBuilder;
78
import java.util.LinkedList;
89
import java.util.List;
910

1011
public class OutputList {
12+
public static final String PREVENT_ACCIDENTAL_EXPRESSIONS =
13+
"PREVENT_ACCIDENTAL_EXPRESSIONS";
1114
private final List<OutputNode> nodes = new LinkedList<>();
1215
private final List<BlockPlaceholderOutputNode> blocks = new LinkedList<>();
1316
private final long maxOutputSize;
@@ -48,6 +51,72 @@ public List<BlockPlaceholderOutputNode> getBlocks() {
4851
public String getValue() {
4952
LengthLimitingStringBuilder val = new LengthLimitingStringBuilder(maxOutputSize);
5053

54+
return JinjavaInterpreter
55+
.getCurrentMaybe()
56+
.map(JinjavaInterpreter::getConfig)
57+
.filter(
58+
config ->
59+
config
60+
.getFeatures()
61+
.getActivationStrategy(PREVENT_ACCIDENTAL_EXPRESSIONS)
62+
.isActive(null)
63+
)
64+
.map(
65+
config -> joinNodesWithoutAddingExpressions(val, config.getTokenScannerSymbols())
66+
)
67+
.orElseGet(() -> joinNodes(val));
68+
}
69+
70+
private String joinNodesWithoutAddingExpressions(
71+
LengthLimitingStringBuilder val,
72+
TokenScannerSymbols tokenScannerSymbols
73+
) {
74+
String separator = getWhitespaceSeparator(tokenScannerSymbols);
75+
String prev = null;
76+
String cur;
77+
for (OutputNode node : nodes) {
78+
try {
79+
cur = node.getValue();
80+
if (
81+
prev != null &&
82+
prev.length() > 0 &&
83+
prev.charAt(prev.length() - 1) == tokenScannerSymbols.getExprStartChar()
84+
) {
85+
if (
86+
cur.length() > 0 &&
87+
TokenScannerSymbols.isNoteTagOrExprChar(tokenScannerSymbols, cur.charAt(0))
88+
) {
89+
val.append(separator);
90+
}
91+
}
92+
prev = cur;
93+
val.append(node.getValue());
94+
} catch (OutputTooBigException e) {
95+
JinjavaInterpreter
96+
.getCurrent()
97+
.addError(TemplateError.fromOutputTooBigException(e));
98+
return val.toString();
99+
}
100+
}
101+
102+
return val.toString();
103+
}
104+
105+
private static String getWhitespaceSeparator(TokenScannerSymbols tokenScannerSymbols) {
106+
@SuppressWarnings("StringBufferReplaceableByString")
107+
String separator = new StringBuilder()
108+
.append('\n')
109+
.append(tokenScannerSymbols.getPrefixChar())
110+
.append(tokenScannerSymbols.getNoteChar())
111+
.append(tokenScannerSymbols.getTrimChar())
112+
.append(' ')
113+
.append(tokenScannerSymbols.getNoteChar())
114+
.append(tokenScannerSymbols.getExprEndChar())
115+
.toString();
116+
return separator;
117+
}
118+
119+
private String joinNodes(LengthLimitingStringBuilder val) {
51120
for (OutputNode node : nodes) {
52121
try {
53122
val.append(node.getValue());

src/main/java/com/hubspot/jinjava/tree/parse/NoteToken.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
**********************************************************************/
1616
package com.hubspot.jinjava.tree.parse;
1717

18+
import org.apache.commons.lang3.StringUtils;
19+
1820
public class NoteToken extends Token {
1921
private static final long serialVersionUID = -3859011447900311329L;
2022

@@ -37,6 +39,9 @@ public int getType() {
3739
*/
3840
@Override
3941
protected void parse() {
42+
if (StringUtils.isNotEmpty(image)) {
43+
handleTrim(image.substring(2, image.length() - 2));
44+
}
4045
content = "";
4146
}
4247

src/main/java/com/hubspot/jinjava/tree/parse/TextToken.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ public TextToken(
2929
super(image, lineNumber, startPosition, symbols);
3030
}
3131

32+
public void mergeImageAndContent(TextToken otherToken) {
33+
String thisOutput = output();
34+
String otherTokenOutput = otherToken.output();
35+
this.image = thisOutput + otherTokenOutput;
36+
this.content = image;
37+
}
38+
3239
@Override
3340
public int getType() {
3441
return getSymbols().getFixed();

src/main/java/com/hubspot/jinjava/tree/parse/Token.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,6 @@ public String getImage() {
5353
return image;
5454
}
5555

56-
public void mergeImageAndContent(Token otherToken) {
57-
this.image = image + otherToken.image;
58-
this.content = content + otherToken.content;
59-
}
60-
6156
public int getLineNumber() {
6257
return lineNumber;
6358
}

src/main/java/com/hubspot/jinjava/tree/parse/TokenScannerSymbols.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,10 @@ public String getClosingComment() {
122122
}
123123
return closingComment;
124124
}
125+
126+
public static boolean isNoteTagOrExprChar(TokenScannerSymbols symbols, char c) {
127+
return (
128+
c == symbols.getNote() || c == symbols.getTag() || c == symbols.getExprStartChar()
129+
);
130+
}
125131
}

0 commit comments

Comments
 (0)