1use anyhow::{anyhow, bail, Context, Result};
2use serde_json::Value;
3use std::collections::{BTreeMap, BTreeSet};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
8pub struct Config {
9 pub dsp_path: PathBuf,
10 pub out_file: String,
11 pub type_name: String,
12 pub crate_path: String,
13}
14
15impl Config {
16 pub fn new(dsp_path: impl Into<PathBuf>) -> Self {
17 Self {
18 dsp_path: dsp_path.into(),
19 out_file: "app_design_system.rs".into(),
20 type_name: "AppDesignSystem".into(),
21 crate_path: "fission_theme".into(),
22 }
23 }
24}
25
26pub fn generate(config: Config) -> Result<PathBuf> {
27 let out_dir = std::env::var_os("OUT_DIR").ok_or_else(|| anyhow!("OUT_DIR is not set"))?;
28 let out_path = PathBuf::from(out_dir).join(&config.out_file);
29 let package = Package::load(&config.dsp_path)?;
30 println!("cargo:rerun-if-changed={}", package.dsp_path.display());
31 println!("cargo:rerun-if-changed={}", package.tokens_path.display());
32 let code = package.generate(&config)?;
33 fs::write(&out_path, code)
34 .with_context(|| format!("failed to write {}", out_path.display()))?;
35 Ok(out_path)
36}
37
38#[derive(Debug, Clone)]
39struct Package {
40 dsp_path: PathBuf,
41 tokens_path: PathBuf,
42 dsp: Value,
43 tokens: TokenStore,
44}
45
46impl Package {
47 fn load(dsp_path: &Path) -> Result<Self> {
48 let dsp_path = dsp_path.to_path_buf();
49 let dsp_text = fs::read_to_string(&dsp_path)
50 .with_context(|| format!("failed to read DSP manifest {}", dsp_path.display()))?;
51 let dsp: Value = serde_json::from_str(&dsp_text)
52 .with_context(|| format!("invalid JSON in {}", dsp_path.display()))?;
53 let dsp_dir = dsp_path
54 .parent()
55 .ok_or_else(|| anyhow!("DSP path has no parent: {}", dsp_path.display()))?;
56 let token_ref = dsp
57 .pointer("/tokens/$ref")
58 .and_then(Value::as_str)
59 .unwrap_or("tokens.json");
60 let tokens_path = dsp_dir.join(token_ref);
61 let tokens_text = fs::read_to_string(&tokens_path)
62 .with_context(|| format!("failed to read token file {}", tokens_path.display()))?;
63 let raw_tokens: Value = serde_json::from_str(&tokens_text)
64 .with_context(|| format!("invalid JSON in {}", tokens_path.display()))?;
65 let tokens = TokenStore::from_value(&raw_tokens)?;
66 Ok(Self {
67 dsp_path,
68 tokens_path,
69 dsp,
70 tokens,
71 })
72 }
73
74 fn generate(&self, config: &Config) -> Result<String> {
75 let krate = &config.crate_path;
76 let type_name = &config.type_name;
77 let info = self.info(krate);
78 let light = self.theme_expr(krate, Mode::Light)?;
79 let dark = self.theme_expr(krate, Mode::Dark)?;
80 let design_tokens = self.design_tokens_expr(krate)?;
81 let components = self.components_expr(krate)?;
82 let patterns = self.patterns_expr(krate)?;
83 let assets = self.assets_expr(krate)?;
84
85 Ok(format!(
86 r#"// @generated by fission-design-system-codegen. Do not edit by hand.
87#[allow(clippy::all)]
88#[allow(dead_code)]
89pub struct {type_name};
90
91static DESIGN_INFO: ::std::sync::OnceLock<{krate}::DesignSystemInfo> = ::std::sync::OnceLock::new();
92static DESIGN_TOKENS: ::std::sync::OnceLock<{krate}::DesignTokenSet> = ::std::sync::OnceLock::new();
93static DESIGN_COMPONENTS: ::std::sync::OnceLock<Vec<{krate}::DesignComponentSpec>> = ::std::sync::OnceLock::new();
94static DESIGN_PATTERNS: ::std::sync::OnceLock<Vec<{krate}::DesignPatternSpec>> = ::std::sync::OnceLock::new();
95static DESIGN_ASSETS: ::std::sync::OnceLock<{krate}::DesignAssetManifest> = ::std::sync::OnceLock::new();
96static LIGHT_THEME: ::std::sync::OnceLock<{krate}::Theme> = ::std::sync::OnceLock::new();
97static DARK_THEME: ::std::sync::OnceLock<{krate}::Theme> = ::std::sync::OnceLock::new();
98
99impl {krate}::DesignSystem for {type_name} {{
100 fn info() -> &'static {krate}::DesignSystemInfo {{
101 DESIGN_INFO.get_or_init(|| {info})
102 }}
103
104 fn tokens() -> &'static {krate}::DesignTokenSet {{
105 DESIGN_TOKENS.get_or_init(|| {design_tokens})
106 }}
107
108 fn components() -> &'static [{krate}::DesignComponentSpec] {{
109 DESIGN_COMPONENTS.get_or_init(|| {components}).as_slice()
110 }}
111
112 fn patterns() -> &'static [{krate}::DesignPatternSpec] {{
113 DESIGN_PATTERNS.get_or_init(|| {patterns}).as_slice()
114 }}
115
116 fn assets() -> &'static {krate}::DesignAssetManifest {{
117 DESIGN_ASSETS.get_or_init(|| {assets})
118 }}
119
120 fn theme_ref(mode: {krate}::DesignMode) -> &'static {krate}::Theme {{
121 match mode {{
122 {krate}::DesignMode::Light => LIGHT_THEME.get_or_init(|| {light}),
123 {krate}::DesignMode::Dark => DARK_THEME.get_or_init(|| {dark}),
124 }}
125 }}
126}}
127"#
128 ))
129 }
130
131 fn info(&self, krate: &str) -> String {
132 let package = self.dsp.get("$package").unwrap_or(&Value::Null);
133 let name = package
134 .get("name")
135 .and_then(Value::as_str)
136 .or_else(|| self.dsp.pointer("/brand/name").and_then(Value::as_str))
137 .unwrap_or("design-system");
138 let version = package
139 .get("version")
140 .and_then(Value::as_str)
141 .unwrap_or("0.0.0");
142 let description = package
143 .get("description")
144 .and_then(Value::as_str)
145 .unwrap_or("");
146 format!(
147 "{krate}::DesignSystemInfo {{ name: {}, version: {}, description: {}, source: {} }}",
148 rust_string(name),
149 rust_string(version),
150 rust_string(description),
151 rust_string(&self.dsp_path.display().to_string()),
152 )
153 }
154
155 fn theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
156 let mode_name = mode.as_str();
157 let colors = self.color_tokens_expr(krate, mode)?;
158 let spacing = self.spacing_tokens_expr(krate)?;
159 let typography = self.typography_tokens_expr(krate)?;
160 let radii = self.radius_tokens_expr(krate)?;
161 let elevations = self.elevation_tokens_expr(krate)?;
162 let motion = self.motion_tokens_expr(krate)?;
163 let data_visualization = self.data_visualization_tokens_expr(krate, mode)?;
164 let components = self.component_theme_expr(krate, mode)?;
165 Ok(format!(
166 r#"{krate}::Theme {{
167 tokens: {krate}::Tokens {{
168 colors: {colors},
169 spacing: {spacing},
170 typography: {typography},
171 radii: {radii},
172 elevations: {elevations},
173 motion: {motion},
174 data_visualization: {data_visualization},
175 }},
176 components: {components},
177 design_system: {krate}::ResolvedDesignSystem {{
178 mode: {krate}::DesignMode::{mode_name},
179 info: <{type_placeholder} as {krate}::DesignSystem>::info().clone(),
180 tokens: <{type_placeholder} as {krate}::DesignSystem>::tokens().clone(),
181 components: <{type_placeholder} as {krate}::DesignSystem>::components().to_vec(),
182 patterns: <{type_placeholder} as {krate}::DesignSystem>::patterns().to_vec(),
183 assets: <{type_placeholder} as {krate}::DesignSystem>::assets().clone(),
184 }},
185 }}"#,
186 type_placeholder = "Self"
187 ))
188 }
189
190 fn color_tokens_expr(&self, krate: &str, mode: Mode) -> Result<String> {
191 let prefix = match mode {
192 Mode::Light => "color.light",
193 Mode::Dark => "color.dark",
194 };
195 let fallback_on_error = match mode {
196 Mode::Light => "#FFFFFF",
197 Mode::Dark => "#020617",
198 };
199 let c =
200 |name: &str| -> Result<String> { self.color_expr(krate, &format!("{prefix}.{name}")) };
201 let c_or = |name: &str, fallback: &str| -> Result<String> {
202 self.color_expr_optional(krate, &format!("{prefix}.{name}"), fallback)
203 };
204 Ok(format!(
205 r#"{krate}::ColorTokens {{
206 primary: {primary},
207 on_primary: {on_primary},
208 primary_hover: {primary_hover},
209 primary_subtle: {primary_subtle},
210 secondary: {secondary},
211 on_secondary: {on_secondary},
212 surface: {surface},
213 on_surface: {on_surface},
214 surface_raised: {surface_raised},
215 surface_sunken: {surface_sunken},
216 background: {background},
217 on_background: {on_background},
218 error: {error},
219 on_error: {on_error},
220 success: {success},
221 warning: {warning},
222 info: {info},
223 border: {border},
224 border_strong: {border_strong},
225 divider: {divider},
226 text_primary: {text_primary},
227 text_secondary: {text_secondary},
228 text_muted: {text_muted},
229 text_link: {text_link},
230 heading: {heading},
231 focus_ring: {focus_ring},
232 }}"#,
233 primary = c("primary")?,
234 on_primary = c("on_primary")?,
235 primary_hover = c_or(
236 "primary_hover",
237 &self.resolve_token_string(&format!("{prefix}.primary"))?
238 )?,
239 primary_subtle = c_or(
240 "primary_subtle",
241 &self.resolve_token_string(&format!("{prefix}.surface"))?
242 )?,
243 secondary = c("secondary")?,
244 on_secondary = c("on_secondary")?,
245 surface = c("surface")?,
246 on_surface = c_or(
247 "on_surface",
248 &self.resolve_token_string(&format!("{prefix}.text_primary"))?
249 )?,
250 surface_raised = c_or(
251 "surface_raised",
252 &self.resolve_token_string(&format!("{prefix}.surface"))?
253 )?,
254 surface_sunken = c_or(
255 "surface_sunken",
256 &self.resolve_token_string(&format!("{prefix}.background"))?
257 )?,
258 background = c("background")?,
259 on_background = c_or(
260 "on_background",
261 &self.resolve_token_string(&format!("{prefix}.text_primary"))?
262 )?,
263 error = c("error")?,
264 on_error = self.color_literal_expr(krate, fallback_on_error)?,
265 success = c_or("success", "#10B981")?,
266 warning = c_or("warning", "#F59E0B")?,
267 info = c_or("info", "#0EA5E9")?,
268 border = c("border")?,
269 border_strong = c_or(
270 "border_strong",
271 &self.resolve_token_string(&format!("{prefix}.border"))?
272 )?,
273 divider = c_or(
274 "divider",
275 &self.resolve_token_string(&format!("{prefix}.border"))?
276 )?,
277 text_primary = c("text_primary")?,
278 text_secondary = c("text_secondary")?,
279 text_muted = c_or(
280 "text_muted",
281 &self.resolve_token_string(&format!("{prefix}.text_secondary"))?
282 )?,
283 text_link = c_or(
284 "text_link",
285 &self.resolve_token_string(&format!("{prefix}.primary"))?
286 )?,
287 heading = c_or(
288 "heading",
289 &self.resolve_token_string(&format!("{prefix}.text_primary"))?
290 )?,
291 focus_ring = c_or(
292 "focus_ring",
293 &self.resolve_token_string(&format!("{prefix}.primary"))?
294 )?,
295 ))
296 }
297
298 fn spacing_tokens_expr(&self, krate: &str) -> Result<String> {
299 Ok(format!(
300 "{krate}::SpacingTokens {{ none: {}, xs: {}, s: {}, m: {}, l: {}, xl: {}, xxl: {}, xxxl: {}, xxxxl: {} }}",
301 f32_lit(self.dimension("spacing.none")?),
302 f32_lit(self.dimension("spacing.xs")?),
303 f32_lit(self.dimension("spacing.s")?),
304 f32_lit(self.dimension("spacing.m")?),
305 f32_lit(self.dimension("spacing.l")?),
306 f32_lit(self.dimension("spacing.xl")?),
307 f32_lit(self.dimension_optional("spacing.2xl", 48.0)?),
308 f32_lit(self.dimension_optional("spacing.3xl", 64.0)?),
309 f32_lit(self.dimension_optional("spacing.4xl", 96.0)?),
310 ))
311 }
312
313 fn typography_tokens_expr(&self, krate: &str) -> Result<String> {
314 Ok(format!(
315 r#"{krate}::TypographyTokens {{
316 font_family_sans: {},
317 font_family_serif: {},
318 font_family_mono: {},
319 font_weight_regular: {},
320 font_weight_medium: {},
321 font_weight_semibold: {},
322 font_weight_bold: {},
323 font_size_xs: {},
324 font_size_sm: {},
325 font_size_base: {},
326 label_large_size: {},
327 body_medium_size: {},
328 body_large_size: {},
329 font_size_lg: {},
330 font_size_xl: {},
331 heading_size: {},
332 heading2_size: {},
333 heading1_size: {},
334 display_sm_size: {},
335 display_md_size: {},
336 line_height_display: {},
337 line_height_heading: {},
338 line_height_snug: {},
339 line_height_normal: {},
340 line_height_relaxed: {},
341 letter_spacing_tight: {},
342 letter_spacing_normal: {},
343 letter_spacing_label: {},
344 letter_spacing_kicker: {},
345 }}"#,
346 rust_string(&self.string_token_optional("typography.font_family.sans", "Inter")?),
347 rust_string(&self.string_token_optional("typography.font_family.serif", "Georgia")?),
348 rust_string(&self.string_token_optional("typography.font_family.mono", "monospace")?),
349 self.number_optional("typography.font_weight.regular", 400.0)? as u16,
350 self.number_optional("typography.font_weight.medium", 500.0)? as u16,
351 self.number_optional("typography.font_weight.semibold", 600.0)? as u16,
352 self.number_optional("typography.font_weight.bold", 700.0)? as u16,
353 f32_lit(self.dimension_optional("typography.font_size.xs", 12.0)?),
354 f32_lit(self.dimension_optional("typography.font_size.sm", 13.0)?),
355 f32_lit(self.dimension_optional("typography.font_size.base", 14.0)?),
356 f32_lit(self.dimension("typography.font_size.label_large")?),
357 f32_lit(self.dimension("typography.font_size.body")?),
358 f32_lit(self.dimension("typography.font_size.body_large")?),
359 f32_lit(self.dimension_optional("typography.font_size.lg", 20.0)?),
360 f32_lit(self.dimension_optional("typography.font_size.xl", 24.0)?),
361 f32_lit(self.dimension("typography.font_size.h3")?),
362 f32_lit(self.dimension_optional("typography.font_size.h2", 36.0)?),
363 f32_lit(self.dimension_optional("typography.font_size.h1", 48.0)?),
364 f32_lit(self.dimension_optional("typography.font_size.display_sm", 60.0)?),
365 f32_lit(self.dimension_optional("typography.font_size.display_md", 72.0)?),
366 f32_lit(self.number_optional("typography.line_height.display", 0.98)?),
367 f32_lit(self.number_optional("typography.line_height.heading", 1.05)?),
368 f32_lit(self.number_optional("typography.line_height.snug", 1.4)?),
369 f32_lit(self.number_optional("typography.line_height.normal", 1.6)?),
370 f32_lit(self.number_optional("typography.line_height.relaxed", 1.68)?),
371 f32_lit(self.dimension_optional("typography.letter_spacing.tight", -0.01)?),
372 f32_lit(self.dimension_optional("typography.letter_spacing.normal", 0.0)?),
373 f32_lit(self.dimension_optional("typography.letter_spacing.label", 0.1)?),
374 f32_lit(self.dimension_optional("typography.letter_spacing.kicker", 0.14)?),
375 ))
376 }
377
378 fn radius_tokens_expr(&self, krate: &str) -> Result<String> {
379 Ok(format!(
380 "{krate}::RadiusTokens {{ none: {}, small: {}, medium: {}, large: {}, xl: {}, xxl: {}, full: {} }}",
381 f32_lit(self.dimension_optional("radius.none", 0.0)?),
382 f32_lit(self.dimension("radius.small")?),
383 f32_lit(self.dimension("radius.medium")?),
384 f32_lit(self.dimension("radius.large")?),
385 f32_lit(self.dimension_optional("radius.xl", 16.0)?),
386 f32_lit(self.dimension_optional("radius.2xl", 24.0)?),
387 f32_lit(self.dimension("radius.full")?),
388 ))
389 }
390
391 fn elevation_tokens_expr(&self, krate: &str) -> Result<String> {
392 Ok(format!(
393 "{krate}::ElevationTokens {{ level0: {}, level1: {}, level2: {}, level3: {}, level4: {}, level5: {}, focus: {} }}",
394 self.shadow_option_expr(krate, "elevation.level0")?,
395 self.shadow_option_expr(krate, "elevation.level1")?,
396 self.shadow_option_expr(krate, "elevation.level2")?,
397 self.shadow_option_expr(krate, "elevation.level3")?,
398 self.shadow_option_expr(krate, "elevation.level4")?,
399 self.shadow_option_expr(krate, "elevation.level5")?,
400 self.shadow_option_expr(krate, "elevation.focus")?,
401 ))
402 }
403
404 fn motion_tokens_expr(&self, krate: &str) -> Result<String> {
405 Ok(format!(
406 "{krate}::MotionTokens {{ duration_instant_ms: {}, duration_micro_ms: {}, duration_fast_ms: {}, duration_normal_ms: {}, duration_slow_ms: {}, duration_deliberate_ms: {}, easing_linear: {}, easing_standard: {}, easing_in: {}, easing_out: {}, easing_ease: {} }}",
407 self.duration_ms_optional("motion.duration.instant", 0)?,
408 self.duration_ms_optional("motion.duration.micro", 120)?,
409 self.duration_ms_optional("motion.duration.fast", 160)?,
410 self.duration_ms_optional("motion.duration.normal", 200)?,
411 self.duration_ms_optional("motion.duration.slow", 300)?,
412 self.duration_ms_optional("motion.duration.deliberate", 480)?,
413 self.easing_expr(krate, "motion.easing.linear")?,
414 self.easing_expr(krate, "motion.easing.standard")?,
415 self.easing_expr(krate, "motion.easing.in")?,
416 self.easing_expr(krate, "motion.easing.out")?,
417 self.easing_expr(krate, "motion.easing.ease")?,
418 ))
419 }
420
421 fn data_visualization_tokens_expr(&self, krate: &str, mode: Mode) -> Result<String> {
422 let palette = self.palette_expr(krate, mode)?;
423 Ok(format!(
424 "{krate}::DataVisualizationTokens {{ palette: vec![{palette}] }}"
425 ))
426 }
427
428 fn component_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
429 let button = self.button_theme_expr(krate, mode)?;
430 let text_input = self.text_input_theme_expr(krate, mode)?;
431 let badge = self.badge_theme_expr(krate, mode)?;
432 let tabs = self.tabs_theme_expr(krate, mode)?;
433 let modal = self.modal_theme_expr(krate, mode)?;
434 let progress = self.progress_theme_expr(krate, mode)?;
435 let tooltip = self.tooltip_theme_expr(krate, mode)?;
436 let card = self.card_theme_expr(krate, mode)?;
437 let feature_icon = self.feature_icon_theme_expr(krate, mode)?;
438 let colors_prefix = match mode {
439 Mode::Light => "color.light",
440 Mode::Dark => "color.dark",
441 };
442 Ok(format!(
443 r#"{krate}::ComponentTheme {{
444 button: {button},
445 text_input: {text_input},
446 calendar: {krate}::CalendarTheme {{ bg_color: {surface}, border_color: {border}, radius: {radius_medium}, selected_bg: {primary}, selected_text: {on_primary}, today_outline: {secondary} }},
447 pagination: {krate}::PaginationTheme {{ spacing: {spacing_s}, active_bg: {primary}, active_text: {on_primary} }},
448 timeline: {krate}::TimelineTheme {{ dot_size: 12.0, line_width: 2.0, dot_color: {primary}, line_color: {border} }},
449 segmented_control: {krate}::SegmentedControlTheme {{ bg_color: {surface}, border_color: {border}, radius: {radius_full}, active_bg: {primary}, active_text: {on_primary} }},
450 alert: {krate}::AlertTheme {{ info_bg: {info_bg}, warning_bg: {warning_bg}, error_bg: {error_bg}, success_bg: {success_bg}, radius: {radius_medium} }},
451 badge: {badge},
452 tabs: {tabs},
453 modal: {modal},
454 tree_view: {krate}::TreeViewTheme {{ indent: 16.0, selected_bg: {primary}.with_alpha(52), hover_bg: {surface} }},
455 progress: {progress},
456 tooltip: {tooltip},
457 card: {card},
458 feature_icon: {feature_icon},
459 }}"#,
460 surface = self.color_expr(krate, &format!("{colors_prefix}.surface"))?,
461 border = self.color_expr(krate, &format!("{colors_prefix}.border"))?,
462 primary = self.color_expr(krate, &format!("{colors_prefix}.primary"))?,
463 on_primary = self.color_expr(krate, &format!("{colors_prefix}.on_primary"))?,
464 secondary = self.color_expr(krate, &format!("{colors_prefix}.secondary"))?,
465 info_bg = self.color_literal_expr(krate, "#E6F2FF")?,
466 warning_bg = self.color_literal_expr(krate, "#FFF4E5")?,
467 error_bg = format!(
468 "{}.with_alpha(30)",
469 self.color_expr(krate, &format!("{colors_prefix}.error"))?
470 ),
471 success_bg = self.color_literal_expr(krate, "#EDF7ED")?,
472 radius_medium = f32_lit(self.dimension("radius.medium")?),
473 radius_full = f32_lit(self.dimension("radius.full")?),
474 spacing_s = f32_lit(self.dimension("spacing.s")?),
475 ))
476 }
477
478 fn button_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
479 let colors_prefix = match mode {
480 Mode::Light => "color.light",
481 Mode::Dark => "color.dark",
482 };
483 let height = self.dsp_dimension_optional(
484 "/components/button/sizes/md/height",
485 self.dimension_optional("component.button.height", 42.0)?,
486 )?;
487 let padding_h = self.dsp_dimension_optional(
488 "/components/button/sizes/md/padding_x",
489 self.dimension_optional("component.button.padding_horizontal", 16.0)?,
490 )?;
491 let padding_v = self.dimension_optional("component.button.padding_vertical", 8.0)?;
492 let radius = self.dsp_dimension_optional(
493 "/components/button/radius",
494 self.dimension_optional("component.button.radius", 9999.0)?,
495 )?;
496 let text_size = self.dsp_dimension_optional(
497 "/components/button/sizes/md/font_size",
498 self.dimension_optional("component.button.text_size", 15.0)?,
499 )?;
500 Ok(format!(
501 r#"{krate}::ButtonTheme {{
502 height: {height},
503 padding_horizontal: {padding_h},
504 padding_vertical: {padding_v},
505 radius: {radius},
506 text_size: {text_size},
507 elevation_rest: {elevation_rest},
508 elevation_hover: {elevation_hover},
509 elevation_pressed: {elevation_pressed},
510 focus_stroke: Some({krate}::Stroke {{ fill: {krate}::Fill::Solid({focus}), width: 2.0, dash_array: None, line_cap: {krate}::LineCap::Round, line_join: {krate}::LineJoin::Round }}),
511 icon_size: {icon_size},
512 font_weight: {font_weight},
513 line_height: {line_height},
514 transition: {transition},
515 sizes: vec![{sizes}],
516 hierarchies: vec![{hierarchies}],
517 }}"#,
518 height = f32_lit(height),
519 padding_h = f32_lit(padding_h),
520 padding_v = f32_lit(padding_v),
521 radius = f32_lit(radius),
522 text_size = f32_lit(text_size),
523 elevation_rest = self.shadow_option_expr(krate, "component.button.elevation_rest")?,
524 elevation_hover = self.shadow_option_expr(krate, "component.button.elevation_hover")?,
525 elevation_pressed =
526 self.shadow_option_expr(krate, "component.button.elevation_pressed")?,
527 focus = self.color_expr_optional(
528 krate,
529 &format!("{colors_prefix}.focus_ring"),
530 &self.resolve_token_string(&format!("{colors_prefix}.primary"))?
531 )?,
532 icon_size = f32_lit(self.style_dimension_optional(
533 mode,
534 self.dsp.pointer("/components/button/icon_size"),
535 20.0,
536 )?),
537 font_weight = self.style_u16_optional(
538 mode,
539 self.dsp.pointer("/components/button/font_weight"),
540 600,
541 )?,
542 line_height = f32_lit(
543 self.dsp_dimension_optional("/components/button/sizes/md/line_height", 20.0,)?
544 ),
545 transition = self.motion_option_expr(
546 krate,
547 mode,
548 self.dsp.pointer("/components/button/transition"),
549 )?,
550 sizes = self.size_styles_expr(krate, mode, "/components/button/sizes")?,
551 hierarchies = self.enum_state_styles_expr(
552 krate,
553 mode,
554 "/components/button/hierarchies",
555 button_hierarchy_variant,
556 )?,
557 ))
558 }
559
560 fn text_input_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
561 let colors_prefix = match mode {
562 Mode::Light => "color.light",
563 Mode::Dark => "color.dark",
564 };
565 let height = self.dsp_dimension_optional(
566 "/components/input/sizes/md/height",
567 self.dimension_optional("component.text_input.height", 40.0)?,
568 )?;
569 let padding_h = self.dsp_dimension_optional(
570 "/components/input/sizes/md/padding_x",
571 self.dimension_optional("component.text_input.padding_h", 16.0)?,
572 )?;
573 let radius = self.dsp_dimension_optional(
574 "/components/input/radius",
575 self.dimension_optional("component.text_input.radius", 4.0)?,
576 )?;
577 let font_size = self.dsp_dimension_optional(
578 "/components/input/font_size",
579 self.dimension_optional("component.text_input.font_size", 17.0)?,
580 )?;
581 Ok(format!(
582 r#"{krate}::TextInputTheme {{
583 height: {height},
584 padding_h: {padding_h},
585 radius: {radius},
586 font_size: {font_size},
587 border_color: {border},
588 border_width: {border_width},
589 focus_color: {focus},
590 text_color: {text},
591 placeholder_color: {placeholder},
592 line_height: {line_height},
593 font_weight: {font_weight},
594 sizes: vec![{sizes}],
595 states: {states},
596 placeholder_style: {placeholder_style},
597 label_style: {label_style},
598 helper_style: {helper_style},
599 }}"#,
600 height = f32_lit(height),
601 padding_h = f32_lit(padding_h),
602 radius = f32_lit(radius),
603 font_size = f32_lit(font_size),
604 border = self.color_expr(krate, &format!("{colors_prefix}.border"))?,
605 border_width =
606 f32_lit(self.dimension_optional("component.text_input.border_width", 1.0)?),
607 focus = self.color_expr_optional(
608 krate,
609 &format!("{colors_prefix}.focus_ring"),
610 &self.resolve_token_string(&format!("{colors_prefix}.primary"))?
611 )?,
612 text = self.color_expr(krate, &format!("{colors_prefix}.text_primary"))?,
613 placeholder = self.color_expr_optional(
614 krate,
615 &format!("{colors_prefix}.text_muted"),
616 &self.resolve_token_string(&format!("{colors_prefix}.text_secondary"))?
617 )?,
618 line_height =
619 f32_lit(self.dsp_dimension_optional("/components/input/line_height", 24.0)?),
620 font_weight = self.style_u16_optional(
621 mode,
622 self.dsp.pointer("/components/input/font_weight"),
623 400,
624 )?,
625 sizes = self.size_styles_expr(krate, mode, "/components/input/sizes")?,
626 states =
627 self.state_styles_expr(krate, mode, self.dsp.pointer("/components/input/states"))?,
628 placeholder_style = self.style_expr(
629 krate,
630 mode,
631 self.dsp.pointer("/components/input/states/placeholder")
632 )?,
633 label_style =
634 self.style_expr(krate, mode, self.dsp.pointer("/components/input/label"))?,
635 helper_style = self.style_expr(
636 krate,
637 mode,
638 self.dsp.pointer("/components/input/helper_text")
639 )?,
640 ))
641 }
642
643 fn badge_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
644 let radius = self.style_dimension_optional(
645 mode,
646 self.dsp.pointer("/components/badge/radius"),
647 self.dimension_optional("component.badge.radius", self.dimension("radius.full")?)?,
648 )?;
649 let font_size = self.dsp_dimension_optional(
650 "/components/badge/sizes/md/font_size",
651 self.dimension_optional("component.badge.font_size", 10.0)?,
652 )?;
653 Ok(format!(
654 r#"{krate}::BadgeTheme {{
655 radius: {radius},
656 font_size: {font_size},
657 font_weight: {font_weight},
658 sizes: vec![{sizes}],
659 tones: vec![{tones}],
660 }}"#,
661 radius = f32_lit(radius),
662 font_size = f32_lit(font_size),
663 font_weight = self.style_u16_optional(
664 mode,
665 self.dsp.pointer("/components/badge/font_weight"),
666 500
667 )?,
668 sizes = self.size_styles_expr(krate, mode, "/components/badge/sizes")?,
669 tones =
670 self.enum_styles_expr(krate, mode, "/components/badge/colors", badge_tone_variant)?,
671 ))
672 }
673
674 fn tabs_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
675 let colors_prefix = match mode {
676 Mode::Light => "color.light",
677 Mode::Dark => "color.dark",
678 };
679 let active = self.color_expr(krate, &format!("{colors_prefix}.primary"))?;
680 let inactive = self.color_expr(krate, &format!("{colors_prefix}.text_secondary"))?;
681 let background = self.color_expr(krate, &format!("{colors_prefix}.background"))?;
682 let divider = self.color_expr_optional(
683 krate,
684 &format!("{colors_prefix}.divider"),
685 &self.resolve_token_string(&format!("{colors_prefix}.border"))?,
686 )?;
687 Ok(format!(
688 r#"{krate}::TabsTheme {{
689 active_color: {active},
690 inactive_color: {inactive},
691 indicator_height: {indicator_height},
692 background: {background},
693 divider_color: {divider}.with_alpha(120),
694 sizes: vec![{sizes}],
695 states: {states},
696 track_style: {track},
697 }}"#,
698 indicator_height = f32_lit(self.dsp_dimension_optional(
699 "/components/tabs/sizes/md/indicator_height",
700 self.dimension_optional("component.tabs.indicator_height", 3.0)?
701 )?),
702 sizes = self.size_styles_expr(krate, mode, "/components/tabs/sizes")?,
703 states =
704 self.state_styles_expr(krate, mode, self.dsp.pointer("/components/tabs/states"))?,
705 track = self.style_expr(krate, mode, self.dsp.pointer("/components/tabs/track"))?,
706 ))
707 }
708
709 fn modal_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
710 let bg = self
711 .style_fill_color_expr(
712 krate,
713 mode,
714 self.dsp.pointer("/components/modal/background"),
715 )?
716 .unwrap_or(self.color_expr(krate, &format!("color.{}.surface", mode.color_name()))?);
717 let radius = self.style_dimension_optional(
718 mode,
719 self.dsp.pointer("/components/modal/radius"),
720 self.dimension_optional("component.modal.radius", self.dimension("radius.large")?)?,
721 )?;
722 let max_width = self.style_dimension_optional(
723 mode,
724 self.dsp.pointer("/components/modal/max_width"),
725 self.dimension_optional("component.modal.max_width", 600.0)?,
726 )?;
727 let shadow = self.shadow_option_expr(krate, "component.modal.shadow")?;
728 let container_style = self.style_expr_for_component(
729 krate,
730 mode,
731 "/components/modal",
732 &["background", "radius", "max_width", "box_shadow"],
733 )?;
734 let scrim_style =
735 self.style_expr(krate, mode, self.dsp.pointer("/components/modal/scrim"))?;
736 let scrim_blur = self
737 .dsp
738 .pointer("/components/modal/scrim/backdrop_filter")
739 .and_then(Value::as_str)
740 .and_then(|value| {
741 value
742 .strip_prefix("blur(")
743 .and_then(|v| v.strip_suffix(')'))
744 })
745 .and_then(|value| parse_dimension(value).ok())
746 .unwrap_or(4.0);
747 Ok(format!(
748 r#"{krate}::ModalTheme {{
749 bg_color: {bg},
750 radius: {radius},
751 shadow: {shadow},
752 max_width: {max_width},
753 container_style: {container_style},
754 scrim_style: {scrim_style},
755 scrim_blur: {scrim_blur},
756 }}"#,
757 radius = f32_lit(radius),
758 max_width = f32_lit(max_width),
759 scrim_blur = f32_lit(scrim_blur),
760 ))
761 }
762
763 fn progress_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
764 let height = self.style_dimension_optional(
765 mode,
766 self.dsp.pointer("/components/progress_bar/height"),
767 self.dimension_optional("component.progress.height", 8.0)?,
768 )?;
769 let radius = self.style_dimension_optional(
770 mode,
771 self.dsp.pointer("/components/progress_bar/radius"),
772 self.dimension("radius.full")?,
773 )?;
774 let track_style = self.style_expr(
775 krate,
776 mode,
777 self.dsp.pointer("/components/progress_bar/track"),
778 )?;
779 let fill_style = self.style_expr(
780 krate,
781 mode,
782 self.dsp.pointer("/components/progress_bar/fill"),
783 )?;
784 let track_color = self
785 .style_fill_color_expr(
786 krate,
787 mode,
788 self.dsp
789 .pointer("/components/progress_bar/track/background"),
790 )?
791 .unwrap_or_else(|| {
792 self.color_expr(krate, &format!("color.{}.border", mode.color_name()))
793 .unwrap()
794 });
795 let fill_color = self
796 .style_fill_color_expr(
797 krate,
798 mode,
799 self.dsp.pointer("/components/progress_bar/fill/background"),
800 )?
801 .unwrap_or_else(|| {
802 self.color_expr(krate, &format!("color.{}.primary", mode.color_name()))
803 .unwrap()
804 });
805 Ok(format!(
806 r#"{krate}::ProgressTheme {{
807 height: {height},
808 track_color: {track_color},
809 bar_color: {fill_color},
810 radius: {radius},
811 track_style: {track_style},
812 fill_style: {fill_style},
813 }}"#,
814 height = f32_lit(height),
815 radius = f32_lit(radius),
816 ))
817 }
818
819 fn tooltip_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
820 let style = self.style_expr_for_component(
821 krate,
822 mode,
823 "/components/tooltip",
824 &[
825 "background",
826 "color",
827 "radius",
828 "font_size",
829 "padding",
830 "max_width",
831 "box_shadow",
832 ],
833 )?;
834 let bg = self
835 .style_fill_color_expr(
836 krate,
837 mode,
838 self.dsp.pointer("/components/tooltip/background"),
839 )?
840 .unwrap_or(self.color_literal_expr(krate, "#323232")?);
841 let text = self
842 .style_fill_color_expr(krate, mode, self.dsp.pointer("/components/tooltip/color"))?
843 .unwrap_or(self.color_literal_expr(krate, "#FFFFFF")?);
844 let radius = self.style_dimension_optional(
845 mode,
846 self.dsp.pointer("/components/tooltip/radius"),
847 self.dimension_optional("component.tooltip.radius", self.dimension("radius.small")?)?,
848 )?;
849 let font_size = self.style_dimension_optional(
850 mode,
851 self.dsp.pointer("/components/tooltip/font_size"),
852 self.dimension_optional("component.tooltip.font_size", 12.0)?,
853 )?;
854 let (padding_x, padding_y) = self
855 .dsp
856 .pointer("/components/tooltip/padding")
857 .and_then(Value::as_str)
858 .and_then(|raw| self.resolve_refs_in_string_for_mode(raw, mode).ok())
859 .and_then(|raw| parse_padding(&raw).ok())
860 .map(|p| (p[0], p[2]))
861 .unwrap_or((10.0, 8.0));
862 let max_width = self.style_dimension_optional(
863 mode,
864 self.dsp.pointer("/components/tooltip/max_width"),
865 240.0,
866 )?;
867 Ok(format!(
868 r#"{krate}::TooltipTheme {{
869 bg_color: {bg},
870 text_color: {text},
871 radius: {radius},
872 font_size: {font_size},
873 padding_x: {padding_x},
874 padding_y: {padding_y},
875 max_width: {max_width},
876 style: {style},
877 }}"#,
878 radius = f32_lit(radius),
879 font_size = f32_lit(font_size),
880 padding_x = f32_lit(padding_x),
881 padding_y = f32_lit(padding_y),
882 max_width = f32_lit(max_width),
883 ))
884 }
885
886 fn card_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
887 let padding = self.style_dimension_optional(
888 mode,
889 self.dsp.pointer("/components/card/padding"),
890 self.dimension_optional("component.card.padding", self.dimension("spacing.l")?)?,
891 )?;
892 let radius = self.style_dimension_optional(
893 mode,
894 self.dsp.pointer("/components/card/radius"),
895 self.dimension_optional("component.card.radius", self.dimension("radius.large")?)?,
896 )?;
897 let patterns = self.enum_styles_expr(
898 krate,
899 mode,
900 "/components/card/patterns",
901 card_pattern_variant,
902 )?;
903 let hover = self.style_expr(
904 krate,
905 mode,
906 self.dsp.pointer("/components/card/interaction/hover"),
907 )?;
908 Ok(format!(
909 r#"{krate}::CardTheme {{
910 padding: {padding},
911 radius: {radius},
912 default_pattern: {krate}::CardPattern::Raised,
913 patterns: vec![{patterns}],
914 hover_style: {hover},
915 }}"#,
916 padding = f32_lit(padding),
917 radius = f32_lit(radius),
918 ))
919 }
920
921 fn feature_icon_theme_expr(&self, krate: &str, mode: Mode) -> Result<String> {
922 Ok(format!(
923 r#"{krate}::FeatureIconTheme {{
924 sizes: vec![{sizes}],
925 tones: vec![{tones}],
926 shadow: {shadow},
927 }}"#,
928 sizes = self.size_styles_expr(krate, mode, "/components/feature_icon/sizes")?,
929 tones = self.enum_styles_expr(
930 krate,
931 mode,
932 "/components/feature_icon/tones",
933 feature_icon_tone_variant
934 )?,
935 shadow = self.shadow_option_from_value_expr(
936 krate,
937 mode,
938 self.dsp.pointer("/components/feature_icon/box_shadow"),
939 )?,
940 ))
941 }
942
943 fn palette_expr(&self, krate: &str, mode: Mode) -> Result<String> {
944 let mut colors = Vec::new();
945 if let Some(items) = self
946 .dsp
947 .pointer("/data_visualization/palette")
948 .or_else(|| self.dsp.pointer("/charts/palette"))
949 .and_then(Value::as_array)
950 {
951 for item in items {
952 if let Some(color) = self.style_fill_color_expr(krate, mode, Some(item))? {
953 colors.push(color);
954 }
955 }
956 }
957 if colors.is_empty() {
958 let token_paths = [
959 "color.teal.700",
960 "color.brand.blue.600",
961 "color.semantic.warning",
962 "color.semantic.error",
963 "color.semantic.success",
964 "color.semantic.info",
965 "color.brand.orange.600",
966 "color.teal.500",
967 ];
968 for path in token_paths {
969 if self.tokens.contains(path) {
970 colors.push(self.color_expr(krate, path)?);
971 }
972 }
973 }
974 if colors.is_empty() {
975 colors = vec![
976 self.color_literal_expr(krate, "#14B8A6")?,
977 self.color_literal_expr(krate, "#4DA6E0")?,
978 self.color_literal_expr(krate, "#F59E0B")?,
979 self.color_literal_expr(krate, "#F43F5E")?,
980 ];
981 }
982 Ok(colors.join(","))
983 }
984
985 fn size_styles_expr(&self, krate: &str, mode: Mode, pointer: &str) -> Result<String> {
986 let Some(obj) = self.dsp.pointer(pointer).and_then(Value::as_object) else {
987 return Ok(String::new());
988 };
989 let mut items = Vec::new();
990 for (name, value) in obj {
991 if let Some(variant) = component_size_variant(krate, name) {
992 items.push(format!(
993 "({variant}, {})",
994 self.style_expr(krate, mode, Some(value))?
995 ));
996 }
997 }
998 Ok(items.join(","))
999 }
1000
1001 fn enum_styles_expr(
1002 &self,
1003 krate: &str,
1004 mode: Mode,
1005 pointer: &str,
1006 variant: fn(&str, &str) -> Option<String>,
1007 ) -> Result<String> {
1008 let Some(obj) = self.dsp.pointer(pointer).and_then(Value::as_object) else {
1009 return Ok(String::new());
1010 };
1011 let mut items = Vec::new();
1012 for (name, value) in obj {
1013 if let Some(variant_expr) = variant(krate, name) {
1014 items.push(format!(
1015 "({variant_expr}, {})",
1016 self.style_expr(krate, mode, Some(value))?
1017 ));
1018 }
1019 }
1020 Ok(items.join(","))
1021 }
1022
1023 fn enum_state_styles_expr(
1024 &self,
1025 krate: &str,
1026 mode: Mode,
1027 pointer: &str,
1028 variant: fn(&str, &str) -> Option<String>,
1029 ) -> Result<String> {
1030 let Some(obj) = self.dsp.pointer(pointer).and_then(Value::as_object) else {
1031 return Ok(String::new());
1032 };
1033 let mut items = Vec::new();
1034 for (name, value) in obj {
1035 if let Some(variant_expr) = variant(krate, name) {
1036 items.push(format!(
1037 "({variant_expr}, {})",
1038 self.state_styles_expr(krate, mode, Some(value))?
1039 ));
1040 }
1041 }
1042 Ok(items.join(","))
1043 }
1044
1045 fn state_styles_expr(&self, krate: &str, mode: Mode, value: Option<&Value>) -> Result<String> {
1046 let state = |name: &str| -> Result<String> {
1047 let expr = value
1048 .and_then(|v| v.get(name))
1049 .map(|v| self.style_expr(krate, mode, Some(v)))
1050 .transpose()?;
1051 Ok(expr
1052 .map(|expr| format!("Some({expr})"))
1053 .unwrap_or_else(|| "None".into()))
1054 };
1055 let default = value
1056 .and_then(|v| v.get("default"))
1057 .map(|v| self.style_expr(krate, mode, Some(v)))
1058 .transpose()?
1059 .unwrap_or_else(|| format!("{krate}::ResolvedComponentStyle::default()"));
1060 Ok(format!(
1061 r#"{krate}::ComponentStateStyles {{
1062 default: {default},
1063 hover: {hover},
1064 active: {active},
1065 focus: {focus},
1066 disabled: {disabled},
1067 error: {error},
1068 selected: {selected},
1069 }}"#,
1070 hover = state("hover")?,
1071 active = state("active")?,
1072 focus = state("focus")?,
1073 disabled = state("disabled")?,
1074 error = state("error")?,
1075 selected = state("selected")?,
1076 ))
1077 }
1078
1079 fn style_expr_for_component(
1080 &self,
1081 krate: &str,
1082 mode: Mode,
1083 pointer: &str,
1084 keys: &[&str],
1085 ) -> Result<String> {
1086 let Some(source) = self.dsp.pointer(pointer).and_then(Value::as_object) else {
1087 return self.style_expr(krate, mode, None);
1088 };
1089 let mut obj = serde_json::Map::new();
1090 for key in keys {
1091 if let Some(value) = source.get(*key) {
1092 obj.insert((*key).to_string(), value.clone());
1093 }
1094 }
1095 self.style_expr(krate, mode, Some(&Value::Object(obj)))
1096 }
1097
1098 fn style_expr(&self, krate: &str, mode: Mode, value: Option<&Value>) -> Result<String> {
1099 let Some(value) = value else {
1100 return Ok(format!("{krate}::ResolvedComponentStyle::default()"));
1101 };
1102 let background = self.fill_option_expr(krate, mode, field(value, "background"))?;
1103 let text_color = self
1104 .style_fill_color_expr(krate, mode, field(value, "color"))?
1105 .map(|expr| format!("Some({expr})"))
1106 .unwrap_or_else(|| "None".into());
1107 let border = self.border_option_expr(
1108 krate,
1109 mode,
1110 field(value, "border").or_else(|| field(value, "border_bottom")),
1111 )?;
1112 let shadows = self.shadow_layers_expr(
1113 krate,
1114 mode,
1115 field(value, "box_shadow").or_else(|| field(value, "shadow")),
1116 )?;
1117 let radius = self.style_dimension_option_expr(mode, field(value, "radius"))?;
1118 let height = self.style_dimension_option_expr(mode, field(value, "height"))?;
1119 let width = self.style_dimension_option_expr(mode, field(value, "width"))?;
1120 let size = self.style_dimension_option_expr(mode, field(value, "size"))?;
1121 let width = if width == "None" { size.clone() } else { width };
1122 let height = if height == "None" { size } else { height };
1123 let padding_x = self.style_dimension_option_expr(mode, field(value, "padding_x"))?;
1124 let padding_y = self.style_dimension_option_expr(mode, field(value, "padding_y"))?;
1125 let padding = self.padding_option_expr(mode, field(value, "padding"))?;
1126 let gap = self.style_dimension_option_expr(mode, field(value, "gap"))?;
1127 let font_size = self.style_dimension_option_expr(mode, field(value, "font_size"))?;
1128 let font_weight = self.style_u16_option_expr(mode, field(value, "font_weight"))?;
1129 let line_height = self.style_dimension_option_expr(mode, field(value, "line_height"))?;
1130 let letter_spacing =
1131 self.style_dimension_option_expr(mode, field(value, "letter_spacing"))?;
1132 let icon_size = self.style_dimension_option_expr(mode, field(value, "icon_size"))?;
1133 let max_width = self.style_dimension_option_expr(mode, field(value, "max_width"))?;
1134 let transition = self.motion_option_expr(krate, mode, field(value, "transition"))?;
1135 Ok(format!(
1136 r#"{krate}::ResolvedComponentStyle {{
1137 background: {background},
1138 text_color: {text_color},
1139 border: {border},
1140 radius: {radius},
1141 height: {height},
1142 width: {width},
1143 padding_x: {padding_x},
1144 padding_y: {padding_y},
1145 padding: {padding},
1146 gap: {gap},
1147 font_size: {font_size},
1148 font_weight: {font_weight},
1149 line_height: {line_height},
1150 letter_spacing: {letter_spacing},
1151 icon_size: {icon_size},
1152 max_width: {max_width},
1153 shadows: {shadows},
1154 transition: {transition},
1155 }}"#
1156 ))
1157 }
1158
1159 fn fill_option_expr(&self, krate: &str, mode: Mode, value: Option<&Value>) -> Result<String> {
1160 let Some(fill) = self.fill_expr(krate, mode, value)? else {
1161 return Ok("None".into());
1162 };
1163 Ok(format!("Some({fill})"))
1164 }
1165
1166 fn fill_expr(&self, krate: &str, mode: Mode, value: Option<&Value>) -> Result<Option<String>> {
1167 let Some(raw) = self.resolved_value_string(mode, value)? else {
1168 return Ok(None);
1169 };
1170 let raw = raw.trim();
1171 if raw.eq_ignore_ascii_case("none") || raw.eq_ignore_ascii_case("auto") {
1172 return Ok(None);
1173 }
1174 if raw.eq_ignore_ascii_case("transparent") {
1175 return Ok(Some(format!(
1176 "{krate}::Fill::Solid({})",
1177 color_expr(krate, 0, 0, 0, 0)
1178 )));
1179 }
1180 if raw.starts_with("linear-gradient(") {
1181 let stops = gradient_stops(raw)
1182 .into_iter()
1183 .enumerate()
1184 .map(|(idx, color)| {
1185 let position = if idx == 0 { 0.0 } else { 1.0 };
1186 Ok(format!(
1187 "({}, {})",
1188 f32_lit(position),
1189 self.color_literal_expr(krate, color)?
1190 ))
1191 })
1192 .collect::<Result<Vec<_>>>()?;
1193 return Ok(Some(format!(
1194 "{krate}::Fill::LinearGradient {{ start: (0.0, 0.0), end: (1.0, 1.0), stops: vec![{}] }}",
1195 stops.join(",")
1196 )));
1197 }
1198 if raw.starts_with("radial-gradient(") {
1199 let colors = gradient_stops(raw);
1200 let mut stops = Vec::new();
1201 for (idx, color) in colors.iter().enumerate() {
1202 let position = if colors.len() <= 1 {
1203 0.0
1204 } else {
1205 idx as f32 / (colors.len() - 1) as f32
1206 };
1207 stops.push(format!(
1208 "({}, {})",
1209 f32_lit(position),
1210 self.color_literal_expr(krate, color)?
1211 ));
1212 }
1213 return Ok(Some(format!(
1214 "{krate}::Fill::RadialGradient {{ center: (0.5, 0.5), radius: 1.0, stops: vec![{}] }}",
1215 stops.join(",")
1216 )));
1217 }
1218 if parse_color(raw).is_ok() {
1219 return Ok(Some(format!(
1220 "{krate}::Fill::Solid({})",
1221 self.color_literal_expr(krate, raw)?
1222 )));
1223 }
1224 Ok(None)
1225 }
1226
1227 fn style_fill_color_expr(
1228 &self,
1229 krate: &str,
1230 mode: Mode,
1231 value: Option<&Value>,
1232 ) -> Result<Option<String>> {
1233 let Some(raw) = self.resolved_value_string(mode, value)? else {
1234 return Ok(None);
1235 };
1236 let raw = raw.trim();
1237 if raw.eq_ignore_ascii_case("transparent") {
1238 return Ok(Some(color_expr(krate, 0, 0, 0, 0)));
1239 }
1240 if parse_color(raw).is_ok() {
1241 return Ok(Some(self.color_literal_expr(krate, raw)?));
1242 }
1243 Ok(None)
1244 }
1245
1246 fn border_option_expr(&self, krate: &str, mode: Mode, value: Option<&Value>) -> Result<String> {
1247 let Some(raw) = self.resolved_value_string(mode, value)? else {
1248 return Ok("None".into());
1249 };
1250 let raw = raw.trim();
1251 if raw.eq_ignore_ascii_case("none") || raw.eq_ignore_ascii_case("transparent") {
1252 return Ok("None".into());
1253 }
1254 let Some((width, color)) = parse_border(raw) else {
1255 return Ok("None".into());
1256 };
1257 Ok(format!(
1258 "Some({krate}::ComponentBorder {{ fill: {krate}::Fill::Solid({}), width: {} }})",
1259 self.color_literal_expr(krate, color)?,
1260 f32_lit(width)
1261 ))
1262 }
1263
1264 fn shadow_layers_expr(&self, krate: &str, mode: Mode, value: Option<&Value>) -> Result<String> {
1265 let Some(raw) = self.resolved_value_string(mode, value)? else {
1266 return Ok("Vec::new()".into());
1267 };
1268 let layers = parse_shadow_layers(&raw);
1269 let exprs = layers
1270 .iter()
1271 .map(|layer| shadow_layer_expr(krate, layer))
1272 .collect::<Vec<_>>();
1273 Ok(format!("vec![{}]", exprs.join(",")))
1274 }
1275
1276 fn shadow_option_from_value_expr(
1277 &self,
1278 krate: &str,
1279 mode: Mode,
1280 value: Option<&Value>,
1281 ) -> Result<String> {
1282 let Some(raw) = self.resolved_value_string(mode, value)? else {
1283 return Ok("None".into());
1284 };
1285 let layers = parse_shadow_layers(&raw);
1286 if let Some(layer) = layers.iter().find(|layer| !layer.inset) {
1287 Ok(format!("Some({})", box_shadow_expr(krate, layer)))
1288 } else {
1289 Ok("None".into())
1290 }
1291 }
1292
1293 fn style_dimension_optional(
1294 &self,
1295 mode: Mode,
1296 value: Option<&Value>,
1297 fallback: f32,
1298 ) -> Result<f32> {
1299 let Some(raw) = self.resolved_value_string(mode, value)? else {
1300 return Ok(fallback);
1301 };
1302 parse_dimension(&raw)
1303 .or_else(|_| raw.parse::<f32>())
1304 .or(Ok(fallback))
1305 }
1306
1307 fn style_dimension_option_expr(&self, mode: Mode, value: Option<&Value>) -> Result<String> {
1308 let Some(raw) = self.resolved_value_string(mode, value)? else {
1309 return Ok("None".into());
1310 };
1311 if raw == "auto" {
1312 return Ok("None".into());
1313 }
1314 let Ok(dimension) = parse_dimension(&raw).or_else(|_| raw.parse::<f32>()) else {
1315 return Ok("None".into());
1316 };
1317 Ok(format!("Some({})", f32_lit(dimension)))
1318 }
1319
1320 fn style_u16_optional(&self, mode: Mode, value: Option<&Value>, fallback: u16) -> Result<u16> {
1321 let Some(raw) = self.resolved_value_string(mode, value)? else {
1322 return Ok(fallback);
1323 };
1324 Ok(raw.parse::<u16>().unwrap_or(fallback))
1325 }
1326
1327 fn style_u16_option_expr(&self, mode: Mode, value: Option<&Value>) -> Result<String> {
1328 let Some(raw) = self.resolved_value_string(mode, value)? else {
1329 return Ok("None".into());
1330 };
1331 Ok(raw
1332 .parse::<u16>()
1333 .map(|value| format!("Some({value})"))
1334 .unwrap_or_else(|_| "None".into()))
1335 }
1336
1337 fn padding_option_expr(&self, mode: Mode, value: Option<&Value>) -> Result<String> {
1338 let Some(raw) = self.resolved_value_string(mode, value)? else {
1339 return Ok("None".into());
1340 };
1341 let Ok(padding) = parse_padding(&raw) else {
1342 return Ok("None".into());
1343 };
1344 Ok(format!(
1345 "Some([{}, {}, {}, {}])",
1346 f32_lit(padding[0]),
1347 f32_lit(padding[1]),
1348 f32_lit(padding[2]),
1349 f32_lit(padding[3])
1350 ))
1351 }
1352
1353 fn motion_option_expr(&self, krate: &str, mode: Mode, value: Option<&Value>) -> Result<String> {
1354 let Some(raw) = self.resolved_value_string(mode, value)? else {
1355 return Ok("None".into());
1356 };
1357 let duration = raw
1358 .split_whitespace()
1359 .find_map(|part| parse_duration_ms(part).ok())
1360 .unwrap_or(self.duration_ms_optional("motion.duration.fast", 160)?);
1361 let easing = if raw.contains("cubic-bezier") {
1362 let start = raw.find("cubic-bezier").unwrap_or(0);
1363 let value = raw[start..].split_whitespace().next().unwrap_or("linear");
1364 easing_expr(krate, value.trim_end_matches(','))
1365 } else if raw.contains("ease") {
1366 format!("{krate}::EasingCurve::Ease")
1367 } else {
1368 self.easing_expr(krate, "motion.easing.standard")?
1369 };
1370 Ok(format!(
1371 "Some({krate}::ComponentMotion {{ duration_ms: {duration}, easing: {easing} }})"
1372 ))
1373 }
1374
1375 fn resolved_value_string(&self, mode: Mode, value: Option<&Value>) -> Result<Option<String>> {
1376 let Some(value) = value else {
1377 return Ok(None);
1378 };
1379 match value {
1380 Value::String(raw) => Ok(Some(self.resolve_refs_in_string_for_mode(raw, mode)?)),
1381 Value::Number(number) => Ok(Some(number.to_string())),
1382 _ => Ok(None),
1383 }
1384 }
1385
1386 fn design_tokens_expr(&self, krate: &str) -> Result<String> {
1387 let mut items = Vec::new();
1388 for path in self.tokens.paths() {
1389 let token = self.tokens.get_raw(path).unwrap();
1390 let kind = token.kind.clone().unwrap_or_else(|| "custom".into());
1391 let resolved = self.resolve_token_string(path)?;
1392 let value_expr = self.design_value_expr(krate, &kind, &resolved)?;
1393 items.push(format!(
1394 "{krate}::DesignToken {{ path: {}, kind: {}, value: {value_expr} }}",
1395 rust_string(path),
1396 rust_string(&kind),
1397 ));
1398 }
1399 Ok(format!(
1400 "{krate}::DesignTokenSet {{ tokens: vec![{}] }}",
1401 items.join(",")
1402 ))
1403 }
1404
1405 fn components_expr(&self, krate: &str) -> Result<String> {
1406 let Some(obj) = self.dsp.get("components").and_then(Value::as_object) else {
1407 return Ok("Vec::new()".into());
1408 };
1409 let mut components = Vec::new();
1410 for (name, value) in obj {
1411 components.push(self.component_spec_expr(krate, name, value)?);
1412 }
1413 Ok(format!("vec![{}]", components.join(",")))
1414 }
1415
1416 fn component_spec_expr(&self, krate: &str, name: &str, value: &Value) -> Result<String> {
1417 let description = value
1418 .get("$description")
1419 .and_then(Value::as_str)
1420 .unwrap_or("");
1421 let anatomy = value
1422 .get("anatomy")
1423 .and_then(Value::as_array)
1424 .map(|arr| {
1425 arr.iter()
1426 .filter_map(Value::as_str)
1427 .map(rust_string)
1428 .collect::<Vec<_>>()
1429 })
1430 .unwrap_or_default();
1431 let props = self.object_properties_expr(krate, value)?;
1432 Ok(format!(
1433 "{krate}::DesignComponentSpec {{ name: {}, description: {}, anatomy: vec![{}], properties: {props} }}",
1434 rust_string(name), rust_string(description), anatomy.join(",")
1435 ))
1436 }
1437
1438 fn patterns_expr(&self, krate: &str) -> Result<String> {
1439 let Some(obj) = self.dsp.get("patterns").and_then(Value::as_object) else {
1440 return Ok("Vec::new()".into());
1441 };
1442 let mut patterns = Vec::new();
1443 for (name, value) in obj {
1444 let description = value
1445 .get("$description")
1446 .and_then(Value::as_str)
1447 .unwrap_or("");
1448 let props = self.object_properties_expr(krate, value)?;
1449 patterns.push(format!(
1450 "{krate}::DesignPatternSpec {{ name: {}, description: {}, properties: {props} }}",
1451 rust_string(name),
1452 rust_string(description)
1453 ));
1454 }
1455 Ok(format!("vec![{}]", patterns.join(",")))
1456 }
1457
1458 fn assets_expr(&self, krate: &str) -> Result<String> {
1459 let assets = self.dsp.get("assets").unwrap_or(&Value::Null);
1460 let mut logos = Vec::new();
1461 if let Some(arr) = assets.get("logos").and_then(Value::as_array) {
1462 for item in arr {
1463 logos.push(asset_expr(krate, item));
1464 }
1465 }
1466 let mut fonts = Vec::new();
1467 if let Some(arr) = assets.get("fonts").and_then(Value::as_array) {
1468 for item in arr {
1469 fonts.push(asset_expr(krate, item));
1470 }
1471 }
1472 Ok(format!(
1473 "{krate}::DesignAssetManifest {{ logos: vec![{}], fonts: vec![{}] }}",
1474 logos.join(","),
1475 fonts.join(",")
1476 ))
1477 }
1478
1479 fn object_properties_expr(&self, krate: &str, value: &Value) -> Result<String> {
1480 let Some(obj) = value.as_object() else {
1481 return Ok("Vec::new()".into());
1482 };
1483 let mut props = Vec::new();
1484 for (key, value) in obj {
1485 if key.starts_with('$') || key == "anatomy" || key == "tab_anatomy" {
1486 continue;
1487 }
1488 let value_expr = self.any_design_value_expr(krate, value)?;
1489 props.push(format!(
1490 "{krate}::DesignProperty {{ name: {}, value: {value_expr} }}",
1491 rust_string(key)
1492 ));
1493 }
1494 Ok(format!("vec![{}]", props.join(",")))
1495 }
1496
1497 fn any_design_value_expr(&self, krate: &str, value: &Value) -> Result<String> {
1498 match value {
1499 Value::String(s) => {
1500 let resolved = self.resolve_refs_in_string(s)?;
1501 self.design_value_expr(krate, "custom", &resolved)
1502 }
1503 Value::Number(n) => Ok(format!(
1504 "{krate}::DesignValue::Number({})",
1505 f32_lit(n.as_f64().unwrap_or_default() as f32)
1506 )),
1507 Value::Bool(b) => Ok(format!("{krate}::DesignValue::Bool({b})")),
1508 Value::Array(arr) => {
1509 let values = arr
1510 .iter()
1511 .map(|v| self.any_design_value_expr(krate, v))
1512 .collect::<Result<Vec<_>>>()?;
1513 Ok(format!(
1514 "{krate}::DesignValue::List(vec![{}])",
1515 values.join(",")
1516 ))
1517 }
1518 Value::Object(obj) => {
1519 if let Some(v) = obj.get("value").or_else(|| obj.get("$value")) {
1520 return self.any_design_value_expr(krate, v);
1521 }
1522 let mut props = Vec::new();
1523 for (key, child) in obj {
1524 if key.starts_with('$') {
1525 continue;
1526 }
1527 let value_expr = self.any_design_value_expr(krate, child)?;
1528 props.push(format!(
1529 "{krate}::DesignProperty {{ name: {}, value: {value_expr} }}",
1530 rust_string(key)
1531 ));
1532 }
1533 Ok(format!(
1534 "{krate}::DesignValue::Object(vec![{}])",
1535 props.join(",")
1536 ))
1537 }
1538 Value::Null => Ok(format!("{krate}::DesignValue::None")),
1539 }
1540 }
1541
1542 fn design_value_expr(&self, krate: &str, kind: &str, value: &str) -> Result<String> {
1543 if kind == "color" || value.trim_start().starts_with('#') || value.starts_with("rgb") {
1544 if let Ok((r, g, b, a)) = parse_color(value) {
1545 return Ok(format!(
1546 "{krate}::DesignValue::Color({})",
1547 color_expr(krate, r, g, b, a)
1548 ));
1549 }
1550 }
1551 if kind == "dimension" || kind == "size" || value.trim_end().ends_with("px") {
1552 if let Ok(px) = parse_dimension(value) {
1553 return Ok(format!("{krate}::DesignValue::Dimension({})", f32_lit(px)));
1554 }
1555 }
1556 if kind == "duration" || value.trim_end().ends_with("ms") {
1557 if let Ok(ms) = parse_duration_ms(value) {
1558 return Ok(format!("{krate}::DesignValue::DurationMs({ms})"));
1559 }
1560 }
1561 if kind == "shadow" || value.contains("rgba(") || value == "none" {
1562 let layers = parse_shadow_layers(value);
1563 let exprs = layers
1564 .iter()
1565 .map(|layer| shadow_layer_expr(krate, layer))
1566 .collect::<Vec<_>>();
1567 return Ok(format!(
1568 "{krate}::DesignValue::Shadow(vec![{}])",
1569 exprs.join(",")
1570 ));
1571 }
1572 if kind == "cubicBezier"
1573 || value.starts_with("cubic-bezier")
1574 || matches!(value, "linear" | "ease")
1575 {
1576 return Ok(format!(
1577 "{krate}::DesignValue::Easing({})",
1578 easing_expr(krate, value)
1579 ));
1580 }
1581 if let Ok(num) = value.parse::<f32>() {
1582 return Ok(format!("{krate}::DesignValue::Number({})", f32_lit(num)));
1583 }
1584 Ok(format!(
1585 "{krate}::DesignValue::Text({})",
1586 rust_string(value)
1587 ))
1588 }
1589
1590 fn color_expr(&self, krate: &str, path: &str) -> Result<String> {
1591 let value = self.resolve_token_string(path)?;
1592 self.color_literal_expr(krate, &value)
1593 }
1594
1595 fn color_expr_optional(&self, krate: &str, path: &str, fallback: &str) -> Result<String> {
1596 if self.tokens.contains(path) {
1597 self.color_expr(krate, path)
1598 } else {
1599 self.color_literal_expr(krate, fallback)
1600 }
1601 }
1602
1603 fn color_literal_expr(&self, krate: &str, value: &str) -> Result<String> {
1604 let (r, g, b, a) = parse_color(value).with_context(|| format!("invalid color {value}"))?;
1605 Ok(color_expr(krate, r, g, b, a))
1606 }
1607
1608 fn dimension(&self, path: &str) -> Result<f32> {
1609 let value = self.resolve_token_string(path)?;
1610 parse_dimension(&value).with_context(|| format!("invalid dimension token {path} = {value}"))
1611 }
1612
1613 fn dimension_optional(&self, path: &str, fallback: f32) -> Result<f32> {
1614 if self.tokens.contains(path) {
1615 self.dimension(path)
1616 } else {
1617 Ok(fallback)
1618 }
1619 }
1620
1621 fn number_optional(&self, path: &str, fallback: f32) -> Result<f32> {
1622 if !self.tokens.contains(path) {
1623 return Ok(fallback);
1624 }
1625 let value = self.resolve_token_string(path)?;
1626 value
1627 .parse::<f32>()
1628 .or_else(|_| parse_dimension(&value))
1629 .with_context(|| format!("invalid number token {path} = {value}"))
1630 }
1631
1632 fn duration_ms_optional(&self, path: &str, fallback: u64) -> Result<u64> {
1633 if !self.tokens.contains(path) {
1634 return Ok(fallback);
1635 }
1636 let value = self.resolve_token_string(path)?;
1637 parse_duration_ms(&value)
1638 .with_context(|| format!("invalid duration token {path} = {value}"))
1639 }
1640
1641 fn string_token_optional(&self, path: &str, fallback: &str) -> Result<String> {
1642 if !self.tokens.contains(path) {
1643 return Ok(fallback.to_string());
1644 }
1645 self.resolve_token_string(path)
1646 }
1647
1648 fn shadow_option_expr(&self, krate: &str, path: &str) -> Result<String> {
1649 let value = if self.tokens.contains(path) {
1650 self.resolve_token_string(path)?
1651 } else {
1652 return Ok("None".into());
1653 };
1654 let layers = parse_shadow_layers(&value);
1655 if let Some(layer) = layers.iter().find(|layer| !layer.inset) {
1656 Ok(format!("Some({})", box_shadow_expr(krate, layer)))
1657 } else {
1658 Ok("None".into())
1659 }
1660 }
1661
1662 fn easing_expr(&self, krate: &str, path: &str) -> Result<String> {
1663 let value = if self.tokens.contains(path) {
1664 self.resolve_token_string(path)?
1665 } else {
1666 "linear".into()
1667 };
1668 Ok(easing_expr(krate, &value))
1669 }
1670
1671 fn dsp_dimension_optional(&self, pointer: &str, fallback: f32) -> Result<f32> {
1672 let Some(value) = self.dsp.pointer(pointer) else {
1673 return Ok(fallback);
1674 };
1675 let raw = match value {
1676 Value::String(s) => self.resolve_refs_in_string(s)?,
1677 Value::Number(n) => n.to_string(),
1678 _ => return Ok(fallback),
1679 };
1680 parse_dimension(&raw)
1681 .or_else(|_| raw.parse::<f32>())
1682 .or(Ok(fallback))
1683 }
1684
1685 fn resolve_token_string(&self, path: &str) -> Result<String> {
1686 self.tokens.resolve(path)
1687 }
1688
1689 fn resolve_refs_in_string(&self, raw: &str) -> Result<String> {
1690 let mut out = String::new();
1691 let mut rest = raw;
1692 while let Some(start) = rest.find('{') {
1693 let (before, after_start) = rest.split_at(start);
1694 out.push_str(before);
1695 let after_start = &after_start[1..];
1696 let Some(end) = after_start.find('}') else {
1697 bail!("unclosed token reference in {raw}");
1698 };
1699 let token = &after_start[..end];
1700 if self.tokens.contains(token) {
1701 out.push_str(&self.resolve_token_string(token)?);
1702 } else {
1703 out.push('{');
1704 out.push_str(token);
1705 out.push('}');
1706 }
1707 rest = &after_start[end + 1..];
1708 }
1709 out.push_str(rest);
1710 Ok(out)
1711 }
1712
1713 fn resolve_refs_in_string_for_mode(&self, raw: &str, mode: Mode) -> Result<String> {
1714 let mut out = String::new();
1715 let mut rest = raw;
1716 while let Some(start) = rest.find('{') {
1717 let (before, after_start) = rest.split_at(start);
1718 out.push_str(before);
1719 let after_start = &after_start[1..];
1720 let Some(end) = after_start.find('}') else {
1721 bail!("unclosed token reference in {raw}");
1722 };
1723 let mut token = after_start[..end].to_string();
1724 match mode {
1725 Mode::Light => {}
1726 Mode::Dark => {
1727 if let Some(suffix) = token.strip_prefix("color.light.") {
1728 let dark = format!("color.dark.{suffix}");
1729 if self.tokens.contains(&dark) {
1730 token = dark;
1731 }
1732 }
1733 }
1734 }
1735 if self.tokens.contains(&token) {
1736 out.push_str(&self.resolve_token_string(&token)?);
1737 } else {
1738 out.push('{');
1739 out.push_str(&token);
1740 out.push('}');
1741 }
1742 rest = &after_start[end + 1..];
1743 }
1744 out.push_str(rest);
1745 Ok(out)
1746 }
1747}
1748
1749#[derive(Debug, Clone, Copy)]
1750enum Mode {
1751 Light,
1752 Dark,
1753}
1754impl Mode {
1755 fn as_str(self) -> &'static str {
1756 match self {
1757 Self::Light => "Light",
1758 Self::Dark => "Dark",
1759 }
1760 }
1761
1762 fn color_name(self) -> &'static str {
1763 match self {
1764 Self::Light => "light",
1765 Self::Dark => "dark",
1766 }
1767 }
1768}
1769
1770#[derive(Debug, Clone)]
1771struct Token {
1772 value: String,
1773 kind: Option<String>,
1774}
1775
1776#[derive(Debug, Clone)]
1777struct TokenStore {
1778 tokens: BTreeMap<String, Token>,
1779}
1780
1781impl TokenStore {
1782 fn from_value(value: &Value) -> Result<Self> {
1783 let mut tokens = BTreeMap::new();
1784 flatten_tokens(value, String::new(), &mut tokens)?;
1785 Ok(Self { tokens })
1786 }
1787
1788 fn contains(&self, path: &str) -> bool {
1789 self.tokens.contains_key(path)
1790 }
1791 fn paths(&self) -> impl Iterator<Item = &str> {
1792 self.tokens.keys().map(String::as_str)
1793 }
1794 fn get_raw(&self, path: &str) -> Option<&Token> {
1795 self.tokens.get(path)
1796 }
1797
1798 fn resolve(&self, path: &str) -> Result<String> {
1799 self.resolve_inner(path, &mut BTreeSet::new())
1800 }
1801
1802 fn resolve_inner(&self, path: &str, seen: &mut BTreeSet<String>) -> Result<String> {
1803 if !seen.insert(path.to_string()) {
1804 bail!("cyclic token reference involving {path}");
1805 }
1806 let token = self
1807 .tokens
1808 .get(path)
1809 .ok_or_else(|| anyhow!("unknown token reference {{{path}}}"))?;
1810 let value = token.value.trim();
1811 if let Some(inner) = value.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
1812 let resolved = self.resolve_inner(inner, seen)?;
1813 seen.remove(path);
1814 return Ok(resolved);
1815 }
1816 let mut out = String::new();
1817 let mut rest = value;
1818 while let Some(start) = rest.find('{') {
1819 let (before, after_start) = rest.split_at(start);
1820 out.push_str(before);
1821 let after_start = &after_start[1..];
1822 let Some(end) = after_start.find('}') else {
1823 bail!("unclosed token reference in {value}");
1824 };
1825 let inner = &after_start[..end];
1826 out.push_str(&self.resolve_inner(inner, seen)?);
1827 rest = &after_start[end + 1..];
1828 }
1829 out.push_str(rest);
1830 seen.remove(path);
1831 Ok(out)
1832 }
1833}
1834
1835fn flatten_tokens(value: &Value, prefix: String, out: &mut BTreeMap<String, Token>) -> Result<()> {
1836 let Some(obj) = value.as_object() else {
1837 return Ok(());
1838 };
1839 for (key, child) in obj {
1840 if key.starts_with('$') {
1841 continue;
1842 }
1843 let path = if prefix.is_empty() {
1844 key.clone()
1845 } else {
1846 format!("{prefix}.{key}")
1847 };
1848 if let Some(child_obj) = child.as_object() {
1849 let value_field = child_obj.get("value").or_else(|| child_obj.get("$value"));
1850 if let Some(raw_value) = value_field {
1851 let token_value = match raw_value {
1852 Value::String(s) => s.clone(),
1853 Value::Number(n) => n.to_string(),
1854 Value::Bool(b) => b.to_string(),
1855 other => other.to_string(),
1856 };
1857 let kind = child_obj
1858 .get("type")
1859 .or_else(|| child_obj.get("$type"))
1860 .and_then(Value::as_str)
1861 .map(ToOwned::to_owned);
1862 out.insert(
1863 path,
1864 Token {
1865 value: token_value,
1866 kind,
1867 },
1868 );
1869 } else {
1870 flatten_tokens(child, path, out)?;
1871 }
1872 }
1873 }
1874 Ok(())
1875}
1876
1877#[derive(Debug, Clone)]
1878struct ShadowLayer {
1879 color: (u8, u8, u8, u8),
1880 offset_x: f32,
1881 offset_y: f32,
1882 blur_radius: f32,
1883 spread_radius: f32,
1884 inset: bool,
1885}
1886
1887fn parse_shadow_layers(value: &str) -> Vec<ShadowLayer> {
1888 if value.trim() == "none" {
1889 return Vec::new();
1890 }
1891 split_css_layers(value)
1892 .into_iter()
1893 .filter_map(|layer| parse_shadow_layer(layer.trim()).ok())
1894 .collect()
1895}
1896
1897fn parse_shadow_layer(layer: &str) -> Result<ShadowLayer> {
1898 let inset = layer.contains("inset");
1899 let color_start = layer
1900 .find("rgba(")
1901 .or_else(|| layer.find("rgb("))
1902 .ok_or_else(|| anyhow!("shadow has no rgb/rgba color: {layer}"))?;
1903 let color_end = layer[color_start..]
1904 .find(')')
1905 .ok_or_else(|| anyhow!("unterminated rgb/rgba in shadow: {layer}"))?
1906 + color_start;
1907 let color_raw = &layer[color_start..=color_end];
1908 let color = parse_rgb_color(color_raw)?;
1909 let nums = layer[..color_start]
1910 .replace("inset", "")
1911 .split_whitespace()
1912 .filter_map(|part| parse_dimension(part).ok())
1913 .collect::<Vec<_>>();
1914 Ok(ShadowLayer {
1915 color,
1916 offset_x: *nums.get(0).unwrap_or(&0.0),
1917 offset_y: *nums.get(1).unwrap_or(&0.0),
1918 blur_radius: *nums.get(2).unwrap_or(&0.0),
1919 spread_radius: *nums.get(3).unwrap_or(&0.0),
1920 inset,
1921 })
1922}
1923
1924fn split_css_layers(value: &str) -> Vec<&str> {
1925 let mut layers = Vec::new();
1926 let mut depth = 0usize;
1927 let mut start = 0usize;
1928 for (idx, ch) in value.char_indices() {
1929 match ch {
1930 '(' => depth += 1,
1931 ')' => depth = depth.saturating_sub(1),
1932 ',' if depth == 0 => {
1933 layers.push(&value[start..idx]);
1934 start = idx + 1;
1935 }
1936 _ => {}
1937 }
1938 }
1939 layers.push(&value[start..]);
1940 layers
1941}
1942
1943fn parse_hex_color(value: &str) -> Result<(u8, u8, u8, u8)> {
1944 let hex = value
1945 .trim()
1946 .strip_prefix('#')
1947 .ok_or_else(|| anyhow!("not a hex color: {value}"))?;
1948 match hex.len() {
1949 3 => {
1950 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)?;
1951 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)?;
1952 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)?;
1953 Ok((r, g, b, 255))
1954 }
1955 6 | 8 => {
1956 let r = u8::from_str_radix(&hex[0..2], 16)?;
1957 let g = u8::from_str_radix(&hex[2..4], 16)?;
1958 let b = u8::from_str_radix(&hex[4..6], 16)?;
1959 let a = if hex.len() == 8 {
1960 u8::from_str_radix(&hex[6..8], 16)?
1961 } else {
1962 255
1963 };
1964 Ok((r, g, b, a))
1965 }
1966 _ => bail!("invalid hex color length: {value}"),
1967 }
1968}
1969
1970fn parse_color(value: &str) -> Result<(u8, u8, u8, u8)> {
1971 let value = value.trim();
1972 if value.eq_ignore_ascii_case("transparent") {
1973 return Ok((0, 0, 0, 0));
1974 }
1975 if value.starts_with('#') {
1976 return parse_hex_color(value);
1977 }
1978 if value.starts_with("rgb(") || value.starts_with("rgba(") {
1979 return parse_rgb_color(value);
1980 }
1981 bail!("unsupported color value: {value}")
1982}
1983
1984fn parse_rgb_color(value: &str) -> Result<(u8, u8, u8, u8)> {
1985 let inner = value
1986 .trim()
1987 .trim_start_matches("rgba(")
1988 .trim_start_matches("rgb(")
1989 .trim_end_matches(')');
1990 let parts = inner.split(',').map(str::trim).collect::<Vec<_>>();
1991 let r = parts
1992 .get(0)
1993 .ok_or_else(|| anyhow!("missing red in {value}"))?
1994 .parse::<u8>()?;
1995 let g = parts
1996 .get(1)
1997 .ok_or_else(|| anyhow!("missing green in {value}"))?
1998 .parse::<u8>()?;
1999 let b = parts
2000 .get(2)
2001 .ok_or_else(|| anyhow!("missing blue in {value}"))?
2002 .parse::<u8>()?;
2003 let a = if let Some(alpha) = parts.get(3) {
2004 let alpha = alpha.parse::<f32>()?.clamp(0.0, 1.0);
2005 (alpha * 255.0).round() as u8
2006 } else {
2007 255
2008 };
2009 Ok((r, g, b, a))
2010}
2011
2012fn parse_border(value: &str) -> Option<(f32, &str)> {
2013 let mut width = None;
2014 let mut color_start = None;
2015 for part in value.split_whitespace() {
2016 if width.is_none() {
2017 if let Ok(px) = parse_dimension(part) {
2018 width = Some(px);
2019 continue;
2020 }
2021 }
2022 if part.starts_with('#') || part.starts_with("rgb") || part == "transparent" {
2023 color_start = value.find(part);
2024 break;
2025 }
2026 }
2027 match (width, color_start) {
2028 (Some(width), Some(start)) => Some((width, value[start..].trim())),
2029 _ => None,
2030 }
2031}
2032
2033fn parse_padding(value: &str) -> Result<[f32; 4]> {
2034 let parts = value
2035 .split_whitespace()
2036 .map(parse_dimension)
2037 .collect::<Result<Vec<_>>>()?;
2038 let (top, right, bottom, left) = match parts.as_slice() {
2039 [all] => (*all, *all, *all, *all),
2040 [vertical, horizontal] => (*vertical, *horizontal, *vertical, *horizontal),
2041 [top, horizontal, bottom] => (*top, *horizontal, *bottom, *horizontal),
2042 [top, right, bottom, left, ..] => (*top, *right, *bottom, *left),
2043 _ => (0.0, 0.0, 0.0, 0.0),
2044 };
2045 Ok([left, right, top, bottom])
2046}
2047
2048fn gradient_stops(value: &str) -> Vec<&str> {
2049 let inner = value
2050 .split_once('(')
2051 .and_then(|(_, rest)| rest.strip_suffix(')'))
2052 .unwrap_or(value);
2053 split_css_layers(inner)
2054 .into_iter()
2055 .map(str::trim)
2056 .filter(|part| part.starts_with('#') || part.starts_with("rgb") || *part == "transparent")
2057 .map(|part| part.split_whitespace().next().unwrap_or(part))
2058 .collect()
2059}
2060
2061fn parse_dimension(value: &str) -> Result<f32> {
2062 let trimmed = value.trim().trim_matches('"');
2063 if let Some(px) = trimmed.strip_suffix("px") {
2064 return Ok(px.trim().parse()?);
2065 }
2066 if let Some(em) = trimmed.strip_suffix("em") {
2067 return Ok(em.trim().parse()?);
2068 }
2069 Ok(trimmed.parse()?)
2070}
2071
2072fn field<'a>(value: &'a Value, name: &str) -> Option<&'a Value> {
2073 value.get(name)
2074}
2075
2076fn component_size_variant(krate: &str, name: &str) -> Option<String> {
2077 let variant = match name {
2078 "sm" => "Sm",
2079 "md" => "Md",
2080 "lg" => "Lg",
2081 "xl" => "Xl",
2082 _ => return None,
2083 };
2084 Some(format!("{krate}::ComponentSize::{variant}"))
2085}
2086
2087fn button_hierarchy_variant(krate: &str, name: &str) -> Option<String> {
2088 let variant = match name {
2089 "primary" => "Primary",
2090 "secondary_color" => "SecondaryColor",
2091 "secondary_gray" => "SecondaryGray",
2092 "tertiary_color" => "TertiaryColor",
2093 "tertiary_gray" => "TertiaryGray",
2094 "link_color" => "LinkColor",
2095 "link_gray" => "LinkGray",
2096 "destructive" => "Destructive",
2097 _ => return None,
2098 };
2099 Some(format!("{krate}::ButtonHierarchy::{variant}"))
2100}
2101
2102fn badge_tone_variant(krate: &str, name: &str) -> Option<String> {
2103 let variant = match name {
2104 "brand" => "Brand",
2105 "gray" => "Gray",
2106 "success" => "Success",
2107 "warning" => "Warning",
2108 "error" => "Error",
2109 "blue" => "Blue",
2110 "orange" => "Orange",
2111 _ => return None,
2112 };
2113 Some(format!("{krate}::BadgeTone::{variant}"))
2114}
2115
2116fn card_pattern_variant(krate: &str, name: &str) -> Option<String> {
2117 let variant = match name {
2118 "plain" => "Plain",
2119 "raised" => "Raised",
2120 "tinted" => "Tinted",
2121 "elevated" => "Elevated",
2122 _ => return None,
2123 };
2124 Some(format!("{krate}::CardPattern::{variant}"))
2125}
2126
2127fn feature_icon_tone_variant(krate: &str, name: &str) -> Option<String> {
2128 let variant = match name {
2129 "brand" => "Brand",
2130 "gray" => "Gray",
2131 "blue" => "Blue",
2132 "orange" => "Orange",
2133 _ => return None,
2134 };
2135 Some(format!("{krate}::FeatureIconTone::{variant}"))
2136}
2137
2138fn parse_duration_ms(value: &str) -> Result<u64> {
2139 let trimmed = value.trim();
2140 if let Some(ms) = trimmed.strip_suffix("ms") {
2141 return Ok(ms.trim().parse()?);
2142 }
2143 if let Some(s) = trimmed.strip_suffix('s') {
2144 return Ok((s.trim().parse::<f32>()? * 1000.0).round() as u64);
2145 }
2146 Ok(trimmed.parse()?)
2147}
2148
2149fn color_expr(krate: &str, r: u8, g: u8, b: u8, a: u8) -> String {
2150 format!("{krate}::Color {{ r: {r}, g: {g}, b: {b}, a: {a} }}")
2151}
2152
2153fn box_shadow_expr(krate: &str, layer: &ShadowLayer) -> String {
2154 format!(
2155 "{krate}::BoxShadow {{ color: {}, offset: ({}, {}), blur_radius: {} }}",
2156 color_expr(
2157 krate,
2158 layer.color.0,
2159 layer.color.1,
2160 layer.color.2,
2161 layer.color.3
2162 ),
2163 f32_lit(layer.offset_x),
2164 f32_lit(layer.offset_y),
2165 f32_lit(layer.blur_radius)
2166 )
2167}
2168
2169fn shadow_layer_expr(krate: &str, layer: &ShadowLayer) -> String {
2170 format!(
2171 "{krate}::ShadowLayer {{ color: {}, offset: ({}, {}), blur_radius: {}, spread_radius: {}, inset: {} }}",
2172 color_expr(krate, layer.color.0, layer.color.1, layer.color.2, layer.color.3),
2173 f32_lit(layer.offset_x),
2174 f32_lit(layer.offset_y),
2175 f32_lit(layer.blur_radius),
2176 f32_lit(layer.spread_radius),
2177 layer.inset
2178 )
2179}
2180
2181fn easing_expr(krate: &str, value: &str) -> String {
2182 let value = value.trim();
2183 if let Some(inner) = value
2184 .strip_prefix("cubic-bezier(")
2185 .and_then(|s| s.strip_suffix(')'))
2186 {
2187 let nums = inner
2188 .split(',')
2189 .filter_map(|n| n.trim().parse::<f32>().ok())
2190 .collect::<Vec<_>>();
2191 if nums.len() == 4 {
2192 return format!(
2193 "{krate}::EasingCurve::CubicBezier({}, {}, {}, {})",
2194 f32_lit(nums[0]),
2195 f32_lit(nums[1]),
2196 f32_lit(nums[2]),
2197 f32_lit(nums[3])
2198 );
2199 }
2200 }
2201 match value {
2202 "linear" => format!("{krate}::EasingCurve::Linear"),
2203 "ease" => format!("{krate}::EasingCurve::Ease"),
2204 _ => format!("{krate}::EasingCurve::Named({})", rust_string(value)),
2205 }
2206}
2207
2208fn asset_expr(krate: &str, item: &Value) -> String {
2209 let id = item
2210 .get("id")
2211 .and_then(Value::as_str)
2212 .or_else(|| item.get("family").and_then(Value::as_str))
2213 .unwrap_or("");
2214 let path = item.get("path").and_then(Value::as_str).unwrap_or("");
2215 let format = item.get("format").and_then(Value::as_str).unwrap_or("");
2216 format!(
2217 "{krate}::DesignAsset {{ id: {}, path: {}, format: {} }}",
2218 rust_string(id),
2219 rust_string(path),
2220 rust_string(format)
2221 )
2222}
2223
2224fn rust_string(value: &str) -> String {
2225 format!("{:?}.to_string()", value)
2226}
2227
2228fn f32_lit(value: f32) -> String {
2229 if value.is_finite() && value.fract() == 0.0 {
2230 format!("{value:.1}")
2231 } else {
2232 let mut out = value.to_string();
2233 if !out.contains('.') && !out.contains('e') && !out.contains('E') {
2234 out.push_str(".0");
2235 }
2236 out
2237 }
2238}