Skip to main content

moss_core/contract/
tokens.rs

1//! W3C Design Tokens loader.
2//!
3//! Reads the embedded `tokens.json` (W3C Design Tokens Community Group format)
4//! and exposes it as ordered structs the codegen consumes.
5//!
6//! ## Invariants
7//! - Tokens are loaded at compile time via `include_str!`. moss-core stays zero-I/O.
8//! - Group order is taken from the top-level `$order` array in tokens.json
9//!   (NOT JSON insertion order — serde_json doesn't preserve insertion order
10//!   by default and moss doesn't enable the `preserve_order` feature).
11//! - Within each group, entries are sorted alphabetically.
12
13const TOKENS_JSON: &str = include_str!("tokens.json");
14
15/// A single design token entry.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct TokenEntry {
18    /// CSS variable name without leading `--` (e.g. `moss-color-accent`).
19    pub name: String,
20    /// CSS value as a string (e.g. `#2d5a2d`, `1.125rem`, `var(--moss-content-width)`).
21    /// When `$value` is an object with `"light"` and `"dark"` keys, this holds the light value.
22    pub value: String,
23    /// Dark-mode CSS value. `None` when `$value` is a plain string (light-only token).
24    /// `Some(...)` when `$value` is `{ "light": "...", "dark": "..." }`.
25    pub dark_value: Option<String>,
26    /// Optional W3C `$type` hint (color, dimension, fontFamily, number).
27    pub type_hint: Option<String>,
28    /// Optional human-readable description.
29    pub description: Option<String>,
30}
31
32/// A group of tokens (e.g. `typography`, `color`, `layout`, `spacing`).
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct TokenGroup {
35    /// Group name as it appears in `tokens.json` (e.g. `color`).
36    pub name: String,
37    /// Optional group-level description.
38    pub description: Option<String>,
39    /// Token entries, sorted alphabetically.
40    pub entries: Vec<TokenEntry>,
41}
42
43/// The full tokens manifest.
44#[derive(Debug, Clone)]
45pub struct Tokens {
46    /// Groups in declared order (from `$order`).
47    pub groups: Vec<TokenGroup>,
48}
49
50/// Load the embedded `tokens.json` into structured form.
51///
52/// Group order is taken from the top-level `$order` array in tokens.json.
53/// Entries within each group are alphabetical.
54///
55/// Returns an error if the JSON is malformed or `$order` is missing.
56pub fn load_tokens() -> Result<Tokens, String> {
57    parse_tokens(TOKENS_JSON)
58}
59
60/// Parse a tokens.json string. Exposed for testing error paths;
61/// production callers use `load_tokens()`.
62pub fn parse_tokens(input: &str) -> Result<Tokens, String> {
63    let value: serde_json::Value = serde_json::from_str(input)
64        .map_err(|e| format!("tokens.json parse error: {}", e))?;
65    let top = value.as_object().ok_or("tokens.json must be a JSON object")?;
66
67    // Read group ordering from the explicit `$order` array.
68    let order: Vec<String> = top
69        .get("$order")
70        .and_then(|v| v.as_array())
71        .ok_or("tokens.json missing top-level `$order` array")?
72        .iter()
73        .filter_map(|v| v.as_str().map(String::from))
74        .collect();
75
76    let mut groups = Vec::with_capacity(order.len());
77
78    for group_name in &order {
79        let group_value = top
80            .get(group_name)
81            .ok_or_else(|| format!("`$order` lists '{}' but group is missing", group_name))?;
82        let group_obj = group_value
83            .as_object()
84            .ok_or_else(|| format!("group '{}' must be an object", group_name))?;
85
86        let mut description = None;
87        let mut entries = Vec::new();
88
89        for (entry_key, entry_value) in group_obj {
90            if entry_key == "$description" {
91                description = entry_value.as_str().map(|s| s.to_string());
92                continue;
93            }
94            if entry_key.starts_with('$') {
95                continue;
96            }
97            let entry_obj = entry_value
98                .as_object()
99                .ok_or_else(|| format!("entry '{}/{}' must be an object", group_name, entry_key))?;
100
101            let type_hint = entry_obj.get("$type").and_then(|v| v.as_str()).map(String::from);
102            let raw_value = entry_obj
103                .get("$value")
104                .ok_or_else(|| format!("entry '{}/{}' missing $value", group_name, entry_key))?;
105            let (entry_value_str, entry_dark_value) = match raw_value {
106                serde_json::Value::String(s) => (s.clone(), None),
107                serde_json::Value::Object(obj) => {
108                    let light = obj
109                        .get("light")
110                        .and_then(|v| v.as_str())
111                        .ok_or_else(|| format!("entry '{}/{}' $value object missing \"light\" key", group_name, entry_key))?
112                        .to_string();
113                    let dark = obj.get("dark").and_then(|v| v.as_str()).map(String::from);
114                    (light, dark)
115                }
116                _ => return Err(format!(
117                    "entry '{}/{}' $value must be a string or {{\"light\",\"dark\"}} object",
118                    group_name, entry_key
119                )),
120            };
121            let entry_description = entry_obj.get("$description").and_then(|v| v.as_str()).map(String::from);
122
123            entries.push(TokenEntry {
124                name: entry_key.clone(),
125                value: entry_value_str,
126                dark_value: entry_dark_value,
127                type_hint,
128                description: entry_description,
129            });
130        }
131
132        // Alphabetical within each group.
133        entries.sort_by(|a, b| a.name.cmp(&b.name));
134
135        groups.push(TokenGroup {
136            name: group_name.clone(),
137            description,
138            entries,
139        });
140    }
141
142    Ok(Tokens { groups })
143}
144
145/// Format the loaded tokens as the CSS `:root` block per the v1 formatter
146/// decisions (see spec § Open Question 3):
147/// - Property order: group-then-alphabetical (groups in source order).
148/// - Color casing: lowercase hex.
149/// - Unit normalization: pass-through (tokens.json owns canonical units).
150/// - Comments: blank line + group-name comment between groups.
151/// - Indentation: 2 spaces.
152/// - Trailing semicolons: always.
153pub fn format_root_block(tokens: &Tokens) -> String {
154    let mut out = String::new();
155    out.push_str(":root {\n");
156
157    for (idx, group) in tokens.groups.iter().enumerate() {
158        if idx > 0 {
159            out.push('\n');
160        }
161        // Group name is title-cased: "typography" → "Typography".
162        let title = title_case(&group.name);
163        out.push_str(&format!("  /* {} */\n", title));
164
165        for entry in &group.entries {
166            let value = normalize_value(&entry.value, entry.type_hint.as_deref());
167            out.push_str(&format!("  --{}: {};\n", entry.name, value));
168        }
169    }
170
171    out.push_str("}\n");
172    out
173}
174
175/// Format the tokens whose `dark_value` is set as a CSS `[data-theme="dark"]` block.
176///
177/// The `:root` prefix is intentionally omitted: layer order (tokens layer < themes
178/// layer) guarantees the tokens block loses to any author override in the themes layer,
179/// regardless of selector specificity. Keeping `:root` here would give the block a
180/// spurious specificity bump that conflicts with the layer contract.
181///
182/// Mirrors `format_root_block`'s style (group comments, 2-space indent, trailing
183/// semicolons). Returns an empty `String` if no token has a dark value.
184pub fn format_dark_root_block(tokens: &Tokens) -> String {
185    // Check whether any dark values exist at all.
186    let has_dark = tokens
187        .groups
188        .iter()
189        .any(|g| g.entries.iter().any(|e| e.dark_value.is_some()));
190    if !has_dark {
191        return String::new();
192    }
193
194    let mut out = String::new();
195    out.push_str("[data-theme=\"dark\"] {\n");
196
197    let mut first_group = true;
198    for group in &tokens.groups {
199        // Only include groups that have at least one dark token.
200        let dark_entries: Vec<&TokenEntry> = group
201            .entries
202            .iter()
203            .filter(|e| e.dark_value.is_some())
204            .collect();
205        if dark_entries.is_empty() {
206            continue;
207        }
208
209        if !first_group {
210            out.push('\n');
211        }
212        first_group = false;
213
214        let title = title_case(&group.name);
215        out.push_str(&format!("  /* {} */\n", title));
216
217        for entry in dark_entries {
218            let dark_val = entry.dark_value.as_deref().unwrap();
219            let value = normalize_value(dark_val, entry.type_hint.as_deref());
220            out.push_str(&format!("  --{}: {};\n", entry.name, value));
221        }
222    }
223
224    out.push_str("}\n");
225    out
226}
227
228/// Format the dark-value tokens as a system-preference fallback block.
229///
230/// Produces:
231/// ```css
232/// @media (prefers-color-scheme: dark) {
233///   :root:not([data-theme]) {
234///     --moss-color-bg: #1c1914;
235///     ...
236///   }
237/// }
238/// ```
239///
240/// Only applies when NO explicit `data-theme` is set. Once the user or a
241/// script sets any `data-theme`, the explicit `:root[data-theme="dark"]` block
242/// (from `format_dark_root_block`) takes over.
243///
244/// Returns an empty `String` if no token has a dark value.
245pub fn format_dark_media_block(tokens: &Tokens) -> String {
246    // Check whether any dark values exist at all.
247    let has_dark = tokens
248        .groups
249        .iter()
250        .any(|g| g.entries.iter().any(|e| e.dark_value.is_some()));
251    if !has_dark {
252        return String::new();
253    }
254
255    let mut out = String::new();
256    out.push_str("@media (prefers-color-scheme: dark) {\n");
257    out.push_str(":root:not([data-theme]) {\n");
258
259    let mut first_group = true;
260    for group in &tokens.groups {
261        let dark_entries: Vec<&TokenEntry> = group
262            .entries
263            .iter()
264            .filter(|e| e.dark_value.is_some())
265            .collect();
266        if dark_entries.is_empty() {
267            continue;
268        }
269
270        if !first_group {
271            out.push('\n');
272        }
273        first_group = false;
274
275        let title = title_case(&group.name);
276        out.push_str(&format!("  /* {} */\n", title));
277
278        for entry in dark_entries {
279            let dark_val = entry.dark_value.as_deref().unwrap();
280            let value = normalize_value(dark_val, entry.type_hint.as_deref());
281            out.push_str(&format!("  --{}: {};\n", entry.name, value));
282        }
283    }
284
285    out.push_str("}\n");
286    out.push_str("}\n");
287    out
288}
289
290/// Emit deprecated-alias `:root` block.
291///
292/// Maps every v1.2 token name to its v1.3 renamed replacement so that
293/// author `.moss/theme/style.css` files that reference old names keep
294/// working for one release. Each alias is documented with a `/* deprecated */`
295/// comment. Remove this block after the next breaking-contract release.
296pub fn format_deprecated_aliases_block() -> String {
297    // (old-name, new-name) pairs — one alias per renamed token.
298    let aliases: &[(&str, &str)] = &[
299        // Font-size scale: --moss-font-* → --moss-size-*
300        ("--moss-font-2xs",     "var(--moss-size-2xs)"),
301        ("--moss-font-xs",      "var(--moss-size-xs)"),
302        ("--moss-font-sm",      "var(--moss-size-sm)"),
303        ("--moss-font-lg",      "var(--moss-size-lg)"),
304        ("--moss-font-xl",      "var(--moss-size-xl)"),
305        ("--moss-font-2xl",     "var(--moss-size-2xl)"),
306        ("--moss-font-3xl",     "var(--moss-size-3xl)"),
307        // Font weight: --moss-font-weight → --moss-font-weight-body
308        ("--moss-font-weight",  "var(--moss-font-weight-body)"),
309        // Accent hover: --moss-accent-hover → --moss-color-accent-hover
310        ("--moss-accent-hover", "var(--moss-color-accent-hover)"),
311        // Text secondary: --moss-text-secondary → --moss-color-text-secondary (distinct role, renamed not consolidated)
312        ("--moss-text-secondary", "var(--moss-color-text-secondary)"),
313    ];
314
315    let mut out = String::new();
316    out.push_str("/* Deprecated token aliases — v1.2 names forwarded to v1.3 renames.\n");
317    out.push_str("   Author .moss/theme/style.css files referencing these names continue to\n");
318    out.push_str("   resolve correctly for one release. Remove after next contract bump. */\n");
319    out.push_str(":root {\n");
320    for (old, new) in aliases {
321        out.push_str(&format!("  {}: {}; /* deprecated: use {} */\n", old, new, new.trim_start_matches("var(").trim_end_matches(')')));
322    }
323    out.push_str("}\n");
324    out
325}
326
327/// Look up the light value (and optionally the dark value) of a named token.
328///
329/// Returns `(light_value, dark_value)`. `dark_value` is `None` for single-value
330/// tokens. Returns `None` from the outer `Option` when the token name is not found.
331///
332/// Used at emit time to derive `<meta name="theme-color">` values from tokens.json
333/// rather than hardcoding hex literals that can drift from the CSS.
334pub fn find_token<'a>(tokens: &'a Tokens, name: &str) -> Option<(&'a str, Option<&'a str>)> {
335    tokens
336        .groups
337        .iter()
338        .flat_map(|g| &g.entries)
339        .find(|e| e.name == name)
340        .map(|e| (e.value.as_str(), e.dark_value.as_deref()))
341}
342
343/// Convenience: return the `--moss-color-bg` light and dark CSS values baked
344/// into the embedded tokens.json, falling back to hardcoded defaults if the
345/// token is absent (should never happen in production).
346pub fn bg_colors(tokens: &Tokens) -> (&str, &str) {
347    match find_token(tokens, "moss-color-bg") {
348        Some((light, Some(dark))) => (light, dark),
349        Some((light, None)) => (light, "#1c1914"),
350        None => ("#faf8f5", "#1c1914"),
351    }
352}
353
354/// Title-case the group name. "typography" → "Typography", "color" → "Color".
355fn title_case(s: &str) -> String {
356    let mut chars = s.chars();
357    match chars.next() {
358        None => String::new(),
359        Some(c) => c.to_uppercase().chain(chars).collect(),
360    }
361}
362
363/// Normalize a token value per the v1 formatter rules.
364fn normalize_value(value: &str, type_hint: Option<&str>) -> String {
365    if matches!(type_hint, Some("color")) {
366        return normalize_hex_color(value);
367    }
368    value.to_string()
369}
370
371/// Normalize a hex color to lowercase 6-digit form. Pass through any value
372/// that isn't a recognized hex literal (e.g., `var()`, `rgb()`, named colors).
373fn normalize_hex_color(value: &str) -> String {
374    let trimmed = value.trim();
375    if let Some(rest) = trimmed.strip_prefix('#') {
376        if rest.chars().all(|c| c.is_ascii_hexdigit())
377            && (rest.len() == 3 || rest.len() == 6 || rest.len() == 8)
378        {
379            let lower = rest.to_lowercase();
380            // Expand 3-digit hex to 6-digit.
381            if lower.len() == 3 {
382                let r = &lower[0..1];
383                let g = &lower[1..2];
384                let b = &lower[2..3];
385                return format!("#{r}{r}{g}{g}{b}{b}");
386            }
387            return format!("#{}", lower);
388        }
389    }
390    value.to_string()
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn format_dark_root_block_emits_only_dark_tokens() {
399        let json = r##"{ "$order": ["color"], "color": {
400          "moss-color-bg": {"$type":"color","$value":{"light":"#faf8f5","dark":"#1c1914"}},
401          "moss-color-accent": {"$type":"color","$value":"#2d5a2d"} } }"##;
402        let t = parse_tokens(json).unwrap();
403        let dark = format_dark_root_block(&t);
404        // Task 2.4: vestigial :root prefix dropped — layer order carries the win.
405        assert!(dark.contains("[data-theme=\"dark\"]"), "must use [data-theme=\"dark\"] selector (no :root prefix)");
406        assert!(!dark.contains(":root[data-theme=\"dark\"]"), "must NOT use :root prefix (vestigial specificity hack removed)");
407        assert!(dark.contains("--moss-color-bg: #1c1914"));
408        assert!(!dark.contains("--moss-color-accent")); // no dark value → not emitted
409    }
410
411    #[test]
412    fn format_dark_root_block_returns_empty_when_no_dark_values() {
413        let json = r##"{ "$order": ["color"], "color": {
414          "moss-color-accent": {"$type":"color","$value":"#2d5a2d"},
415          "moss-color-bg": {"$type":"color","$value":"#faf8f5"} } }"##;
416        let t = parse_tokens(json).unwrap();
417        assert_eq!(format_dark_root_block(&t), "");
418    }
419
420    #[test]
421    fn parse_tokens_accepts_object_value_with_dark() {
422        let json = r##"{ "$order": ["color"], "color": { "moss-color-bg": {
423            "$type": "color",
424            "$value": { "light": "#faf8f5", "dark": "#1c1914" },
425            "$description": "Page background" } } }"##;
426        let tokens = parse_tokens(json).expect("parses");
427        let bg = tokens.groups.iter().flat_map(|g| &g.entries)
428            .find(|t| t.name == "moss-color-bg").expect("bg token");
429        assert_eq!(bg.value, "#faf8f5");
430        assert_eq!(bg.dark_value.as_deref(), Some("#1c1914"));
431    }
432
433    #[test]
434    fn parse_tokens_string_value_has_no_dark() {
435        let json = r##"{ "$order": ["color"], "color": { "moss-color-accent": {
436            "$type": "color", "$value": "#2d5a2d", "$description": "Accent" } } }"##;
437        let t = parse_tokens(json).unwrap();
438        let a = t.groups.iter().flat_map(|g| &g.entries).find(|t| t.name == "moss-color-accent").unwrap();
439        assert_eq!(a.value, "#2d5a2d");
440        assert_eq!(a.dark_value, None);
441    }
442
443    #[test]
444    fn tokens_json_includes_internal_tokens() {
445        let t = load_tokens().unwrap();
446        let names: Vec<_> = t.groups.iter().flat_map(|g| &g.entries).map(|t| t.name.as_str()).collect();
447        // Task 1.3: renamed tokens — assert NEW names present, old names absent.
448        for n in [
449            // color tokens (renamed)
450            "moss-color-text-secondary",   // was moss-text-secondary
451            "moss-color-accent-hover",     // was moss-accent-hover
452            "moss-border-light",
453            "moss-border-medium",
454            "moss-code-background",
455            "moss-code-border",
456            "moss-code-accent-primary",
457            "moss-code-accent-secondary",
458            "moss-code-accent-tertiary",
459            "moss-code-accent-quaternary",
460            "moss-hl-keyword",
461            "moss-hl-string",
462            "moss-hl-comment",
463            "moss-hl-number",
464            "moss-hl-function",
465            "moss-hl-type",
466            "moss-hl-tag",
467            "moss-hl-attr",
468            "moss-hl-operator",
469            "moss-hl-builtin",
470            "moss-hl-meta",
471            "moss-hl-deletion",
472            "moss-hl-addition-bg",
473            "moss-hl-deletion-bg",
474            // font size scale (renamed)
475            "moss-size-2xs",  // was moss-font-2xs
476            "moss-size-xs",   // was moss-font-xs
477            "moss-size-sm",   // was moss-font-sm
478            "moss-size-md",   // new: equals --moss-reading-size-base
479            "moss-size-lg",   // was moss-font-lg
480            "moss-size-xl",   // was moss-font-xl
481            "moss-size-2xl",  // was moss-font-2xl
482            "moss-size-3xl",  // was moss-font-3xl
483            // font weight (renamed)
484            "moss-font-weight-body",  // was moss-font-weight
485            "moss-font-heading-weight",
486        ] {
487            assert!(names.contains(&n), "missing token: {n}");
488        }
489        // assert OLD names are gone
490        for old in [
491            "moss-text-secondary",
492            "moss-accent-hover",
493            "moss-font-2xs",
494            "moss-font-xs",
495            "moss-font-sm",
496            "moss-font-lg",
497            "moss-font-xl",
498            "moss-font-2xl",
499            "moss-font-3xl",
500            "moss-font-weight",
501        ] {
502            assert!(!names.contains(&old), "old token still present: {old}");
503        }
504        assert!(names.contains(&"moss-color-ui-accent"), "missing token: moss-color-ui-accent");
505        assert!(names.len() >= 47, "expected >=47 tokens (added moss-color-ui-accent), got {}", names.len());
506    }
507
508    #[test]
509    fn format_dark_media_block_wraps_dark_tokens_in_media_query() {
510        let json = r##"{ "$order": ["color"], "color": {
511          "moss-color-bg": {"$type":"color","$value":{"light":"#faf8f5","dark":"#1c1914"}},
512          "moss-color-accent": {"$type":"color","$value":"#2d5a2d"} } }"##;
513        let t = parse_tokens(json).unwrap();
514        let media = format_dark_media_block(&t);
515        assert!(media.contains("@media (prefers-color-scheme: dark)"), "must be wrapped in @media");
516        assert!(media.contains(":root:not([data-theme])"), "must target :root:not([data-theme])");
517        assert!(media.contains("--moss-color-bg: #1c1914"), "must contain dark value");
518        assert!(!media.contains("--moss-color-accent"), "light-only token must not appear");
519    }
520
521    #[test]
522    fn format_dark_media_block_returns_empty_when_no_dark_values() {
523        let json = r##"{ "$order": ["color"], "color": {
524          "moss-color-accent": {"$type":"color","$value":"#2d5a2d"} } }"##;
525        let t = parse_tokens(json).unwrap();
526        assert_eq!(format_dark_media_block(&t), "");
527    }
528
529    /// Task 1.2/1.3: assert moss-color-accent-hover (renamed from moss-accent-hover) is
530    /// derived via color-mix in both modes.
531    #[test]
532    fn accent_hover_is_derived_from_accent_via_color_mix() {
533        let t = load_tokens().unwrap();
534        let entry = t
535            .groups
536            .iter()
537            .flat_map(|g| &g.entries)
538            .find(|e| e.name == "moss-color-accent-hover")
539            .expect("moss-color-accent-hover token must exist (renamed from moss-accent-hover in 1.3)");
540
541        // Light value must use color-mix (derives from --moss-color-accent).
542        assert!(
543            entry.value.contains("color-mix"),
544            "moss-color-accent-hover light value must contain 'color-mix', got: {:?}",
545            entry.value
546        );
547        assert!(
548            entry.value.contains("var(--moss-color-accent)"),
549            "moss-color-accent-hover light value must reference var(--moss-color-accent), got: {:?}",
550            entry.value
551        );
552
553        // Dark value must also be present and use color-mix (lightens accent on hover).
554        let dark = entry
555            .dark_value
556            .as_deref()
557            .expect("moss-color-accent-hover must have a dark value");
558        assert!(
559            dark.contains("color-mix"),
560            "moss-color-accent-hover dark value must contain 'color-mix', got: {:?}",
561            dark
562        );
563        assert!(
564            dark.contains("var(--moss-color-accent)"),
565            "moss-color-accent-hover dark value must reference var(--moss-color-accent), got: {:?}",
566            dark
567        );
568    }
569
570    /// Dark-theme legibility: `--moss-color-accent` must carry a dark override.
571    /// The light forest green (#2d5a2d) is too dark to read as text/icon/border on
572    /// the dark page background (#1c1914) — ~2.2:1, failing WCAG AA (needs 4.5:1).
573    /// This was the root cause of the unreadable comment "回复" button in dark mode.
574    /// The dark value #6a9a5a clears ~5.3:1; it is the same green the codebase
575    /// already derives for accent-hover / code-green in dark
576    /// (color-mix(in oklch, #2d5a2d 80%, white)), so dark mode stays internally
577    /// consistent. Light mode is unchanged.
578    #[test]
579    fn accent_has_legible_dark_value() {
580        let t = load_tokens().unwrap();
581        let accent = t
582            .groups
583            .iter()
584            .flat_map(|g| &g.entries)
585            .find(|e| e.name == "moss-color-accent")
586            .expect("moss-color-accent token must exist");
587        assert_eq!(accent.value, "#2d5a2d", "light accent must stay unchanged");
588        assert_eq!(
589            accent.dark_value.as_deref(),
590            Some("#6a9a5a"),
591            "moss-color-accent needs a dark override legible on #1c1914 (WCAG AA); \
592             raw #2d5a2d is only ~2.2:1"
593        );
594        // The dark value must actually reach the emitted [data-theme="dark"] block.
595        let dark = format_dark_root_block(&t);
596        assert!(
597            dark.contains("--moss-color-accent: #6a9a5a"),
598            "dark block must set --moss-color-accent to the legible green"
599        );
600    }
601
602    /// Ripple guard: now that `--moss-color-accent` is itself lightened in dark,
603    /// `--moss-code-accent-primary` must NOT re-derive from it via color-mix — that
604    /// would double-lighten the syntax green and drift code-block colors. Its dark
605    /// value is pinned to the concrete green so the accent fix is decoupled from
606    /// syntax highlighting.
607    #[test]
608    fn code_accent_primary_dark_is_pinned_not_derived_from_accent() {
609        let t = load_tokens().unwrap();
610        let code = t
611            .groups
612            .iter()
613            .flat_map(|g| &g.entries)
614            .find(|e| e.name == "moss-code-accent-primary")
615            .expect("moss-code-accent-primary token must exist");
616        let dark = code
617            .dark_value
618            .as_deref()
619            .expect("moss-code-accent-primary must have a dark value");
620        assert!(
621            !dark.contains("var(--moss-color-accent)"),
622            "code-accent-primary dark must not re-derive from accent (would \
623             double-lighten); pin it to a concrete value instead, got: {dark:?}"
624        );
625    }
626}