Skip to content

Commit bc94cdc

Browse files
committed
Add new option keepTrailingNewline to Config. Default is false, same as Python jinja2.
1 parent caecc9c commit bc94cdc

9 files changed

Lines changed: 133 additions & 3 deletions

File tree

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 Config},
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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,17 @@ public boolean isEnableFilterChainOptimization() {
216216
return false;
217217
}
218218

219+
/**
220+
* When {@code false} (default), a single trailing newline is stripped from the rendered
221+
* output, matching Python Jinja2's default.
222+
* When {@code true}, the trailing newline of
223+
* the rendered output is preserved — matching Jinjava's historical behaviour.
224+
*/
225+
@Value.Default
226+
public boolean isKeepTrailingNewline() {
227+
return false;
228+
}
229+
219230
@Value.Default
220231
public ObjectMapper getObjectMapper() {
221232
ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jdk8Module());
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.hubspot.jinjava;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.google.common.collect.ImmutableMap;
6+
import com.hubspot.jinjava.Jinjava;
7+
import java.util.HashMap;
8+
import org.junit.Test;
9+
10+
public class TrailingNewlineTest {
11+
12+
private static final String TEMPLATE_WITH_TRAILING_NEWLINE = "hello\n";
13+
private static final String TEMPLATE_WITHOUT_TRAILING_NEWLINE = "hello";
14+
private static final String TEMPLATE_MULTIPLE_TRAILING_NEWLINES = "hello\n\n";
15+
16+
// ── keepTrailingNewline=true (legacy default: preserve \n) ─────────────────
17+
18+
@Test
19+
public void itKeepsTrailingNewlineIsTrue() {
20+
Jinjava jinjava = new Jinjava(
21+
JinjavaConfig.newBuilder().withKeepTrailingNewline(true).build()
22+
);
23+
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
24+
.isEqualTo("hello\n");
25+
}
26+
27+
@Test
28+
public void itStripsTrailingNewlineDefault() {
29+
// Defaults keepTrailingNewline=false (matching Python behaviour)
30+
Jinjava jinjava = new Jinjava();
31+
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
32+
.isEqualTo("hello");
33+
}
34+
35+
// ── keepTrailingNewline=false (Python-compatible: strip trailing \n) ────────
36+
37+
@Test
38+
public void itStripsTrailingNewlineIsFalse() {
39+
Jinjava jinjava = new Jinjava(
40+
JinjavaConfig.newBuilder().withKeepTrailingNewline(false).build()
41+
);
42+
43+
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
44+
.isEqualTo("hello");
45+
}
46+
47+
// ── Edge cases ──────────────────────────────────────────────────────────────
48+
49+
@Test
50+
public void itDoesNotAffectOutputWithNoTrailingNewline() {
51+
Jinjava jinjava = new Jinjava(
52+
JinjavaConfig.newBuilder().withKeepTrailingNewline(true).build()
53+
);
54+
55+
assertThat(jinjava.render(TEMPLATE_WITHOUT_TRAILING_NEWLINE, new HashMap<>()))
56+
.isEqualTo("hello");
57+
}
58+
59+
@Test
60+
public void itStripsOnlyOneTrailingNewlineNotMultiple() {
61+
// Python only strips a single trailing newline, not all of them.
62+
Jinjava jinjava = new Jinjava();
63+
assertThat(jinjava.render(TEMPLATE_MULTIPLE_TRAILING_NEWLINES, new HashMap<>()))
64+
.isEqualTo("hello\n");
65+
}
66+
67+
@Test
68+
public void itStripsTrailingNewlineFromRenderedExpressions() {
69+
Jinjava jinjava = new Jinjava();
70+
assertThat(jinjava.render("{{ greeting }}\n", ImmutableMap.of("greeting", "hello")))
71+
.isEqualTo("hello");
72+
}
73+
}

