Nodes
The runtime node classes the compiled template instantiates.
Node class
Base class for the runtime nodes the template compiler output instantiates.
A node renders to a body part (a str or a nested CitryRender) against the render-scoped CitryContext. Concrete nodes override render.
A node sitting in a fill group (a component body that contains <c-fill> tags) takes part in fill collection through Node.collect_fills() instead of Node.render(). The default says the node is not allowed there; nodes that are (FillNode, the control-flow nodes) override it.
collect_fills function
collect_fills(context: CitryContext, sink: FillSink) -> NoneRegister this node's fills into sink.
Called instead of render when the node sits in a fill group. The base implementation rejects the node: when a component body contains <c-fill> tags, all other content must be inside the fills. A node kind that may sit beside fills (for example one injected by an extension via on_template_compiled) overrides this to register its fills with sink.add(...), recursing into its own bodies with collect_fills_from_body.
ComponentNode class
Bases: Node
A component node (<c-Card>, <c-component>, any <c-*>).
Generated as::
ComponentNode(source, (start, end), (attrs,...), [body], (used_vars,), "name", contains_fills)
Example
Template
<c-Card title="Hi">body</c-Card>produces::ComponentNode( source, (0, 21,), (StaticHtmlAttr(source, (8, 18,), "title", "Hi", ()),), ["body"], (), "card", False, )Component names are lowercased (
Card->card); kebab names are preserved (my-cardstaysmy-card).
A tag carrying the #c-key framework attribute appends one trailing argument, the key expression as an ExprHtmlAttr; unkeyed tags emit no argument and key defaults to None. The key never joins attrs (so it can never become a kwarg); its evaluated value travels as CitryElement.morph_key and is stamped onto the child's root element(s) as the data-citry-key attribute (see docs/design/events.md section 5.3).
render function
render(context: CitryContext) -> DeferredComponentWork out the child's inputs, but don't render the child yet.
This turns the tag's attributes into the child's kwargs, collects the body into the child's slots, and returns a DeferredComponent. It does not render the child here: doing so would make one component render the next and so on, hitting Python's recursion limit on deeply nested pages. render_impl renders the child later, with its own CitryContext, and copies its dependencies into the parent.
The attributes and fill structure are read now, while this component is still rendering, so a loop variable from an enclosing <c-for> has the right value. Fill bodies stay lazy: each becomes a Slot that closes over the current scope and renders only when the child invokes it.
ElementAttrsNode class
Bases: Node
The attribute region of a plain HTML start tag with dynamic attributes.
Generated as: ElementAttrsNode(source, (start, end), (attrs...), ("var1", ...))
Emitted when an HTML element (not a component) has at least one dynamic attribute: a c-* value or a c-bind spread. The node covers ALL of the tag's attributes, static ones included, because the set resolves as one unit:
- Contributions collect left to right in source order;
c-bindcontributes each entry of its mapping (which must be aMapping). classandstylemerge across contributions and accept the structured value forms (string / dict / nested list); every other key resolves last-one-wins.Truerenders the bare attribute,FalseandNoneomit it, everything else renders escaped (__html__values pass through).
Renders to one string like ' class="btn" disabled' (leading space included) or "" when every attribute resolved away.
Example
Template
<div id="x" c-class="cls">hi</div>produces::ElementAttrsNode(source, (0, 26,), (StaticHtmlAttr(...), ExprHtmlAttr(...),), ("cls",))
tag_name attribute
tag_name: strThe element's tag name (e.g. "div"), read from the source slice.
ExprNode class
Bases: Node
A {{ expr }} expression node.
Generated as: ExprNode(source, (start, end), "expr", ("var1", ...))
Example
Template
{{ name }}compiles to::ExprNode(source, (0, 10,), "name ", ("name",))
evaluate function
Evaluate the expression against variables and return the raw value.
The compiled evaluator is built on first use and reused (the node is cached across renders, so the expression compiles once). sandboxed chooses the security sandbox or plain evaluation; it is read only on the first call, when the evaluator is compiled, and ignored afterwards (the instance's setting is fixed, so every call passes the same value). Called by render, and by the Const optimization (citry/constness.py), which evaluates an expression ahead of time when all of its variables are marked constant.
ForNode class
Bases: Node
A loop node (<c-for>/<c-empty>).
Generated as: ForNode(source, (for_branch, empty_branch?), (used_vars,))
Each branch is a tuple: ((start, end), (attrs,), [body], (introduced_vars,))
Example
Template
<c-for each="item in items">{{ item }}</c-for>produces aForNodewith one branch. Adding<c-empty>none</c-empty>after it adds a second branch for the empty state.
Each branch is ((start, end), (attrs,), [body], (introduced_vars,)). The loop branch carries an each attribute holding a Python comprehension clause ("item in items", or the full "x in xs for y in ys if ..."); introduced_vars are the loop targets it binds.
iter_bodies function
iter_bodies(context: CitryContext) -> Iterator[tuple[list[BodyItem], CitryContext]]Yield (body, context) once per loop iteration.
The each clause is a Python comprehension clause, so the loop is evaluated by wrapping it in a generator expression that yields the loop targets as a tuple: each="x in xs if x > 0" becomes ((x,) for x in xs if x > 0). This reuses Python's own comprehension semantics, so multi-target unpacking and if filters work for free.
Each iteration's context overlays the loop bindings on the surrounding variables; it shares the parent's component and extra bag, so the loop introduces a variable scope without crossing a component boundary. With no iterations, the optional <c-empty> branch's body is yielded once, with the surrounding context.
Shared by render and by fill collection (ComponentNode walks the iterations when gathering <c-fill> tags, so each collected fill closes over its own iteration's bindings).
render function
render(context: CitryContext) -> CitryRenderRender the loop body once per item; the empty branch if there are none.
See iter_bodies for the loop evaluation and scoping rules.
collect_fills function
collect_fills(context: CitryContext, sink: FillSink) -> NoneIn a fill group, a <c-for> contributes its fills once per iteration.
Each iteration's fills close over that iteration's loop bindings, so a fill body using the loop variable keeps the right value no matter when the child invokes it.
IfNode class
Bases: Node
A conditional node (<c-if>/<c-elif>/<c-else>).
Generated as: IfNode(source, (branch1, branch2, ...), (used_vars,))
Each branch is a tuple: ((start, end), (attrs,), [body], (introduced_vars,))
Example
Template
<c-if cond="x">yes</c-if><c-else>no</c-else>produces anIfNodewith two branches - one for the if-body and one for the else-body.
Each branch is ((start, end), (attrs,), [body], (introduced_vars,)). The c-if/c-elif branches carry a cond attribute (an ExprHtmlAttr); the c-else branch has none and always matches.
active_branch_body function
active_branch_body(context: CitryContext) -> list[BodyItem] | NoneReturn the body of the first branch that matches, or None.
Branches are tried in source order (c-if then each c-elif then c-else). A branch's cond attribute is resolved against the context; the first truthy one wins. A branch with no cond (the c-else) always matches.
Shared by render and by fill collection (ComponentNode walks the matching branch when gathering <c-fill> tags).
render function
render(context: CitryContext) -> CitryRenderRender the first branch whose cond is truthy.
If no branch matches, the render is empty. The body renders against the surrounding context unchanged: an <c-if> introduces no variables, so there is no new scope.
collect_fills function
collect_fills(context: CitryContext, sink: FillSink) -> NoneIn a fill group, an <c-if> contributes the fills of its matching branch.
SlotNode class
Bases: Node
A slot definition (<c-slot>): the insertion point for slot content.
Generated as::
SlotNode(source, (start, end), (attrs,), [body], (used_vars,), (introduced_vars,))
Example
Template
<c-slot name="header" />produces::SlotNode(source, (0, 24,), (StaticHtmlAttr(...),), [], (), ())
Rendering resolves the slot name, looks up the fill the component received, and invokes it with the slot data; with no fill, the slot's own body renders as the fallback.
render function
render(context: CitryContext) -> RenderPartRender the fill given for this slot, or the slot's own body as fallback.
The slot data (the tag's extra attributes) resolves against the current context per render of this site, so a slot inside a loop passes per-iteration data. The fill and the fallback render through the same path: both are Slots, invoked with (data, fallback). A fill renders against the scope where it was written (it closed over it at collection); the fallback body renders against the current context, as if the <c-slot> tags were not there.
A required slot with no fill raises, with a "did you mean" hint over the fills the component received.
FillNode class
Bases: Node
A slot fill (<c-fill>).
Generated as::
FillNode(source, (start, end), (attrs,), [body], (used_vars,), (introduced_vars,))
Example
Template
<c-fill name="header">content</c-fill>produces::FillNode(source, (0, 40,), (StaticHtmlAttr(...),), ["content"], (), ())
A fill is consumed during fill collection (collect_fills wraps its body as a Slot and registers it), so it is never rendered as output; it inherits Node.render raising, and reaching it would mean a parser/runtime bug.
collect_fills function
collect_fills(context: CitryContext, sink: FillSink) -> NoneResolve this fill's attributes and register its body as a Slot.
The Slot closes over context, the scope where the fill was written (including any loop bindings from an enclosing <c-for>); the body stays unrendered until the child component invokes the slot.
TemplateNode class
Bases: Node
A nested template value on an HTML tag's dynamic attribute.
Emitted when a c-* attribute value is itself a template (starts with a tag and ends with a closing tag), as opposed to a plain expression (which becomes an ExprNode). The expr field holds the nested template source string.
Generated as: TemplateNode(source, (start, end), "template", ("var1", ...))
Example
Template
<div c-body="<span>{{ x }}</span>">compiles thec-bodyvalue to::TemplateNode(source, (13, 33,), "<span>{{ x }}</span>", ("x",))
HtmlAttr class
Base class for HTML attribute nodes (a component's or slot's inputs).
An attribute resolves to a value (which becomes a component kwarg), not to a rendered body part. Concrete attributes override resolve.
ExprHtmlAttr class
Bases: HtmlAttr
A dynamic expression attribute (c-class="expr").
Generated as: ExprHtmlAttr(source, (start, end), "c-class", "expr", ("var",))
Example
Template
<c-Card c-title="t" />produces::ExprHtmlAttr(source, (8, 19,), "c-title", "t", ("t",))
resolve function
resolve(context: CitryContext) -> AnyEvaluate the expression and return the raw value.
The value is NOT escaped or stringified: it becomes a component kwarg (a Python object). Escaping happens later, when the child component renders the value through an ExprNode. The value is returned without the Const marker; for an expression that uses no variables (a literal written in the template), ComponentNode._resolve_inputs adds the marker where the value becomes a component input.
StaticHtmlAttr class
Bases: HtmlAttr
A static HTML attribute (key="value").
Generated as: StaticHtmlAttr(source, (start, end), "key", "value", ())
Example
Template
<c-Card title="Hello" />produces::StaticHtmlAttr(source, (8, 21,), "title", "Hello", ())
resolve function
resolve(context: CitryContext) -> AnyReturn the static value (a string, or True for a boolean attribute).
The value is returned as-is, without the Const marker ("this is the same on every render"). Attribute values serve double duty: they can be slot and fill names, provide keys, or component inputs, and only the component-input use benefits from the marker. So the marking happens in ComponentNode._resolve_inputs, where the value becomes a component input, not here.
TemplateHtmlAttr class
Bases: HtmlAttr
A nested template attribute (c-body="<div>...</div>").
Generated as: TemplateHtmlAttr(source, (start, end), "c-body", "<div>...</div>", ("var",))
Example
Template
<c-Card c-body="<span>{{ x }}</span>" />produces::TemplateHtmlAttr(source, (8, 37,), "c-body", "<span>{{ x }}</span>", ("x",))
resolve function
resolve(context: CitryContext) -> CitryRenderRender the nested template and return it as a CitryRender kwarg value.
The template is defined in the parent's scope, so it renders against the surrounding component's context (the same rule as TemplateNode).