Jinja Templating¶
Macros and defines handle reusable snippets and compile-time lists. But sometimes you need to generate structurally repetitive code -- twenty UI bindings that follow the same pattern, a tag array whose size is computed from external data, or a block of code that should only exist for certain product configurations. This is where Jinja templating comes in.
IVLS integrates the Jinja2 template engine directly into the compilation pipeline. Jinja templates are processed after Python blocks but before macro expansion, letting you programmatically generate KSP source code using loops, conditionals, and computed values.
The Delimiters¶
Standard Jinja uses {{ }} and {% %}, but those conflict with KSP's brace syntax. IVLS uses custom delimiters:
Inline values: <: expression :> -- evaluates a Python expression and inserts the result as text:
declare entries[<: BROWSER_ENTRIES :>]If BROWSER_ENTRIES is 20 in the Python namespace, this becomes declare entries[20].
Control flow: <! statement !> -- executes a Jinja control statement (loops, conditionals):
<! for i in range(0, 4) !>
my.slider[<: i :>] := get_ui_id(Main.Slider.<: i :>)
<! endfor !>This generates four lines with i substituted as 0, 1, 2, 3.
Comments: {# comment #} -- a Jinja comment that is stripped from the output entirely. These are not <# #> -- the comment delimiters use standard Jinja syntax with curly braces and hash marks:
{# This line will not appear in the compiled output #}Loops¶
Jinja for loops are the most common use. They generate repetitive code from Python data:
<! for entry in range(0, BROWSER_ENTRIES) !>
plc.browser.entry_btn[<: entry :>] := get_ui_id(plc.ui.Browser.Entry.<: entry :>)
plc.browser.gm_lbl[<: entry :>] := get_ui_id(plc.ui.Browser.GM.<: entry :>)
<! endfor !>With BROWSER_ENTRIES = 20, this expands to 40 lines of UI binding code. Writing this by hand would be tedious and error-prone. Changing the entry count means changing one Python variable, not editing twenty pairs of lines.
You can iterate over lists, not just ranges:
run<<
layers = ['Keys', 'Pads', 'Shimmer', 'FX']
>>
<! for i, name in enumerate(layers) !>
channel_names[<: i :>] := "<: name :>"
<! endfor !>Conditionals¶
Jinja if blocks let you include or exclude code based on Python values:
<! if single_layer !>
console.ui.Channel.Ch1 -> hide := HIDE_WHOLE_CONTROL
console.ui.Channel.Ch2 -> hide := HIDE_WHOLE_CONTROL
<! endif !>If single_layer is True in the Python namespace, the hide statements are included. If False, they are omitted entirely -- they do not exist in the compiled output.
You can also use else and elif:
Polycore.GLOBAL.CTRL(BG) -> picture_state := <: 1 if single_layer else 0 :>This is a Jinja inline conditional -- a Python ternary expression inside <: :> delimiters.
The Polycore Pattern¶
The Polycore product makes extensive use of Jinja for its browser UI bindings. The browser has up to 116 tag buttons, 20 entry rows, and multiple tag subsets. Binding each one manually would mean hundreds of near-identical lines. Instead, Jinja loops generate all of them:
<! for tag in range(0, TOTAL_TAGS) !>
plc.browser.tag_btn[<: tag :>] := get_ui_id(plc.ui.Browser.Filter.<: tag :>)
<! endfor !>
<! for layer in range(0, (1 if single_layer else 4)) !>
uicb.Bind(get_ui_id(plc.ui.MainPage.Rack.<: layer :>.Source), plc.source_browse_action)
uicb.Bind(get_ui_id(plc.ui.MainPage.Rack.<: layer :>.SourceNext), plc.source_browse_next)
<! endfor !>The layer count is itself computed from the single_layer flag. When building a single-layer variant of Polycore, the loop runs once; for the full version, four times. The same source code produces both configurations.
Jinja vs Macros¶
Both Jinja and macros generate code at compile time. When should you use which?
Use macros for reusable snippets that are called from multiple places with different arguments. Macros are named, parameterized, and can be called anywhere after definition. They are the right tool when you have a pattern that repeats with variation -- like a macro that sets up an engine parameter with validation.
Use Jinja for structurally repetitive code that follows a mechanical pattern -- N copies of the same binding, a block that should exist conditionally, or values that are computed from external data. Jinja is the right tool when the repetition is structural (the same shape, just different indices) rather than semantic (the same concept, different parameters).
A rule of thumb: if you would write it as a for loop if KSP supported compile-time loops, use Jinja. If you would write it as a function call, use a macro.
They combine naturally. A Jinja loop can emit macro calls, and macros can contain Jinja expressions:
<! for ch in range(0, NUM_CHANNELS) !>
console.engine.enable_channel_fx(<: ch :>)
<! endfor !>Where Jinja Data Comes From¶
The values used in Jinja expressions (BROWSER_ENTRIES, single_layer, polycore.tags) come from the Python namespace -- the same namespace populated by run<<>> blocks. This connection between Python and Jinja is what makes the system powerful: Python computes the data, Jinja stamps it into KSP code.
We will cover the Python side in the next chapter.
Jinja templating fills the gap between macros (reusable named snippets) and manual code (writing every line by hand). For repetitive UI bindings, configurable product variants, and data-driven code generation, Jinja loops and conditionals keep your source compact and maintainable while generating exactly the KSP the compiler needs.