magical markdown slides
at main 24 kB view raw
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}