zenith_core/ast/node/common.rs
1//! Shared node-layer types: forward-compat property storage, text spans,
2//! geometry primitives, and the top-level [`Node`] enum.
3
4use crate::ast::value::PropertyValue;
5use crate::data::DataFormat;
6
7use super::container::{FrameNode, GroupNode, TableNode};
8use super::effect::{LightNode, MeshNode};
9use super::leaf::{
10 ChartNode, CodeNode, EllipseNode, ImageNode, LineNode, PatternNode, PolygonNode, PolylineNode,
11 RectNode, TextNode,
12};
13use super::special::{
14 ConnectorNode, FieldNode, FootnoteNode, InstanceNode, ShapeNode, TocNode, UnknownNode,
15};
16
17/// The typed value of an unrecognized KDL property, preserved for forward-compat.
18///
19/// Mirrors the KDL v2 value space so that the original KDL type is never
20/// discarded during a parse→format→parse round-trip.
21#[derive(Debug, Clone, PartialEq)]
22pub enum UnknownValue {
23 String(String),
24 Integer(i128),
25 Float(f64),
26 Bool(bool),
27 Null,
28}
29
30/// A typed KDL value retained for an unrecognized property (forward-compat).
31///
32/// Storing the full `UnknownValue` variant keeps the AST lossless for
33/// round-trip: a boolean `magic=#true` round-trips back as a boolean, not
34/// as the string `"true"`. Any KDL type annotation on the value (e.g. `px`
35/// from `(px)10`) is retained in `ty` so annotated values round-trip
36/// byte-identically.
37#[derive(Debug, Clone, PartialEq)]
38pub struct UnknownProperty {
39 /// The typed representation of the KDL value.
40 pub value: UnknownValue,
41 /// The KDL type annotation, if any (e.g. `px` from `(px)10`, `token` from
42 /// `(token)"color.navy"`). Preserved so annotated values round-trip losslessly.
43 pub ty: Option<String>,
44}
45
46/// A text content span — a run of text with optional inline style overrides.
47///
48/// This is deliberately named `TextSpan` to avoid colliding with the source-
49/// location type [`Span`](crate::ast::Span).
50#[derive(Debug, Clone, PartialEq)]
51pub struct TextSpan {
52 /// The literal text content.
53 pub text: String,
54 /// Per-span fill override (usually a token ref).
55 pub fill: Option<PropertyValue>,
56 /// Per-span font-weight override.
57 pub font_weight: Option<PropertyValue>,
58 /// Italic override.
59 pub italic: Option<bool>,
60 /// Underline decoration.
61 pub underline: Option<bool>,
62 /// Strikethrough decoration.
63 pub strikethrough: Option<bool>,
64 /// Vertical alignment of the span relative to the run baseline. `Some("super")`
65 /// raises the span (superscript); `Some("sub")` lowers it (subscript). Both
66 /// typeset the span at a reduced font size. `None` (or any other value) keeps
67 /// the span on the baseline at full size. See the scene `compile_text`
68 /// super/subscript handling for the exact scale + baseline-shift factors.
69 pub vertical_align: Option<String>,
70 /// Footnote reference — the id of a page-level [`FootnoteNode`]. When
71 /// `Some(id)`, the renderer emits the referenced footnote's auto-number as a
72 /// SUPERSCRIPT marker run immediately AFTER this span's text (reusing the
73 /// [`TextSpan::vertical_align`] `"super"` rendering: reduced size + raised
74 /// baseline). An id that names no footnote on the same page yields an
75 /// advisory `footnote.unresolved_ref` and no marker. KDL: `footnote-ref="fn.1"`.
76 pub footnote_ref: Option<String>,
77 /// Runtime data-field reference for the span's TEXT CONTENT. When `Some(path)`,
78 /// the scene compiler's data pre-pass looks `path` up in the active
79 /// [`DataContext`](crate::data::DataContext) and REPLACES [`TextSpan::text`]
80 /// with the resolved value (styled by [`TextSpan::data_format`] when set). A
81 /// missing field emits `data.missing_field` and leaves the authored `text`
82 /// (the fallback). `None` keeps the literal `text` unchanged (byte-identical
83 /// to a span without the attribute). KDL: `data-ref="revenue.total"`.
84 pub data_ref: Option<String>,
85 /// Optional display format applied to the resolved [`TextSpan::data_ref`]
86 /// value (currency / percent / number, with optional precision + locale). Only
87 /// meaningful when `data_ref` is `Some`. `None` substitutes the raw field
88 /// value verbatim. KDL: `format="currency" precision=2`.
89 pub data_format: Option<DataFormat>,
90 /// Per-span highlight (text background) color; usually a token ref. `None` =
91 /// no highlight (byte-identical to a span without it). When `Some`, the
92 /// renderer emits a filled rect behind the span's glyphs covering the full
93 /// ascent-to-descent band so the text reads like a marker-pen highlight.
94 /// KDL: `span highlight=(token)"color.mark" "text"`.
95 pub highlight: Option<PropertyValue>,
96 /// Inline code mark: when `Some(true)`, the span is rendered in the bundled
97 /// monospace family ("Noto Sans Mono") with a subtle background rect behind
98 /// it (the internal `CODE_BG` default color). `None` or `Some(false)` keeps
99 /// the node's own font family (byte-identical to a span without it).
100 /// KDL: `span "text" code=#true`.
101 pub code: Option<bool>,
102 /// Hyperlink URL carried on the span. `None` = no link (byte-identical to a
103 /// span without it). When `Some(url)`, the span is rendered with an underline
104 /// and the internal `LINK_COLOR` default color when the span has no explicit
105 /// `fill`; a span that already has a `fill` keeps its author color. In PDF
106 /// output the URL becomes a clickable `/Link` annotation over the span (when
107 /// the text is `selectable`). KDL: `span "text" link="https://example.com"`.
108 pub link: Option<String>,
109}
110
111/// How an `image` node aligns its content within the declared box when the
112/// `fit` mode leaves slack on an axis (`contain`, `cover`, `none`).
113///
114/// `Pct(n)` is an arbitrary 0–100 position; `Start`/`Center`/`End` are the
115/// named anchors (equivalent to `Pct(0)`, `Pct(50)`, `Pct(100)`).
116#[derive(Debug, Clone, PartialEq)]
117pub enum ObjectPosition {
118 Start,
119 Center,
120 End,
121 Pct(f64),
122}
123
124/// A single vertex in a polygon or polyline point list.
125///
126/// Both `x` and `y` are `Option` for consistency with line endpoint geometry
127/// — validate-time checks enforce their presence.
128#[derive(Debug, Clone, PartialEq)]
129pub struct Point {
130 pub x: Option<crate::ast::value::Dimension>,
131 pub y: Option<crate::ast::value::Dimension>,
132}
133
134/// A renderable content node within a page.
135#[derive(Debug, Clone, PartialEq)]
136pub enum Node {
137 // Boxed: `RectNode` grew large enough to trigger `large_enum_variant`.
138 // Boxing keeps `Node` compact so moving it around stays cheap.
139 // Mirrors the existing `Text(Box<TextNode>)` pattern.
140 Rect(Box<RectNode>),
141 Ellipse(EllipseNode),
142 Line(LineNode),
143 // Boxed: `TextNode` is by far the largest node variant (many optional
144 // typography/geometry fields). Boxing keeps `Node` compact so moving it
145 // around (and the `large_enum_variant` lint) stays cheap.
146 Text(Box<TextNode>),
147 Code(CodeNode),
148 Frame(FrameNode),
149 Group(GroupNode),
150 Image(ImageNode),
151 Polygon(PolygonNode),
152 Polyline(PolylineNode),
153 Instance(InstanceNode),
154 Field(FieldNode),
155 Footnote(FootnoteNode),
156 /// A compile-time table-of-contents placeholder; resolved to a
157 /// tab-leader text block by the scene compiler.
158 Toc(TocNode),
159 // Boxed: `TableNode` is large (many optional visual fields + nested
160 // columns/rows/cells). Boxing keeps `Node` compact for the
161 // `large_enum_variant` lint, mirroring `Rect`/`Text`.
162 Table(Box<TableNode>),
163 // Boxed: `ShapeNode` is large (box geometry + visual fields + owned label
164 // spans). Boxing keeps `Node` compact for the `large_enum_variant` lint,
165 // mirroring `Rect`/`Text`/`Table`.
166 Shape(Box<ShapeNode>),
167 // Boxed: `ConnectorNode` carries many optional attrs (from/to + anchors +
168 // route + markers + visual fields). Boxing keeps `Node` compact for the
169 // `large_enum_variant` lint, mirroring `Rect`/`Text`/`Table`/`Shape`.
170 Connector(Box<ConnectorNode>),
171 // Boxed: `UnknownNode` now carries preserved props + recursive children for
172 // lossless forward-compat round-trip. Boxing keeps `Node` compact for the
173 // `large_enum_variant` lint, mirroring `Rect`/`Text`/`Table`/`Shape`.
174 Unknown(Box<UnknownNode>),
175 // Boxed: `PatternNode` carries the full common-field spread plus a boxed
176 // motif. Boxing keeps `Node` compact for the `large_enum_variant` lint,
177 // mirroring `Rect`/`Text`/`Table`/`Shape`.
178 Pattern(Box<PatternNode>),
179 // Boxed: `ChartNode` carries the full common-field spread plus a Vec of
180 // series. Boxing keeps `Node` compact for the `large_enum_variant` lint,
181 // mirroring `Rect`/`Text`/`Table`/`Shape`/`Pattern`.
182 Chart(Box<ChartNode>),
183 // Boxed for the same forward-growth reason as the other rich authoring
184 // variants: future effect nodes may gain more fields without bloating Node.
185 Light(Box<LightNode>),
186 // Boxed: procedural meshes are effect nodes with enough forward-growth
187 // fields to keep out of the enum payload.
188 Mesh(Box<MeshNode>),
189}