Skip to main content

moire_web/api/
theme.rs

1use arborium::theme::builtin;
2use arborium_theme::highlights::HIGHLIGHTS;
3use arborium_theme::theme::{Style, Theme};
4use axum::http::header;
5use axum::response::IntoResponse;
6use std::collections::{HashMap, HashSet};
7use std::fmt::Write;
8
9/// Generates `--hl-{tag}: {hex};` variable declarations for a theme.
10/// Only emits variables for tags that have a non-empty style.
11fn theme_to_css_vars(theme: &Theme) -> String {
12    let mut css = String::new();
13
14    // Build name -> style index map for alias + parent lookups
15    let mut name_to_idx: HashMap<&str, usize> = HashMap::new();
16    for (i, def) in HIGHLIGHTS.iter().enumerate() {
17        name_to_idx.insert(def.name, i);
18        for alias in def.aliases {
19            name_to_idx.entry(alias).or_insert(i);
20        }
21    }
22
23    // Build tag -> style for parent fallback
24    let mut tag_to_style: HashMap<&str, &Style> = HashMap::new();
25    for (i, def) in HIGHLIGHTS.iter().enumerate() {
26        if !def.tag.is_empty() && !theme.styles[i].is_empty() {
27            tag_to_style.insert(def.tag, &theme.styles[i]);
28        }
29    }
30
31    if let Some(bg) = &theme.background {
32        writeln!(css, "  --theme-bg: {};", bg.to_hex()).unwrap();
33    }
34    if let Some(fg) = &theme.foreground {
35        writeln!(css, "  --theme-fg: {};", fg.to_hex()).unwrap();
36    }
37
38    let mut emitted: HashSet<&str> = HashSet::new();
39    for (i, def) in HIGHLIGHTS.iter().enumerate() {
40        if def.tag.is_empty() || emitted.contains(def.tag) {
41            continue;
42        }
43
44        // Try own index, then aliases, then parent tag
45        let style = if !theme.styles[i].is_empty() {
46            &theme.styles[i]
47        } else if let Some(s) = def.aliases.iter().find_map(|a| {
48            name_to_idx.get(a).and_then(|&j| {
49                let s = &theme.styles[j];
50                if !s.is_empty() { Some(s) } else { None }
51            })
52        }) {
53            s
54        } else if !def.parent_tag.is_empty() {
55            match tag_to_style.get(def.parent_tag) {
56                Some(s) => s,
57                None => continue,
58            }
59        } else {
60            continue;
61        };
62
63        if style.is_empty() {
64            continue;
65        }
66
67        emitted.insert(def.tag);
68
69        if let Some(fg) = &style.fg {
70            writeln!(css, "  --hl-{}: {};", def.tag, fg.to_hex()).unwrap();
71        }
72    }
73
74    css
75}
76
77/// Generates the element rules that reference CSS variables.
78/// These are theme-independent — emitted once.
79fn element_rules() -> String {
80    let mut css = String::new();
81    let mut emitted: HashSet<&str> = HashSet::new();
82
83    for def in HIGHLIGHTS.iter() {
84        if def.tag.is_empty() || emitted.contains(def.tag) {
85            continue;
86        }
87        emitted.insert(def.tag);
88        writeln!(css, "a-{} {{ color: var(--hl-{}); }}", def.tag, def.tag).unwrap();
89    }
90
91    css
92}
93
94/// Serves arborium syntax highlighting CSS for both light and dark modes.
95///
96/// Emits CSS custom properties (`--hl-{tag}`) in `:root` for each theme,
97/// then a single set of element rules referencing those variables.
98/// This lets other UI styles reuse `--hl-keyword`, `--hl-function`, etc.
99pub async fn api_arborium_theme_css() -> impl IntoResponse {
100    let light = builtin::github_light();
101    let dark = builtin::catppuccin_mocha();
102
103    let mut css = String::new();
104
105    // Light theme variables
106    writeln!(css, ":root {{").unwrap();
107    css.push_str(&theme_to_css_vars(&light));
108    writeln!(css, "}}").unwrap();
109
110    // Dark theme variables override
111    writeln!(css, "@media (prefers-color-scheme: dark) {{").unwrap();
112    writeln!(css, "  :root {{").unwrap();
113    for line in theme_to_css_vars(&dark).lines() {
114        writeln!(css, "  {line}").unwrap();
115    }
116    writeln!(css, "  }}").unwrap();
117    writeln!(css, "}}").unwrap();
118
119    // Element rules — emitted once, reference vars
120    css.push('\n');
121    css.push_str(&element_rules());
122
123    ([(header::CONTENT_TYPE, "text/css; charset=utf-8")], css)
124}