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