Skip to content
Merged
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
14 changes: 13 additions & 1 deletion src/main/java/com/hubspot/jinjava/Jinjava.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public RenderResult renderForResult(
.getInterpreterFactory()
.newInstance(this, context, renderConfig);
try {
String result = interpreter.render(template);
String result = stripTrailingNewlineIfNeeded(interpreter.render(template));
return new RenderResult(
result,
interpreter.getContext(),
Expand Down Expand Up @@ -293,6 +293,18 @@ public RenderResult renderForResult(
}
}

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

/**
* Creates a new interpreter instance using the global context and global config
*
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/hubspot/jinjava/JinjavaConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,18 @@ public boolean isEnableFilterChainOptimization() {
return false;
}

/**
* When {@code false}, a single trailing newline is stripped from the rendered output,
* matching Python Jinja2's default.
* When {@code true}, the trailing newline of the rendered output is preserved —
* matching Jinjava's historical behaviour.
* Defaults to {@link LegacyOverrides#getDefaultKeepTrailingNewlineBehavior()}.
*/
@Value.Default
public boolean isKeepTrailingNewline() {
return getLegacyOverrides().getDefaultKeepTrailingNewlineBehavior();
}

@Value.Default
public ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jdk8Module());
Expand Down
26 changes: 25 additions & 1 deletion src/main/java/com/hubspot/jinjava/LegacyOverrides.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class LegacyOverrides {
.withKeepNullableLoopValues(true)
.withIteratorOnlyReverseFilter(true)
.withHandleBackslashInQuotesOnly(true)
.withDefaultKeepTrailingNewlineBehavior(false)
.build();
public static final LegacyOverrides ALL = new LegacyOverrides.Builder()
.withEvaluateMapKeys(true)
Expand All @@ -33,6 +34,7 @@ public class LegacyOverrides {
.withKeepNullableLoopValues(true)
.withIteratorOnlyReverseFilter(true)
.withHandleBackslashInQuotesOnly(true)
.withDefaultKeepTrailingNewlineBehavior(false)
.build();
private final boolean evaluateMapKeys;
private final boolean iterateOverMapKeys;
Expand All @@ -45,6 +47,7 @@ public class LegacyOverrides {
private final boolean keepNullableLoopValues;
private final boolean iteratorOnlyReverseFilter;
private final boolean handleBackslashInQuotesOnly;
private final boolean defaultKeepTrailingNewlineBehavior;

private LegacyOverrides(Builder builder) {
evaluateMapKeys = builder.evaluateMapKeys;
Expand All @@ -58,6 +61,7 @@ private LegacyOverrides(Builder builder) {
keepNullableLoopValues = builder.keepNullableLoopValues;
iteratorOnlyReverseFilter = builder.iteratorOnlyReverseFilter;
handleBackslashInQuotesOnly = builder.handleBackslashInQuotesOnly;
defaultKeepTrailingNewlineBehavior = builder.defaultKeepTrailingNewlineBehavior;
}

public static Builder newBuilder() {
Expand Down Expand Up @@ -108,6 +112,15 @@ public boolean isHandleBackslashInQuotesOnly() {
return handleBackslashInQuotesOnly;
}

/**
* The default value of {@link com.hubspot.jinjava.JinjavaConfig#isKeepTrailingNewline()}.
* {@code true} preserves Jinjava's historical behaviour of keeping the trailing newline;
* {@code false} matches Python Jinja2's default of stripping it.
*/
public boolean getDefaultKeepTrailingNewlineBehavior() {
return defaultKeepTrailingNewlineBehavior;
}

public static class Builder {

private boolean evaluateMapKeys = false;
Expand All @@ -121,6 +134,7 @@ public static class Builder {
private boolean keepNullableLoopValues = false;
private boolean iteratorOnlyReverseFilter = false;
private boolean handleBackslashInQuotesOnly = false;
private boolean defaultKeepTrailingNewlineBehavior = true;

private Builder() {}

Expand All @@ -144,7 +158,10 @@ public static Builder from(LegacyOverrides legacyOverrides) {
)
.withKeepNullableLoopValues(legacyOverrides.keepNullableLoopValues)
.withIteratorOnlyReverseFilter(legacyOverrides.iteratorOnlyReverseFilter)
.withHandleBackslashInQuotesOnly(legacyOverrides.handleBackslashInQuotesOnly);
.withHandleBackslashInQuotesOnly(legacyOverrides.handleBackslashInQuotesOnly)
.withDefaultKeepTrailingNewlineBehavior(
legacyOverrides.defaultKeepTrailingNewlineBehavior
);
}

public Builder withEvaluateMapKeys(boolean evaluateMapKeys) {
Expand Down Expand Up @@ -207,5 +224,12 @@ public Builder withHandleBackslashInQuotesOnly(boolean handleBackslashInQuotesOn
this.handleBackslashInQuotesOnly = handleBackslashInQuotesOnly;
return this;
}

public Builder withDefaultKeepTrailingNewlineBehavior(
boolean defaultKeepTrailingNewlineBehavior
) {
this.defaultKeepTrailingNewlineBehavior = defaultKeepTrailingNewlineBehavior;
return this;
}
}
}
73 changes: 73 additions & 0 deletions src/test/java/com/hubspot/jinjava/TrailingNewlineTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.hubspot.jinjava;

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

import com.google.common.collect.ImmutableMap;
import com.hubspot.jinjava.Jinjava;
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 itKeepsTrailingNewlineIsTrue() {
Jinjava jinjava = new Jinjava(
JinjavaConfig.newBuilder().withKeepTrailingNewline(true).build()
);
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
.isEqualTo("hello\n");
}

@Test
public void itStripsTrailingNewlineDefault() {
// Defaults keepTrailingNewline=false (matching Python behaviour)
Jinjava jinjava = new Jinjava();
assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
.isEqualTo("hello");
}

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

@Test
public void itStripsTrailingNewlineIsFalse() {
Jinjava jinjava = new Jinjava(
JinjavaConfig.newBuilder().withKeepTrailingNewline(false).build()
);

assertThat(jinjava.render(TEMPLATE_WITH_TRAILING_NEWLINE, new HashMap<>()))
.isEqualTo("hello");
}

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

@Test
public void itDoesNotAffectOutputWithNoTrailingNewline() {
Jinjava jinjava = new Jinjava(
JinjavaConfig.newBuilder().withKeepTrailingNewline(true).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();
assertThat(jinjava.render(TEMPLATE_MULTIPLE_TRAILING_NEWLINES, new HashMap<>()))
.isEqualTo("hello\n");
}

@Test
public void itStripsTrailingNewlineFromRenderedExpressions() {
Jinjava jinjava = new Jinjava();
assertThat(jinjava.render("{{ greeting }}\n", ImmutableMap.of("greeting", "hello")))
.isEqualTo("hello");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ public class AstFilterChainTest {
public void setup() {
jinjava =
new Jinjava(
BaseJinjavaTest.newConfigBuilder().withEnableFilterChainOptimization(true).build()
BaseJinjavaTest
.newConfigBuilder()
.withEnableFilterChainOptimization(true)
.withKeepTrailingNewline(true)
.build()
);

context = new HashMap<>();
Expand Down Expand Up @@ -123,6 +127,7 @@ public void itSkipsDisabledFilterAndContinuesChain() {
BaseJinjavaTest
.newConfigBuilder()
.withEnableFilterChainOptimization(true)
.withKeepTrailingNewline(true)
.withDisabled(disabled)
.build()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ public void setUp() throws Exception {
new Jinjava(
BaseJinjavaTest
.newConfigBuilder()
.withKeepTrailingNewline(true)
.withLegacyOverrides(LegacyOverrides.NONE)
.build()
);
modern =
new Jinjava(
BaseJinjavaTest
.newConfigBuilder()
.withKeepTrailingNewline(true)
.withLegacyOverrides(
LegacyOverrides.newBuilder().withUseNaturalOperatorPrecedence(true).build()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ public void setUp() throws Exception {
new Jinjava(
BaseJinjavaTest
.newConfigBuilder()
.withKeepTrailingNewline(true)
.withLegacyOverrides(LegacyOverrides.NONE)
.build()
);
modern =
new Jinjava(
BaseJinjavaTest
.newConfigBuilder()
.withKeepTrailingNewline(true)
.withLegacyOverrides(
LegacyOverrides.newBuilder().withParseWhitespaceControlStrictly(true).build()
)
Expand Down
10 changes: 10 additions & 0 deletions src/test/java/com/hubspot/jinjava/lib/filter/SliceFilterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,27 @@
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import com.hubspot.jinjava.BaseJinjavaTest;
import com.hubspot.jinjava.Jinjava;
import com.hubspot.jinjava.interpret.RenderResult;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.Before;
import org.junit.Test;

public class SliceFilterTest extends BaseJinjavaTest {

@Before
public void setup() {
jinjava =
new Jinjava(
BaseJinjavaTest.newConfigBuilder().withKeepTrailingNewline(true).build()
);
}

@Test
public void itSlicesLists() throws Exception {
Document dom = Jsoup.parseBodyFragment(
Expand Down
7 changes: 7 additions & 0 deletions src/test/java/com/hubspot/jinjava/lib/tag/ForTagTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import com.google.common.io.Resources;
import com.hubspot.jinjava.BaseInterpretingTest;
import com.hubspot.jinjava.BaseJinjavaTest;
import com.hubspot.jinjava.Jinjava;
import com.hubspot.jinjava.JinjavaConfig;
import com.hubspot.jinjava.LegacyOverrides;
import com.hubspot.jinjava.interpret.InterpretException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
Expand Down Expand Up @@ -40,6 +42,11 @@ public class ForTagTest extends BaseInterpretingTest {
@Override
public void baseSetup() {
super.baseSetup();

jinjava =
new Jinjava(
BaseJinjavaTest.newConfigBuilder().withKeepTrailingNewline(true).build()
);
tag = new ForTag();

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ public void itRespectsTrimBlocksWithAngleSymbols() {
.newConfigBuilder()
.withTokenScannerSymbols(ANGLE_SYMBOLS)
.withTrimBlocks(true)
.withKeepTrailingNewline(true)
.build()
);
// Without trimBlocks the newline after <% if show %> would appear in output.
Expand All @@ -274,6 +275,7 @@ public void itRespectsTrimBlocksWithLatexSymbols() {
.newConfigBuilder()
.withTokenScannerSymbols(LATEX_SYMBOLS)
.withTrimBlocks(true)
.withKeepTrailingNewline(true)
.build()
);
String result = j.render(
Expand All @@ -291,6 +293,7 @@ public void itRespectsLstripBlocksWithAngleSymbols() {
.withTokenScannerSymbols(ANGLE_SYMBOLS)
.withLstripBlocks(true)
.withTrimBlocks(true)
.withKeepTrailingNewline(true)
.build()
);
// Leading spaces before the tag are stripped by lstripBlocks (TreeParser).
Expand All @@ -310,6 +313,7 @@ public void itRespectsLstripBlocksWithLatexSymbols() {
.withTokenScannerSymbols(LATEX_SYMBOLS)
.withLstripBlocks(true)
.withTrimBlocks(true)
.withKeepTrailingNewline(true)
.build()
);
String result = j.render(
Expand Down Expand Up @@ -503,7 +507,11 @@ public void itHandlesBothLinePrefixesTogether() {

private Jinjava jinjavaWith(StringTokenScannerSymbols symbols) {
return new Jinjava(
BaseJinjavaTest.newConfigBuilder().withTokenScannerSymbols(symbols).build()
BaseJinjavaTest
.newConfigBuilder()
.withKeepTrailingNewline(true)
.withTokenScannerSymbols(symbols)
.build()
);
}
}
Loading