Skip to main content

zenith_core/ast/
token.rs

1//! Token block and token AST types.
2
3use super::Span;
4use super::value::Dimension;
5
6/// The five v0 token types, plus an extensibility variant for unknown types.
7#[derive(Debug, Clone, PartialEq)]
8pub enum TokenType {
9    Color,
10    Dimension,
11    Number,
12    FontFamily,
13    FontWeight,
14    Gradient,
15    Shadow,
16    Filter,
17    Mask,
18    /// An unrecognized token type (forward-compat; version-relative).
19    Unknown(String),
20}
21
22impl TokenType {
23    /// Parse the token type from the `type` property string. Infallible: an
24    /// unrecognized type is preserved as `TokenType::Unknown` (forward-compat).
25    pub fn from_type_name(s: &str) -> Self {
26        match s {
27            "color" => Self::Color,
28            "dimension" => Self::Dimension,
29            "number" => Self::Number,
30            "fontFamily" => Self::FontFamily,
31            "fontWeight" => Self::FontWeight,
32            "gradient" => Self::Gradient,
33            "shadow" => Self::Shadow,
34            "filter" => Self::Filter,
35            "mask" => Self::Mask,
36            other => Self::Unknown(other.to_owned()),
37        }
38    }
39}
40
41/// A literal value held by a token definition.
42#[derive(Debug, Clone, PartialEq)]
43pub enum TokenLiteral {
44    /// A quoted string, e.g. `"#f8fafc"` or `"Inter"`.
45    String(String),
46    /// A dimensioned number, e.g. `(pt)48` or `(px)28`.
47    Dimension(Dimension),
48    /// An unannotated finite number, e.g. `1.05` or `700`.
49    Number(f64),
50    /// A gradient definition built from child `stop` nodes plus an optional
51    /// `angle`. Gradients have no scalar value; they are carried by this
52    /// dedicated literal variant.
53    Gradient(GradientLiteral),
54    /// A shadow definition built from child `layer` nodes. Shadows have no
55    /// scalar value; they are carried by this dedicated literal variant.
56    Shadow(ShadowLiteral),
57    /// A filter definition built from child op nodes. Filters have no scalar
58    /// value; they are carried by this dedicated literal variant.
59    Filter(FilterLiteral),
60    /// A mask definition built from a single shape child node. Masks have no
61    /// scalar value; they are carried by this dedicated literal variant.
62    Mask(MaskLiteral),
63}
64
65/// The spatial coverage shape of a mask token.
66#[derive(Debug, Clone, Copy, PartialEq)]
67pub enum MaskShape {
68    Rect,
69    RoundedRect,
70    Ellipse,
71}
72
73impl MaskShape {
74    /// Parse a mask shape from its KDL shape-child name, or `None` if
75    /// unrecognized.
76    pub fn from_shape_name(s: &str) -> Option<Self> {
77        match s {
78            "rect" => Some(Self::Rect),
79            "rounded" => Some(Self::RoundedRect),
80            "ellipse" => Some(Self::Ellipse),
81            _ => None,
82        }
83    }
84
85    /// The canonical KDL shape-child name for this mask shape (inverse of
86    /// [`MaskShape::from_shape_name`]).
87    pub fn as_shape_name(&self) -> &'static str {
88        match self {
89            Self::Rect => "rect",
90            Self::RoundedRect => "rounded",
91            Self::Ellipse => "ellipse",
92        }
93    }
94}
95
96/// A mask token literal: a single spatial coverage shape plus a feather and an
97/// invert flag.
98#[derive(Debug, Clone, PartialEq)]
99pub struct MaskLiteral {
100    /// The coverage shape.
101    pub shape: MaskShape,
102    /// RoundedRect corner radius (px); ignored for other shapes.
103    pub radius: Option<f64>,
104    /// Gaussian feather sigma (px, >= 0); 0 = hard edge.
105    pub feather: f64,
106    /// Invert coverage.
107    pub invert: bool,
108}
109
110/// The color/image filter operations, applied in source order.
111#[derive(Debug, Clone, Copy, PartialEq)]
112pub enum FilterKind {
113    Grayscale,
114    Invert,
115    Sepia,
116    Saturate,
117    Brightness,
118    Contrast,
119    HueRotate,
120    /// Duotone: maps each pixel's luma to a blend between a shadow color (dark
121    /// areas) and a highlight color (light areas), with an optional `amount` mix
122    /// factor against the original. This is the only kind that references color
123    /// tokens (carried on [`FilterOp::shadow`] / [`FilterOp::highlight`]).
124    Duotone,
125    /// Noise: deterministic seeded film grain. Adds the same per-pixel delta to
126    /// R, G, and B, derived from an integer hash of the page-absolute pixel cell
127    /// and a `seed`. `amount` scales the grain magnitude; `scale` sets the grain
128    /// cell size in pixels. Same inputs → same grain on any machine.
129    Noise,
130}
131
132impl FilterKind {
133    /// Parse a filter kind from its KDL op-node name, or `None` if unrecognized.
134    pub fn from_op_name(s: &str) -> Option<Self> {
135        match s {
136            "grayscale" => Some(Self::Grayscale),
137            "invert" => Some(Self::Invert),
138            "sepia" => Some(Self::Sepia),
139            "saturate" => Some(Self::Saturate),
140            "brightness" => Some(Self::Brightness),
141            "contrast" => Some(Self::Contrast),
142            "hue-rotate" => Some(Self::HueRotate),
143            "duotone" => Some(Self::Duotone),
144            "noise" => Some(Self::Noise),
145            _ => None,
146        }
147    }
148
149    /// The canonical KDL op-node name for this filter kind (inverse of
150    /// [`FilterKind::from_op_name`]).
151    pub fn as_op_name(&self) -> &'static str {
152        match self {
153            Self::Grayscale => "grayscale",
154            Self::Invert => "invert",
155            Self::Sepia => "sepia",
156            Self::Saturate => "saturate",
157            Self::Brightness => "brightness",
158            Self::Contrast => "contrast",
159            Self::HueRotate => "hue-rotate",
160            Self::Duotone => "duotone",
161            Self::Noise => "noise",
162        }
163    }
164}
165
166/// A filter token literal: an ordered list of filter operations, applied in
167/// source order. At least one op is required (enforced at resolution).
168#[derive(Debug, Clone, PartialEq)]
169pub struct FilterLiteral {
170    /// Ordered list of filter ops, in source order.
171    pub ops: Vec<FilterOp>,
172}
173
174/// A single filter operation: a kind plus an optional amount. The amount is a
175/// unitless number whose meaning depends on the kind (e.g. fraction for
176/// `grayscale`, degrees for `hue-rotate`); `None` means "use the op's default".
177#[derive(Debug, Clone, PartialEq)]
178pub struct FilterOp {
179    /// The filter operation kind.
180    pub kind: FilterKind,
181    /// The optional amount for this op. For `Duotone` this is the mix factor
182    /// against the original (default applied later); for scalar kinds it is the
183    /// op's own amount.
184    pub amount: Option<f64>,
185    /// Shadow color token id (dark-area color) — `Some` only for `Duotone` ops.
186    pub shadow: Option<String>,
187    /// Highlight color token id (light-area color) — `Some` only for `Duotone`
188    /// ops.
189    pub highlight: Option<String>,
190    /// Grain pattern seed — used only by `noise`; `None` defaults to 0.
191    pub seed: Option<i64>,
192    /// Grain cell size in pixels — used only by `noise`; `None` defaults to 1.0.
193    pub scale: Option<f64>,
194}
195
196/// Whether a gradient token is linear or radial.
197#[derive(Debug, Clone, Copy, PartialEq, Default)]
198pub enum GradientKind {
199    /// A linear gradient (default). An `angle_deg` controls the gradient line.
200    #[default]
201    Linear,
202    /// A radial gradient. `center_x`, `center_y`, and `radius` control the
203    /// origin and extent, each as a fraction of the bounding box.
204    Radial,
205}
206
207/// A gradient token literal: either linear (angle + stops) or radial
208/// (center + radius + stops).
209#[derive(Debug, Clone, PartialEq)]
210pub struct GradientLiteral {
211    /// Whether the gradient is linear or radial.
212    pub kind: GradientKind,
213    /// Angle in degrees, clockwise from +x (0 = left→right, 90 = top→bottom).
214    /// Relevant only for `kind == Linear`; ignored for radial.
215    pub angle_deg: f64,
216    /// Radial gradient center X as a fraction of the bounding-box width (0..1).
217    /// `None` defaults to `0.5` (center). Ignored for linear.
218    pub center_x: Option<f64>,
219    /// Radial gradient center Y as a fraction of the bounding-box height (0..1).
220    /// `None` defaults to `0.5` (center). Ignored for linear.
221    pub center_y: Option<f64>,
222    /// Radial gradient radius as a fraction of the box diagonal (`hypot(w,h)/2`).
223    /// `None` defaults to `1.0`. Must be > 0 if specified.
224    pub radius: Option<f64>,
225    /// Ordered list of stop references, in source order.
226    pub stops: Vec<GradientStopRef>,
227}
228
229/// A single gradient stop: an offset in `0..1` and a reference to a color token.
230#[derive(Debug, Clone, PartialEq)]
231pub struct GradientStopRef {
232    /// Position of the stop along the gradient axis, in `0.0..=1.0`.
233    pub offset: f64,
234    /// The id of the color token this stop renders with.
235    pub color_token: String,
236}
237
238/// A shadow token literal: an ordered list of shadow layers (e.g. a drop
239/// shadow plus an outer glow). At least one layer is required (enforced at
240/// resolution).
241#[derive(Debug, Clone, PartialEq)]
242pub struct ShadowLiteral {
243    /// Ordered list of layer references, in source order.
244    pub layers: Vec<ShadowLayerRef>,
245}
246
247/// A single shadow layer: x/y offsets and blur radius (pixels) plus a reference
248/// to a color token. A layer with nonzero dx/dy is a drop shadow; a layer with
249/// dx=dy=0 and a blur is an outer glow.
250#[derive(Debug, Clone, PartialEq)]
251pub struct ShadowLayerRef {
252    /// Horizontal offset in pixels.
253    pub dx: f64,
254    /// Vertical offset in pixels.
255    pub dy: f64,
256    /// Blur radius in pixels.
257    pub blur: f64,
258    /// The id of the color token this layer renders with.
259    pub color_token: String,
260}
261
262/// The value of a token — either an inline literal or an alias to another token.
263#[derive(Debug, Clone, PartialEq)]
264pub enum TokenValue {
265    /// A literal token value.
266    Literal(TokenLiteral),
267    /// An alias to another token, e.g. `(token)"color.text.primary"`.
268    Reference { token_id: String },
269}
270
271/// A single design token within a `tokens` block.
272#[derive(Debug, Clone, PartialEq)]
273pub struct Token {
274    /// Globally unique token ID.
275    pub id: String,
276    /// The token's declared type.
277    pub token_type: TokenType,
278    /// The token's declared value (literal or reference).
279    pub value: TokenValue,
280    /// Source declaration span, when available.
281    pub source_span: Option<Span>,
282}
283
284/// The top-level `tokens` block with its required `format` attribute.
285#[derive(Debug, Clone, PartialEq)]
286pub struct TokenBlock {
287    /// Must be `"zenith-token-v1"` in v0.
288    pub format: String,
289    /// The ordered list of token definitions.
290    pub tokens: Vec<Token>,
291}
292
293impl Default for TokenBlock {
294    fn default() -> Self {
295        Self {
296            format: "zenith-token-v1".to_owned(),
297            tokens: Vec::new(),
298        }
299    }
300}