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}