Pattern: The Enforcer
Building Crash-Proof Templates with Guards
So far, we have talked about how to share data (Providers) and collect data (Aggregators). But what happens when a user uses your components wrong?
What if they put a TableCell inside a Footnote? Or try to use a Chapter without a Book wrapper? In standard Typst, this often leads to "Silent Failures"βthe content just renders weirdly, or variables are missing, and the user has no idea why.
Loom solves this with the Enforcer Pattern.
The Problem: "Silent Failures"β
Imagine you are building a slideshow template. You have a slide component that expects to be inside a presentation.
The Unsafe Way:
If a user pastes a #slide[...] into a normal document, it might try to read ctx.page-width (which doesn't exist) and crash with a cryptic error: "key 'page-width' not found in dictionary."
Or worse, it might render perfectly fine, but look completely broken because it's missing the styling from the root.
The Solution: Guardsβ
Loom provides a guards module that allows your components to assert where they are allowed to live. If the rules are violated, Loom stops the compilation with a clear, helpful error message.
This turns a "runtime bug" into a "usage instruction."
1. Hierarchy Guards (Strict Nesting)β
The most common check is enforcing parent-child relationships.
#import "@preview/loom:0.1.0": *
#let (motif, weave, context, guards) = construct-loom(<my-lib>)
// CHILD: Ingredient
#let ingredient(name) = motif(
measure: (ctx, _) => {
// RULE: I MUST be inside a 'recipe' component.
// If not, stop compilation immediately.
guards.assert-inside(ctx, "recipe")
( (name: name), none )
},
draw: (ctx, public, view, body) => [ - #name ]
)
// PARENT: Recipe
// We give this component the specific name "recipe"
#let recipe(name, body) = motif(name: "recipe",
draw: (ctx, public, view, body) => block(body),
body
)
Now, if a user tries this:
#ingredient("Salt") // β ERROR: Component must be inside 'recipe'.
They get a clear message telling them exactly what they did wrong.
2. Context Guards (Required Data)β
Sometimes, you don't care where a component is, but you care what data it has.
While the Provider Pattern suggests using defaults (auto), some components simply cannot function without specific data.
#let plot-point(x, y) = data-motif(
"plot-point",
measure: (ctx) => {
// RULE: The coordinate system MUST be defined.
// We can't default this; if it's missing, the plot is invalid.
guards.assert-has-key(ctx, "plot-axis-x")
guards.assert-has-key(ctx, "plot-axis-y")
(x: x, y: y)
},
// ...
)
3. Root Guards (Singletons)β
Some components only make sense if they are the Director (the root of the Loom weave). For example, a book or presentation wrapper.
#let presentation(body) = manged-motif(
"presentation",
measure: (ctx, _) => {
// RULE: I must be the top-level component.
guards.assert-root(ctx)
// ...
},
body
)
Available Guardsβ
The loom.guards module covers the most common architectural constraints:
| Guard | Checks For... | Use Case |
|---|---|---|
assert-inside(ctx, ..names) | Ancestor existence | "Slide must be in Presentation" |
assert-not-inside(ctx, ..names) | Ancestor absence | "Don't put a Chapter inside a Footer" |
assert-direct-parent(ctx, ..names) | Immediate parent | "Tab Item must be directly in Tabs" |
assert-root(ctx) | Being the root | Top-level wrappers |
assert-has-key(ctx, key) | Context data | Mandatory configuration |
assert-max-depth(ctx, n) | Nesting limits | Preventing infinite recursion |
Best Practicesβ
"Fail Loud" vs. "Adapt"β
You now have two opposing patterns:
- Provider Pattern: "If data is missing, use a default." (Adapt)
- Enforcer Pattern: "If data is missing, crash." (Fail Loud)
When to use which?
- Use Enforcers when the usage is invalid. (e.g., A
TabItemoutside ofTabsmakes no sense logically). - Use Providers when the usage is optional. (e.g., A
Buttonoutside of aThemeshould just look boring, not crash).
Guard the measure phaseβ
Always place your guards in the measure function, not draw.
measureruns first, so the error happens sooner.measureis used for logic;drawshould be safe and dumb.
// β
Good
measure: (ctx, _) => {
guards.assert-inside(ctx, "list")
// ...
}