magical markdown slides
1use owo_colors::{OwoColorize, Style};
2use serde::Deserialize;
3use terminal_colorsaurus::{QueryOptions, ThemeMode, theme_mode};
4
5/// Parses a hex color string to RGB values.
6///
7/// Supports both `#RRGGBB` and `RRGGBB` formats.
8fn parse_hex_color(hex: &str) -> Option<(u8, u8, u8)> {
9 let hex = hex.trim_start_matches('#');
10
11 if hex.len() != 6 {
12 return None;
13 }
14
15 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
16 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
17 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
18
19 Some((r, g, b))
20}
21
22/// Base16 color scheme specification.
23///
24/// Defines a standard 16-color palette that can be mapped to semantic theme roles.
25#[derive(Debug, Clone, Deserialize)]
26pub struct Base16Scheme {
27 pub system: String,
28 pub name: String,
29 pub author: String,
30 pub variant: String,
31 pub palette: Base16Palette,
32}
33
34/// Base16 color palette with 16 standardized color slots.
35///
36/// Each base color serves a semantic purpose in the base16 specification:
37/// - base00-03: Background shades (darkest to lighter)
38/// - base04-07: Foreground shades (darker to lightest)
39/// - base08-0F: Accent colors (red, orange, yellow, green, cyan, blue, magenta, brown)
40#[derive(Debug, Clone, Deserialize)]
41pub struct Base16Palette {
42 pub base00: String,
43 pub base01: String,
44 pub base02: String,
45 pub base03: String,
46 pub base04: String,
47 pub base05: String,
48 pub base06: String,
49 pub base07: String,
50 pub base08: String,
51 pub base09: String,
52 #[serde(rename = "base0A")]
53 pub base0a: String,
54 #[serde(rename = "base0B")]
55 pub base0b: String,
56 #[serde(rename = "base0C")]
57 pub base0c: String,
58 #[serde(rename = "base0D")]
59 pub base0d: String,
60 #[serde(rename = "base0E")]
61 pub base0e: String,
62 #[serde(rename = "base0F")]
63 pub base0f: String,
64}
65
66static CATPPUCCIN_LATTE: &str = include_str!("themes/catppuccin-latte.yml");
67static CATPPUCCIN_MOCHA: &str = include_str!("themes/catppuccin-mocha.yml");
68static GRUVBOX_MATERIAL_DARK: &str = include_str!("themes/gruvbox-material-dark-medium.yml");
69static GRUVBOX_MATERIAL_LIGHT: &str = include_str!("themes/gruvbox-material-light-medium.yml");
70static NORD_LIGHT: &str = include_str!("themes/nord-light.yml");
71static NORD: &str = include_str!("themes/nord.yml");
72static OXOCARBON_DARK: &str = include_str!("themes/oxocarbon-dark.yml");
73static OXOCARBON_LIGHT: &str = include_str!("themes/oxocarbon-light.yml");
74static SOLARIZED_DARK: &str = include_str!("themes/solarized-dark.yml");
75static SOLARIZED_LIGHT: &str = include_str!("themes/solarized-light.yml");
76
77/// RGB color value for use with both owo-colors and ratatui
78#[derive(Debug, Clone, Copy)]
79pub struct Color {
80 pub r: u8,
81 pub g: u8,
82 pub b: u8,
83}
84
85impl Color {
86 pub const fn new(r: u8, g: u8, b: u8) -> Self {
87 Self { r, g, b }
88 }
89
90 /// Apply this color to text using owo-colors
91 pub fn to_owo_color<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
92 text.style(self.into())
93 }
94}
95
96impl From<Color> for Style {
97 fn from(color: Color) -> Self {
98 Style::new().truecolor(color.r, color.g, color.b)
99 }
100}
101
102impl From<&Color> for Style {
103 fn from(color: &Color) -> Self {
104 Style::new().truecolor(color.r, color.g, color.b)
105 }
106}
107
108/// Detects if the terminal background is dark.
109///
110/// Uses [terminal_colorsaurus] to query the terminal theme mode.
111/// Defaults to true (dark) if detection fails.
112pub fn detect_is_dark() -> bool {
113 match theme_mode(QueryOptions::default()) {
114 Ok(mode) => {
115 let is_dark = mode == ThemeMode::Dark;
116 tracing::debug!("Terminal theme detection: mode={:?} -> is_dark={}", mode, is_dark);
117 is_dark
118 }
119 Err(e) => {
120 tracing::debug!("Terminal theme detection failed: {}, defaulting to dark", e);
121 true
122 }
123 }
124}
125
126/// Color theme abstraction for slides with semantic roles for consistent theming across the application.
127///
128/// Stores RGB colors that can be converted to both owo-colors Style (for terminal output) and ratatui Color (for TUI rendering).
129#[derive(Debug, Clone, Copy)]
130pub struct ThemeColors {
131 pub heading: Color,
132 pub heading_bold: bool,
133 pub body: Color,
134 pub accent: Color,
135 pub code: Color,
136 pub dimmed: Color,
137 pub code_fence: Color,
138 pub rule: Color,
139 pub list_marker: Color,
140 pub blockquote_border: Color,
141 pub table_border: Color,
142 pub emphasis: Color,
143 pub strong: Color,
144 pub link: Color,
145 pub inline_code_bg: Color,
146 pub ui_border: Color,
147 pub ui_title: Color,
148 pub ui_text: Color,
149 pub ui_background: Color,
150 pub admonition_note: Color,
151 pub admonition_tip: Color,
152 pub admonition_warning: Color,
153 pub admonition_danger: Color,
154 pub admonition_success: Color,
155 pub admonition_info: Color,
156}
157
158impl Default for ThemeColors {
159 fn default() -> Self {
160 let is_dark = detect_is_dark();
161 let theme_name = if is_dark { "oxocarbon-dark" } else { "oxocarbon-light" };
162 tracing::debug!("ThemeColors::default() selecting theme: {}", theme_name);
163 ThemeRegistry::get(theme_name)
164 }
165}
166
167impl ThemeColors {
168 /// Create a ThemeColors from a base16 color scheme.
169 ///
170 /// Maps base16 colors to semantic theme roles following base16 styling guidelines:
171 ///
172 /// Content colors:
173 /// - base05: body text (main foreground)
174 /// - base0D: headings (blue - classes/functions)
175 /// - base0E: strong emphasis (magenta/purple - keywords)
176 /// - base0B: code blocks (green - strings)
177 /// - base03: dimmed/borders (comment color)
178 /// - base0A: list markers (yellow - classes/constants)
179 /// - base09: emphasis/italics (orange - integers/constants)
180 /// - base0C: links (cyan - support/regex)
181 /// - base08: accents (red - variables/tags)
182 /// - base02: inline code background (selection background)
183 ///
184 /// UI chrome colors:
185 /// - base00: UI background (darkest background)
186 /// - base02: UI borders/status bar background (selection background)
187 /// - base06: UI titles (bright foreground)
188 /// - base05: UI text (default foreground)
189 fn from_base16(scheme: &Base16Scheme) -> Option<Self> {
190 let palette = &scheme.palette;
191
192 let heading = parse_hex_color(&palette.base0d)?;
193 let body = parse_hex_color(&palette.base05)?;
194 let accent = parse_hex_color(&palette.base08)?;
195 let code = parse_hex_color(&palette.base0b)?;
196 let dimmed = parse_hex_color(&palette.base03)?;
197 let code_fence = dimmed;
198 let rule = dimmed;
199 let list_marker = parse_hex_color(&palette.base0a)?;
200 let blockquote_border = dimmed;
201 let table_border = dimmed;
202 let emphasis = parse_hex_color(&palette.base09)?;
203 let strong = parse_hex_color(&palette.base0e)?;
204 let link = parse_hex_color(&palette.base0c)?;
205 let inline_code_bg = parse_hex_color(&palette.base02)?;
206 let ui_background = parse_hex_color(&palette.base00)?;
207 let ui_border = parse_hex_color(&palette.base02)?;
208 let ui_title = parse_hex_color(&palette.base06)?;
209 let ui_text = parse_hex_color(&palette.base05)?;
210
211 let admonition_note = parse_hex_color(&palette.base0d)?;
212 let admonition_tip = parse_hex_color(&palette.base0e)?;
213 let admonition_warning = parse_hex_color(&palette.base0a)?;
214 let admonition_danger = parse_hex_color(&palette.base08)?;
215 let admonition_success = parse_hex_color(&palette.base0b)?;
216 let admonition_info = parse_hex_color(&palette.base0c)?;
217
218 Some(Self {
219 heading: Color::new(heading.0, heading.1, heading.2),
220 heading_bold: true,
221 body: Color::new(body.0, body.1, body.2),
222 accent: Color::new(accent.0, accent.1, accent.2),
223 code: Color::new(code.0, code.1, code.2),
224 dimmed: Color::new(dimmed.0, dimmed.1, dimmed.2),
225 code_fence: Color::new(code_fence.0, code_fence.1, code_fence.2),
226 rule: Color::new(rule.0, rule.1, rule.2),
227 list_marker: Color::new(list_marker.0, list_marker.1, list_marker.2),
228 blockquote_border: Color::new(blockquote_border.0, blockquote_border.1, blockquote_border.2),
229 table_border: Color::new(table_border.0, table_border.1, table_border.2),
230 emphasis: Color::new(emphasis.0, emphasis.1, emphasis.2),
231 strong: Color::new(strong.0, strong.1, strong.2),
232 link: Color::new(link.0, link.1, link.2),
233 inline_code_bg: Color::new(inline_code_bg.0, inline_code_bg.1, inline_code_bg.2),
234 ui_border: Color::new(ui_border.0, ui_border.1, ui_border.2),
235 ui_title: Color::new(ui_title.0, ui_title.1, ui_title.2),
236 ui_text: Color::new(ui_text.0, ui_text.1, ui_text.2),
237 ui_background: Color::new(ui_background.0, ui_background.1, ui_background.2),
238 admonition_note: Color::new(admonition_note.0, admonition_note.1, admonition_note.2),
239 admonition_tip: Color::new(admonition_tip.0, admonition_tip.1, admonition_tip.2),
240 admonition_warning: Color::new(admonition_warning.0, admonition_warning.1, admonition_warning.2),
241 admonition_danger: Color::new(admonition_danger.0, admonition_danger.1, admonition_danger.2),
242 admonition_success: Color::new(admonition_success.0, admonition_success.1, admonition_success.2),
243 admonition_info: Color::new(admonition_info.0, admonition_info.1, admonition_info.2),
244 })
245 }
246
247 /// Apply heading style to text
248 pub fn heading<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
249 let mut style: Style = (&self.heading).into();
250 if self.heading_bold {
251 style = style.bold();
252 }
253 text.style(style)
254 }
255
256 /// Apply body style to text
257 pub fn body<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
258 text.style((&self.body).into())
259 }
260
261 /// Apply accent style to text
262 pub fn accent<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
263 text.style((&self.accent).into())
264 }
265
266 /// Apply code style to text
267 pub fn code<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
268 text.style((&self.code).into())
269 }
270
271 /// Apply dimmed style to text
272 pub fn dimmed<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
273 text.style((&self.dimmed).into())
274 }
275
276 /// Apply code fence style to text
277 pub fn code_fence<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
278 text.style((&self.code_fence).into())
279 }
280
281 /// Apply horizontal rule style to text
282 pub fn rule<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
283 text.style((&self.rule).into())
284 }
285
286 /// Apply list marker style to text
287 pub fn list_marker<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
288 text.style((&self.list_marker).into())
289 }
290
291 /// Apply blockquote border style to text
292 pub fn blockquote_border<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
293 text.style((&self.blockquote_border).into())
294 }
295
296 /// Apply table border style to text
297 pub fn table_border<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
298 text.style((&self.table_border).into())
299 }
300
301 /// Apply emphasis (italic) style to text
302 pub fn emphasis<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
303 text.style((&self.emphasis).into())
304 }
305
306 /// Apply strong (bold) style to text
307 pub fn strong<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
308 let style: Style = (&self.strong).into();
309 text.style(style.bold())
310 }
311
312 /// Apply link style to text
313 pub fn link<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
314 text.style((&self.link).into())
315 }
316
317 /// Apply inline code background style to text
318 pub fn inline_code_bg<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> {
319 text.style((&self.inline_code_bg).into())
320 }
321}
322
323/// Theme registry for loading prebuilt base16 themes from YAML files.
324///
325/// Themes are embedded at compile time using include_str! for zero runtime I/O.
326/// Supports all base16 color schemes in the themes directory.
327pub struct ThemeRegistry;
328
329impl ThemeRegistry {
330 /// Get a theme by name.
331 ///
332 /// Loads and parses the corresponding YAML theme file embedded at compile time.
333 /// "default" maps to oxocarbon-dark or oxocarbon-light based on terminal background detection.
334 /// Falls back to Nord theme if the requested theme is not found or parsing fails.
335 pub fn get(name: &str) -> ThemeColors {
336 let yaml = match name.to_lowercase().as_str() {
337 "default" => {
338 let is_dark = detect_is_dark();
339 if is_dark { OXOCARBON_DARK } else { OXOCARBON_LIGHT }
340 }
341 "catppuccin-latte" => CATPPUCCIN_LATTE,
342 "catppuccin-mocha" => CATPPUCCIN_MOCHA,
343 "gruvbox-material-dark" => GRUVBOX_MATERIAL_DARK,
344 "gruvbox-material-light" => GRUVBOX_MATERIAL_LIGHT,
345 "nord-light" => NORD_LIGHT,
346 "nord" => NORD,
347 "oxocarbon-dark" => OXOCARBON_DARK,
348 "oxocarbon-light" => OXOCARBON_LIGHT,
349 "solarized-dark" => SOLARIZED_DARK,
350 "solarized-light" => SOLARIZED_LIGHT,
351 _ => NORD,
352 };
353
354 serde_yml::from_str::<Base16Scheme>(yaml)
355 .ok()
356 .and_then(|scheme| ThemeColors::from_base16(&scheme))
357 .unwrap_or_else(|| {
358 serde_yml::from_str::<Base16Scheme>(NORD)
359 .ok()
360 .and_then(|scheme| ThemeColors::from_base16(&scheme))
361 .expect("Failed to parse fallback Nord theme")
362 })
363 }
364
365 /// List all available theme names.
366 pub fn available_themes() -> Vec<&'static str> {
367 vec![
368 "catppuccin-latte",
369 "catppuccin-mocha",
370 "gruvbox-material-dark",
371 "gruvbox-material-light",
372 "nord-light",
373 "nord",
374 "oxocarbon-dark",
375 "oxocarbon-light",
376 "solarized-dark",
377 "solarized-light",
378 ]
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn parse_hex_color_with_hash() {
388 let result = parse_hex_color("#FF8040");
389 assert_eq!(result, Some((255, 128, 64)));
390 }
391
392 #[test]
393 fn parse_hex_color_without_hash() {
394 let result = parse_hex_color("FF8040");
395 assert_eq!(result, Some((255, 128, 64)));
396 }
397
398 #[test]
399 fn parse_hex_color_lowercase() {
400 let result = parse_hex_color("#ff8040");
401 assert_eq!(result, Some((255, 128, 64)));
402 }
403
404 #[test]
405 fn parse_hex_color_invalid_length() {
406 assert_eq!(parse_hex_color("#FFF"), None);
407 assert_eq!(parse_hex_color("#FFFFFFF"), None);
408 }
409
410 #[test]
411 fn parse_hex_color_invalid_chars() {
412 assert_eq!(parse_hex_color("#GGGGGG"), None);
413 assert_eq!(parse_hex_color("#XYZ123"), None);
414 }
415
416 #[test]
417 fn color_new() {
418 let color = Color::new(255, 128, 64);
419 assert_eq!(color.r, 255);
420 assert_eq!(color.g, 128);
421 assert_eq!(color.b, 64);
422 }
423
424 #[test]
425 fn color_into_style() {
426 let color = Color::new(100, 150, 200);
427 let style: Style = color.into();
428 let text = "Test";
429 let styled = text.style(style);
430 assert!(styled.to_string().contains("Test"));
431 }
432
433 #[test]
434 fn color_ref_into_style() {
435 let color = Color::new(100, 150, 200);
436 let style: Style = (&color).into();
437 let text = "Test";
438 let styled = text.style(style);
439 assert!(styled.to_string().contains("Test"));
440 }
441
442 #[test]
443 fn base16_scheme_deserializes() {
444 let yaml = r##"
445system: "base16"
446name: "Test Theme"
447author: "Test Author"
448variant: "dark"
449palette:
450 base00: "#000000"
451 base01: "#111111"
452 base02: "#222222"
453 base03: "#333333"
454 base04: "#444444"
455 base05: "#555555"
456 base06: "#666666"
457 base07: "#777777"
458 base08: "#888888"
459 base09: "#999999"
460 base0A: "#aaaaaa"
461 base0B: "#bbbbbb"
462 base0C: "#cccccc"
463 base0D: "#dddddd"
464 base0E: "#eeeeee"
465 base0F: "#ffffff"
466"##;
467 let scheme: Result<Base16Scheme, _> = serde_yml::from_str(yaml);
468 assert!(scheme.is_ok());
469 }
470
471 #[test]
472 fn theme_colors_from_base16() {
473 let yaml = r##"
474system: "base16"
475name: "Test Theme"
476author: "Test Author"
477variant: "dark"
478palette:
479 base00: "#000000"
480 base01: "#111111"
481 base02: "#222222"
482 base03: "#333333"
483 base04: "#444444"
484 base05: "#555555"
485 base06: "#666666"
486 base07: "#777777"
487 base08: "#ff0000"
488 base09: "#ff7f00"
489 base0A: "#ffff00"
490 base0B: "#00ff00"
491 base0C: "#00ffff"
492 base0D: "#0000ff"
493 base0E: "#ff00ff"
494 base0F: "#ffffff"
495"##;
496 let scheme: Base16Scheme = serde_yml::from_str(yaml).unwrap();
497 let theme = ThemeColors::from_base16(&scheme);
498 assert!(theme.is_some());
499
500 let theme = theme.unwrap();
501 assert_eq!(theme.body.r, 85); // base05 - #555555
502 assert_eq!(theme.heading.r, 0); // base0D - #0000ff
503 assert_eq!(theme.code.r, 0); // base0B - #00ff00
504 assert_eq!(theme.accent.r, 255); // base08 - #ff0000
505 assert_eq!(theme.emphasis.r, 255); // base09 - #ff7f00
506 assert_eq!(theme.strong.r, 255); // base0E - #ff00ff
507 assert_eq!(theme.link.r, 0); // base0C - #00ffff
508 assert_eq!(theme.inline_code_bg.r, 34); // base02 - #222222
509 assert_eq!(theme.ui_background.r, 0); // base00 - #000000
510 assert_eq!(theme.ui_border.r, 34); // base02 - #222222
511 assert_eq!(theme.ui_title.r, 102); // base06 - #666666
512 assert_eq!(theme.ui_text.r, 85); // base05 - #555555
513 }
514
515 #[test]
516 fn theme_colors_default() {
517 let theme = ThemeColors::default();
518 let text = "Test";
519 let heading = theme.heading(&text);
520 assert!(heading.to_string().contains("Test"));
521 }
522
523 #[test]
524 fn theme_colors_apply_styles() {
525 let theme = ThemeColors::default();
526
527 assert!(theme.heading(&"Heading").to_string().contains("Heading"));
528 assert!(theme.body(&"Body").to_string().contains("Body"));
529 assert!(theme.accent(&"Accent").to_string().contains("Accent"));
530 assert!(theme.code(&"Code").to_string().contains("Code"));
531 assert!(theme.dimmed(&"Dimmed").to_string().contains("Dimmed"));
532 }
533
534 #[test]
535 fn theme_registry_get_nord() {
536 let theme = ThemeRegistry::get("nord");
537 let text = "Test";
538 let styled = theme.heading(&text);
539 assert!(styled.to_string().contains("Test"));
540 assert_eq!(theme.heading.r, 129);
541 assert_eq!(theme.heading.g, 161);
542 assert_eq!(theme.heading.b, 193);
543 }
544
545 #[test]
546 fn theme_registry_get_nord_light() {
547 let theme = ThemeRegistry::get("nord-light");
548 let text = "Test";
549 let styled = theme.heading(&text);
550 assert!(styled.to_string().contains("Test"));
551 }
552
553 #[test]
554 fn theme_registry_get_catppuccin_mocha() {
555 let theme = ThemeRegistry::get("catppuccin-mocha");
556 let text = "Test";
557 let styled = theme.heading(&text);
558 assert!(styled.to_string().contains("Test"));
559 }
560
561 #[test]
562 fn theme_registry_get_catppuccin_latte() {
563 let theme = ThemeRegistry::get("catppuccin-latte");
564 let text = "Test";
565 let styled = theme.heading(&text);
566 assert!(styled.to_string().contains("Test"));
567 }
568
569 #[test]
570 fn theme_registry_get_gruvbox_dark() {
571 let theme = ThemeRegistry::get("gruvbox-material-dark");
572 let text = "Test";
573 let styled = theme.heading(&text);
574 assert!(styled.to_string().contains("Test"));
575 }
576
577 #[test]
578 fn theme_registry_get_gruvbox_light() {
579 let theme = ThemeRegistry::get("gruvbox-material-light");
580 let text = "Test";
581 let styled = theme.heading(&text);
582 assert!(styled.to_string().contains("Test"));
583 }
584
585 #[test]
586 fn theme_registry_get_oxocarbon_dark() {
587 let theme = ThemeRegistry::get("oxocarbon-dark");
588 let text = "Test";
589 let styled = theme.heading(&text);
590 assert!(styled.to_string().contains("Test"));
591 }
592
593 #[test]
594 fn theme_registry_get_oxocarbon_light() {
595 let theme = ThemeRegistry::get("oxocarbon-light");
596 let text = "Test";
597 let styled = theme.heading(&text);
598 assert!(styled.to_string().contains("Test"));
599 }
600
601 #[test]
602 fn theme_registry_get_solarized_dark() {
603 let theme = ThemeRegistry::get("solarized-dark");
604 let text = "Test";
605 let styled = theme.heading(&text);
606 assert!(styled.to_string().contains("Test"));
607 }
608
609 #[test]
610 fn theme_registry_get_solarized_light() {
611 let theme = ThemeRegistry::get("solarized-light");
612 let text = "Test";
613 let styled = theme.heading(&text);
614 assert!(styled.to_string().contains("Test"));
615 }
616
617 #[test]
618 fn theme_registry_get_unknown_fallback() {
619 let theme = ThemeRegistry::get("nonexistent");
620 let text = "Test";
621 let styled = theme.heading(&text);
622 assert!(styled.to_string().contains("Test"));
623 }
624
625 #[test]
626 fn theme_registry_case_insensitive() {
627 let theme1 = ThemeRegistry::get("NORD");
628 let theme2 = ThemeRegistry::get("nord");
629 let text = "Test";
630 assert!(theme1.heading(&text).to_string().contains("Test"));
631 assert!(theme2.heading(&text).to_string().contains("Test"));
632 }
633
634 #[test]
635 fn theme_registry_available_themes() {
636 let themes = ThemeRegistry::available_themes();
637 assert!(themes.contains(&"nord"));
638 assert!(themes.contains(&"nord-light"));
639 assert!(themes.contains(&"catppuccin-mocha"));
640 assert!(themes.contains(&"catppuccin-latte"));
641 assert!(themes.contains(&"gruvbox-material-dark"));
642 assert!(themes.contains(&"gruvbox-material-light"));
643 assert!(themes.contains(&"oxocarbon-dark"));
644 assert!(themes.contains(&"oxocarbon-light"));
645 assert!(themes.contains(&"solarized-dark"));
646 assert!(themes.contains(&"solarized-light"));
647 assert_eq!(themes.len(), 10);
648 }
649
650 #[test]
651 fn theme_colors_all_semantic_roles() {
652 let theme = ThemeColors::default();
653
654 assert!(theme.heading(&"Test").to_string().contains("Test"));
655 assert!(theme.body(&"Test").to_string().contains("Test"));
656 assert!(theme.accent(&"Test").to_string().contains("Test"));
657 assert!(theme.code(&"Test").to_string().contains("Test"));
658 assert!(theme.dimmed(&"Test").to_string().contains("Test"));
659 assert!(theme.code_fence(&"Test").to_string().contains("Test"));
660 assert!(theme.rule(&"Test").to_string().contains("Test"));
661 assert!(theme.list_marker(&"Test").to_string().contains("Test"));
662 assert!(theme.blockquote_border(&"Test").to_string().contains("Test"));
663 assert!(theme.table_border(&"Test").to_string().contains("Test"));
664 assert!(theme.emphasis(&"Test").to_string().contains("Test"));
665 assert!(theme.strong(&"Test").to_string().contains("Test"));
666 assert!(theme.link(&"Test").to_string().contains("Test"));
667 assert!(theme.inline_code_bg(&"Test").to_string().contains("Test"));
668
669 let _ = theme.ui_border;
670 let _ = theme.ui_title;
671 let _ = theme.ui_text;
672 let _ = theme.ui_background;
673 }
674
675 #[test]
676 fn all_embedded_themes_parse() {
677 for theme_name in ThemeRegistry::available_themes() {
678 let theme = ThemeRegistry::get(theme_name);
679 let styled = theme.heading(&"Test");
680 assert!(
681 styled.to_string().contains("Test"),
682 "Theme '{theme_name}' failed to parse or apply styles"
683 );
684 }
685 }
686}