src/test/java/com/hubspot/jinjava/el/ext/AstFilterChainTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ public class AstFilterChainTest {
2727
public void setup() {
2828
jinjava =
2929
new Jinjava(
30-
BaseJinjavaTest.newConfigBuilder().withEnableFilterChainOptimization(true).build()
30+
BaseJinjavaTest
31+
.newConfigBuilder()
32+
.withEnableFilterChainOptimization(true)
33+
.withKeepTrailingNewline(true)
34+
.build()
3135
);
3236

3337
context = new HashMap<>();
@@ -123,6 +127,7 @@ public void itSkipsDisabledFilterAndContinuesChain() {
123127
BaseJinjavaTest
124128
.newConfigBuilder()
125129
.withEnableFilterChainOptimization(true)
130+
.withKeepTrailingNewline(true)
126131
.withDisabled(disabled)
127132
.build()
128133
);

src/test/java/com/hubspot/jinjava/interpret/LegacyOperatorPrecedenceTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ public void setUp() throws Exception {
2121
new Jinjava(
2222
BaseJinjavaTest
2323
.newConfigBuilder()
24+
.withKeepTrailingNewline(true)
2425
.withLegacyOverrides(LegacyOverrides.NONE)
2526
.build()
2627
);
2728
modern =
2829
new Jinjava(
2930
BaseJinjavaTest
3031
.newConfigBuilder()
32+
.withKeepTrailingNewline(true)
3133
.withLegacyOverrides(
3234
LegacyOverrides.newBuilder().withUseNaturalOperatorPrecedence(true).build()
3335
)

src/test/java/com/hubspot/jinjava/interpret/LegacyWhitespaceControlParsingTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ public void setUp() throws Exception {
2121
new Jinjava(
2222
BaseJinjavaTest
2323
.newConfigBuilder()
24+
.withKeepTrailingNewline(true)
2425
.withLegacyOverrides(LegacyOverrides.NONE)
2526
.build()
2627
);
2728
modern =
2829
new Jinjava(
2930
BaseJinjavaTest
3031
.newConfigBuilder()
32+
.withKeepTrailingNewline(true)
3133
.withLegacyOverrides(
3234
LegacyOverrides.newBuilder().withParseWhitespaceControlStrictly(true).build()
3335
)

src/test/java/com/hubspot/jinjava/lib/filter/SliceFilterTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,27 @@
77
import com.google.common.collect.Lists;
88
import com.google.common.io.Resources;
99
import com.hubspot.jinjava.BaseJinjavaTest;
10+
import com.hubspot.jinjava.Jinjava;
1011
import com.hubspot.jinjava.interpret.RenderResult;
1112
import java.nio.charset.StandardCharsets;
1213
import java.util.ArrayList;
1314
import java.util.List;
1415
import java.util.Random;
1516
import org.jsoup.Jsoup;
1617
import org.jsoup.nodes.Document;
18+
import org.junit.Before;
1719
import org.junit.Test;
1820

1921
public class SliceFilterTest extends BaseJinjavaTest {
2022

23+
@Before
24+
public void setup() {
25+
jinjava =
26+
new Jinjava(
27+
BaseJinjavaTest.newConfigBuilder().withKeepTrailingNewline(true).build()
28+
);
29+
}
30+
2131
@Test
2232
public void itSlicesLists() throws Exception {
2333
Document dom = Jsoup.parseBodyFragment(

src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import com.google.common.io.Resources;
1212
import com.hubspot.jinjava.BaseInterpretingTest;
1313
import com.hubspot.jinjava.BaseJinjavaTest;
14+
import com.hubspot.jinjava.Jinjava;
15+
import com.hubspot.jinjava.JinjavaConfig;
1416
import com.hubspot.jinjava.LegacyOverrides;
1517
import com.hubspot.jinjava.interpret.InterpretException;
1618
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
@@ -40,6 +42,11 @@ public class ForTagTest extends BaseInterpretingTest {
4042
@Override
4143
public void baseSetup() {
4244
super.baseSetup();
45+
46+
jinjava =
47+
new Jinjava(
48+
BaseJinjavaTest.newConfigBuilder().withKeepTrailingNewline(true).build()
49+
);
4350
tag = new ForTag();
4451

4552
try {

src/test/java/com/hubspot/jinjava/tree/parse/StringTokenScannerSymbolsTest.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ public void itRespectsTrimBlocksWithAngleSymbols() {
256256
.newConfigBuilder()
257257
.withTokenScannerSymbols(ANGLE_SYMBOLS)
258258
.withTrimBlocks(true)
259+
.withKeepTrailingNewline(true)
259260
.build()
260261
);
261262
// Without trimBlocks the newline after <% if show %> would appear in output.
@@ -274,6 +275,7 @@ public void itRespectsTrimBlocksWithLatexSymbols() {
274275
.newConfigBuilder()
275276
.withTokenScannerSymbols(LATEX_SYMBOLS)
276277
.withTrimBlocks(true)
278+
.withKeepTrailingNewline(true)
277279
.build()
278280
);
279281
String result = j.render(
@@ -291,6 +293,7 @@ public void itRespectsLstripBlocksWithAngleSymbols() {
291293
.withTokenScannerSymbols(ANGLE_SYMBOLS)
292294
.withLstripBlocks(true)
293295
.withTrimBlocks(true)
296+
.withKeepTrailingNewline(true)
294297
.build()
295298
);
296299
// Leading spaces before the tag are stripped by lstripBlocks (TreeParser).
@@ -310,6 +313,7 @@ public void itRespectsLstripBlocksWithLatexSymbols() {
310313
.withTokenScannerSymbols(LATEX_SYMBOLS)
311314
.withLstripBlocks(true)
312315
.withTrimBlocks(true)
316+
.withKeepTrailingNewline(true)
313317
.build()
314318
);
315319
String result = j.render(
@@ -503,7 +507,11 @@ public void itHandlesBothLinePrefixesTogether() {
503507

504508
private Jinjava jinjavaWith(StringTokenScannerSymbols symbols) {
505509
return new Jinjava(
506-
BaseJinjavaTest.newConfigBuilder().withTokenScannerSymbols(symbols).build()
510+
BaseJinjavaTest
511+
.newConfigBuilder()
512+
.withKeepTrailingNewline(true)
513+
.withTokenScannerSymbols(symbols)
514+
.build()
507515
);
508516
}
509517
}

0 commit comments

Comments
 (0)