Skip to content

Commit 5d5542a

Browse files
committed
Add withKeepTrailingNewline to LegacyOverrides.
1 parent a6c5d0b commit 5d5542a

3 files changed

Lines changed: 150 additions & 2 deletions

File tree

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public interface LegacyOverrides extends WithLegacyOverrides {
2121
.withAllowAdjacentTextNodes(true)
2222
.withUseTrimmingForNotesAndExpressions(true)
2323
.withKeepNullableLoopValues(true)
24+
.withKeepTrailingNewline(true)
2425
.build();
2526
LegacyOverrides ALL = new Builder()
2627
.withEvaluateMapKeys(true)
@@ -32,6 +33,7 @@ public interface LegacyOverrides extends WithLegacyOverrides {
3233
.withAllowAdjacentTextNodes(true)
3334
.withUseTrimmingForNotesAndExpressions(true)
3435
.withKeepNullableLoopValues(true)
36+
.withKeepTrailingNewline(false)
3537
.build();
3638

3739
@Value.Default
@@ -79,6 +81,17 @@ default boolean isKeepNullableLoopValues() {
7981
return false;
8082
}
8183

84+
/**
85+
* When {@code false} (default, legacy behaviour), the trailing newline of
86+
* the rendered output is preserved — matching Jinjava's historical behaviour.
87+
* When {@code true}, a single trailing newline is stripped from the rendered
88+
* output, matching Python Jinja2's default ({@code keep_trailing_newline=False}).
89+
*/
90+
@Value.Default
91+
default boolean isKeepTrailingNewline() {
92+
return true;
93+
}
94+
8295
class Builder extends ImmutableLegacyOverrides.Builder {}
8396

8497
static Builder newBuilder() {

src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ private String render(Node root, boolean processExtendRoots, long renderLimit) {
510510
}
511511

512512
if (ignoredOutput.length() > 0) {
513-
return (
513+
return stripTrailingNewlineIfNeeded(
514514
EagerReconstructionUtils.labelWithNotes(
515515
EagerReconstructionUtils.wrapInTag(
516516
ignoredOutput.toString(),
@@ -524,14 +524,26 @@ private String render(Node root, boolean processExtendRoots, long renderLimit) {
524524
output.getValue()
525525
);
526526
}
527-
return output.getValue();
527+
return stripTrailingNewlineIfNeeded(output.getValue());
528528
} finally {
529529
if (pushed) {
530530
JinjavaInterpreter.popCurrent();
531531
}
532532
}
533533
}
534534

535+
/**
536+
* Strips a single trailing newline from the rendered output when
537+
* {@code keepTrailingNewline} is {@code false} in {@link LegacyOverrides},
538+
* matching Python Jinja2's default {@code keep_trailing_newline=False} behaviour.
539+
*/
540+
private String stripTrailingNewlineIfNeeded(String output) {
541+
if (!config.getLegacyOverrides().isKeepTrailingNewline() && output.endsWith("\n")) {
542+
return output.substring(0, output.length() - 1);
543+
}
544+
return output;
545+
}
546+
535547
private void resolveBlockStubs(OutputList output) {
536548
resolveBlockStubs(output, new Stack<>());
537549
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 java.util.HashMap;
7+
import org.junit.Test;
8+
9+
public class TrailingNewlineTest {
10+
11+
private static final String TEMPLATE_WITH_TRAILING_NEWLINE = "hello\n";
12+
private static final String TEMPLATE_WITHOUT_TRAILING_NEWLINE = "hello";
13+
private static final String TEMPLATE_MULTIPLE_TRAILING_NEWLINES = "hello\n\n";
14+
15+
// ── keepTrailingNewline=true (legacy default: preserve \n) ─────────────────
16+
17+
@Test
18+
public void itKeepsTrailingNewlineWhenLegacyOverrideIsTrue() {
19+
Jinjava jinjava = new Jinjava(
20+
JinjavaConfig
21+
.newBuilder()
22+
.withLegacyOverrides(
23+
LegacyOverrides.newBuilder().withKeepTrailingNewline(true).build()
24+
)
25+
.build()
26+
);
27+
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
28+
.isEqualTo("hello\n");
29+
}
30+
31+
@Test
32+
public void itKeepsTrailingNewlineWithNoneLegacyOverrides() {
33+
// LegacyOverrides.NONE defaults keepTrailingNewline=true (legacy behaviour)
34+
Jinjava jinjava = new Jinjava(
35+
JinjavaConfig.newBuilder().withLegacyOverrides(LegacyOverrides.NONE).build()
36+
);
37+
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
38+
.isEqualTo("hello\n");
39+
}
40+
41+
// ── keepTrailingNewline=false (Python-compatible: strip trailing \n) ────────
42+
43+
@Test
44+
public void itStripsTrailingNewlineWhenLegacyOverrideIsFalse() {
45+
Jinjava jinjava = new Jinjava(
46+
JinjavaConfig
47+
.newBuilder()
48+
.withLegacyOverrides(
49+
LegacyOverrides.newBuilder().withKeepTrailingNewline(false).build()
50+
)
51+
.build()
52+
);
53+
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
54+
.isEqualTo("hello");
55+
}
56+
57+
@Test
58+
public void itKeepsTrailingNewlineWithThreePointOLegacyOverrides() {
59+
// LegacyOverrides.THREE_POINT_0 does not opt into Python-compatible stripping
60+
Jinjava jinjava = new Jinjava(
61+
JinjavaConfig
62+
.newBuilder()
63+
.withLegacyOverrides(LegacyOverrides.THREE_POINT_0)
64+
.build()
65+
);
66+
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
67+
.isEqualTo("hello\n");
68+
}
69+
70+
@Test
71+
public void itStripsTrailingNewlineWithAllLegacyOverrides() {
72+
Jinjava jinjava = new Jinjava(
73+
JinjavaConfig.newBuilder().withLegacyOverrides(LegacyOverrides.ALL).build()
74+
);
75+
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
76+
.isEqualTo("hello");
77+
}
78+
79+
// ── Edge cases ──────────────────────────────────────────────────────────────
80+
81+
@Test
82+
public void itDoesNotAffectOutputWithNoTrailingNewline() {
83+
Jinjava jinjava = new Jinjava(
84+
JinjavaConfig
85+
.newBuilder()
86+
.withLegacyOverrides(
87+
LegacyOverrides.newBuilder().withKeepTrailingNewline(false).build()
88+
)
89+
.build()
90+
);
91+
assertThat(jinjava.render(TEMPLATE_WITHOUT_TRAILING_NEWLINE, new HashMap<>()))
92+
.isEqualTo("hello");
93+
}
94+
95+
@Test
96+
public void itStripsOnlyOneTrailingNewlineNotMultiple() {
97+
// Python only strips a single trailing newline, not all of them.
98+
Jinjava jinjava = new Jinjava(
99+
JinjavaConfig
100+
.newBuilder()
101+
.withLegacyOverrides(
102+
LegacyOverrides.newBuilder().withKeepTrailingNewline(false).build()
103+
)
104+
.build()
105+
);
106+
assertThat(jinjava.render(TEMPLATE_MULTIPLE_TRAILING_NEWLINES, new HashMap<>()))
107+
.isEqualTo("hello\n");
108+
}
109+
110+
@Test
111+
public void itStripsTrailingNewlineFromRenderedExpressions() {
112+
Jinjava jinjava = new Jinjava(
113+
JinjavaConfig
114+
.newBuilder()
115+
.withLegacyOverrides(
116+
LegacyOverrides.newBuilder().withKeepTrailingNewline(false).build()
117+
)
118+
.build()
119+
);
120+
assertThat(jinjava.render("{{ greeting }}\n", ImmutableMap.of("greeting", "hello")))
121+
.isEqualTo("hello");
122+
}
123+
}

0 commit comments

Comments
 (0)