Theme
Version
GitHub PyPI Discord
On this page

Nodes

The runtime node classes the compiled template instantiates.

View source

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.

View source

render function

render(context: CitryContext) -> RenderPart
View source

collect_fills function

collect_fills(context: CitryContext, sink: FillSink) -> None

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

View source

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-card stays my-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).

View source

render function

render(context: CitryContext) -> DeferredComponent

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

View source

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-bind contributes each entry of its mapping (which must be a Mapping).
  • class and style merge across contributions and accept the structured value forms (string / dict / nested list); every other key resolves last-one-wins.
  • True renders the bare attribute, False and None omit 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",))
View source

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",))
View source

evaluate function

evaluate(variables: Mapping[str, Any], sandboxed: bool = True) -> Any

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.

View source

render function

render(context: CitryContext) -> RenderPart
View source

collect_fills function

collect_fills(context: CitryContext, sink: FillSink) -> None
View source

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 a ForNode with 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.

View source

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

View source

render function

render(context: CitryContext) -> CitryRender

Render the loop body once per item; the empty branch if there are none.

See iter_bodies for the loop evaluation and scoping rules.

View source

collect_fills function

collect_fills(context: CitryContext, sink: FillSink) -> None

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

View source

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 an IfNode with 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.

View source

active_branch_body function

active_branch_body(context: CitryContext) -> list[BodyItem] | None

Return 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).

View source

render function

render(context: CitryContext) -> CitryRender

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

View source

collect_fills function

collect_fills(context: CitryContext, sink: FillSink) -> None

In a fill group, an <c-if> contributes the fills of its matching branch.

View source

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.

View source

render function

render(context: CitryContext) -> RenderPart

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

View source

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.

View source

collect_fills function

collect_fills(context: CitryContext, sink: FillSink) -> None

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

View source

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 the c-body value to::

TemplateNode(source, (13, 33,), "<span>{{ x }}</span>", ("x",))
View source

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.

View source

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",))
View source

resolve function

resolve(context: CitryContext) -> Any

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

View source

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", ())
View source

resolve function

resolve(context: CitryContext) -> Any

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

View source

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",))
View source

resolve function

resolve(context: CitryContext) -> CitryRender

Render 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).

Citry version: 0.3.0