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 @@ -203,6 +203,17 @@ default 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
default boolean isKeepTrailingNewline() {
return false;
}

@Value.Default
default ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jdk8Module());
Expand Down
19 changes: 19 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)
.withHandleBackslashInQuotesOnly(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)
.withHandleBackslashInQuotesOnly(true)
.build();

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

/**
* When {@code true}, the token scanner treats backslash as an escape character
* only inside quoted string literals, leaving bare backslashes outside quotes
* untouched for the expression parser (JUEL) to handle. This matches the
* behaviour of Python's Jinja2, where the template scanner is not responsible
* for backslash interpretation at all.
*
* <p>When {@code false} (the default), the scanner consumes a backslash and
* the following character unconditionally, regardless of quote context. This
* is the legacy Jinjava behaviour, which prevents closing delimiters from
* being recognized after a backslash but diverges from Jinja2.
*/
@Value.Default
default boolean isHandleBackslashInQuotesOnly() {
return false;
}

class Builder extends ImmutableLegacyOverrides.Builder {}

static Builder newBuilder() {
Expand Down
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