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