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}