Skip to content

Commit df3e3d8

Browse files
authored
Merge pull request #1325 from HubSpot/jasmith_hubspot/backport-1305-1306-1311-2.8.x
feat: backport configurable delimiters, backslash handling & keepTrailingNewline to 2.8.x
2 parents 5afaece + 8511a71 commit df3e3d8

14 files changed

Lines changed: 1973 additions & 8 deletions

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ public RenderResult renderForResult(
250250
.getInterpreterFactory()
251251
.newInstance(this, context, renderConfig);
252252
try {
253-
String result = interpreter.render(template);
253+
String result = stripTrailingNewlineIfNeeded(interpreter.render(template));
254254
return new RenderResult(
255255
result,
256256
interpreter.getContext(),
@@ -293,6 +293,18 @@ public RenderResult renderForResult(
293293
}
294294
}
295295

296+
/**
297+
* Strips a single trailing newline from the rendered output when
298+
* {@code keepTrailingNewline} is {@code false} in {@link JinjavaConfig},
299+
* matching Python Jinja2's default behaviour.
300+
*/
301+
private String stripTrailingNewlineIfNeeded(String output) {
302+
if (!globalConfig.isKeepTrailingNewline() && output.endsWith("\n")) {
303+
return output.substring(0, output.length() - 1);
304+
}
305+
return output;
306+
}
307+
296308
/**
297309
* Creates a new interpreter instance using the global context and global config
298310
*

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public class JinjavaConfig {
8989
private final LegacyOverrides legacyOverrides;
9090
private final boolean enablePreciseDivideFilter;
9191
private final boolean enableFilterChainOptimization;
92+
private final boolean keepTrailingNewline;
9293
private final ObjectMapper objectMapper;
9394

9495
private final Features features;
@@ -153,6 +154,10 @@ private JinjavaConfig(Builder builder) {
153154
dateTimeProvider = builder.dateTimeProvider;
154155
enablePreciseDivideFilter = builder.enablePreciseDivideFilter;
155156
enableFilterChainOptimization = builder.enableFilterChainOptimization;
157+
keepTrailingNewline =
158+
builder.keepTrailingNewline != null
159+
? builder.keepTrailingNewline
160+
: legacyOverrides.getDefaultKeepTrailingNewlineBehavior();
156161
objectMapper = setupObjectMapper(builder.objectMapper);
157162
objectUnwrapper = builder.objectUnwrapper;
158163
processors = builder.processors;
@@ -313,6 +318,17 @@ public boolean isEnableFilterChainOptimization() {
313318
return enableFilterChainOptimization;
314319
}
315320

321+
/**
322+
* When {@code false}, a single trailing newline is stripped from the rendered output,
323+
* matching Python Jinja2's default.
324+
* When {@code true}, the trailing newline of the rendered output is preserved —
325+
* matching Jinjava's historical behaviour.
326+
* Defaults to {@link LegacyOverrides#getDefaultKeepTrailingNewlineBehavior()}.
327+
*/
328+
public boolean isKeepTrailingNewline() {
329+
return keepTrailingNewline;
330+
}
331+
316332
public DateTimeProvider getDateTimeProvider() {
317333
return dateTimeProvider;
318334
}
@@ -356,6 +372,7 @@ public static class Builder {
356372
private LegacyOverrides legacyOverrides = LegacyOverrides.NONE;
357373
private boolean enablePreciseDivideFilter = false;
358374
private boolean enableFilterChainOptimization = false;
375+
private Boolean keepTrailingNewline = null;
359376
private ObjectMapper objectMapper = null;
360377

361378
private ObjectUnwrapper objectUnwrapper = new JinjavaObjectUnwrapper();
@@ -534,6 +551,11 @@ public Builder withEnableFilterChainOptimization(
534551
return this;
535552
}
536553

554+
public Builder withKeepTrailingNewline(boolean keepTrailingNewline) {
555+
this.keepTrailingNewline = keepTrailingNewline;
556+
return this;
557+
}
558+
537559
public Builder withObjectMapper(ObjectMapper objectMapper) {
538560
this.objectMapper = objectMapper;
539561
return this;

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public class LegacyOverrides {
1919
.withAllowAdjacentTextNodes(true)
2020
.withUseTrimmingForNotesAndExpressions(true)
2121
.withKeepNullableLoopValues(true)
22+
.withHandleBackslashInQuotesOnly(true)
23+
.withDefaultKeepTrailingNewlineBehavior(false)
2224
.build();
2325
private final boolean evaluateMapKeys;
2426
private final boolean iterateOverMapKeys;
@@ -30,6 +32,8 @@ public class LegacyOverrides {
3032
private final boolean allowAdjacentTextNodes;
3133
private final boolean useTrimmingForNotesAndExpressions;
3234
private final boolean keepNullableLoopValues;
35+
private final boolean handleBackslashInQuotesOnly;
36+
private final boolean defaultKeepTrailingNewlineBehavior;
3337

3438
private LegacyOverrides(Builder builder) {
3539
evaluateMapKeys = builder.evaluateMapKeys;
@@ -42,6 +46,8 @@ private LegacyOverrides(Builder builder) {
4246
allowAdjacentTextNodes = builder.allowAdjacentTextNodes;
4347
useTrimmingForNotesAndExpressions = builder.useTrimmingForNotesAndExpressions;
4448
keepNullableLoopValues = builder.keepNullableLoopValues;
49+
handleBackslashInQuotesOnly = builder.handleBackslashInQuotesOnly;
50+
defaultKeepTrailingNewlineBehavior = builder.defaultKeepTrailingNewlineBehavior;
4551
}
4652

4753
public static Builder newBuilder() {
@@ -88,6 +94,19 @@ public boolean isKeepNullableLoopValues() {
8894
return keepNullableLoopValues;
8995
}
9096

97+
public boolean isHandleBackslashInQuotesOnly() {
98+
return handleBackslashInQuotesOnly;
99+
}
100+
101+
/**
102+
* The default value of {@link JinjavaConfig#isKeepTrailingNewline()}.
103+
* {@code true} preserves Jinjava's historical behaviour of keeping the trailing newline;
104+
* {@code false} matches Python Jinja2's default of stripping it.
105+
*/
106+
public boolean getDefaultKeepTrailingNewlineBehavior() {
107+
return defaultKeepTrailingNewlineBehavior;
108+
}
109+
91110
public static class Builder {
92111

93112
private boolean evaluateMapKeys = false;
@@ -100,6 +119,8 @@ public static class Builder {
100119
private boolean allowAdjacentTextNodes = false;
101120
private boolean useTrimmingForNotesAndExpressions = false;
102121
private boolean keepNullableLoopValues = false;
122+
private boolean handleBackslashInQuotesOnly = false;
123+
private boolean defaultKeepTrailingNewlineBehavior = true;
103124

104125
private Builder() {}
105126

@@ -123,6 +144,10 @@ public static Builder from(LegacyOverrides legacyOverrides) {
123144
.withAllowAdjacentTextNodes(legacyOverrides.allowAdjacentTextNodes)
124145
.withUseTrimmingForNotesAndExpressions(
125146
legacyOverrides.useTrimmingForNotesAndExpressions
147+
)
148+
.withHandleBackslashInQuotesOnly(legacyOverrides.handleBackslashInQuotesOnly)
149+
.withDefaultKeepTrailingNewlineBehavior(
150+
legacyOverrides.defaultKeepTrailingNewlineBehavior
126151
);
127152
}
128153

@@ -187,5 +212,17 @@ public Builder withKeepNullableLoopValues(boolean keepNullableLoopValues) {
187212
this.keepNullableLoopValues = keepNullableLoopValues;
188213
return this;
189214
}
215+
216+
public Builder withHandleBackslashInQuotesOnly(boolean handleBackslashInQuotesOnly) {
217+
this.handleBackslashInQuotesOnly = handleBackslashInQuotesOnly;
218+
return this;
219+
}
220+
221+
public Builder withDefaultKeepTrailingNewlineBehavior(
222+
boolean defaultKeepTrailingNewlineBehavior
223+
) {
224+
this.defaultKeepTrailingNewlineBehavior = defaultKeepTrailingNewlineBehavior;
225+
return this;
226+
}
190227
}
191228
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
import com.hubspot.jinjava.lib.tag.FlexibleTag;
3333
import com.hubspot.jinjava.lib.tag.Tag;
3434
import com.hubspot.jinjava.tree.parse.ExpressionToken;
35+
import com.hubspot.jinjava.tree.parse.StringTokenScanner;
3536
import com.hubspot.jinjava.tree.parse.TagToken;
3637
import com.hubspot.jinjava.tree.parse.TextToken;
3738
import com.hubspot.jinjava.tree.parse.Token;
3839
import com.hubspot.jinjava.tree.parse.TokenScanner;
3940
import com.hubspot.jinjava.tree.parse.TokenScannerSymbols;
4041
import com.hubspot.jinjava.tree.parse.UnclosedToken;
4142
import com.hubspot.jinjava.tree.parse.WhitespaceControlParser;
43+
import java.util.Iterator;
4244
import org.apache.commons.lang3.StringUtils;
4345

4446
public class TreeParser {
@@ -52,7 +54,7 @@ public class TreeParser {
5254

5355
public TreeParser(JinjavaInterpreter interpreter, String input) {
5456
this.scanner =
55-
Iterators.peekingIterator(new TokenScanner(input, interpreter.getConfig()));
57+
Iterators.peekingIterator(createScanner(input, interpreter.getConfig()));
5658
this.interpreter = interpreter;
5759
this.symbols = interpreter.getConfig().getTokenScannerSymbols();
5860
this.whitespaceControlParser =
@@ -104,6 +106,13 @@ public Node buildTree() {
104106
return root;
105107
}
106108

109+
private static Iterator<Token> createScanner(String input, JinjavaConfig config) {
110+
if (config.getTokenScannerSymbols().isStringBased()) {
111+
return new StringTokenScanner(input, config);
112+
}
113+
return new TokenScanner(input, config);
114+
}
115+
107116
/**
108117
* @return null if EOF or error
109118
*/

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ public int getType() {
5454

5555
@Override
5656
protected void parse() {
57-
this.expr = WhitespaceUtils.unwrap(image, "{{", "}}");
57+
// Use the symbols-derived delimiter strings instead of the hardcoded "{{" / "}}"
58+
// so that custom delimiters (e.g. "\VAR{" / "}") are stripped correctly.
59+
this.expr =
60+
WhitespaceUtils.unwrap(
61+
image,
62+
getSymbols().getExpressionStart(),
63+
getSymbols().getExpressionEnd()
64+
);
5865
this.expr = handleTrim(expr);
5966
this.expr = StringUtils.trimToEmpty(this.expr);
6067
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ public int getType() {
4848
*/
4949
@Override
5050
protected void parse() {
51-
if (image.length() > 4) { // {# #}
52-
handleTrim(image.substring(2, image.length() - 2));
51+
int startLen = getSymbols().getCommentStartLength();
52+
int endLen = getSymbols().getCommentEndLength();
53+
54+
if (image.length() > startLen + endLen) {
55+
handleTrim(image.substring(startLen, image.length() - endLen));
5356
}
5457
content = "";
5558
}

0 commit comments

Comments
 (0)