Skip to main content

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}