Skip to content

Commit 270634b

Browse files
hs-lsongclaude
andcommitted
feat: Add preserveComments flag to preserve comment tags in output
Multi-pass rendering scenarios may need to preserve Jinjava comment tags ({# comment #}) for later processing stages. This adds a context flag that, when enabled, outputs comments as-is instead of stripping them. PreserveUndefinedExecutionMode now enables this flag by default. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent f394530 commit 270634b

File tree

6 files changed

+135
-4
lines changed

6 files changed

+135
-4
lines changed

preserve-undefined-execution-mode.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ Set tags are preserved with their evaluated RHS values, enabling the variable to
5959
| Set with known RHS | `{% set x = name %}{{ x }}` | `{name: "World"}` | `{% set x = 'World' %}World` |
6060
| Set with unknown RHS | `{% set x = unknown %}{{ x }}` | `{}` | `{% set x = unknown %}{{ x }}` |
6161

62+
### Comments
63+
64+
Comment tags are preserved in output for multi-pass scenarios where comments may contain instructions for later processing stages:
65+
66+
| Feature | Input | Context | Output |
67+
|---------|-------|---------|--------|
68+
| Simple comment | `{# this is a comment #}` | `{}` | `{# this is a comment #}` |
69+
| Inline comment | `Hello {# comment #} World` | `{}` | `Hello {# comment #} World` |
70+
| Comment with variables | `Hello {{ name }}{# comment #}!` | `{name: "World"}` | `Hello World{# comment #}!` |
71+
6272
### Macros
6373

6474
Macros are executed and their output is rendered, with only undefined variables within the macro output being preserved:
@@ -109,6 +119,7 @@ String secondPass = jinjava.render(firstPass, dynamicContext, defaultConfig);
109119
2. **DynamicVariableResolver** - Returns `DeferredValue.instance()` for undefined variables, triggering preservation
110120
3. **PartialMacroEvaluation** - Allows macros to execute and return partial results with undefined parts preserved
111121
4. **PreserveResolvedSetTags** - Preserves set tags even when RHS is fully resolved, enabling multi-pass variable binding
122+
5. **PreserveComments** - Outputs comment tags (`{# ... #}`) as-is instead of stripping them
112123

113124
### New Context Flag: `isPreserveResolvedSetTags`
114125

@@ -126,11 +137,28 @@ context.setPreserveResolvedSetTags(true);
126137

127138
This flag is checked in `EagerSetTagStrategy` to determine whether fully resolved set tags should be preserved in output or consumed during rendering.
128139

140+
### Context Flag: `isPreserveComments`
141+
142+
A context configuration flag to preserve comment tags in output:
143+
144+
```java
145+
// In ContextConfigurationIF
146+
default boolean isPreserveComments() {
147+
return false;
148+
}
149+
150+
// Usage in Context
151+
context.setPreserveComments(true);
152+
```
153+
154+
This flag is checked in `TreeParser` when processing note tokens. When enabled, comments are output as `TextNode` instead of being discarded.
155+
129156
## Files Changed
130157

131158
- `PreserveUndefinedExecutionMode.java` - Main execution mode implementation
132159
- `PreserveUndefinedExpressionStrategy.java` - Expression strategy for preserving original syntax
133-
- `ContextConfigurationIF.java` - Added `isPreserveResolvedSetTags` flag
134-
- `Context.java` - Added getter/setter for new flag
135-
- `EagerSetTagStrategy.java` - Modified to check new flag
136-
- `PreserveUndefinedExecutionModeTest.java` - Comprehensive test coverage
160+
- `ContextConfigurationIF.java` - Added `isPreserveResolvedSetTags` and `isPreserveComments` flags
161+
- `Context.java` - Added getter/setter for new flags
162+
- `EagerSetTagStrategy.java` - Modified to check `isPreserveResolvedSetTags` flag
163+
- `TreeParser.java` - Modified to check `isPreserveComments` flag
164+
- `PreserveUndefinedExecutionModeTest.java` - Test coverage

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,14 @@ public void setUnwrapRawOverride(boolean unwrapRawOverride) {
892892
contextConfiguration = contextConfiguration.withUnwrapRawOverride(unwrapRawOverride);
893893
}
894894

895+
public boolean isPreserveComments() {
896+
return contextConfiguration.isPreserveComments();
897+
}
898+
899+
public void setPreserveComments(boolean preserveComments) {
900+
contextConfiguration = contextConfiguration.withPreserveComments(preserveComments);
901+
}
902+
895903
public TemporaryValueClosable<Boolean> withUnwrapRawOverride() {
896904
TemporaryValueClosable<Boolean> temporaryValueClosable = new TemporaryValueClosable<>(
897905
isUnwrapRawOverride(),

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ default boolean isUnwrapRawOverride() {
4848
return false;
4949
}
5050

51+
@Default
52+
default boolean isPreserveComments() {
53+
return false;
54+
}
55+
5156
@Default
5257
default ErrorHandlingStrategy getErrorHandlingStrategy() {
5358
return ErrorHandlingStrategy.of();

src/main/java/com/hubspot/jinjava/mode/PreserveUndefinedExecutionMode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ public void prepareContext(Context context) {
4242
context.setDynamicVariableResolver(varName -> DeferredValue.instance());
4343
context.setPartialMacroEvaluation(true);
4444
context.setPreserveResolvedSetTags(true);
45+
context.setPreserveComments(true);
4546
}
4647
}

src/main/java/com/hubspot/jinjava/tree/TreeParser.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ private Node nextNode() {
147147
)
148148
);
149149
}
150+
if (interpreter.getContext().isPreserveComments()) {
151+
TextToken commentAsText = new TextToken(
152+
token.getImage(),
153+
token.getLineNumber(),
154+
token.getStartPosition(),
155+
symbols
156+
);
157+
TextNode n = new TextNode(commentAsText);
158+
n.setParent(parent);
159+
return n;
160+
}
150161
} else {
151162
interpreter.addError(
152163
TemplateError.fromException(

src/test/java/com/hubspot/jinjava/mode/PreserveUndefinedExecutionModeTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,82 @@ public void itPreservesUndefinedInFromImportMacro() {
221221
// Macro executes, but undefined variables are preserved
222222
assertThat(output).isEqualTo("Hello {{ unknown }}!");
223223
}
224+
225+
@Test
226+
public void itRendersExtendsTagWithStaticPath() {
227+
jinjava.setResourceLocator((fullName, encoding, interpreter) -> {
228+
if (fullName.equals("base.html")) {
229+
return "Base: {% block content %}default{% endblock %}";
230+
}
231+
return "";
232+
});
233+
234+
String template =
235+
"{% extends 'base.html' %}{% block content %}child content{% endblock %}";
236+
String output = render(template);
237+
assertThat(output).isEqualTo("Base: child content");
238+
}
239+
240+
@Test
241+
public void itRendersExtendsTagWithDefinedVariablePath() {
242+
jinjava.setResourceLocator((fullName, encoding, interpreter) -> {
243+
if (fullName.equals("base.html")) {
244+
return "Base: {% block content %}default{% endblock %}";
245+
}
246+
return "";
247+
});
248+
249+
Map<String, Object> context = new HashMap<>();
250+
context.put("templatePath", "base.html");
251+
String template =
252+
"{% extends templatePath %}{% block content %}child content{% endblock %}";
253+
String output = render(template, context);
254+
assertThat(output).isEqualTo("Base: child content");
255+
}
256+
257+
@Test
258+
public void itPreservesExtendsTagWithUndefinedVariablePath() {
259+
String template =
260+
"{% extends templatePath %}{% block content %}child content{% endblock %}";
261+
String output = render(template);
262+
assertThat(output)
263+
.isEqualTo(
264+
"{% extends templatePath %}{% block content %}child content{% endblock %}"
265+
);
266+
}
267+
268+
@Test
269+
public void itPreservesUndefinedVariablesInExtendedTemplate() {
270+
jinjava.setResourceLocator((fullName, encoding, interpreter) -> {
271+
if (fullName.equals("base.html")) {
272+
return "Title: {{ title }} - {% block content %}default{% endblock %}";
273+
}
274+
return "";
275+
});
276+
277+
String template =
278+
"{% extends 'base.html' %}{% block content %}{{ message }}{% endblock %}";
279+
String output = render(template);
280+
assertThat(output).isEqualTo("Title: {{ title }} - {{ message }}");
281+
}
282+
283+
@Test
284+
public void itPreservesComments() {
285+
String output = render("{# this is a comment #}");
286+
assertThat(output).isEqualTo("{# this is a comment #}");
287+
}
288+
289+
@Test
290+
public void itPreservesCommentsWithSurroundingContent() {
291+
String output = render("Hello {# inline comment #} World");
292+
assertThat(output).isEqualTo("Hello {# inline comment #} World");
293+
}
294+
295+
@Test
296+
public void itPreservesCommentsWithVariables() {
297+
Map<String, Object> context = new HashMap<>();
298+
context.put("name", "World");
299+
String output = render("Hello {{ name }}{# comment #}!", context);
300+
assertThat(output).isEqualTo("Hello World{# comment #}!");
301+
}
224302
}

0 commit comments

Comments
 (0)