1pub use oxidoc_highlight::token::TokenKind;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum SyntaxTheme {
9 #[default]
10 Dark,
11 Light,
12}
13
14impl SyntaxTheme {
15 pub fn from_name(s: &str) -> Option<Self> {
17 if s.eq_ignore_ascii_case("dark") {
18 Some(Self::Dark)
19 } else if s.eq_ignore_ascii_case("light") {
20 Some(Self::Light)
21 } else {
22 None
23 }
24 }
25
26 pub fn as_str(self) -> &'static str {
28 match self {
29 SyntaxTheme::Dark => "dark",
30 SyntaxTheme::Light => "light",
31 }
32 }
33}
34
35pub fn token_id_for_kind(kind: TokenKind) -> &'static str {
39 match kind {
40 TokenKind::Keyword => "syntax.keyword",
41 TokenKind::String => "syntax.string",
42 TokenKind::Comment => "syntax.comment",
43 TokenKind::Number => "syntax.number",
44 TokenKind::Function => "syntax.function",
45 TokenKind::Type => "syntax.type",
46 TokenKind::Operator => "syntax.operator",
47 TokenKind::Punctuation => "syntax.punctuation",
48 TokenKind::Property => "syntax.property",
49 TokenKind::Builtin => "syntax.builtin",
50 TokenKind::Attr => "syntax.attr",
51 TokenKind::Variable => "syntax.variable",
52 TokenKind::Plain => "syntax.plain",
53 }
54}
55
56pub fn builtin_color(theme: SyntaxTheme, kind: TokenKind) -> &'static str {
60 match theme {
61 SyntaxTheme::Dark => match kind {
62 TokenKind::Keyword => "#c792ea",
63 TokenKind::String => "#c3e88d",
64 TokenKind::Comment => "#546e7a",
65 TokenKind::Number => "#f78c6c",
66 TokenKind::Function => "#82aaff",
67 TokenKind::Type => "#ffcb6b",
68 TokenKind::Operator => "#89ddff",
69 TokenKind::Punctuation => "#89ddff",
70 TokenKind::Property => "#f07178",
71 TokenKind::Builtin => "#c792ea",
72 TokenKind::Attr => "#ffcb6b",
73 TokenKind::Variable => "#eeffff",
74 TokenKind::Plain => "#eeffff",
75 },
76 SyntaxTheme::Light => match kind {
77 TokenKind::Keyword => "#cf222e",
78 TokenKind::String => "#0a3069",
79 TokenKind::Comment => "#6e7781",
80 TokenKind::Number => "#0550ae",
81 TokenKind::Function => "#8250df",
82 TokenKind::Type => "#953800",
83 TokenKind::Operator => "#cf222e",
84 TokenKind::Punctuation => "#1f2328",
85 TokenKind::Property => "#0550ae",
86 TokenKind::Builtin => "#8250df",
87 TokenKind::Attr => "#116329",
88 TokenKind::Variable => "#1f2328",
89 TokenKind::Plain => "#1f2328",
90 },
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 const ALL: [TokenKind; 13] = [
99 TokenKind::Keyword,
100 TokenKind::String,
101 TokenKind::Comment,
102 TokenKind::Number,
103 TokenKind::Function,
104 TokenKind::Type,
105 TokenKind::Operator,
106 TokenKind::Punctuation,
107 TokenKind::Property,
108 TokenKind::Builtin,
109 TokenKind::Attr,
110 TokenKind::Variable,
111 TokenKind::Plain,
112 ];
113
114 fn is_hex_color(s: &str) -> bool {
115 let b = s.as_bytes();
116 b.len() == 7 && b[0] == b'#' && b[1..].iter().all(|c| c.is_ascii_hexdigit())
117 }
118
119 #[test]
120 fn builtin_color_all_kinds_both_themes_are_valid_hex() {
121 for kind in ALL {
122 let dark = builtin_color(SyntaxTheme::Dark, kind);
123 assert!(
124 is_hex_color(dark),
125 "Dark {:?} -> {:?} is not valid #rrggbb",
126 kind,
127 dark
128 );
129 let light = builtin_color(SyntaxTheme::Light, kind);
130 assert!(
131 is_hex_color(light),
132 "Light {:?} -> {:?} is not valid #rrggbb",
133 kind,
134 light
135 );
136 }
137 }
138
139 #[test]
140 fn token_id_for_kind_all_kinds_are_valid() {
141 for kind in ALL {
142 let id = token_id_for_kind(kind);
143 assert!(
144 id.starts_with("syntax."),
145 "id {:?} does not start with 'syntax.'",
146 id
147 );
148 assert!(
149 !id.chars().any(|c| c.is_whitespace() || c.is_uppercase()),
150 "id {:?} contains whitespace or uppercase",
151 id
152 );
153 }
154 }
155
156 #[test]
157 fn syntax_theme_default_is_dark() {
158 assert_eq!(SyntaxTheme::default(), SyntaxTheme::Dark);
159 }
160
161 #[test]
162 fn from_name_parses_known_and_unknown() {
163 assert_eq!(SyntaxTheme::from_name("dark"), Some(SyntaxTheme::Dark));
164 assert_eq!(SyntaxTheme::from_name("DARK"), Some(SyntaxTheme::Dark));
165 assert_eq!(SyntaxTheme::from_name("light"), Some(SyntaxTheme::Light));
166 assert_eq!(SyntaxTheme::from_name("nope"), None);
167 }
168
169 #[test]
170 fn as_str_round_trips_for_both_variants() {
171 for t in [SyntaxTheme::Dark, SyntaxTheme::Light] {
172 assert_eq!(
173 SyntaxTheme::from_name(t.as_str()),
174 Some(t),
175 "as_str/from_name round-trip failed for {:?}",
176 t
177 );
178 }
179 }
180
181 #[test]
182 fn dark_and_light_differ_for_keyword() {
183 let dark = builtin_color(SyntaxTheme::Dark, TokenKind::Keyword);
184 let light = builtin_color(SyntaxTheme::Light, TokenKind::Keyword);
185 assert_ne!(dark, light, "Dark and Light keyword colors must differ");
186 }
187}