Skip to main content

zenith_core/ast/node/
special.rs

1//! Specialized node structs: the compound shape, the derived connector,
2//! component instances + overrides, the forward-compat unknown node, and the
3//! book-interior furniture (field, footnote, toc).
4
5use std::collections::BTreeMap;
6
7use crate::ast::Span;
8use crate::ast::value::{Dimension, PropertyValue};
9
10use super::common::{Node, TextSpan, UnknownProperty};
11
12/// A `shape` node — a COMPOUND node: a background box that OWNS a centered text
13/// label (like a flowchart process box).
14///
15/// Structurally this mirrors [`TextNode`](super::TextNode): it carries box geometry + visual
16/// properties AND a list of owned label [`TextSpan`]s (NOT child `Node`s). The
17/// background primitive emitted depends on [`ShapeNode::kind`]
18/// (`process`/`decision`/`terminator`/`ellipse`, default `process`). The owned
19/// label text is rendered centered inside the box.
20#[derive(Debug, Clone, PartialEq)]
21pub struct ShapeNode {
22    pub id: String,
23    pub name: Option<String>,
24    pub role: Option<String>,
25    pub x: Option<PropertyValue>,
26    pub y: Option<PropertyValue>,
27    pub w: Option<PropertyValue>,
28    pub h: Option<PropertyValue>,
29    /// Shape kind string (`process`/`decision`/`terminator`/`ellipse`).
30    /// Validated, not enum-typed, so unknown values survive for forward-compat.
31    /// Absent or unrecognized is treated as `"process"` at compile time.
32    pub kind: Option<String>,
33    pub fill: Option<PropertyValue>,
34    pub stroke: Option<PropertyValue>,
35    pub stroke_width: Option<PropertyValue>,
36    /// Corner radius for the `process` rounded-rect (token-required dimension).
37    pub radius: Option<PropertyValue>,
38    /// Stroke alignment (`inside`/`center`/`outside`), same model as `rect`.
39    pub stroke_alignment: Option<String>,
40    /// Text inset inside the box (token-required dimension), applied to the
41    /// owned label.
42    pub padding: Option<PropertyValue>,
43    /// Horizontal label alignment in the box (`start`/`center`/`end`, default
44    /// `center`), applied to the owned label.
45    pub h_align: Option<String>,
46    /// Vertical label alignment in the box (`top`/`middle`/`bottom`, default
47    /// `middle`), applied to the owned label.
48    pub v_align: Option<String>,
49    /// Style ref for the owned label text, applied to the label.
50    pub text_style: Option<String>,
51    /// The owned label spans (same model as a `text` node's spans), rendered
52    /// centered inside the box on top of the background.
53    pub spans: Vec<TextSpan>,
54    /// Box style ref.
55    pub style: Option<String>,
56    pub opacity: Option<f64>,
57    pub visible: Option<bool>,
58    pub locked: Option<bool>,
59    pub rotate: Option<Dimension>,
60    /// Page-relative placement anchor (one of the nine named positions, e.g.
61    /// `"bottom-right"`). When present and recognized, the compile step derives
62    /// the node's x and/or y from the page and node dimensions. An explicitly-
63    /// authored x or y always wins.
64    pub anchor: Option<String>,
65    /// Optional safe-zone reference for the anchor. See [`RectNode::anchor_zone`](super::RectNode::anchor_zone).
66    pub anchor_zone: Option<String>,
67    /// Optional sibling node id for sibling-relative anchor positioning.
68    /// See [`RectNode::anchor_sibling`](super::RectNode::anchor_sibling).
69    pub anchor_sibling: Option<String>,
70    /// Adjacent-placement edge relative to `anchor-sibling`: `above`/`below`/`before`/`after`.
71    /// See [`RectNode::anchor_edge`](super::RectNode::anchor_edge).
72    pub anchor_edge: Option<String>,
73    /// Gap (px) between this node and its `anchor-sibling` edge when `anchor-edge` is set.
74    /// See [`RectNode::anchor_gap`](super::RectNode::anchor_gap).
75    pub anchor_gap: Option<Dimension>,
76    /// Parent-relative anchor toggle. See [`RectNode::anchor_parent`](super::RectNode::anchor_parent).
77    pub anchor_parent: Option<bool>,
78    /// Source declaration span, when available.
79    pub source_span: Option<Span>,
80    /// Unknown properties preserved for forward-compat.
81    pub unknown_props: BTreeMap<String, UnknownProperty>,
82}
83
84/// A `connector` node — a semantic arrow that declares `from`/`to` target node
85/// ids and, at COMPILE time, resolves those targets' bounding boxes to draw a
86/// straight line between anchor points on their edges.
87///
88/// A connector has NO authored geometry (`x`/`y`/`w`/`h`): its endpoints are
89/// DERIVED from the resolved boxes of `from` and `to`, so when a target moves
90/// the connector reroutes automatically (the boxes are recomputed each compile).
91/// It is a stroke-only LEAF: it has a `stroke`/`stroke_width` (no `fill`).
92///
93/// An optional owned **label** is authored as `span` children inside the
94/// connector's `{ … }` block (the same model as a `shape` or `text` node).
95/// When spans are present the label is rendered at the geometric midpoint of the
96/// routed polyline, centered in a small auto-sized text box. `text-style` is
97/// the style ref applied to those spans. When `spans` is empty (the default)
98/// the connector renders exactly as today — no extra output, byte-identical.
99///
100/// Unit 1 renders a STRAIGHT line between the two resolved anchors with NO
101/// arrowhead markers (Unit 2) and NO orthogonal routing (Unit 3); the `route`
102/// and `marker_*` attributes are stored + validated now but render straight /
103/// headless until those units land.
104#[derive(Debug, Clone, PartialEq)]
105pub struct ConnectorNode {
106    pub id: String,
107    pub name: Option<String>,
108    pub role: Option<String>,
109    /// The source target node id (the box the arrow starts from).
110    pub from: Option<String>,
111    /// The destination target node id (the box the arrow points to).
112    pub to: Option<String>,
113    /// Source-edge anchor (`top`/`bottom`/`left`/`right`/`center`/`auto`).
114    /// Absent or unrecognized is treated as `"auto"` at compile time.
115    pub from_anchor: Option<String>,
116    /// Destination-edge anchor (`top`/`bottom`/`left`/`right`/`center`/`auto`).
117    pub to_anchor: Option<String>,
118    /// Routing mode (`straight`(default)/`orthogonal`). Orthogonal is Unit 3:
119    /// stored + validated now, rendered as a straight line until then.
120    pub route: Option<String>,
121    /// Start-cap marker (`none`(default)/`arrow`). Markers are Unit 2: stored +
122    /// validated now, rendered headless until then.
123    pub marker_start: Option<String>,
124    /// End-cap marker (`none`(default)/`arrow`). Markers are Unit 2.
125    pub marker_end: Option<String>,
126    pub stroke: Option<PropertyValue>,
127    pub stroke_width: Option<PropertyValue>,
128    pub opacity: Option<f64>,
129    pub visible: Option<bool>,
130    pub locked: Option<bool>,
131    pub rotate: Option<Dimension>,
132    pub style: Option<String>,
133    /// Style ref applied to the owned label text (mirrors `ShapeNode::text_style`).
134    /// `None` when no label style is authored (label inherits document defaults).
135    pub text_style: Option<String>,
136    /// The owned label spans rendered at the connector's midpoint. Empty (the
137    /// default) means no label — the connector renders exactly as a span-less
138    /// connector. Same model as `ShapeNode::spans` and `TextNode::spans`.
139    pub spans: Vec<TextSpan>,
140    /// Source declaration span, when available.
141    pub source_span: Option<Span>,
142    /// Unknown properties preserved for forward-compat.
143    pub unknown_props: BTreeMap<String, UnknownProperty>,
144}
145
146/// An unrecognized node kind, preserved for forward-compat.
147///
148/// When a `.zen` document contains a node kind that this binary does not
149/// recognise (e.g. authored with a newer version), the node is wrapped in this
150/// variant instead of triggering a hard error.
151#[derive(Debug, Clone, PartialEq)]
152pub struct UnknownNode {
153    /// The KDL node name (e.g. `"sparkle"`, `"table"`, `"chart"`).
154    pub kind: String,
155    /// The node's `id` attribute, if present. Captured first-class so unknown
156    /// nodes are addressable and participate in duplicate-id detection.
157    pub id: Option<String>,
158    /// All other attributes, preserved with typed values + annotations.
159    pub unknown_props: BTreeMap<String, UnknownProperty>,
160    /// Child nodes (may be known OR unknown), preserved for lossless round-trip.
161    pub children: Vec<Node>,
162    /// Source declaration span, when available.
163    pub source_span: Option<Span>,
164}
165
166/// An instance-local override applied to a single descendant of the referenced
167/// component when an [`InstanceNode`] is expanded at compile time.
168///
169/// An `override` is an `override ref="<local-descendant-id>" { … }` child of an
170/// instance. `ref_id` names a descendant by its component-LOCAL id (the id as
171/// declared inside the [`ComponentDef`](crate::ast::ComponentDef), before instance-id prefixing).
172///
173/// v0 supported override set (documented; richer overrides are a follow-up):
174/// - `spans` — replaces the target text node's `spans` wholesale (the override's
175///   `span` children become the target's new spans).
176/// - `fill` — replaces the target node's `fill` visual property.
177/// - `visible` — replaces the target node's `visible` flag.
178///
179/// Each field is `None` when the override does not touch that aspect; a `None`
180/// field leaves the corresponding property on the cloned target untouched.
181#[derive(Debug, Clone, PartialEq)]
182pub struct Override {
183    /// The component-LOCAL id of the descendant this override targets.
184    pub ref_id: String,
185    /// Replacement text spans (only meaningful for a text target).
186    pub spans: Option<Vec<TextSpan>>,
187    /// Replacement fill (color token ref or literal — validated like any fill).
188    pub fill: Option<PropertyValue>,
189    /// Replacement visibility flag.
190    pub visible: Option<bool>,
191    /// Source declaration span, when available.
192    pub source_span: Option<Span>,
193}
194
195/// An `instance` node — a placement of a declared [`ComponentDef`](crate::ast::ComponentDef) at an origin
196/// `(x, y)`, with an optional opacity/visible cascade and instance-local
197/// overrides.
198///
199/// At compile time the instance expands to the component's child subtree treated
200/// as a GROUP translated by `(x, y)`, cascading `opacity`/`visible` exactly like
201/// a [`GroupNode`](super::GroupNode). Every expanded descendant id is PREFIXED with the instance id
202/// (`<instance-id>/<local-id>`) so multiple instances of the same component never
203/// collide. The instance node itself emits no scene command; its expanded subtree
204/// does. Expansion happens at COMPILE time only — the instance stays a single node
205/// in the canonical AST so parse→format→parse round-trips.
206#[derive(Debug, Clone, PartialEq)]
207pub struct InstanceNode {
208    pub id: String,
209    pub name: Option<String>,
210    pub role: Option<String>,
211    /// The referenced [`ComponentDef`](crate::ast::ComponentDef) id.
212    pub component: String,
213    /// Instance origin x-translation applied to the expanded subtree (default 0).
214    pub x: Option<Dimension>,
215    /// Instance origin y-translation applied to the expanded subtree (default 0).
216    pub y: Option<Dimension>,
217    /// Opacity that cascades (multiplies) into all expanded descendant alphas.
218    pub opacity: Option<f64>,
219    /// When `Some(false)` the entire expanded subtree is excluded from the render.
220    pub visible: Option<bool>,
221    pub locked: Option<bool>,
222    /// Instance-local overrides applied to component descendants on expansion.
223    pub overrides: Vec<Override>,
224    /// Source declaration span, when available.
225    pub source_span: Option<Span>,
226    /// Unknown properties preserved for forward-compat.
227    pub unknown_props: BTreeMap<String, UnknownProperty>,
228}
229
230/// A `field` node — an auto-resolved text placeholder for book interiors.
231///
232/// A field is a LEAF that, at compile time, resolves to a single-line text run
233/// against the page it is projected onto. It is the building block of the
234/// master-page / running-head / folio system: a master declares a field once
235/// (e.g. a running head or a page-number) and every page that uses the master
236/// gets the field resolved against that page's index and parity.
237///
238/// Field types (v0):
239/// - `"running-head"` → renders [`FieldNode::recto`] on odd (recto) pages and
240///   [`FieldNode::verso`] on even (verso) pages; an absent side renders nothing.
241/// - `"page-number"` → renders the page's folio (its 1-based index in
242///   `doc.body.pages`) as a decimal string.
243/// - `"page-ref"` → renders the 1-based page index of the page that CONTAINS the
244///   node whose id equals [`FieldNode::target`] (document-wide search). A missing
245///   target produces an advisory `field.unresolved_ref` and renders nothing.
246///
247/// Geometry: when `x`/`w` are omitted the field defaults to the page's live
248/// area (so a running head auto-mirrors recto/verso x via the page margins).
249/// `y`/`h` default to the live area's top/height when omitted. The resolved run
250/// is shaped like a single-line text node: `running-head` / `page-number`
251/// default to `align="center"`, `page-ref` to `align="start"`.
252#[derive(Debug, Clone, PartialEq)]
253pub struct FieldNode {
254    pub id: String,
255    pub name: Option<String>,
256    pub role: Option<String>,
257    /// The field kind string (`"running-head"`/`"page-number"`/`"page-ref"`).
258    /// Validated, not enum-typed, so unknown values survive for forward-compat.
259    pub field_type: String,
260    /// Recto-side text for a `running-head` field (odd, 1-based pages).
261    pub recto: Option<String>,
262    /// Verso-side text for a `running-head` field (even pages).
263    pub verso: Option<String>,
264    /// Target node id for a `page-ref` field.
265    pub target: Option<String>,
266    /// Folio numbering style for numeric fields (`page-number`, `page-count`,
267    /// `page-ref`): `"decimal"` (default), `"lower-roman"`, or `"upper-roman"`.
268    /// Ignored by `running-head`. Unknown values fall back to decimal.
269    pub folio_style: Option<String>,
270    /// When `true`, a numeric field renders nothing on document page 1 (the
271    /// title page). Used to suppress the folio on the first page.
272    pub suppress_first: Option<bool>,
273    pub x: Option<PropertyValue>,
274    pub y: Option<PropertyValue>,
275    pub w: Option<PropertyValue>,
276    pub h: Option<PropertyValue>,
277    pub style: Option<String>,
278    pub fill: Option<PropertyValue>,
279    pub font_family: Option<PropertyValue>,
280    pub font_size: Option<PropertyValue>,
281    pub opacity: Option<f64>,
282    pub visible: Option<bool>,
283    pub locked: Option<bool>,
284    /// Page-relative placement anchor (one of the nine named positions, e.g.
285    /// `"bottom-right"`). When present and recognized, the compile step derives
286    /// the node's x and/or y from the page and node dimensions. An explicitly-
287    /// authored x or y always wins.
288    pub anchor: Option<String>,
289    /// Optional safe-zone reference for the anchor. See [`RectNode::anchor_zone`](super::RectNode::anchor_zone).
290    pub anchor_zone: Option<String>,
291    /// Optional sibling node id for sibling-relative anchor positioning.
292    /// See [`RectNode::anchor_sibling`](super::RectNode::anchor_sibling).
293    pub anchor_sibling: Option<String>,
294    /// Adjacent-placement edge relative to `anchor-sibling`: `above`/`below`/`before`/`after`.
295    /// See [`RectNode::anchor_edge`](super::RectNode::anchor_edge).
296    pub anchor_edge: Option<String>,
297    /// Gap (px) between this node and its `anchor-sibling` edge when `anchor-edge` is set.
298    /// See [`RectNode::anchor_gap`](super::RectNode::anchor_gap).
299    pub anchor_gap: Option<Dimension>,
300    /// Parent-relative anchor toggle. See [`RectNode::anchor_parent`](super::RectNode::anchor_parent).
301    pub anchor_parent: Option<bool>,
302    /// Source declaration span, when available.
303    pub source_span: Option<Span>,
304    /// Unknown properties preserved for forward-compat.
305    pub unknown_props: BTreeMap<String, UnknownProperty>,
306}
307
308/// A `footnote` node — page-level book-interior furniture that auto-numbers and
309/// renders in a reserved zone at the bottom of the page.
310///
311/// A footnote is NOT positioned by the author: it has NO `x`/`y`/`w`/`h`. At
312/// compile time every `footnote` that is a DIRECT child of a [`Page`](crate::ast::Page) is
313/// collected in source order, auto-numbered `1..N` (a footnote that declares an
314/// explicit [`marker`](FootnoteNode::marker) uses that string instead of a
315/// number but still occupies a slot), and rendered stacked above the page's
316/// bottom margin with a separator rule. A [`TextSpan`] that carries a matching
317/// [`footnote_ref`](TextSpan::footnote_ref) gets the footnote's marker emitted
318/// inline as a superscript after its text.
319///
320/// KDL: `footnote id="fn.1" { span "See also Chapter 4." }`. The content is a
321/// list of [`TextSpan`]s (the same span model as a `text` node), so it inherits
322/// the text shaping/wrap path verbatim.
323#[derive(Debug, Clone, PartialEq)]
324pub struct FootnoteNode {
325    pub id: String,
326    pub name: Option<String>,
327    pub role: Option<String>,
328    /// Explicit marker override. When `Some(s)`, the footnote renders `s` as its
329    /// marker (both inline and in the zone) instead of its auto-number; the
330    /// footnote still occupies a numbering slot. `None` → use the auto-number.
331    pub marker: Option<String>,
332    /// The footnote's content spans (same model as a `text` node's spans).
333    pub spans: Vec<TextSpan>,
334    pub style: Option<String>,
335    /// Fill for the footnote content + the separator rule. `None` → a sensible
336    /// muted default for the rule and opaque black for the text.
337    pub fill: Option<PropertyValue>,
338    pub font_family: Option<PropertyValue>,
339    pub font_size: Option<PropertyValue>,
340    /// Source declaration span, when available.
341    pub source_span: Option<Span>,
342    /// Unknown properties preserved for forward-compat.
343    pub unknown_props: BTreeMap<String, UnknownProperty>,
344}
345
346/// A `toc` node — a compile-time table-of-contents placeholder.
347///
348/// A `toc` is a LEAF that, at compile time, resolves to a multi-line
349/// tab-leader text block by collecting all heading nodes across the whole
350/// document that match its selector (`match-role` and/or `match-style`).
351/// Each row in the output is formatted as:
352/// `{heading text}\t{page number}`, joined by newlines.
353///
354/// The synthesised [`TextNode`](super::TextNode) uses `tab-leader` mode so the text engine
355/// fills the gap between heading text and page number with the leader glyph
356/// (default `"."`), and right-aligns the page number.
357///
358/// At least one of `match_role` or `match_style` must be set; when both are
359/// absent the toc collects nothing and an advisory `toc.no_selector` is
360/// emitted by the validator.
361#[derive(Debug, Clone, PartialEq)]
362pub struct TocNode {
363    pub id: String,
364    pub name: Option<String>,
365    pub role: Option<String>,
366    /// Select heading nodes whose `role` equals this. `None` = no role filter.
367    pub match_role: Option<String>,
368    /// Select heading nodes whose `style` equals this. `None` = no style filter.
369    pub match_style: Option<String>,
370    /// Leader glyph for the dotted fill between title and page number
371    /// (default `"."` when omitted).
372    pub leader: Option<String>,
373    /// Folio numbering style for the page numbers
374    /// (`"decimal"` / `"lower-roman"` / `"upper-roman"`).
375    pub folio_style: Option<String>,
376    pub x: Option<PropertyValue>,
377    pub y: Option<PropertyValue>,
378    pub w: Option<PropertyValue>,
379    pub h: Option<PropertyValue>,
380    pub style: Option<String>,
381    pub fill: Option<PropertyValue>,
382    pub font_family: Option<PropertyValue>,
383    pub font_size: Option<PropertyValue>,
384    pub opacity: Option<f64>,
385    pub visible: Option<bool>,
386    pub locked: Option<bool>,
387    /// Page-relative placement anchor (one of the nine named positions, e.g.
388    /// `"bottom-right"`). When present and recognized, the compile step derives
389    /// the node's x and/or y from the page and node dimensions. An explicitly-
390    /// authored x or y always wins.
391    pub anchor: Option<String>,
392    /// Optional safe-zone reference for the anchor. See [`RectNode::anchor_zone`](super::RectNode::anchor_zone).
393    pub anchor_zone: Option<String>,
394    /// Optional sibling node id for sibling-relative anchor positioning.
395    /// See [`RectNode::anchor_sibling`](super::RectNode::anchor_sibling).
396    pub anchor_sibling: Option<String>,
397    /// Adjacent-placement edge relative to `anchor-sibling`: `above`/`below`/`before`/`after`.
398    /// See [`RectNode::anchor_edge`](super::RectNode::anchor_edge).
399    pub anchor_edge: Option<String>,
400    /// Gap (px) between this node and its `anchor-sibling` edge when `anchor-edge` is set.
401    /// See [`RectNode::anchor_gap`](super::RectNode::anchor_gap).
402    pub anchor_gap: Option<Dimension>,
403    /// Parent-relative anchor toggle. See [`RectNode::anchor_parent`](super::RectNode::anchor_parent).
404    pub anchor_parent: Option<bool>,
405    /// Source declaration span, when available.
406    pub source_span: Option<Span>,
407    /// Unknown properties preserved for forward-compat.
408    pub unknown_props: BTreeMap<String, UnknownProperty>,
409}