Skip to main content

zenith_core/ast/node/
leaf.rs

1//! Leaf node structs: shapes and text-bearing primitives that have no child
2//! `Node`s of their own (rect, line, ellipse, image, text, code, polygon,
3//! polyline, pattern, chart).
4
5use std::collections::BTreeMap;
6
7use crate::ast::Span;
8use crate::ast::block_style::BlockStyle;
9use crate::ast::value::{Dimension, PropertyValue};
10use crate::tokens::SyntaxTheme;
11
12use super::common::{Node, ObjectPosition, Point, TextSpan, UnknownProperty};
13
14/// An `image` node — a LEAF that draws a raster (PNG) asset into a declared
15/// `[x, y, w, h]` box with a `fit` mode, ALWAYS clipped to that box
16/// (normative image box-clip).
17///
18/// The `asset` field references an [`AssetDecl`](crate::ast::AssetDecl) by its
19/// stable id, declared in the document's `assets {}` block.
20#[derive(Debug, Clone, PartialEq)]
21pub struct ImageNode {
22    pub id: String,
23    pub name: Option<String>,
24    pub role: Option<String>,
25    /// Required: the referenced asset id (matches an `AssetDecl.id`).
26    pub asset: String,
27    pub x: Option<PropertyValue>,
28    pub y: Option<PropertyValue>,
29    pub w: Option<PropertyValue>,
30    pub h: Option<PropertyValue>,
31    /// Optional source-sub-rectangle: left edge within the source image (pixels).
32    /// All four src-* fields must be present together; partial presence is a hard
33    /// error (`image.partial_src_rect`). Absent ⇒ the full source image is used.
34    pub src_x: Option<Dimension>,
35    /// Source-sub-rectangle: top edge within the source image (pixels).
36    pub src_y: Option<Dimension>,
37    /// Source-sub-rectangle: width within the source image (pixels, must be > 0).
38    pub src_w: Option<Dimension>,
39    /// Source-sub-rectangle: height within the source image (pixels, must be > 0).
40    pub src_h: Option<Dimension>,
41    /// Fit mode string (`contain`/`cover`/`stretch`/`none`); validated, not
42    /// enum-typed in the AST so unknown values survive for forward-compat.
43    pub fit: Option<String>,
44    /// Clip-to-shape mode (`"ellipse"`/`"rounded"`/`"rect"`); absent or an
45    /// unrecognized value means the default rectangular box-clip. Validated as a
46    /// plain string so unknown values survive for forward-compat.
47    pub clip: Option<String>,
48    /// Corner radius for `clip="rounded"`, as a `(token)` dimension ref. Only
49    /// meaningful when `clip="rounded"`; absent → radius 0 (sharp corners).
50    pub clip_radius: Option<PropertyValue>,
51    /// Horizontal object-position anchor (string anchor or `(pct)N`).
52    pub object_position_x: Option<ObjectPosition>,
53    /// Vertical object-position anchor (string anchor or `(pct)N`).
54    pub object_position_y: Option<ObjectPosition>,
55    pub opacity: Option<f64>,
56    /// Drop shadow / outer glow, as a `(token)` ref to a `shadow` token.
57    pub shadow: Option<PropertyValue>,
58    /// Color/image filter ops, as a `(token)` ref to a `filter` token.
59    pub filter: Option<PropertyValue>,
60    /// Spatial coverage mask, as a `(token)` ref to a `mask` token.
61    pub mask: Option<PropertyValue>,
62    /// Compositing blend mode: `"normal"` (default) or one of the 11 separable
63    /// blends. `None`/`"normal"` render source-over (byte-identical).
64    pub blend_mode: Option<String>,
65    /// Gaussian blur radius applied to the node's own rendered ink (sigma in
66    /// the declared unit, resolved to pixels at compile time). `None` / 0 →
67    /// no blur (byte-identical to having no attribute).
68    pub blur: Option<Dimension>,
69    pub visible: Option<bool>,
70    pub locked: Option<bool>,
71    pub rotate: Option<Dimension>,
72    pub style: Option<String>,
73    /// Page-relative placement anchor (one of the nine named positions, e.g.
74    /// `"bottom-right"`). When present and recognized, the compile step derives
75    /// the node's x and/or y from the page and node dimensions. An explicitly-
76    /// authored x or y always wins.
77    pub anchor: Option<String>,
78    /// Optional safe-zone id selecting the reference rectangle for `anchor`
79    /// (page-relative when absent). See [`Anchor`](super::Anchor).
80    pub anchor_zone: Option<String>,
81    /// Optional sibling node id for sibling-relative anchor positioning.
82    /// See [`RectNode::anchor_sibling`].
83    pub anchor_sibling: Option<String>,
84    /// Adjacent-placement edge relative to `anchor-sibling`: `above`/`below`/`before`/`after`.
85    /// See [`RectNode::anchor_edge`].
86    pub anchor_edge: Option<String>,
87    /// Gap (px) between this node and its `anchor-sibling` edge when `anchor-edge` is set.
88    /// See [`RectNode::anchor_gap`].
89    pub anchor_gap: Option<Dimension>,
90    /// Parent-relative anchor toggle. See [`RectNode::anchor_parent`].
91    pub anchor_parent: Option<bool>,
92    /// Source declaration span, when available.
93    pub source_span: Option<Span>,
94    /// Unknown properties preserved for forward-compat.
95    pub unknown_props: BTreeMap<String, UnknownProperty>,
96}
97
98/// A `rect` node.
99#[derive(Debug, Clone, PartialEq)]
100pub struct RectNode {
101    pub id: String,
102    pub name: Option<String>,
103    pub role: Option<String>,
104    pub x: Option<PropertyValue>,
105    pub y: Option<PropertyValue>,
106    pub w: Option<PropertyValue>,
107    pub h: Option<PropertyValue>,
108    pub radius: Option<PropertyValue>,
109    /// Per-corner radius overrides (top-left, top-right, bottom-right, bottom-left).
110    /// When `Some`, the value overrides the uniform `radius` for that corner only.
111    /// When `None`, the uniform `radius` applies. All four are `None` for existing docs.
112    pub radius_tl: Option<PropertyValue>,
113    pub radius_tr: Option<PropertyValue>,
114    pub radius_br: Option<PropertyValue>,
115    pub radius_bl: Option<PropertyValue>,
116    pub style: Option<String>,
117    pub fill: Option<PropertyValue>,
118    pub stroke: Option<PropertyValue>,
119    pub stroke_width: Option<PropertyValue>,
120    pub stroke_alignment: Option<String>,
121    /// Dash segment length in pixels; `None` = solid stroke.
122    pub stroke_dash: Option<PropertyValue>,
123    /// Gap length in pixels between dashes; defaults to `stroke_dash` when absent.
124    pub stroke_gap: Option<PropertyValue>,
125    /// Dash end-cap style: `"butt"` (default), `"round"`, or `"square"`.
126    pub stroke_linecap: Option<String>,
127    /// Per-side border color for the top edge. Token-required (color token).
128    /// When `Some`, a `StrokeLine` is emitted along the top edge of the rect.
129    pub border_top: Option<PropertyValue>,
130    /// Per-side border color for the bottom edge. Token-required (color token).
131    pub border_bottom: Option<PropertyValue>,
132    /// Per-side border color for the left edge. Token-required (color token).
133    pub border_left: Option<PropertyValue>,
134    /// Per-side border color for the right edge. Token-required (color token).
135    pub border_right: Option<PropertyValue>,
136    /// Shared border width for per-side borders. Token-required (dimension).
137    /// Falls back to `stroke_width`, then to 1px when absent.
138    pub border_width: Option<PropertyValue>,
139    /// Outer stroke color: a SECOND stroke painted OUTSIDE the rect geometry.
140    /// Token-required (color token). When `Some`, a `StrokeRect` /
141    /// `StrokeRoundedRect` is emitted at outset geometry in addition to the
142    /// primary stroke. `None` → no outer stroke (byte-identical).
143    pub stroke_outer: Option<PropertyValue>,
144    /// Outer stroke width for `stroke_outer`. Token-required (dimension).
145    /// Defaults to 1px when absent. Only effective when `stroke_outer` is set.
146    pub stroke_outer_width: Option<PropertyValue>,
147    /// Drop shadow / outer glow, as a `(token)` ref to a `shadow` token.
148    pub shadow: Option<PropertyValue>,
149    /// Color/image filter ops, as a `(token)` ref to a `filter` token.
150    pub filter: Option<PropertyValue>,
151    /// Spatial coverage mask, as a `(token)` ref to a `mask` token.
152    pub mask: Option<PropertyValue>,
153    /// Compositing blend mode: `"normal"` (default) or one of the 11 separable
154    /// blends (`multiply`, `screen`, `overlay`, …). `None`/`"normal"` render
155    /// source-over (byte-identical to having no blend).
156    pub blend_mode: Option<String>,
157    /// Gaussian blur radius applied to the node's own rendered ink (sigma in
158    /// the declared unit, resolved to pixels at compile time). `None` / 0 →
159    /// no blur (byte-identical to having no attribute).
160    pub blur: Option<Dimension>,
161    pub opacity: Option<f64>,
162    pub visible: Option<bool>,
163    pub locked: Option<bool>,
164    pub rotate: Option<Dimension>,
165    /// Page-relative placement anchor (one of the nine named positions, e.g.
166    /// `"bottom-right"`). When present and recognized, the compile step derives
167    /// the node's x and/or y from the page and node dimensions. An explicitly-
168    /// authored x or y always wins.
169    pub anchor: Option<String>,
170    /// Optional safe-zone reference for the anchor. When `Some(id)` and a
171    /// safe-zone with that id is declared on the page, the `anchor` is resolved
172    /// relative to that zone's rectangle instead of the full page. Requires
173    /// `anchor` to be set; `anchor_zone` without `anchor` has no effect and
174    /// triggers an `anchor.zone_without_anchor` warning.
175    pub anchor_zone: Option<String>,
176    /// Optional sibling node id for sibling-relative anchor positioning.
177    /// Requires `anchor` to be set; `anchor_sibling` without `anchor` has no
178    /// effect and triggers an `anchor.sibling_without_anchor` warning.
179    pub anchor_sibling: Option<String>,
180    /// Adjacent-placement edge relative to `anchor-sibling`: `above`/`below`/`before`/`after`.
181    /// When `Some`, positions this node's corresponding edge flush to the named
182    /// edge of `anchor-sibling`. Requires `anchor-sibling` to be set.
183    pub anchor_edge: Option<String>,
184    /// Gap (px) between this node and its `anchor-sibling` edge when `anchor-edge` is set.
185    /// A positive value pushes the node away from the sibling; negative pulls it closer.
186    pub anchor_gap: Option<Dimension>,
187    /// Parent-relative anchor toggle. When `Some(true)` AND a recognized
188    /// `anchor` is present (and `anchor_zone` is absent), the `anchor` is
189    /// resolved relative to this node's DIRECT PARENT CONTAINER's box (a frame
190    /// or group) instead of the full page. An explicitly-authored `x`/`y` still
191    /// wins. `anchor_zone` takes precedence when both are set. Requires the node
192    /// to be inside a frame/group with a usable box; otherwise the validator
193    /// emits `anchor.unresolvable_parent`. `anchor_parent` without `anchor`
194    /// triggers an `anchor.parent_without_anchor` warning. `None`/`Some(false)`
195    /// keeps page/zone-relative behavior (byte-identical).
196    pub anchor_parent: Option<bool>,
197    /// Source declaration span, when available.
198    pub source_span: Option<Span>,
199    /// Unknown properties preserved for forward-compat.
200    pub unknown_props: BTreeMap<String, UnknownProperty>,
201}
202
203/// A `line` node (stroke-only; defined by two endpoints x1/y1/x2/y2).
204///
205/// Unlike `rect` and `ellipse` there is no bounding box, no fill, no radius,
206/// no rotate, and no stroke-alignment — a line is a 1-D geometry whose only
207/// visual property is its centered stroke.
208#[derive(Debug, Clone, PartialEq)]
209pub struct LineNode {
210    pub id: String,
211    pub name: Option<String>,
212    pub role: Option<String>,
213    pub x1: Option<Dimension>,
214    pub y1: Option<Dimension>,
215    pub x2: Option<Dimension>,
216    pub y2: Option<Dimension>,
217    pub style: Option<String>,
218    pub stroke: Option<PropertyValue>,
219    pub stroke_width: Option<PropertyValue>,
220    /// Dash segment length in pixels; `None` = solid stroke.
221    pub stroke_dash: Option<PropertyValue>,
222    /// Gap length in pixels between dashes; defaults to `stroke_dash` when absent.
223    pub stroke_gap: Option<PropertyValue>,
224    /// Dash end-cap style: `"butt"` (default), `"round"`, or `"square"`.
225    pub stroke_linecap: Option<String>,
226    pub opacity: Option<f64>,
227    pub visible: Option<bool>,
228    pub locked: Option<bool>,
229    /// Source declaration span, when available.
230    pub source_span: Option<Span>,
231    /// Unknown properties preserved for forward-compat.
232    pub unknown_props: BTreeMap<String, UnknownProperty>,
233}
234
235/// An `ellipse` node (fill + centered stroke; bounded by x/y/w/h bounding box).
236///
237/// `stroke-alignment` is not supported for ellipse in v0 — stroke is always
238/// centered on the ellipse path. `stroke_alignment` may be added in a later
239/// schema version.
240#[derive(Debug, Clone, PartialEq)]
241pub struct EllipseNode {
242    pub id: String,
243    pub name: Option<String>,
244    pub role: Option<String>,
245    pub x: Option<PropertyValue>,
246    pub y: Option<PropertyValue>,
247    pub w: Option<PropertyValue>,
248    pub h: Option<PropertyValue>,
249    /// Explicit x-radius override (half-width of the ellipse). When absent, the
250    /// ellipse is inscribed in the bounding box (w/2). Backward-compatible: None
251    /// leaves all existing ellipses byte-identical.
252    pub rx: Option<PropertyValue>,
253    /// Explicit y-radius override (half-height of the ellipse). When absent, the
254    /// ellipse is inscribed in the bounding box (h/2). Backward-compatible: None
255    /// leaves all existing ellipses byte-identical.
256    pub ry: Option<PropertyValue>,
257    pub style: Option<String>,
258    pub fill: Option<PropertyValue>,
259    pub stroke: Option<PropertyValue>,
260    pub stroke_width: Option<PropertyValue>,
261    /// Dash segment length in pixels; `None` = solid stroke.
262    pub stroke_dash: Option<PropertyValue>,
263    /// Gap length in pixels between dashes; defaults to `stroke_dash` when absent.
264    pub stroke_gap: Option<PropertyValue>,
265    /// Dash end-cap style: `"butt"` (default), `"round"`, or `"square"`.
266    pub stroke_linecap: Option<String>,
267    /// Drop shadow / outer glow, as a `(token)` ref to a `shadow` token.
268    pub shadow: Option<PropertyValue>,
269    /// Color/image filter ops, as a `(token)` ref to a `filter` token.
270    pub filter: Option<PropertyValue>,
271    /// Spatial coverage mask, as a `(token)` ref to a `mask` token.
272    pub mask: Option<PropertyValue>,
273    /// Compositing blend mode: `"normal"` (default) or one of the 11 separable
274    /// blends. `None`/`"normal"` render source-over (byte-identical).
275    pub blend_mode: Option<String>,
276    /// Gaussian blur radius applied to the node's own rendered ink (sigma in
277    /// the declared unit, resolved to pixels at compile time). `None` / 0 →
278    /// no blur (byte-identical to having no attribute).
279    pub blur: Option<Dimension>,
280    pub opacity: Option<f64>,
281    pub visible: Option<bool>,
282    pub locked: Option<bool>,
283    pub rotate: Option<Dimension>,
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`].
290    pub anchor_zone: Option<String>,
291    /// Optional sibling node id for sibling-relative anchor positioning.
292    /// See [`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`].
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`].
299    pub anchor_gap: Option<Dimension>,
300    /// Parent-relative anchor toggle. See [`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 `text` node.
309#[derive(Debug, Clone, PartialEq)]
310pub struct TextNode {
311    pub id: String,
312    pub name: Option<String>,
313    pub role: Option<String>,
314    pub x: Option<PropertyValue>,
315    pub y: Option<PropertyValue>,
316    pub w: Option<PropertyValue>,
317    pub h: Option<PropertyValue>,
318    pub align: Option<String>,
319    /// Vertical text-block alignment within the box (`top`/`middle`/`bottom`,
320    /// default `top` = today's behavior: no y offset applied). When the box
321    /// height exceeds the laid-out text block height, the block is offset by
322    /// `0` (top), `(box_h - text_h)/2` (middle), or `box_h - text_h`
323    /// (bottom). Unknown values are treated as `top` (byte-identical to absent).
324    pub v_align: Option<String>,
325    pub direction: Option<String>,
326    pub overflow: Option<String>,
327    /// Overflow-wrap mode. `Some("break-word")` lets the line packer break an
328    /// unbreakable token (a long URL/compound with no space or hyphen point) that
329    /// is wider than the line box at a CHARACTER boundary, so it no longer
330    /// overflows; a forced break emits an advisory `text.forced_break`. `None` or
331    /// `"normal"` keeps the default (the overlong token overflows/clips,
332    /// byte-identical to a node without the attribute). KDL:
333    /// `overflow-wrap="break-word"`.
334    pub overflow_wrap: Option<String>,
335    pub style: Option<String>,
336    pub fill: Option<PropertyValue>,
337    /// Glyph outline (stroke) color. Token-required (like `fill`). When `Some`,
338    /// each glyph path is filled then stroked with this color. `None` → no
339    /// outline; byte-identical to a node without `stroke`. KDL:
340    /// `stroke=(token)"color.ink.outline"`.
341    pub stroke: Option<PropertyValue>,
342    /// Glyph outline width in pixels. Token-required (like `font-size`). Only
343    /// effective when `stroke` is also set. `None` / 0 → no outline.
344    /// KDL: `stroke-width=(token)"size.stroke.hairline"`.
345    pub stroke_width: Option<PropertyValue>,
346    /// WCAG contrast hint: an explicit background color (token ref) the text
347    /// visually sits ON, for nodes placed over an `image` or other non-fillable
348    /// backdrop the validator cannot sample. When set, the contrast check uses
349    /// THIS color as the background (highest priority, over any detected backdrop
350    /// and the page background). Token-only, like `fill`. `None` → unchanged
351    /// backdrop detection. KDL: `contrast-bg=(token)"color.photo.shadow"`.
352    pub contrast_bg: Option<PropertyValue>,
353    pub font_family: Option<PropertyValue>,
354    pub font_size: Option<PropertyValue>,
355    /// Floor font size for `overflow="autofit"` — the node's font shrinks no
356    /// smaller than this when fitting. Token-only, like `font-size`. `None` → a
357    /// default floor (`(declared * 0.5).max(8.0)`). KDL:
358    /// `font-size-min=(token)"size.min"`.
359    pub font_size_min: Option<PropertyValue>,
360    /// Numeric font weight (100–900), usually a `fontWeight` token ref.
361    pub font_weight: Option<PropertyValue>,
362    /// Drop shadow / outer glow, as a `(token)` ref to a `shadow` token.
363    pub shadow: Option<PropertyValue>,
364    /// Color/image filter ops, as a `(token)` ref to a `filter` token.
365    pub filter: Option<PropertyValue>,
366    /// Spatial coverage mask, as a `(token)` ref to a `mask` token.
367    pub mask: Option<PropertyValue>,
368    /// Compositing blend mode: `"normal"` (default) or one of the 11 separable
369    /// blends. `None`/`"normal"` render source-over (byte-identical).
370    pub blend_mode: Option<String>,
371    /// Gaussian blur radius applied to the node's own rendered ink (sigma in
372    /// the declared unit, resolved to pixels at compile time). `None` / 0 →
373    /// no blur (byte-identical to having no attribute).
374    pub blur: Option<Dimension>,
375    pub opacity: Option<f64>,
376    pub visible: Option<bool>,
377    pub locked: Option<bool>,
378    /// PDF text-extraction toggle. `None`/`Some(true)` (default) → the text is
379    /// emitted as real, selectable/searchable/indexable text with a ToUnicode
380    /// map, and any `link` spans become clickable. `Some(false)` → the text is
381    /// drawn as filled glyph outlines instead, so it is visually identical but
382    /// cannot be selected, copied, searched, or indexed. PDF-only; the raster
383    /// backend renders identically either way. KDL: `selectable=#false`.
384    pub selectable: Option<bool>,
385    pub rotate: Option<Dimension>,
386    /// Threaded-text-flow chain id. When `Some(id)`, this text node is a member
387    /// of the chain named `id`; all text nodes sharing the same `chain` id form
388    /// an ordered chain (ordering = document source order). A long article
389    /// placed in the FIRST member's spans flows across every member's box in
390    /// order: each box consumes as much text as fits, the remainder continues in
391    /// the next member. Continuation members carry `chain=id` with empty spans.
392    ///
393    /// v0 semantics (documented):
394    /// - Content source: the first member (source order) that has non-empty
395    ///   spans is the sole content source; later members' spans are ignored
396    ///   (no concatenation).
397    /// - Shared style: all members are assumed to share font family/size/weight/
398    ///   fill; the whole chain is shaped with the FIRST member's resolved style.
399    ///   Each box re-wraps to its OWN width, so line height stays uniform.
400    pub chain: Option<String>,
401    /// Drop-cap initial: the FIRST grapheme of the paragraph is typeset large,
402    /// spanning `Some(n)` body lines at the top-left, with the first `n` body
403    /// lines wrapping to a narrower measure beside it and line `n+1` onward
404    /// returning to the full box width. `Some(0)` or `None` disables the drop
405    /// cap (rendered byte-identically to a node without the attribute). Honored
406    /// only on the single-box wrap path (a box with a width whose text overflows
407    /// it); chain/flow integration is a documented v0 follow-up. KDL:
408    /// `drop-cap-lines=3`.
409    pub drop_cap_lines: Option<u32>,
410    /// Knuth–Liang hyphenation toggle. When `Some(true)`, the greedy line packer
411    /// may break a word that does not fit the remaining space on a non-empty line
412    /// at an embedded (en-US) hyphenation point, placing `fragment-` on the
413    /// current line and carrying the remainder to the next. `None`/`Some(false)`
414    /// disables hyphenation (byte-identical to a node without the attribute).
415    /// KDL: `hyphenate=#true`.
416    pub hyphenate: Option<bool>,
417    /// Widow/orphan control: keep at least `Some(n)` lines of a paragraph
418    /// together across a chain box/page break. `n=2` prevents a lone first line
419    /// (orphan) from being stranded at a box bottom and a lone last line (widow)
420    /// from starting the next box. Applied only at the CHAIN distribution
421    /// boundary, read from the chain source node. `None` disables the control
422    /// (byte-identical to a node without the attribute). KDL: `widow-orphan=2`.
423    pub widow_orphan: Option<u32>,
424    /// Tab-stop leader character. When `Some(s)` with a non-empty `s`, the node
425    /// renders in TAB-LEADER mode (table-of-contents rows): the combined span
426    /// text is split into rows on `\n`, each row is split on its FIRST `\t` into
427    /// a LEFT and RIGHT segment, the LEFT segment is placed at the box left edge,
428    /// the RIGHT segment is right-aligned to the box right edge, and the gap
429    /// between them is filled with the leader glyph `s` (e.g. `"."`) repeated.
430    /// A row with no tab renders left-aligned with no leader. `None` or an empty
431    /// string disables tab-leader mode (byte-identical to a node without the
432    /// attribute). KDL: `tab-leader="."`.
433    pub tab_leader: Option<String>,
434    /// Text-runaround exclusion: the id of ANOTHER node on the same page whose
435    /// bounding box becomes an exclusion zone this text wraps around. For each
436    /// wrapped line whose vertical band intersects the excluded rect, the line
437    /// flows into the LARGER free horizontal segment (left or right of the rect);
438    /// a line with no segment wide enough is left blank so text flows above and
439    /// below a full-width exclusion ("largest-area / jump" wrap). An id naming no
440    /// resolvable node yields an advisory `text-exclusion.unresolved_ref` and the
441    /// text renders with no exclusion (byte-identical to a node without the
442    /// attribute). Honored on the single-box wrap path; chain-member runaround is a
443    /// documented v0 follow-up. KDL: `text-exclusion="author.portrait"`.
444    pub text_exclusion: Option<String>,
445    /// Left padding in pixels applied to EVERY wrapped line (indents the text-box
446    /// left edge inward, reducing the measure). Combine with a negative
447    /// [`TextNode::text_indent`] for a hanging indent (bulleted lists). `None` → 0.
448    /// KDL: `padding-left=(px)44`.
449    pub padding_left: Option<Dimension>,
450    /// First-line horizontal offset in pixels RELATIVE to the padded left edge.
451    /// May be NEGATIVE to pull the first line back out (a hanging bullet glyph sits
452    /// left of the wrapped continuation lines). Applies to line 0 of the box only
453    /// (per-paragraph first-line indent is a documented v0 follow-up). `None` → 0.
454    /// KDL: `text-indent=(px)-44`.
455    pub text_indent: Option<Dimension>,
456    /// Auto-aligning list bullet. When `Some(marker)` (a non-empty string like "•",
457    /// "–", "1."), the node renders as a hanging-indent list item: the marker is
458    /// drawn once in the left margin at the first line's baseline, and ALL text
459    /// lines (first and wrapped) are indented to a column at `marker_advance + gap`
460    /// from the box left edge, so continuation lines auto-align with the text after
461    /// the marker — measured from the marker shaped at the node's own font, hence
462    /// font/size-independent. The span text holds only the content (no bullet glyph).
463    /// `None` → not a list item (byte-identical to a node without the attribute).
464    /// Honored on the plain single-box wrap path; drop-cap/runaround/chain are a
465    /// documented v0 follow-up. KDL: `bullet="•"`.
466    pub bullet: Option<String>,
467    /// Gap between the bullet marker and the text column, in pixels. `None` → a
468    /// default proportional to the font size (`0.4 × font_size`). KDL:
469    /// `bullet-gap=(px)16`.
470    pub bullet_gap: Option<Dimension>,
471    /// Content format for this text node's span text.
472    ///
473    /// When `Some("markdown")`, the scene compile pass re-parses the concatenated
474    /// span text (AFTER data-binding substitution) as inline markdown, replacing
475    /// `node.spans` with the parsed styled spans. This enables `**bold**`,
476    /// `*italic*`, `~~strike~~`, `==highlight==`, `++underline++`, `` `code` ``,
477    /// and `[label](url)` in both literal text and data-bound (`data-ref`) content.
478    ///
479    /// `Some("plain")` or `None` keeps the current behavior — spans are used
480    /// verbatim without any markdown interpretation (byte-identical to before).
481    ///
482    /// Any other value emits a `text.invalid_format` warning and is treated as
483    /// plain (byte-identical to a node without the attribute).
484    ///
485    /// KDL: `format="markdown"`.
486    pub content_format: Option<String>,
487    /// Path to an external text or markdown file whose contents become this
488    /// node's text content, relative to the document's project directory.
489    ///
490    /// When `Some(path)`, the CLI render layer reads the file and replaces
491    /// `spans` with a single plain span carrying the file's raw UTF-8 text
492    /// before compilation. When `format="markdown"` is also set, the existing
493    /// `markdown_resolve` compile pass then parses the loaded text into styled
494    /// spans automatically. When the file cannot be read, a `text.src_missing`
495    /// Error diagnostic is emitted and the node's existing spans are left
496    /// unchanged.
497    ///
498    /// The field is retained on the node after loading so that a future editor
499    /// can write edits back to the original file.
500    ///
501    /// A text node WITHOUT `src` is completely unaffected by the loader
502    /// (byte-identical to before).
503    ///
504    /// KDL: `src="copy/article.md"`.
505    pub src: Option<String>,
506    /// Inline text spans.
507    pub spans: Vec<TextSpan>,
508    /// Per-role markdown block style declarations at text-node scope. Empty when
509    /// no `block role="…"` children are declared on this text node. Highest
510    /// cascade precedence (text > page > document). Data-only in this unit; the
511    /// layout engine consumes them later.
512    pub block_styles: Vec<BlockStyle>,
513    /// Page-relative placement anchor (one of the nine named positions, e.g.
514    /// `"bottom-right"`). When present and recognized, the compile step derives
515    /// the node's x and/or y from the page and node dimensions. An explicitly-
516    /// authored x or y always wins.
517    pub anchor: Option<String>,
518    /// Optional safe-zone reference for the anchor. See [`RectNode::anchor_zone`].
519    pub anchor_zone: Option<String>,
520    /// Optional sibling node id for sibling-relative anchor positioning.
521    /// See [`RectNode::anchor_sibling`].
522    pub anchor_sibling: Option<String>,
523    /// Adjacent-placement edge relative to `anchor-sibling`: `above`/`below`/`before`/`after`.
524    /// See [`RectNode::anchor_edge`].
525    pub anchor_edge: Option<String>,
526    /// Gap (px) between this node and its `anchor-sibling` edge when `anchor-edge` is set.
527    /// See [`RectNode::anchor_gap`].
528    pub anchor_gap: Option<Dimension>,
529    /// Parent-relative anchor toggle. See [`RectNode::anchor_parent`].
530    pub anchor_parent: Option<bool>,
531    /// Source declaration span, when available.
532    pub source_span: Option<Span>,
533    /// Unknown properties preserved for forward-compat.
534    pub unknown_props: BTreeMap<String, UnknownProperty>,
535}
536
537/// A `code` node — a multi-line MONOSPACE text block.
538///
539/// Structurally this mirrors [`TextNode`] but carries a single verbatim source
540/// blob instead of styled `spans`. The blob is stored DECODED (newlines and
541/// tabs are literal characters); the formatter re-encodes it with escapes.
542///
543/// The verbatim source is carried in the KDL as a `content` child node with one
544/// escaped string argument (NOT a bare `r#"..."#` raw string): KDL v2 multi-line
545/// string dedent semantics make the raw form lossy, whereas a single-line
546/// escaped string round-trips `\n \t \" \\` exactly through the `kdl` crate.
547/// See `transform_code` / `write_code` for the parse/format sides.
548#[derive(Debug, Clone, PartialEq)]
549pub struct CodeNode {
550    pub id: String,
551    pub name: Option<String>,
552    pub role: Option<String>,
553    pub x: Option<PropertyValue>,
554    pub y: Option<PropertyValue>,
555    pub w: Option<PropertyValue>,
556    pub h: Option<PropertyValue>,
557    /// "clip" (default) or "visible"; v0 does not word-wrap.
558    pub overflow: Option<String>,
559    /// Open string naming the source language; drives built-in syntax
560    /// highlighting when the language is supported, otherwise renders as plain text.
561    pub language: Option<String>,
562    /// Render a line-number gutter (default false).
563    pub line_numbers: Option<bool>,
564    /// Rendered column width of a tab (default 4).
565    pub tab_width: Option<u32>,
566    pub style: Option<String>,
567    pub fill: Option<PropertyValue>,
568    pub font_family: Option<PropertyValue>,
569    pub font_size: Option<PropertyValue>,
570    /// Numeric font weight (100–900), usually a `fontWeight` token ref.
571    pub font_weight: Option<PropertyValue>,
572    /// Optional built-in syntax-highlight color theme; `None` = use default (`Dark`).
573    pub syntax_theme: Option<SyntaxTheme>,
574    pub opacity: Option<f64>,
575    pub visible: Option<bool>,
576    pub locked: Option<bool>,
577    /// PDF text-extraction toggle (see [`TextNode::selectable`]). `None`/
578    /// `Some(true)` (default) → real selectable/searchable text; `Some(false)` →
579    /// filled glyph outlines (visually identical, not extractable). PDF-only.
580    pub selectable: Option<bool>,
581    pub rotate: Option<Dimension>,
582    /// Verbatim source text (decoded; newlines/tabs are literal characters).
583    pub content: String,
584    /// Page-relative placement anchor (one of the nine named positions, e.g.
585    /// `"bottom-right"`). When present and recognized, the compile step derives
586    /// the node's x and/or y from the page and node dimensions. An explicitly-
587    /// authored x or y always wins.
588    pub anchor: Option<String>,
589    /// Optional safe-zone reference for the anchor. See [`RectNode::anchor_zone`].
590    pub anchor_zone: Option<String>,
591    /// Optional sibling node id for sibling-relative anchor positioning.
592    /// See [`RectNode::anchor_sibling`].
593    pub anchor_sibling: Option<String>,
594    /// Adjacent-placement edge relative to `anchor-sibling`: `above`/`below`/`before`/`after`.
595    /// See [`RectNode::anchor_edge`].
596    pub anchor_edge: Option<String>,
597    /// Gap (px) between this node and its `anchor-sibling` edge when `anchor-edge` is set.
598    /// See [`RectNode::anchor_gap`].
599    pub anchor_gap: Option<Dimension>,
600    /// Parent-relative anchor toggle. See [`RectNode::anchor_parent`].
601    pub anchor_parent: Option<bool>,
602    /// Source declaration span, when available.
603    pub source_span: Option<Span>,
604    /// Unknown properties preserved for forward-compat.
605    pub unknown_props: BTreeMap<String, UnknownProperty>,
606}
607
608/// A `polygon` node — a CLOSED filled shape defined by an ordered list of
609/// `point` child nodes.
610///
611/// `polygon` supports both fill and stroke (stroke is centered in v0).
612/// `fill-rule` controls the winding rule for self-intersecting fills.
613/// `stroke-alignment` is parsed and preserved for future use but the stroke
614/// is ALWAYS rendered centered in v0.
615#[derive(Debug, Clone, PartialEq)]
616pub struct PolygonNode {
617    pub id: String,
618    pub name: Option<String>,
619    pub role: Option<String>,
620    pub fill: Option<PropertyValue>,
621    pub stroke: Option<PropertyValue>,
622    pub stroke_width: Option<PropertyValue>,
623    /// Stroke alignment: `"center"` (default), `"inside"`, or `"outside"`.
624    /// `inside`/`outside` shift closed-shape strokes; open paths stroke centered.
625    pub stroke_alignment: Option<String>,
626    /// `"nonzero"` (default) or `"evenodd"`.
627    pub fill_rule: Option<String>,
628    pub opacity: Option<f64>,
629    pub visible: Option<bool>,
630    pub locked: Option<bool>,
631    pub rotate: Option<Dimension>,
632    pub style: Option<String>,
633    /// Ordered vertex list parsed from `point` child nodes.
634    pub points: Vec<Point>,
635    /// Source declaration span, when available.
636    pub source_span: Option<Span>,
637    /// Unknown properties preserved for forward-compat.
638    pub unknown_props: BTreeMap<String, UnknownProperty>,
639}
640
641/// A `pattern` node — a compact procedural primitive.
642///
643/// A `pattern` carries one TEMPLATE child — the [`motif`](PatternNode::motif) —
644/// a single [`Node`] that will be expanded deterministically into many native
645/// shapes (a grid or scatter of the motif). The node currently renders nothing;
646/// expansion is not yet implemented. The motif is NOT an addressable/rendered
647/// node — id-collection, validation, anchor, and tx passes treat the pattern as
648/// a LEAF and never descend into the motif.
649///
650/// The common visual/geometry fields mirror [`RectNode`]; the pattern-specific
651/// fields (`kind`, `seed`, `count`, `spacing`, `jitter`) describe the expansion.
652#[derive(Debug, Clone, PartialEq)]
653pub struct PatternNode {
654    pub id: String,
655    pub name: Option<String>,
656    pub role: Option<String>,
657    pub x: Option<PropertyValue>,
658    pub y: Option<PropertyValue>,
659    pub w: Option<PropertyValue>,
660    pub h: Option<PropertyValue>,
661    pub radius: Option<PropertyValue>,
662    /// Per-corner radius overrides (top-left, top-right, bottom-right, bottom-left).
663    pub radius_tl: Option<PropertyValue>,
664    pub radius_tr: Option<PropertyValue>,
665    pub radius_br: Option<PropertyValue>,
666    pub radius_bl: Option<PropertyValue>,
667    pub style: Option<String>,
668    pub fill: Option<PropertyValue>,
669    pub stroke: Option<PropertyValue>,
670    pub stroke_width: Option<PropertyValue>,
671    pub stroke_alignment: Option<String>,
672    /// Dash segment length in pixels; `None` = solid stroke.
673    pub stroke_dash: Option<PropertyValue>,
674    /// Gap length in pixels between dashes; defaults to `stroke_dash` when absent.
675    pub stroke_gap: Option<PropertyValue>,
676    /// Dash end-cap style: `"butt"` (default), `"round"`, or `"square"`.
677    pub stroke_linecap: Option<String>,
678    /// Per-side border color for the top edge. Token-required (color token).
679    pub border_top: Option<PropertyValue>,
680    /// Per-side border color for the bottom edge. Token-required (color token).
681    pub border_bottom: Option<PropertyValue>,
682    /// Per-side border color for the left edge. Token-required (color token).
683    pub border_left: Option<PropertyValue>,
684    /// Per-side border color for the right edge. Token-required (color token).
685    pub border_right: Option<PropertyValue>,
686    /// Shared border width for per-side borders. Token-required (dimension).
687    pub border_width: Option<PropertyValue>,
688    /// Outer stroke color: a SECOND stroke painted OUTSIDE the geometry.
689    pub stroke_outer: Option<PropertyValue>,
690    /// Outer stroke width for `stroke_outer`. Token-required (dimension).
691    pub stroke_outer_width: Option<PropertyValue>,
692    /// Drop shadow / outer glow, as a `(token)` ref to a `shadow` token.
693    pub shadow: Option<PropertyValue>,
694    /// Color/image filter ops, as a `(token)` ref to a `filter` token.
695    pub filter: Option<PropertyValue>,
696    /// Spatial coverage mask, as a `(token)` ref to a `mask` token.
697    pub mask: Option<PropertyValue>,
698    /// Compositing blend mode: `"normal"` (default) or one of the separable blends.
699    pub blend_mode: Option<String>,
700    /// Gaussian blur radius applied to the node's own rendered ink.
701    pub blur: Option<Dimension>,
702    pub opacity: Option<f64>,
703    pub visible: Option<bool>,
704    pub locked: Option<bool>,
705    pub rotate: Option<Dimension>,
706    /// Page-relative placement anchor. See [`RectNode::anchor`].
707    pub anchor: Option<String>,
708    /// Optional safe-zone reference for the anchor. See [`RectNode::anchor_zone`].
709    pub anchor_zone: Option<String>,
710    /// Optional sibling node id for sibling-relative anchor positioning.
711    pub anchor_sibling: Option<String>,
712    /// Adjacent-placement edge relative to `anchor-sibling`: `above`/`below`/`before`/`after`.
713    /// See [`RectNode::anchor_edge`].
714    pub anchor_edge: Option<String>,
715    /// Gap (px) between this node and its `anchor-sibling` edge when `anchor-edge` is set.
716    /// See [`RectNode::anchor_gap`].
717    pub anchor_gap: Option<Dimension>,
718    /// Parent-relative anchor toggle. See [`RectNode::anchor_parent`].
719    pub anchor_parent: Option<bool>,
720    /// Required: the pattern kind (`"grid"` | `"scatter"`; freeform, validated later).
721    pub kind: String,
722    /// Deterministic jitter seed.
723    pub seed: Option<i64>,
724    /// Scatter: number of instances.
725    pub count: Option<i64>,
726    /// Grid: cell spacing.
727    pub spacing: Option<Dimension>,
728    /// Positional jitter amount in `0..1`.
729    pub jitter: Option<f64>,
730    /// The single template child shape expanded by the pattern (mandatory).
731    /// This is a TEMPLATE, NOT an addressable/rendered node: id-collection,
732    /// validation, anchor, and tx passes never descend into it.
733    pub motif: Box<Node>,
734    /// Source declaration span, when available.
735    pub source_span: Option<Span>,
736    /// Unknown properties preserved for forward-compat.
737    pub unknown_props: BTreeMap<String, UnknownProperty>,
738}
739
740/// One data series within a [`ChartNode`].
741///
742/// A series is PURE DATA — it is not a renderable [`Node`] and is never
743/// descended into by id-collection, validation, anchor, or tx passes. It
744/// carries an ordered list of numeric values and optional legend/styling hints.
745#[derive(Debug, Clone, PartialEq)]
746pub struct ChartSeries {
747    /// Optional legend or category label for this series.
748    pub label: Option<String>,
749    /// Optional series color; a `(token)` color ref. When absent the renderer
750    /// picks a palette color by series index.
751    pub color: Option<PropertyValue>,
752    /// Per-series value-label color override; falls back to the chart
753    /// `value_color` then the default on-fill contrasting color.
754    pub label_color: Option<PropertyValue>,
755    /// Optional binding to a whole series from a [`DataContext`](crate::data::DataContext) field.
756    /// `None` means the values are inline in [`ChartSeries::values`].
757    pub data_ref: Option<String>,
758    /// Ordered numeric data points for this series.
759    pub values: Vec<f64>,
760}
761
762/// A `chart` node — a compact data-visualization primitive.
763///
764/// A `chart` declares its data inline via [`series`](ChartNode::series) children
765/// (one child KDL node per series, each with positional f64 arguments) and
766/// paints into its `[x, y, w, h]` bounding box. The node currently renders
767/// nothing; chart rendering is deferred. The series children are pure DATA,
768/// not renderable nodes: id-collection, validation, anchor, and tx passes
769/// treat the chart as a LEAF and never descend into them.
770///
771/// The common visual/geometry fields mirror [`PatternNode`]; the chart-specific
772/// fields (`kind`, `title`, `caption`, `legend`, `axis_*`, `bar_mode`,
773/// `orientation`, `point_placement`, `value_labels`, `value_color`, `label_colors`,
774/// `slice_colors`, `categories`, `series`, `legend_position`, `legend_layout`,
775/// `legend_align`) describe the chart content.
776#[derive(Debug, Clone, PartialEq)]
777pub struct ChartNode {
778    pub id: String,
779    pub name: Option<String>,
780    pub role: Option<String>,
781    pub x: Option<PropertyValue>,
782    pub y: Option<PropertyValue>,
783    pub w: Option<PropertyValue>,
784    pub h: Option<PropertyValue>,
785    pub radius: Option<PropertyValue>,
786    /// Per-corner radius overrides (top-left, top-right, bottom-right, bottom-left).
787    pub radius_tl: Option<PropertyValue>,
788    pub radius_tr: Option<PropertyValue>,
789    pub radius_br: Option<PropertyValue>,
790    pub radius_bl: Option<PropertyValue>,
791    pub style: Option<String>,
792    pub fill: Option<PropertyValue>,
793    pub stroke: Option<PropertyValue>,
794    pub stroke_width: Option<PropertyValue>,
795    pub stroke_alignment: Option<String>,
796    /// Dash segment length in pixels; `None` = solid stroke.
797    pub stroke_dash: Option<PropertyValue>,
798    /// Gap length in pixels between dashes; defaults to `stroke_dash` when absent.
799    pub stroke_gap: Option<PropertyValue>,
800    /// Dash end-cap style: `"butt"` (default), `"round"`, or `"square"`.
801    pub stroke_linecap: Option<String>,
802    /// Per-side border color for the top edge. Token-required (color token).
803    pub border_top: Option<PropertyValue>,
804    /// Per-side border color for the bottom edge. Token-required (color token).
805    pub border_bottom: Option<PropertyValue>,
806    /// Per-side border color for the left edge. Token-required (color token).
807    pub border_left: Option<PropertyValue>,
808    /// Per-side border color for the right edge. Token-required (color token).
809    pub border_right: Option<PropertyValue>,
810    /// Shared border width for per-side borders. Token-required (dimension).
811    pub border_width: Option<PropertyValue>,
812    /// Outer stroke color: a SECOND stroke painted OUTSIDE the geometry.
813    pub stroke_outer: Option<PropertyValue>,
814    /// Outer stroke width for `stroke_outer`. Token-required (dimension).
815    pub stroke_outer_width: Option<PropertyValue>,
816    /// Drop shadow / outer glow, as a `(token)` ref to a `shadow` token.
817    pub shadow: Option<PropertyValue>,
818    /// Color/image filter ops, as a `(token)` ref to a `filter` token.
819    pub filter: Option<PropertyValue>,
820    /// Spatial coverage mask, as a `(token)` ref to a `mask` token.
821    pub mask: Option<PropertyValue>,
822    /// Compositing blend mode: `"normal"` (default) or one of the separable blends.
823    pub blend_mode: Option<String>,
824    /// Gaussian blur radius applied to the node's own rendered ink.
825    pub blur: Option<Dimension>,
826    pub opacity: Option<f64>,
827    pub visible: Option<bool>,
828    pub locked: Option<bool>,
829    pub rotate: Option<Dimension>,
830    /// Page-relative placement anchor. See [`RectNode::anchor`].
831    pub anchor: Option<String>,
832    /// Optional safe-zone reference for the anchor. See [`RectNode::anchor_zone`].
833    pub anchor_zone: Option<String>,
834    /// Optional sibling node id for sibling-relative anchor positioning.
835    pub anchor_sibling: Option<String>,
836    /// Adjacent-placement edge relative to `anchor-sibling`: `above`/`below`/`before`/`after`.
837    /// See [`RectNode::anchor_edge`].
838    pub anchor_edge: Option<String>,
839    /// Gap (px) between this node and its `anchor-sibling` edge when `anchor-edge` is set.
840    /// See [`RectNode::anchor_gap`].
841    pub anchor_gap: Option<Dimension>,
842    /// Parent-relative anchor toggle. See [`RectNode::anchor_parent`].
843    pub anchor_parent: Option<bool>,
844    /// Required: the chart kind (`"bar"` | `"line"` | `"sparkline"` | `"pie"` | `"donut"`;
845    /// freeform, validated later).
846    pub kind: String,
847    /// Optional chart title rendered above the plot area.
848    pub title: Option<String>,
849    /// Optional caption rendered below the chart.
850    pub caption: Option<String>,
851    /// Whether to render a legend. `None` defers to the renderer default.
852    pub legend: Option<bool>,
853    /// Legend placement: `"right"` (default) | `"left"` | `"top"` | `"bottom"`.
854    /// freeform, validated later.
855    pub legend_position: Option<String>,
856    /// Legend layout for top/bottom placement: `"wrapped"` (default; horizontal
857    /// flow) | `"list"` (vertical stack). Ignored for left/right (always a
858    /// vertical list). freeform, validated later.
859    pub legend_layout: Option<String>,
860    /// Legend alignment for top/bottom placement: `"center"` (default) | `"left"`
861    /// | `"right"`. freeform, validated later.
862    pub legend_align: Option<String>,
863    /// Minimum value for the value axis. `None` = auto-fit to data.
864    pub axis_min: Option<f64>,
865    /// Maximum value for the value axis. `None` = auto-fit to data.
866    pub axis_max: Option<f64>,
867    /// Style string for the axis (e.g. `"hidden"`, `"minimal"`); freeform for now.
868    pub axis_style: Option<String>,
869    /// Bar layout mode: `"grouped"` (default) | `"stacked"`; freeform,
870    /// validated later. Mirrors how `kind` is typed/documented.
871    pub bar_mode: Option<String>,
872    /// Bar orientation: `"vertical"` (default; bars grow up from the X axis) |
873    /// `"horizontal"` (bars grow right from the Y axis, categories on the Y
874    /// axis). Applies to bar charts; freeform, validated later.
875    pub orientation: Option<String>,
876    /// X placement for line/area points: `"edge"` (default; first point on the
877    /// value axis, last at the right edge) | `"center"` (category-band centers).
878    /// freeform, validated later.
879    pub point_placement: Option<String>,
880    /// Value-label display/placement: `"auto"` (default) | `"none"` | `"top"` |
881    /// `"center"`. freeform, validated later.
882    pub value_labels: Option<String>,
883    /// Explicit color (token) for value labels; when absent the renderer
884    /// auto-picks a contrasting color.
885    pub value_color: Option<PropertyValue>,
886    /// Per-slice value-label colors for pie/donut (one per category, in order);
887    /// empty = use the chart `value_color` or the white on-fill default.
888    /// Populated from a `label-colors` child node whose positional arguments
889    /// are each a `PropertyValue` (e.g. `(token)"color.x"`).
890    pub label_colors: Vec<PropertyValue>,
891    /// Per-slice FILL colors for pie/donut (one per category, in order);
892    /// empty = fall back to the palette (`slice_color(idx)`).
893    /// Populated from a `slice-colors` child node whose positional arguments
894    /// are each a `PropertyValue` (e.g. `(token)"color.x"`).
895    pub slice_colors: Vec<PropertyValue>,
896    /// X-axis category labels (one per category slot); empty = derive index
897    /// labels at render. Populated from a `categories` child node whose
898    /// positional arguments are the label strings.
899    pub categories: Vec<String>,
900    /// Ordered data series. Each series carries labels, an optional color, and
901    /// a list of f64 data points.
902    pub series: Vec<ChartSeries>,
903    /// Source declaration span, when available.
904    pub source_span: Option<Span>,
905    /// Unknown properties preserved for forward-compat.
906    pub unknown_props: BTreeMap<String, UnknownProperty>,
907}
908
909/// A `polyline` node — an OPEN stroked path defined by an ordered list of
910/// `point` child nodes.
911///
912/// `polyline` has stroke (required for visible output) and optional fill.
913/// Unlike `polygon`, `polyline` does NOT support `stroke-alignment`.
914#[derive(Debug, Clone, PartialEq)]
915pub struct PolylineNode {
916    pub id: String,
917    pub name: Option<String>,
918    pub role: Option<String>,
919    pub fill: Option<PropertyValue>,
920    pub stroke: Option<PropertyValue>,
921    pub stroke_width: Option<PropertyValue>,
922    /// `"nonzero"` (default) or `"evenodd"`.
923    pub fill_rule: Option<String>,
924    pub opacity: Option<f64>,
925    pub visible: Option<bool>,
926    pub locked: Option<bool>,
927    pub rotate: Option<Dimension>,
928    pub style: Option<String>,
929    /// Ordered vertex list parsed from `point` child nodes.
930    pub points: Vec<Point>,
931    /// Source declaration span, when available.
932    pub source_span: Option<Span>,
933    /// Unknown properties preserved for forward-compat.
934    pub unknown_props: BTreeMap<String, UnknownProperty>,
935}