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}