1use std::sync::{Arc, LazyLock};
9
10use serde_json::Value;
11
12use crate::generated::themes as g;
13use crate::theme::{try_load_theme, ResolvedTheme, ThemeIssue, ThemeOptions, ThemeRef};
14
15pub const BUILTIN_THEME_IDS: &[&str] = g::BUILTIN_THEME_IDS;
17pub const BUILTIN_THEME_NAMES: &[&str] = g::BUILTIN_THEME_NAMES;
18pub const DEFAULT_THEME_ID: &str = g::DEFAULT_THEME_ID;
19
20static BUILTINS: LazyLock<Vec<Arc<ResolvedTheme>>> = LazyLock::new(|| {
21 g::BUILTIN_THEME_JSON
22 .iter()
23 .map(|src| Arc::new(serde_json::from_str::<ResolvedTheme>(src).expect("built-in theme JSON")))
24 .collect()
25});
26
27pub fn builtin_theme(id: &str) -> Option<Arc<ResolvedTheme>> {
29 BUILTINS.iter().find(|t| t.id == id).cloned()
30}
31
32pub fn builtin_themes() -> Vec<Arc<ResolvedTheme>> {
34 BUILTINS.clone()
35}
36
37pub fn builtin_refs() -> Vec<ThemeRef> {
39 BUILTINS.iter().cloned().map(ThemeRef::Resolved).collect()
40}
41
42pub fn default_theme() -> Arc<ResolvedTheme> {
44 builtin_theme(DEFAULT_THEME_ID).expect("default theme is a built-in")
45}
46
47pub fn theme_or_default(input: &Value) -> (Arc<ResolvedTheme>, Vec<ThemeIssue>) {
53 if let Some(id) = input.as_str() {
54 return match builtin_theme(id) {
55 Some(t) => (t, vec![]),
56 None => (default_theme(), vec![ThemeIssue { path: "theme".into(), message: format!("unknown built-in theme \"{id}\"; known: {}", BUILTIN_THEME_IDS.join("|")) }]),
57 };
58 }
59 let refs = builtin_refs();
60 match try_load_theme(input, &ThemeOptions::core(&refs)) {
61 Ok(t) => (Arc::new(t), vec![]),
62 Err(issues) => (default_theme(), issues),
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[cfg(feature = "builtin-themes")]
72 #[test]
73 fn theme_or_default_never_fails() {
74 let (t, issues) = theme_or_default(&serde_json::json!("nope"));
75 assert_eq!(t.id, DEFAULT_THEME_ID);
76 assert_eq!(issues[0].path, "theme");
77 let (t, issues) = theme_or_default(&serde_json::json!({"id": "x", "name": "X", "extends": "neutral"}));
78 assert_eq!(t.id, DEFAULT_THEME_ID);
79 assert_eq!(issues[0].path, "$schema");
80 let (t, issues) = theme_or_default(&serde_json::json!({"$schema": crate::theme::THEME_SCHEMA_ID, "id": "x", "name": "X", "extends": "neutral"}));
81 assert_eq!((t.id.as_str(), issues.len()), ("x", 0));
82 assert_eq!(theme_or_default(&serde_json::json!("playful")).0.id, "playful");
83 }
84
85 #[test]
86 fn builtins_parse_in_source_order() {
87 let ids: Vec<String> = builtin_themes().iter().map(|t| t.id.clone()).collect();
88 assert_eq!(ids, BUILTIN_THEME_IDS);
89 let names: Vec<String> = builtin_themes().iter().map(|t| t.name.clone()).collect();
90 assert_eq!(names, BUILTIN_THEME_NAMES);
91 assert_eq!(default_theme().id, "exponential");
92 assert!(builtin_theme("nope").is_none());
93 }
94
95 #[test]
96 fn builtins_reserialize_to_the_embedded_json() {
97 for (src, theme) in g::BUILTIN_THEME_JSON.iter().zip(builtin_themes()) {
100 let out = serde_json::to_string(&*theme).unwrap();
101 let embedded: serde_json::Value = serde_json::from_str(src).unwrap();
102 let again: serde_json::Value = serde_json::from_str(&out).unwrap();
103 assert_eq!(again, embedded, "{}", theme.id);
104 assert_eq!(out.len(), src.len(), "{}", theme.id);
105 }
106 }
107}