Skip to content

Commit f394530

Browse files
hs-lsongclaude
andcommitted
docs: Add documentation for PreserveUndefinedExecutionMode
Document the new execution mode including: - Use case and purpose - Usage examples - Behavior tables for expressions, control structures, set tags, macros - Multi-pass rendering example - Implementation details and new context flags 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 1bb5593 commit f394530

File tree

1 file changed

+136
-0
lines changed

1 file changed

+136
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# PreserveUndefinedExecutionMode
2+
3+
A new execution mode for Jinjava that preserves unknown/undefined variables as their original template syntax instead of rendering them as empty strings. This enables multi-pass rendering scenarios where templates are processed in stages with different variable contexts available at each stage.
4+
5+
## Use Case
6+
7+
Multi-pass template rendering is useful when:
8+
- Some variables are known at compile/build time (static values)
9+
- Other variables are only known at runtime (dynamic values)
10+
- You want to pre-render static parts while preserving dynamic placeholders
11+
12+
## Usage
13+
14+
```java
15+
import com.hubspot.jinjava.Jinjava;
16+
import com.hubspot.jinjava.JinjavaConfig;
17+
import com.hubspot.jinjava.mode.PreserveUndefinedExecutionMode;
18+
19+
Jinjava jinjava = new Jinjava();
20+
JinjavaConfig config = JinjavaConfig.newBuilder()
21+
.withExecutionMode(PreserveUndefinedExecutionMode.instance())
22+
.build();
23+
24+
Map<String, Object> context = new HashMap<>();
25+
context.put("staticValue", "STATIC");
26+
27+
String template = "{{ staticValue }} - {{ dynamicValue }}";
28+
String result = jinjava.render(template, context, config);
29+
// Result: "STATIC - {{ dynamicValue }}"
30+
```
31+
32+
## Behavior Summary
33+
34+
| Feature | Input | Context | Output |
35+
|---------|-------|---------|--------|
36+
| Undefined expression | `{{ unknown }}` | `{}` | `{{ unknown }}` |
37+
| Defined expression | `{{ name }}` | `{name: "World"}` | `World` |
38+
| Expression with filter | `{{ name \| upper }}` | `{}` | `{{ name \| upper }}` |
39+
| Property access | `{{ obj.property }}` | `{}` | `{{ obj.property }}` |
40+
| Null value | `{{ nullVar }}` | `{nullVar: null}` | `{{ nullVar }}` |
41+
| Mixed | `Hello {{ name }}, {{ unknown }}!` | `{name: "World"}` | `Hello World, {{ unknown }}!` |
42+
43+
### Control Structures
44+
45+
| Feature | Input | Context | Output |
46+
|---------|-------|---------|--------|
47+
| If with known condition | `{% if true %}Hello{% endif %}` | `{}` | `Hello` |
48+
| If with unknown condition | `{% if unknown %}Hello{% endif %}` | `{}` | `{% if unknown %}Hello{% endif %}` |
49+
| If-else with unknown | `{% if unknown %}A{% else %}B{% endif %}` | `{}` | `{% if unknown %}A{% else %}B{% endif %}` |
50+
| For with known iterable | `{% for x in items %}{{ x }}{% endfor %}` | `{items: ["a","b"]}` | `ab` |
51+
| For with unknown iterable | `{% for x in items %}{{ x }}{% endfor %}` | `{}` | `{% for x in items %}{{ x }}{% endfor %}` |
52+
53+
### Set Tags
54+
55+
Set tags are preserved with their evaluated RHS values, enabling the variable to be set in subsequent rendering passes:
56+
57+
| Feature | Input | Context | Output |
58+
|---------|-------|---------|--------|
59+
| Set with known RHS | `{% set x = name %}{{ x }}` | `{name: "World"}` | `{% set x = 'World' %}World` |
60+
| Set with unknown RHS | `{% set x = unknown %}{{ x }}` | `{}` | `{% set x = unknown %}{{ x }}` |
61+
62+
### Macros
63+
64+
Macros are executed and their output is rendered, with only undefined variables within the macro output being preserved:
65+
66+
```jinja
67+
{# macros.jinja #}
68+
{% macro greet(name) %}Hello {{ name }}, {{ title }}!{% endmacro %}
69+
```
70+
71+
| Feature | Input | Context | Output |
72+
|---------|-------|---------|--------|
73+
| Macro with undefined var | `{{ m.greet('World') }}` | `{}` | `Hello World, {{ title }}!` |
74+
| Macro fully defined | `{{ m.greet('World') }}` | `{title: "Mr"}` | `Hello World, Mr!` |
75+
76+
## Multi-Pass Rendering Example
77+
78+
```java
79+
// First pass: render static values
80+
Map<String, Object> staticContext = new HashMap<>();
81+
staticContext.put("appName", "MyApp");
82+
staticContext.put("version", "1.0");
83+
84+
JinjavaConfig preserveConfig = JinjavaConfig.newBuilder()
85+
.withExecutionMode(PreserveUndefinedExecutionMode.instance())
86+
.build();
87+
88+
String template = "{{ appName }} v{{ version }} - Welcome {{ userName }}!";
89+
String firstPass = jinjava.render(template, staticContext, preserveConfig);
90+
// Result: "MyApp v1.0 - Welcome {{ userName }}!"
91+
92+
// Second pass: render dynamic values
93+
Map<String, Object> dynamicContext = new HashMap<>();
94+
dynamicContext.put("userName", "Alice");
95+
96+
JinjavaConfig defaultConfig = JinjavaConfig.newBuilder()
97+
.withExecutionMode(DefaultExecutionMode.instance())
98+
.build();
99+
100+
String secondPass = jinjava.render(firstPass, dynamicContext, defaultConfig);
101+
// Result: "MyApp v1.0 - Welcome Alice!"
102+
```
103+
104+
## Implementation Details
105+
106+
`PreserveUndefinedExecutionMode` extends `EagerExecutionMode` and configures the context with:
107+
108+
1. **PreserveUndefinedExpressionStrategy** - Returns original expression syntax when variables are undefined, instead of internal representations
109+
2. **DynamicVariableResolver** - Returns `DeferredValue.instance()` for undefined variables, triggering preservation
110+
3. **PartialMacroEvaluation** - Allows macros to execute and return partial results with undefined parts preserved
111+
4. **PreserveResolvedSetTags** - Preserves set tags even when RHS is fully resolved, enabling multi-pass variable binding
112+
113+
### New Context Flag: `isPreserveResolvedSetTags`
114+
115+
A new context configuration flag was added to allow independent control over set tag preservation:
116+
117+
```java
118+
// In ContextConfigurationIF
119+
default boolean isPreserveResolvedSetTags() {
120+
return false;
121+
}
122+
123+
// Usage in Context
124+
context.setPreserveResolvedSetTags(true);
125+
```
126+
127+
This flag is checked in `EagerSetTagStrategy` to determine whether fully resolved set tags should be preserved in output or consumed during rendering.
128+
129+
## Files Changed
130+
131+
- `PreserveUndefinedExecutionMode.java` - Main execution mode implementation
132+
- `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

0 commit comments

Comments
 (0)