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::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}