|
| 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