Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/java/com/hubspot/jinjava/LegacyOverrides.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public interface LegacyOverrides extends WithLegacyOverrides {
.withAllowAdjacentTextNodes(true)
.withUseTrimmingForNotesAndExpressions(true)
.withKeepNullableLoopValues(true)
.withKeepTrailingNewline(true)
.build();
LegacyOverrides ALL = new Builder()
.withEvaluateMapKeys(true)
Expand All @@ -32,6 +33,7 @@ public interface LegacyOverrides extends WithLegacyOverrides {
.withAllowAdjacentTextNodes(true)
.withUseTrimmingForNotesAndExpressions(true)
.withKeepNullableLoopValues(true)
.withKeepTrailingNewline(false)
.build();

@Value.Default
Expand Down Expand Up @@ -79,6 +81,17 @@ default boolean isKeepNullableLoopValues() {
return false;
}

/**
* When {@code false} (default, legacy behaviour), the trailing newline of
* the rendered output is preserved — matching Jinjava's historical behaviour.
* When {@code true}, a single trailing newline is stripped from the rendered
* output, matching Python Jinja2's default ({@code keep_trailing_newline=False}).
*/
@Value.Default
default boolean isKeepTrailingNewline() {
return true;
}

class Builder extends ImmutableLegacyOverrides.Builder {}

static Builder newBuilder() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ private String render(Node root, boolean processExtendRoots, long renderLimit) {
}

if (ignoredOutput.length() > 0) {
return (
return stripTrailingNewlineIfNeeded(
EagerReconstructionUtils.labelWithNotes(
EagerReconstructionUtils.wrapInTag(
ignoredOutput.toString(),
Expand All @@ -524,14 +524,26 @@ private String render(Node root, boolean processExtendRoots, long renderLimit) {
output.getValue()
);
}
return output.getValue();
return stripTrailingNewlineIfNeeded(output.getValue());
} finally {
if (pushed) {
JinjavaInterpreter.popCurrent();
}
}
}

/**
* Strips a single trailing newline from the rendered output when
* {@code keepTrailingNewline} is {@code false} in {@link LegacyOverrides},
* matching Python Jinja2's default {@code keep_trailing_newline=False} behaviour.
*/
private String stripTrailingNewlineIfNeeded(String output) {
if (!config.getLegacyOverrides().isKeepTrailingNewline() && output.endsWith("\n")) {
return output.substring(0, output.length() - 1);
}
return output;
}

private void resolveBlockStubs(OutputList output) {
resolveBlockStubs(output, new Stack<>());
}
Expand Down
123 changes: 123 additions & 0 deletions src/test/java/com/hubspot/jinjava/TrailingNewlineTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.hubspot.jinjava;

import static org.assertj.core.api.Assertions.assertThat;

import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
import org.junit.Test;

public class TrailingNewlineTest {

private static final String TEMPLATE_WITH_TRAILING_NEWLINE = "hello\n";
private static final String TEMPLATE_WITHOUT_TRAILING_NEWLINE = "hello";
private static final String TEMPLATE_MULTIPLE_TRAILING_NEWLINES = "hello\n\n";

// ── keepTrailingNewline=true (legacy default: preserve \n) ─────────────────

@Test
public void itKeepsTrailingNewlineWhenLegacyOverrideIsTrue() {
Jinjava jinjava = new Jinjava(
JinjavaConfig
.newBuilder()
.withLegacyOverrides(
LegacyOverrides.newBuilder().withKeepTrailingNewline(true).build()
)
.build()
);
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
.isEqualTo("hello\n");
}

@Test
public void itKeepsTrailingNewlineWithNoneLegacyOverrides() {
// LegacyOverrides.NONE defaults keepTrailingNewline=true (legacy behaviour)
Jinjava jinjava = new Jinjava(
JinjavaConfig.newBuilder().withLegacyOverrides(LegacyOverrides.NONE).build()
);
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
.isEqualTo("hello\n");
}

// ── keepTrailingNewline=false (Python-compatible: strip trailing \n) ────────

@Test
public void itStripsTrailingNewlineWhenLegacyOverrideIsFalse() {
Jinjava jinjava = new Jinjava(
JinjavaConfig
.newBuilder()
.withLegacyOverrides(
LegacyOverrides.newBuilder().withKeepTrailingNewline(false).build()
)
.build()
);
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
.isEqualTo("hello");
}

@Test
public void itKeepsTrailingNewlineWithThreePointOLegacyOverrides() {
// LegacyOverrides.THREE_POINT_0 does not opt into Python-compatible stripping
Jinjava jinjava = new Jinjava(
JinjavaConfig
.newBuilder()
.withLegacyOverrides(LegacyOverrides.THREE_POINT_0)
.build()
);
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
.isEqualTo("hello\n");
}

@Test
public void itStripsTrailingNewlineWithAllLegacyOverrides() {
Jinjava jinjava = new Jinjava(
JinjavaConfig.newBuilder().withLegacyOverrides(LegacyOverrides.ALL).build()
);
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
.isEqualTo("hello");
}

// ── Edge cases ──────────────────────────────────────────────────────────────

@Test
public void itDoesNotAffectOutputWithNoTrailingNewline() {
Jinjava jinjava = new Jinjava(
JinjavaConfig
.newBuilder()
.withLegacyOverrides(
LegacyOverrides.newBuilder().withKeepTrailingNewline(false).build()
)
.build()
);
assertThat(jinjava.render(TEMPLATE_WITHOUT_TRAILING_NEWLINE, new HashMap<>()))
.isEqualTo("hello");
}

@Test
public void itStripsOnlyOneTrailingNewlineNotMultiple() {
// Python only strips a single trailing newline, not all of them.
Jinjava jinjava = new Jinjava(
JinjavaConfig
.newBuilder()
.withLegacyOverrides(
LegacyOverrides.newBuilder().withKeepTrailingNewline(false).build()
)
.build()
);
assertThat(jinjava.render(TEMPLATE_MULTIPLE_TRAILING_NEWLINES, new HashMap<>()))
.isEqualTo("hello\n");
}

@Test
public void itStripsTrailingNewlineFromRenderedExpressions() {
Jinjava jinjava = new Jinjava(
JinjavaConfig
.newBuilder()
.withLegacyOverrides(
LegacyOverrides.newBuilder().withKeepTrailingNewline(false).build()
)
.build()
);
assertThat(jinjava.render("{{ greeting }}\n", ImmutableMap.of("greeting", "hello")))
.isEqualTo("hello");
}
}