//! Port of `cli/engine/design-system.mjs`: DESIGN.md discovery (walk-up with //! project boundaries), the frontmatter YAML subset, sidecar parsing, the //! normalized allowlists, the source-scan design-system rules, and the //! finding merge/dedupe helpers. //! //! The DOM-backed `collectStaticDesignSystemFindings` lives with the static //! HTML engine (crates/html); it builds on the `pub` helpers here //! (`primary_font`, `is_allowed_*`, `css_color_label`, `extract_radius_tokens`, //! `make_design_finding`, `is_transparent_css`, `STATIC_DESIGN_SKIP_TAGS`), so //! the html crate depends on this crate, never the reverse. use std::collections::HashMap; use std::rc::Rc; use impeccable_core::checks::measures::resolve_length_px; use impeccable_core::color::{parse_any_color, Rgba}; use impeccable_core::constants::GENERIC_FONTS; use impeccable_core::findings::{finding, Finding}; use impeccable_core::js::{self, ci, math_round, number_to_string, parse_float, parse_int}; use once_cell::sync::Lazy; use regex::Regex; use serde_json::{Map, Value}; use crate::jsp; use crate::util::{exists, js_string, re, read_json, read_text, ANY, WS}; const DESIGN_NAMES: &[&str] = &["DESIGN.md", "Design.md", "design.md"]; const FALLBACK_DIRS: &[&str] = &[".agents/context", "docs"]; const PROJECT_ROOT_MARKERS: &[&str] = &[".git", "package.json", ".impeccable"]; const COLOR_CHANNEL_TOLERANCE: f64 = 6.0; const SHADOW_ALPHA_TOLERANCE: f64 = 0.02; const RADIUS_TOLERANCE_PX: f64 = 0.5; const FONT_SIZE_TOLERANCE_PX: f64 = 0.5; /// JS `STATIC_DESIGN_SKIP_TAGS`. pub const STATIC_DESIGN_SKIP_TAGS: &[&str] = &[ "head", "title", "meta", "link", "style", "script", "noscript", "template", "source", ]; re!( FONT_SIZE_LITERAL_RE, format!("^-?[{D}.]+(?:px|rem)$", D = "0-9") ); re!( CSS_COLOR_RE, format!( "#[0-9a-fA-F]{{3,8}}(?-u:\\b)|{rgb}[aA]?\\([^)]+\\)|{oklch}\\([^)]+\\)|{hsl}[aA]?\\([^)]+\\)", rgb = ci("rgb"), oklch = ci("oklch"), hsl = ci("hsl") ) ); re!( FONT_DECL_RE, format!("{}{WS}*:{WS}*([^;}}\n]+)", ci("font-family")) ); re!( FONT_JS_RE, format!("fontFamily{WS}*[:=]{WS}*[\"'`]([^\"'`]+)[\"'`]") ); re!( GOOGLE_FONT_RE, format!( "{}\\.{}\\.{}/{}2?\\?[^\"'{}){}<>]*", ci("fonts"), ci("googleapis"), ci("com"), ci("css"), impeccable_core::js::WS_CHARS, "" ) ); re!( BORDER_RADIUS_RE, format!("{}{WS}*:{WS}*([^;}}\n]+)", ci("border-radius")) ); re!( BORDER_RADIUS_JS_RE, format!("borderRadius{WS}*[:=]{WS}*[\"'`]([^\"'`]+)[\"'`]") ); re!( FONT_SIZE_DECL_RE, format!("{}{WS}*:{WS}*([^;}}\n]+)", ci("font-size")) ); re!( FONT_SIZE_JS_RE, format!("fontSize{WS}*[:=]{WS}*[\"'`]([^\"'`]+)[\"'`]") ); re!( TAILWIND_FONT_SIZE_RE, format!("(?-u:\\b)text-\\[(-?[0-9.]+(?:px|rem))\\]") ); re!(GOOGLE_FAMILY_PARAM_RE, "[?&]family=([^&]+)"); re!( IMPORTANT_TAIL_RE, format!("{WS}*{}{WS}*$", ci("!important")) ); re!(IMPORTANT_TAIL_CS_RE, format!("{WS}*!important{WS}*$")); re!(EDGE_QUOTE_RE, r#"^["']|["']$"#); re!(WS_RUN_RE, format!("{WS}+")); re!(VAR_RE, format!("{}\\(", ci("var"))); fn first_existing(dir: &str, names: &[&str]) -> Option { for name in names { let abs = jsp::join(&[dir, name]); if exists(&abs) { return Some(abs); } } None } /// `{ path, contextDir }` of the DESIGN.md that governs `cwd`. #[derive(Debug, Clone, PartialEq)] pub struct DesignMdPath { pub path: String, pub context_dir: String, } /// JS: design-system.mjs#resolveDesignMdPath pub fn resolve_design_md_path(cwd: &str) -> Option { if let Some(root) = first_existing(cwd, DESIGN_NAMES) { return Some(DesignMdPath { path: root, context_dir: cwd.to_string(), }); } for rel in FALLBACK_DIRS { let dir = jsp::resolve(cwd, &[rel]); if let Some(found) = first_existing(&dir, DESIGN_NAMES) { return Some(DesignMdPath { path: found, context_dir: dir, }); } } None } /// JS: design-system.mjs#resolveDesignSidecarPath pub fn resolve_design_sidecar_path(cwd: &str, context_dir: &str) -> Option { let candidates = [ jsp::join(&[cwd, ".impeccable", "design.json"]), jsp::join(&[cwd, "DESIGN.json"]), jsp::join(&[context_dir, "DESIGN.json"]), ]; for (index, candidate) in candidates.iter().enumerate() { let first = candidates .iter() .position(|c| c == candidate) .unwrap_or(index); if first == index && exists(candidate) { return Some(candidate.clone()); } } None } // ─── Frontmatter YAML subset ───────────────────────────────────────────────── re!(CRLF_RE, "\r?\n"); re!(COMMENT_LINE_RE, format!("^{WS}*#")); re!(LEADING_WS_RE, format!("^{WS}*")); /// JS: design-system.mjs#parseFrontmatter. `None` when there is no /// `---` block; otherwise the parsed object (possibly empty). pub fn parse_frontmatter(md: &str) -> Option> { let lines: Vec<&str> = CRLF_RE.split(md).collect(); if js::trim(lines.first().copied().unwrap_or("")) != "---" { return None; } let mut end = None; for (i, line) in lines.iter().enumerate().skip(1) { if js::trim(line) == "---" { end = Some(i); break; } } let end = end?; Some(parse_yaml_subset(&lines[1..end].join("\n"))) } /// JS: design-system.mjs#parseYamlSubset (nested maps and scalars only). pub fn parse_yaml_subset(yaml: &str) -> Map { // Build a tree of paths, then materialize: JS mutates nested objects by // reference; here we track the key path of each open mapping. let mut root = Map::new(); let mut stack: Vec<(i64, Vec)> = vec![(-1, vec![])]; for raw in CRLF_RE.split(yaml) { if js::trim(raw).is_empty() || COMMENT_LINE_RE.is_match(raw) { continue; } let indent_bytes = LEADING_WS_RE.find(raw).map(|m| m.end()).unwrap_or(0); let indent = raw[..indent_bytes] .chars() .map(|c| c.len_utf16()) .sum::() as i64; let content = &raw[indent_bytes..]; let Some(colon_idx) = find_top_level_colon(content) else { continue; }; while stack.len() > 1 && stack.last().map(|s| s.0 >= indent).unwrap_or(false) { stack.pop(); } let key = unquote_yaml_key(js::trim(&content[..colon_idx])).to_string(); let rest = strip_inline_yaml_comment(js::trim(&content[colon_idx + 1..])); let parent_path = stack.last().map(|s| s.1.clone()).unwrap_or_default(); let parent = get_path_mut(&mut root, &parent_path); if rest.is_empty() { parent.insert(key.clone(), Value::Object(Map::new())); let mut path = parent_path; path.push(key); stack.push((indent, path)); } else { parent.insert(key, parse_scalar(&rest)); } } root } fn get_path_mut<'a>( root: &'a mut Map, path: &[String], ) -> &'a mut Map { let mut cur = root; for key in path { // A key on the stack was inserted as an object; a later scalar write to // the same key at the parent level would already have popped the stack. let entry = cur .entry(key.clone()) .or_insert_with(|| Value::Object(Map::new())); if !entry.is_object() { *entry = Value::Object(Map::new()); } cur = entry.as_object_mut().unwrap(); } cur } fn find_top_level_colon(s: &str) -> Option { let mut in_quote: Option = None; let mut prev: Option = None; for (i, ch) in s.char_indices() { if let Some(q) = in_quote { if ch == q && prev != Some('\\') { in_quote = None; } } else if ch == '"' || ch == '\'' { in_quote = Some(ch); } else if ch == ':' { return Some(i); } prev = Some(ch); } None } /// JS: design-system.mjs#unquoteYamlKey pub fn unquote_yaml_key(key: &str) -> &str { if (key.starts_with('"') && key.ends_with('"')) || (key.starts_with('\'') && key.ends_with('\'')) { if key.chars().count() >= 2 { return &key[1..key.len() - 1]; } // JS slice(1, -1) on a one-char string yields ''. return ""; } key } fn strip_inline_yaml_comment(s: &str) -> String { let mut in_quote: Option = None; let mut prev: Option = None; for (i, ch) in s.char_indices() { if let Some(q) = in_quote { if ch == q && prev != Some('\\') { in_quote = None; } } else if ch == '"' || ch == '\'' { in_quote = Some(ch); } else if ch == '#' && i > 0 && prev.map(js::is_js_whitespace).unwrap_or(false) { return s[..i].trim_end_matches(js::is_js_whitespace).to_string(); } prev = Some(ch); } s.to_string() } fn yaml_simple_escape(c: char) -> Option { Some(match c { '0' => '\0', 'a' => '\x07', 'b' => '\x08', 't' => '\t', 'n' => '\n', 'v' => '\x0B', 'f' => '\x0C', 'r' => '\r', 'e' => '\x1b', ' ' => ' ', '"' => '"', '/' => '/', '\\' => '\\', 'N' => '\u{85}', '_' => '\u{a0}', 'L' => '\u{2028}', 'P' => '\u{2029}', _ => return None, }) } fn unescape_yaml_double_quoted(body: &str) -> String { let chars: Vec = body.chars().collect(); let mut out = String::new(); let mut i = 0; while i < chars.len() { let ch = chars[i]; if ch != '\\' || i == chars.len() - 1 { out.push(ch); i += 1; continue; } let next = chars[i + 1]; if let Some(mapped) = yaml_simple_escape(next) { out.push(mapped); i += 2; continue; } let hex_len = match next { 'x' => Some(2), 'u' => Some(4), 'U' => Some(8), _ => None, }; if let Some(hex_len) = hex_len { let hex: String = chars[(i + 2).min(chars.len())..(i + 2 + hex_len).min(chars.len())] .iter() .collect(); let code_point: i64 = if hex.chars().count() == hex_len && hex.chars().all(|c| c.is_ascii_hexdigit()) { parse_int(&hex, 16) as i64 } else { -1 }; if (0..=0x10ffff).contains(&code_point) { // String.fromCodePoint: a surrogate code point has no char // representation; substitute U+FFFD as the closest thing. out.push(char::from_u32(code_point as u32).unwrap_or('\u{fffd}')); i += 2 + hex_len; continue; } } out.push(ch); i += 1; } out } re!(INT_RE, "^-?[0-9]+$"); re!(FLOAT_RE, "^-?[0-9]*\\.[0-9]+$"); fn parse_scalar(raw: &str) -> Value { let s = js::trim(raw); let n = s.chars().count(); if n >= 2 && s.starts_with('"') && s.ends_with('"') { return Value::String(unescape_yaml_double_quoted(&s[1..s.len() - 1])); } if n >= 2 && s.starts_with('\'') && s.ends_with('\'') { return Value::String(s[1..s.len() - 1].replace("''", "'")); } if s == "true" { return Value::Bool(true); } if s == "false" { return Value::Bool(false); } if s == "null" || s == "~" { return Value::Null; } if INT_RE.is_match(s) || FLOAT_RE.is_match(s) { let v = js::string_to_number(s); return serde_json::Number::from_f64(v) .map(Value::Number) .unwrap_or(Value::String(s.to_string())); } Value::String(s.to_string()) } // ─── Normalized design system ──────────────────────────────────────────────── /// JS `normalizeFontName`. pub fn normalize_font_name(value: &str) -> String { let t = js::trim(value); let t = IMPORTANT_TAIL_RE.replace(t, ""); let t = js::trim(&t); let t = EDGE_QUOTE_RE.replace_all(t, ""); let t = t.replace('+', " "); let t = WS_RUN_RE.replace_all(&t, " "); js::to_lower_case(&t) } /// JS `splitFontStack`. pub fn split_font_stack(stack: &str) -> Vec { let t = IMPORTANT_TAIL_RE.replace(stack, ""); t.split(',') .map(normalize_font_name) .filter(|f| !f.is_empty()) .collect() } fn is_generic_font(font: &str) -> bool { GENERIC_FONTS.contains(&font) } re!(NON_LITERAL_STACK_RE, format!("[$`{{}}]|{WS}\\+{WS}|\\|\\|")); fn is_literal_font_stack(stack: &str) -> bool { !NON_LITERAL_STACK_RE.is_match(stack) } /// JS: design-system.mjs#primaryFont pub fn primary_font(stack: &str) -> String { if stack.is_empty() || VAR_RE.is_match(stack) || !is_literal_font_stack(stack) { return String::new(); } split_font_stack(stack) .into_iter() .find(|f| !is_generic_font(f)) .unwrap_or_default() } /// JS: design-system.mjs#cssColorLabel pub fn css_color_label(raw: &str) -> String { WS_RUN_RE.replace_all(js::trim(raw), " ").into_owned() } /// JS `colorKey`. pub fn color_key(color: &Rgba) -> String { format!( "{},{},{}", number_to_string(color.r), number_to_string(color.g), number_to_string(color.b) ) } fn colors_close(a: &Rgba, b: &Rgba) -> bool { js::math_max3((a.r - b.r).abs(), (a.g - b.g).abs(), (a.b - b.b).abs()) <= COLOR_CHANNEL_TOLERANCE } fn hsl_to_rgb(hh: f64, ss: f64, ll: f64, alpha: f64) -> Rgba { let h = (((hh % 360.0) + 360.0) % 360.0) / 360.0; let s = js::math_max(0.0, js::math_min(1.0, ss)); let l = js::math_max(0.0, js::math_min(1.0, ll)); let hue2rgb = |p: f64, q: f64, mut t: f64| { if t < 0.0 { t += 1.0; } if t > 1.0 { t -= 1.0; } if t < 1.0 / 6.0 { return p + (q - p) * 6.0 * t; } if t < 1.0 / 2.0 { return q; } if t < 2.0 / 3.0 { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; } p }; let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s }; let p = 2.0 * l - q; Rgba::new( math_round(hue2rgb(p, q, h + 1.0 / 3.0) * 255.0), math_round(hue2rgb(p, q, h) * 255.0), math_round(hue2rgb(p, q, h - 1.0 / 3.0) * 255.0), alpha, ) } re!( HSL_FALLBACK_RE, format!( "{hsl}[aA]?\\({WS}*([-0-9.]+)(?:{deg})?{WS}*,?{WS}*([0-9.]+)%{WS}*,?{WS}*([0-9.]+)%(?:{WS}*[,/]{WS}*([0-9.]+))?{WS}*\\)", hsl = ci("hsl"), deg = ci("deg") ) ); /// JS: design-system.mjs#parseDesignColor pub fn parse_design_color(value: &str) -> Option { let text = js::trim(value); if let Some(parsed) = parse_any_color(Some(text)) { return Some(parsed); } let m = HSL_FALLBACK_RE.captures(text)?; Some(hsl_to_rgb( parse_float(&m[1]), parse_float(&m[2]) / 100.0, parse_float(&m[3]) / 100.0, m.get(4).map(|a| parse_float(a.as_str())).unwrap_or(1.0), )) } #[derive(Debug, Clone, PartialEq)] pub struct AllowedColor { pub color: Rgba, pub labels: Vec, } #[derive(Debug, Clone, PartialEq)] pub struct AllowedRadius { pub name: String, pub value: String, pub px: f64, } #[derive(Debug, Clone, PartialEq)] pub struct AllowedFontSize { pub value: String, pub px: f64, pub fluid: bool, } /// JS `normalizeDesignSystem` result. #[derive(Debug, Clone, PartialEq, Default)] pub struct DesignSystem { pub present: bool, pub source_path: Option, pub sidecar_path: Option, pub md_newer_than_json: bool, /// JS `Set` (insertion order). pub allowed_fonts: Vec, /// JS `Map` (insertion order). pub allowed_color_keys: Vec<(String, AllowedColor)>, pub allowed_radii: Vec, pub allowed_font_sizes: Vec, pub allowed_shadow_colors: Vec, pub has_pill_radius: bool, pub has_fonts: bool, pub has_colors: bool, pub has_radii: bool, pub has_font_sizes: bool, } fn add_design_color(out: &mut DesignSystem, value: &str, label: Option<&str>) { let Some(parsed) = parse_design_color(value) else { return; }; let key = color_key(&parsed); let label = label .map(|l| l.to_string()) .unwrap_or_else(|| css_color_label(value)); if let Some((_, entry)) = out.allowed_color_keys.iter_mut().find(|(k, _)| *k == key) { entry.labels.push(label); } else { out.allowed_color_keys.push(( key, AllowedColor { color: parsed, labels: vec![label], }, )); } } fn add_color_object(out: &mut DesignSystem, colors: Option<&Value>, prefix: &str) { let Some(Value::Object(colors)) = colors else { return; }; for (name, value) in colors { if let Value::String(s) = value { add_design_color(out, s, Some(&format!("{prefix}.{name}"))); } } } fn add_sidecar_colors(out: &mut DesignSystem, sidecar: Option<&Value>) { let Some(color_meta) = sidecar .and_then(|s| s.get("extensions")) .and_then(|e| e.get("colorMeta")) .and_then(|c| c.as_object()) else { return; }; for (name, meta) in color_meta { let Some(meta) = meta.as_object() else { continue; }; if let Some(Value::String(c)) = meta.get("canonical") { add_design_color(out, c, Some(&format!("sidecar.{name}"))); } if let Some(Value::Array(ramp)) = meta.get("tonalRamp") { for (index, value) in ramp.iter().enumerate() { if let Value::String(v) = value { add_design_color(out, v, Some(&format!("sidecar.{name}.tonalRamp[{index}]"))); } } } } } fn add_typography_fonts(out: &mut DesignSystem, typography: Option<&Value>) { let Some(Value::Object(typography)) = typography else { return; }; for role in typography.values() { let Some(role) = role.as_object() else { continue; }; let Some(Value::String(ff)) = role.get("fontFamily") else { continue; }; for font in split_font_stack(ff) { if !is_generic_font(&font) && !out.allowed_fonts.contains(&font) { out.allowed_fonts.push(font); } } } } fn value_to_string_nullish(v: Option<&Value>) -> String { match v { None | Some(Value::Null) => String::new(), Some(v) => js_string(v), } } fn add_font_size_step(out: &mut DesignSystem, raw: &str, fluid: bool) { let text = js::to_lower_case(js::trim(raw)); if !FONT_SIZE_LITERAL_RE.is_match(&text) { return; } let Some(px) = resolve_length_px(Some(&text), 16.0) else { return; }; if !px.is_finite() || px <= 0.0 { return; } out.allowed_font_sizes.push(AllowedFontSize { value: text, px, fluid, }); } re!( CLAMP_RE, format!("^{}\\({WS}*({ANY}+){WS}*\\)$", ci("clamp")) ); fn parse_clamp_args(raw: &str) -> Option> { let m = CLAMP_RE.captures(js::trim(raw))?; let args = split_top_level_args(&m[1]); if args.len() == 3 { Some(args) } else { None } } fn add_clamp_endpoints(out: &mut DesignSystem, raw: &str) -> bool { let Some(args) = parse_clamp_args(raw) else { return false; }; add_font_size_step(out, &args[0], true); add_font_size_step(out, &args[2], true); true } fn split_top_level_args(s: &str) -> Vec { let mut args = Vec::new(); let mut depth = 0i32; let mut current = String::new(); for ch in s.chars() { if ch == '(' { depth += 1; } else if ch == ')' { depth -= 1; } if ch == ',' && depth == 0 { args.push(js::trim(¤t).to_string()); current.clear(); continue; } current.push(ch); } if !js::trim(¤t).is_empty() { args.push(js::trim(¤t).to_string()); } args } fn is_string_or_number(v: &Value) -> bool { matches!(v, Value::String(_) | Value::Number(_)) } fn add_typography_sizes(out: &mut DesignSystem, typography: Option<&Value>) { let Some(Value::Object(typography)) = typography else { return; }; if let Some(Value::Object(scale)) = typography.get("scale") { for value in scale.values() { if !is_string_or_number(value) { continue; } add_font_size_step(out, &js_string(value), false); } } for (name, role) in typography { if name == "scale" { continue; } let Some(role) = role.as_object() else { continue; }; let raw = js::to_lower_case(js::trim(&value_to_string_nullish(role.get("fontSize")))); if add_clamp_endpoints(out, &raw) { continue; } add_font_size_step(out, &raw, false); } } re!(PILL_NAME_RE, "(^|\\.)(full|pill|round|rounded-full)$"); re!(PILL_EXACT_RE, "^(full|pill|round|rounded-full)$"); re!( PILL_ROLE_RE, format!("^({}|{}|{})$", ci("full"), ci("pill"), ci("round")) ); fn add_rounded_scale(out: &mut DesignSystem, rounded: Option<&Value>) { let Some(Value::Object(rounded)) = rounded else { return; }; for (raw_name, value) in rounded { let name = js::to_lower_case(unquote_yaml_key(raw_name)); add_rounded_token(out, &name, value); } } fn add_rounded_token(out: &mut DesignSystem, name: &str, value: &Value) { if !is_string_or_number(value) { return; } let raw = js::trim(&js_string(value)).to_string(); if raw.is_empty() || VAR_RE.is_match(&raw) || raw.contains('%') { return; } let Some(px) = resolve_length_px(Some(&raw), 16.0) else { return; }; if !px.is_finite() { return; } out.allowed_radii.push(AllowedRadius { name: name.to_string(), value: raw, px, }); if PILL_NAME_RE.is_match(name) { out.has_pill_radius = true; } } fn add_sidecar_radii(out: &mut DesignSystem, sidecar: Option<&Value>) { let Some(rounded_meta) = sidecar .and_then(|s| s.get("extensions")) .and_then(|e| e.get("roundedMeta")) .and_then(|c| c.as_object()) else { return; }; for (raw_name, meta) in rounded_meta { let name = js::to_lower_case(unquote_yaml_key(raw_name)); if is_string_or_number(meta) { add_rounded_token(out, &format!("sidecar.{name}"), meta); continue; } let Some(meta) = meta.as_object() else { continue; }; for key in ["canonical", "value"] { if let Some(v) = meta.get(key) { if is_string_or_number(v) { add_rounded_token(out, &format!("sidecar.{name}.{key}"), v); } } } for key in ["values", "aliases"] { let Some(Value::Array(list)) = meta.get(key) else { continue; }; for (index, value) in list.iter().enumerate() { add_rounded_token(out, &format!("sidecar.{name}.{key}[{index}]"), value); } } let role = value_to_string_nullish(meta.get("role")); if PILL_EXACT_RE.is_match(&name) || PILL_ROLE_RE.is_match(&role) { out.has_pill_radius = true; } } } fn add_sidecar_shadows(out: &mut DesignSystem, sidecar: Option<&Value>) { let Some(Value::Array(shadows)) = sidecar .and_then(|s| s.get("extensions")) .and_then(|e| e.get("shadows")) else { return; }; for entry in shadows { let Some(Value::String(value)) = entry.get("value") else { continue; }; for m in CSS_COLOR_RE.find_iter(value) { if let Some(parsed) = parse_design_color(m.as_str()) { out.allowed_shadow_colors.push(parsed); } } } } /// JS: design-system.mjs#normalizeDesignSystem pub fn normalize_design_system( frontmatter: Option<&Map>, sidecar: Option<&Value>, source_path: Option<&str>, sidecar_path: Option<&str>, md_newer_than_json: bool, ) -> DesignSystem { let empty = Map::new(); let frontmatter = frontmatter.unwrap_or(&empty); let mut out = DesignSystem { present: true, source_path: source_path.map(|s| s.to_string()), sidecar_path: sidecar_path.map(|s| s.to_string()), md_newer_than_json, ..Default::default() }; add_typography_fonts(&mut out, frontmatter.get("typography")); add_typography_sizes(&mut out, frontmatter.get("typography")); add_color_object(&mut out, frontmatter.get("colors"), "colors"); add_sidecar_colors(&mut out, sidecar); add_rounded_scale(&mut out, frontmatter.get("rounded")); add_sidecar_radii(&mut out, sidecar); add_sidecar_shadows(&mut out, sidecar); out.has_fonts = !out.allowed_fonts.is_empty(); out.has_colors = !out.allowed_color_keys.is_empty(); out.has_radii = !out.allowed_radii.is_empty(); out.has_font_sizes = out.allowed_font_sizes.iter().any(|e| !e.fluid); out } fn mtime_ms(p: &str) -> Option { let meta = std::fs::metadata(p).ok()?; let m = meta.modified().ok()?; let d = m.duration_since(std::time::UNIX_EPOCH).ok()?; Some(d.as_secs_f64() * 1000.0) } /// JS: design-system.mjs#loadDesignSystemForCwd pub fn load_design_system_for_cwd(cwd: &str) -> Option { let md = resolve_design_md_path(cwd)?; let md_stat = mtime_ms(&md.path); let text = read_text(&md.path)?; let frontmatter = parse_frontmatter(&text)?; let sidecar_path = resolve_design_sidecar_path(cwd, &md.context_dir); let sidecar = sidecar_path.as_deref().and_then(read_json); let sidecar_stat = sidecar_path.as_deref().and_then(mtime_ms); let md_newer = matches!((md_stat, sidecar_stat), (Some(m), Some(s)) if m > s + 1000.0); Some(normalize_design_system( Some(&frontmatter), sidecar.as_ref(), Some(&md.path), sidecar_path.as_deref(), md_newer, )) } /// JS `designSystemStartDir(targetPath, cwd)`. pub fn design_system_start_dir(target_path: &str, cwd: &str) -> String { let abs = if jsp::is_absolute(target_path) { target_path.to_string() } else { jsp::resolve(cwd, &[target_path]) }; match std::fs::metadata(&abs) { Ok(m) => { if m.is_dir() { abs } else { jsp::dirname(&abs) } } Err(_) => { if !jsp::extname(&abs).is_empty() { jsp::dirname(&abs) } else { abs } } } } /// `{ dir, hasDesign }` from `findDesignRoot`. #[derive(Debug, Clone, PartialEq)] pub struct DesignRoot { pub dir: String, pub has_design: bool, } /// JS: design-system.mjs#readWorkspacePatternGroups — Impeccable projectRoots /// govern any path they match (positive or negated); package-manager globs /// only apply to paths the Impeccable group does not match. fn read_workspace_pattern_groups(dir: &str) -> (Vec, Vec) { let mut impeccable: Vec = Vec::new(); for name in ["config.json", "config.local.json"] { let roots = read_json(&jsp::join(&[dir, ".impeccable", name])) .and_then(|v| v.get("projectRoots").cloned()); if let Some(Value::Array(roots)) = roots { for entry in roots { if let Value::String(s) = entry { let t = js::trim(&s); if !t.is_empty() { impeccable.push(t.to_string()); } } } } } let mut pkg: Vec = Vec::new(); let workspaces = read_json(&jsp::join(&[dir, "package.json"])).and_then(|v| v.get("workspaces").cloned()); match &workspaces { Some(Value::Array(ws)) => pkg.extend(ws.iter().map(js_string)), Some(other) => { if let Some(Value::Array(ws)) = other.get("packages") { pkg.extend(ws.iter().map(js_string)); } } None => {} } if let Some(Value::Array(ws)) = read_json(&jsp::join(&[dir, "lerna.json"])).and_then(|v| v.get("packages").cloned()) { pkg.extend(ws.iter().map(js_string)); } if let Some(text) = read_text(&jsp::join(&[dir, "pnpm-workspace.yaml"])) { let mut in_packages = false; for line in text.split("\r\n").flat_map(|l| l.split('\n')) { let stripped = strip_inline_yaml_comment(line); let trimmed = js::trim(&stripped); if trimmed.is_empty() || trimmed.starts_with('#') { continue; } if let Some(caps) = PNPM_FLOW_RE.captures(trimmed) { for entry in caps.get(1).map(|m| m.as_str()).unwrap_or("").split(',') { let e = EDGE_QUOTE_RE.replace_all(js::trim(entry), "").into_owned(); if !e.is_empty() { pkg.push(e); } } break; } if PNPM_PACKAGES_RE.is_match(trimmed) { in_packages = true; continue; } if !in_packages { continue; } if let Some(caps) = PNPM_ITEM_RE.captures(trimmed) { pkg.push(EDGE_QUOTE_RE.replace_all(js::trim(caps.get(1).map(|m| m.as_str()).unwrap_or("")), "").into_owned()); } else if PNPM_KEY_RE.is_match(trimmed) { break; } } } (impeccable, pkg) } /// JS: design-system.mjs#readWorkspacePatterns fn read_workspace_patterns(dir: &str) -> Vec { let (mut a, mut b) = read_workspace_pattern_groups(dir); a.append(&mut b); a } const MONOREPO_MARKER_FILES: &[&str] = &["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json"]; const MONOREPO_FALLBACK_PROJECT_DIRS: &[&str] = &["apps", "packages"]; re!(PNPM_FLOW_RE, format!("^packages:{WS}*\\[(.*)\\]{WS}*$")); re!(PNPM_PACKAGES_RE, format!("^packages:{WS}*$")); re!(PNPM_ITEM_RE, format!("^-{WS}*(.+)$")); re!(PNPM_KEY_RE, format!("^[A-Za-z0-9_-]+:{WS}*")); /// JS: design-system.mjs#isMonorepoRoot fn is_monorepo_root(dir: &str) -> bool { if read_workspace_patterns(dir).iter().any(|p| !js::trim(p).starts_with('!')) { return true; } if !MONOREPO_MARKER_FILES.iter().any(|f| exists(&jsp::join(&[dir, f]))) { return false; } MONOREPO_FALLBACK_PROJECT_DIRS.iter().any(|name| { std::fs::read_dir(jsp::join(&[dir, name])) .map(|entries| { entries .flatten() .any(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) }) .unwrap_or(false) }) } /// JS: design-system.mjs#monorepoOwnsPath > normalizeWorkspacePattern fn normalize_workspace_pattern(pattern: &str) -> String { let mut s = EDGE_QUOTE_RE.replace_all(js::trim(pattern), "").into_owned(); if let Some(rest) = s.strip_prefix("./") { s = rest.to_string(); } while s.ends_with('/') { s.pop(); } s } /// JS: design-system.mjs#monorepoOwnsPath > segmentMatches fn segment_matches(pattern_segment: &str, rel_segment: &str) -> bool { if pattern_segment == "*" { return true; } if !pattern_segment.contains('*') { return pattern_segment == rel_segment; } let escaped = regex::escape(pattern_segment).replace("\\*", "[^/]*"); Regex::new(&format!("^{}$", escaped)).map(|re| re.is_match(rel_segment)).unwrap_or(false) } /// JS: design-system.mjs#monorepoOwnsPath > matchGlobSegments fn match_glob_segments(pattern_segments: &[&str], rel_segments: &[&str]) -> bool { fn rec(pattern_segments: &[&str], rel_segments: &[&str], pi: usize, ri: usize) -> bool { if pi == pattern_segments.len() { return ri == rel_segments.len(); } if pattern_segments[pi] == "**" { if pi == pattern_segments.len() - 1 { return true; } for k in ri..=rel_segments.len() { if rec(pattern_segments, rel_segments, pi + 1, k) { return true; } } return false; } if ri >= rel_segments.len() { return false; } if !segment_matches(pattern_segments[pi], rel_segments[ri]) { return false; } rec(pattern_segments, rel_segments, pi + 1, ri + 1) } rec(pattern_segments, rel_segments, 0, 0) } /// JS: design-system.mjs#monorepoOwnsPath fn monorepo_owns_path(root: &str, boundary_dir: &str) -> bool { let rel = jsp::relative("/", root, boundary_dir); if rel.is_empty() || rel.starts_with("..") || jsp::is_absolute(&rel) { return false; } let rel_segments: Vec<&str> = rel.split(jsp::SEP_CHAR).filter(|s| !s.is_empty()).collect(); // Negations like !packages/excluded must also cover nested dirs under // that path. let matches_negation = |pattern: &str| -> bool { let normalized = normalize_workspace_pattern(pattern); let pattern_segments: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect(); if pattern_segments.is_empty() { return false; } if pattern_segments.contains(&"**") { return match_glob_segments(&pattern_segments, &rel_segments); } if rel_segments.len() < pattern_segments.len() { return false; } pattern_segments .iter() .zip(rel_segments.iter()) .all(|(p, r)| segment_matches(p, r)) }; // Positive globs identify workspace packages at exact depth (`*` is a // direct child). A nested package.json under that package is still owned: // the ancestor directory of glob length must itself be a package. let positive_owns = |pattern: &str| -> bool { let normalized = normalize_workspace_pattern(pattern); let pattern_segments: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect(); if pattern_segments.is_empty() { return false; } if pattern_segments.contains(&"**") { return match_glob_segments(&pattern_segments, &rel_segments); } if rel_segments.len() < pattern_segments.len() { return false; } if !pattern_segments.iter().zip(rel_segments.iter()).all(|(p, r)| segment_matches(p, r)) { return false; } if rel_segments.len() == pattern_segments.len() { return true; } let mut parts: Vec<&str> = vec![root]; parts.extend(&rel_segments[..pattern_segments.len()]); let ancestor_dir = jsp::join(&parts); exists(&jsp::join(&[&ancestor_dir, "package.json"])) }; let group_owns = |raw_patterns: &[String]| -> Option { let patterns: Vec = raw_patterns .iter() .map(|p| normalize_workspace_pattern(p)) .filter(|p| !p.is_empty()) .collect(); if patterns.is_empty() { return None; } let excluded = patterns .iter() .any(|pattern| pattern.starts_with('!') && matches_negation(&pattern[1..])); let included = patterns .iter() .filter(|pattern| !pattern.starts_with('!')) .any(|pattern| positive_owns(pattern)); if !excluded && !included { return None; } if excluded { return Some(false); } Some(true) }; let (impeccable, pkg) = read_workspace_pattern_groups(root); if let Some(from_impeccable) = group_owns(&impeccable) { return from_impeccable; } if let Some(from_pkg) = group_owns(&pkg) { return from_pkg; } if impeccable .iter() .chain(pkg.iter()) .any(|pattern| !normalize_workspace_pattern(pattern).starts_with('!')) { return false; } rel_segments.len() >= 2 && MONOREPO_FALLBACK_PROJECT_DIRS.contains(&rel_segments[0]) } /// JS: design-system.mjs#homeDirForms — both forms of the home directory. The /// walk compares path strings, and a symlinked home (e.g. /home -> /var/home) /// never string-matches the physical paths a cwd-resolved target produces, /// which would let the post-boundary walk sail through $HOME and inherit /// from it. fn home_dir_forms(cwd: &str, home: &str) -> Vec { let home_dir = jsp::resolve(cwd, &[home]); let mut forms = vec![home_dir.clone()]; if let Ok(real) = std::fs::canonicalize(&home_dir) { let real = real.to_string_lossy().into_owned(); let real = real.strip_prefix("\\\\?\\").map(|s| s.to_string()).unwrap_or(real); if !forms.contains(&real) { forms.push(real); } } forms } /// JS: design-system.mjs#findDesignRoot. `home` is `os.homedir()`. /// /// A directory carrying a project marker but no DESIGN.md is a project /// BOUNDARY. A nested package.json inherits the ancestor DESIGN.md only when /// that ancestor's workspace declarations include the path (negations win; a /// nested package under a matched workspace still inherits). Marker-only /// roots (turbo/nx/lerna/pnpm with no globs) still own apps/ and /// packages/. A stray nested package that matches no glob does not /// inherit, and a nested separate repository (.git with no workspace /// declaration) still inherits nothing (issue #570). pub fn find_design_root(start_dir: &str, cwd: &str, home: &str) -> Option { let mut dir = jsp::resolve(cwd, &[start_dir]); let home_dirs = home_dir_forms(cwd, home); let mut boundary: Option = None; loop { if boundary.is_none() && resolve_design_md_path(&dir).is_some() { return Some(DesignRoot { dir, has_design: true }); } if let Some(b) = &boundary { // Past the boundary the walk only looks for the monorepo root // that owns the workspace path. Monorepo-root before .git, same // order as context.mjs: a workspace root carrying its own .git is // still recognized, while a .git that declares no workspaces is a // separate repository and stops the walk with nothing inherited. // The home directory is never an owning root. if !home_dirs.contains(&dir) && is_monorepo_root(&dir) { if monorepo_owns_path(&dir, &b.dir) { let has_design = resolve_design_md_path(&dir).is_some(); return Some(DesignRoot { dir, has_design }); } return boundary; } if exists(&jsp::join(&[&dir, ".git"])) { return boundary; } } else if PROJECT_ROOT_MARKERS.iter().any(|marker| exists(&jsp::join(&[&dir, marker]))) { boundary = Some(DesignRoot { dir: dir.clone(), has_design: false }); // A boundary that is itself a monorepo root, or a separate // repository with its own .git, inherits nothing from above. if is_monorepo_root(&dir) || exists(&jsp::join(&[&dir, ".git"])) { return boundary; } } if home_dirs.contains(&dir) { return boundary; } let parent = jsp::dirname(&dir); if parent == dir { return boundary; } dir = parent; } } /// Memo for `load_design_system_for_target`, keyed by design root. pub type DesignSystemCache = HashMap>>; /// JS: design-system.mjs#loadDesignSystemForTarget pub fn load_design_system_for_target( target_path: &str, cache: Option<&mut DesignSystemCache>, cwd: &str, home: &str, ) -> Option> { let start_dir = design_system_start_dir(target_path, cwd); let found = find_design_root(&start_dir, cwd, home); let key = match &found { Some(f) => format!("root:{}", f.dir), None => "\0none".to_string(), }; if let Some(cache) = &cache { if let Some(hit) = cache.get(&key) { return hit.clone(); } } let loaded = match &found { Some(f) if f.has_design => load_design_system_for_cwd(&f.dir).map(Rc::new), _ => None, }; if let Some(cache) = cache { cache.insert(key, loaded.clone()); } loaded } // ─── Allowlist predicates ──────────────────────────────────────────────────── /// JS: design-system.mjs#isAllowedFont pub fn is_allowed_font(font: &str, ds: Option<&DesignSystem>) -> bool { if font.is_empty() || is_generic_font(font) { return true; } match ds { Some(ds) if ds.has_fonts => ds.allowed_fonts.iter().any(|f| f == font), _ => true, } } /// JS: design-system.mjs#isAllowedColorRaw pub fn is_allowed_color_raw(raw: &str, ds: Option<&DesignSystem>) -> bool { let Some(ds) = ds.filter(|d| d.has_colors) else { return true; }; let text = js::to_lower_case(js::trim(raw)); if text.is_empty() || text == "transparent" || text == "currentcolor" || text == "inherit" || text == "initial" { return true; } if text.contains("var(") { return true; } let Some(parsed) = parse_design_color(&text) else { return true; }; if parsed.alpha_or_one() <= 0.05 { return true; } ds.allowed_color_keys .iter() .any(|(_, entry)| colors_close(&parsed, &entry.color)) } /// JS: design-system.mjs#isAllowedShadowColorRaw pub fn is_allowed_shadow_color_raw(raw: &str, ds: Option<&DesignSystem>) -> bool { let Some(ds) = ds.filter(|d| !d.allowed_shadow_colors.is_empty()) else { return false; }; let Some(parsed) = parse_design_color(&js::to_lower_case(js::trim(raw))) else { return false; }; ds.allowed_shadow_colors.iter().any(|entry| { colors_close(&parsed, entry) && (parsed.alpha_or_one() - entry.alpha_or_one()).abs() <= SHADOW_ALPHA_TOLERANCE }) } /// JS: design-system.mjs#isAllowedRadiusRaw pub fn is_allowed_radius_raw(raw: &str, ds: Option<&DesignSystem>) -> bool { let Some(ds) = ds.filter(|d| d.has_radii) else { return true; }; let text = js::to_lower_case(js::trim(raw)); if text.is_empty() || text == "0" || text == "none" || text == "initial" || text == "inherit" { return true; } if text.contains("var(") || text.contains('%') { return true; } let Some(px) = resolve_length_px(Some(&text), 16.0) else { return true; }; if !px.is_finite() || px <= RADIUS_TOLERANCE_PX { return true; } if ds.has_pill_radius && px >= 99.0 { return true; } ds.allowed_radii .iter() .any(|entry| (entry.px - px).abs() <= RADIUS_TOLERANCE_PX) } #[derive(Debug, Clone, Copy, PartialEq)] enum StepStatus { Unjudgeable, OnRamp, OffRamp, } fn font_size_step_status(raw: &str, ds: &DesignSystem) -> StepStatus { let text = js::to_lower_case(js::trim(raw)); if !FONT_SIZE_LITERAL_RE.is_match(&text) { return StepStatus::Unjudgeable; } let Some(px) = resolve_length_px(Some(&text), 16.0) else { return StepStatus::Unjudgeable; }; if !px.is_finite() || px <= 0.0 { return StepStatus::Unjudgeable; } if ds .allowed_font_sizes .iter() .any(|entry| (entry.px - px).abs() <= FONT_SIZE_TOLERANCE_PX) { StepStatus::OnRamp } else { StepStatus::OffRamp } } /// JS: design-system.mjs#offRampClampEndpoints. `None` when `raw` is not a /// fluid value (or the system has no ramp). pub fn off_ramp_clamp_endpoints(raw: &str, ds: Option<&DesignSystem>) -> Option> { let ds = ds.filter(|d| d.has_font_sizes)?; let cleaned = IMPORTANT_TAIL_RE.replace(js::trim(raw), ""); let args = parse_clamp_args(&cleaned)?; Some( [args[0].clone(), args[2].clone()] .into_iter() .filter(|endpoint| font_size_step_status(endpoint, ds) == StepStatus::OffRamp) .collect(), ) } /// JS: design-system.mjs#isAllowedFontSizeRaw pub fn is_allowed_font_size_raw(raw: &str, ds: Option<&DesignSystem>) -> bool { let Some(ds) = ds.filter(|d| d.has_font_sizes) else { return true; }; let text = js::to_lower_case(js::trim(raw)); let text = IMPORTANT_TAIL_CS_RE.replace(&text, ""); if let Some(off) = off_ramp_clamp_endpoints(&text, Some(ds)) { return off.is_empty(); } font_size_step_status(&text, ds) != StepStatus::OffRamp } fn line_looks_commented(line: &str) -> bool { let t = js::trim(line); t.starts_with("//") || t.starts_with("/*") || t.starts_with('*') || t.starts_with("