Skip to content
Open
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
11 changes: 11 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,17 @@ public boolean isEnableFilterChainOptimization() {
return false;
}

/**
* When {@code false} (default), 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.
*/
@Value.Default
public boolean isKeepTrailingNewline() {
return false;
}

@Value.Default
public ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jdk8Module());
Expand Down
17 changes: 16 additions & 1 deletion src/main/java/com/hubspot/jinjava/LegacyOverrides.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class LegacyOverrides {
.withUseTrimmingForNotesAndExpressions(true)
.withKeepNullableLoopValues(true)
.withIteratorOnlyReverseFilter(true)
.withHandleBackslashInQuotesOnly(true)
.build();
public static final LegacyOverrides ALL = new LegacyOverrides.Builder()
.withEvaluateMapKeys(true)
Expand All @@ -31,6 +32,7 @@ public class LegacyOverrides {
.withUseTrimmingForNotesAndExpressions(true)
.withKeepNullableLoopValues(true)
.withIteratorOnlyReverseFilter(true)
.withHandleBackslashInQuotesOnly(true)
.build();
private final boolean evaluateMapKeys;
private final boolean iterateOverMapKeys;
Expand All @@ -42,6 +44,7 @@ public class LegacyOverrides {
private final boolean useTrimmingForNotesAndExpressions;
private final boolean keepNullableLoopValues;
private final boolean iteratorOnlyReverseFilter;
private final boolean handleBackslashInQuotesOnly;

private LegacyOverrides(Builder builder) {
evaluateMapKeys = builder.evaluateMapKeys;
Expand All @@ -54,6 +57,7 @@ private LegacyOverrides(Builder builder) {
useTrimmingForNotesAndExpressions = builder.useTrimmingForNotesAndExpressions;
keepNullableLoopValues = builder.keepNullableLoopValues;
iteratorOnlyReverseFilter = builder.iteratorOnlyReverseFilter;
handleBackslashInQuotesOnly = builder.handleBackslashInQuotesOnly;
}

public static Builder newBuilder() {
Expand Down Expand Up @@ -100,6 +104,10 @@ public boolean isIteratorOnlyReverseFilter() {
return iteratorOnlyReverseFilter;
}

public boolean isHandleBackslashInQuotesOnly() {
return handleBackslashInQuotesOnly;
}

public static class Builder {

private boolean evaluateMapKeys = false;
Expand All @@ -112,6 +120,7 @@ public static class Builder {
private boolean useTrimmingForNotesAndExpressions = false;
private boolean keepNullableLoopValues = false;
private boolean iteratorOnlyReverseFilter = false;
private boolean handleBackslashInQuotesOnly = false;

private Builder() {}

Expand All @@ -134,7 +143,8 @@ public static Builder from(LegacyOverrides legacyOverrides) {
legacyOverrides.useTrimmingForNotesAndExpressions
)
.withKeepNullableLoopValues(legacyOverrides.keepNullableLoopValues)
.withIteratorOnlyReverseFilter(legacyOverrides.iteratorOnlyReverseFilter);
.withIteratorOnlyReverseFilter(legacyOverrides.iteratorOnlyReverseFilter)
.withHandleBackslashInQuotesOnly(legacyOverrides.handleBackslashInQuotesOnly);
}

public Builder withEvaluateMapKeys(boolean evaluateMapKeys) {
Expand Down Expand Up @@ -192,5 +202,10 @@ public Builder withIteratorOnlyReverseFilter(boolean iteratorOnlyReverseFilter)
this.iteratorOnlyReverseFilter = iteratorOnlyReverseFilter;
return this;
}

public Builder withHandleBackslashInQuotesOnly(boolean handleBackslashInQuotesOnly) {
this.handleBackslashInQuotesOnly = handleBackslashInQuotesOnly;
return this;
}
}
}
11 changes: 10 additions & 1 deletion src/main/java/com/hubspot/jinjava/tree/TreeParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
import com.hubspot.jinjava.lib.tag.FlexibleTag;
import com.hubspot.jinjava.lib.tag.Tag;
import com.hubspot.jinjava.tree.parse.ExpressionToken;
import com.hubspot.jinjava.tree.parse.StringTokenScanner;
import com.hubspot.jinjava.tree.parse.TagToken;
import com.hubspot.jinjava.tree.parse.TextToken;
import com.hubspot.jinjava.tree.parse.Token;
import com.hubspot.jinjava.tree.parse.TokenScanner;
import com.hubspot.jinjava.tree.parse.TokenScannerSymbols;
import com.hubspot.jinjava.tree.parse.UnclosedToken;
import com.hubspot.jinjava.tree.parse.WhitespaceControlParser;
import java.util.Iterator;
import org.apache.commons.lang3.StringUtils;

public class TreeParser {
Expand All @@ -52,7 +54,7 @@ public class TreeParser {

public TreeParser(JinjavaInterpreter interpreter, String input) {
this.scanner =
Iterators.peekingIterator(new TokenScanner(input, interpreter.getConfig()));
Iterators.peekingIterator(createScanner(input, interpreter.getConfig()));
this.interpreter = interpreter;
this.symbols = interpreter.getConfig().getTokenScannerSymbols();
this.whitespaceControlParser =
Expand Down Expand Up @@ -104,6 +106,13 @@ public Node buildTree() {
return root;
}

private static Iterator<Token> createScanner(String input, JinjavaConfig config) {
if (config.getTokenScannerSymbols().isStringBased()) {
return new StringTokenScanner(input, config);
}
return new TokenScanner(input, config);
}

/**
* @return null if EOF or error
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ public int getType() {

@Override
protected void parse() {
this.expr = WhitespaceUtils.unwrap(image, "{{", "}}");
// Use the symbols-derived delimiter strings instead of the hardcoded "{{" / "}}"
// so that custom delimiters (e.g. "\VAR{" / "}") are stripped correctly.
this.expr =
WhitespaceUtils.unwrap(
image,
getSymbols().getExpressionStart(),
getSymbols().getExpressionEnd()
);
this.expr = handleTrim(expr);
this.expr = StringUtils.trimToEmpty(this.expr);
}
Expand Down
7 changes: 5 additions & 2 deletions src/main/java/com/hubspot/jinjava/tree/parse/NoteToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ public int getType() {
*/
@Override
protected void parse() {
if (image.length() > 4) { // {# #}
handleTrim(image.substring(2, image.length() - 2));
int startLen = getSymbols().getCommentStartLength();
int endLen = getSymbols().getCommentEndLength();

if (image.length() > startLen + endLen) {
handleTrim(image.substring(startLen, image.length() - endLen));
}
content = "";
}
Expand Down
Loading