//! Theme configuration for markdown rendering. //! //! Provides customizable styling for all markdown elements using the `colored` //! crate. use colored::{Color, ColoredString, Colorize}; use streamdown_parser::decode_html_entities; use crate::style::{HeadingStyler, InlineStyler, ListStyler, TableStyler}; /// Style configuration for a single element. #[derive(Clone, Debug, Default)] pub struct Style { pub fg: Option, pub bg: Option, pub bold: bool, pub italic: bool, pub underline: bool, pub strikethrough: bool, pub dimmed: bool, } impl Style { pub fn new() -> Self { Self::default() } pub fn fg(mut self, color: Color) -> Self { self.fg = Some(color); self } pub fn bg(mut self, color: Color) -> Self { self.bg = Some(color); self } pub fn bold(mut self) -> Self { self.bold = true; self } pub fn italic(mut self) -> Self { self.italic = true; self } pub fn underline(mut self) -> Self { self.underline = true; self } pub fn strikethrough(mut self) -> Self { self.strikethrough = true; self } pub fn dimmed(mut self) -> Self { self.dimmed = true; self } /// Apply this style to a string. pub fn apply(&self, text: &str) -> ColoredString { let mut result = text.normal(); if let Some(fg) = self.fg { result = result.color(fg); } if let Some(bg) = self.bg { result = result.on_color(bg); } if self.bold { result = result.bold(); } if self.italic { result = result.italic(); } if self.underline { result = result.underline(); } if self.strikethrough { result = result.strikethrough(); } if self.dimmed { result = result.dimmed(); } result } } /// Theme containing styles for all markdown elements. #[derive(Clone, Debug)] pub struct Theme { // Inline styles pub bold: Style, pub italic: Style, pub code: Style, pub strikethrough: Style, pub link: Style, pub link_url: Style, // Block styles pub heading1: Style, pub heading2: Style, pub heading3: Style, pub heading4: Style, pub heading5: Style, pub heading6: Style, // List styles pub bullet_dash: Style, pub bullet_asterisk: Style, pub bullet_plus: Style, pub bullet_plus_expand: Style, pub list_number: Style, pub checkbox_checked: Style, pub checkbox_unchecked: Style, // Table styles pub table_header: Style, pub table_border: Style, pub table_cell: Style, // Quote/Think styles pub blockquote: Style, pub blockquote_border: Style, pub think: Style, pub think_border: Style, // Code block pub code_block_lang: Style, // Horizontal rule pub hr: Style, } impl Default for Theme { fn default() -> Self { Self::detect() } } /// Theme-based styler that outputs ANSI codes. impl InlineStyler for Theme { fn text(&self, text: &str) -> String { decode_html_entities(text) } fn bold(&self, text: &str) -> String { self.bold.apply(&decode_html_entities(text)).to_string() } fn italic(&self, text: &str) -> String { self.italic.apply(&decode_html_entities(text)).to_string() } fn bold_italic(&self, text: &str) -> String { let decoded = decode_html_entities(text); let styled = self.bold.apply(&decoded); self.italic.apply(&styled.to_string()).to_string() } fn strikethrough(&self, text: &str) -> String { self.strikethrough .apply(&decode_html_entities(text)) .to_string() } fn underline(&self, text: &str) -> String { format!("\x1b[4m{}\x1b[24m", decode_html_entities(text)) } fn code(&self, text: &str) -> String { self.code.apply(text).to_string() } fn link(&self, text: &str, url: &str) -> String { let mut result = String::new(); result.push_str("\x1b]8;;"); result.push_str(url); result.push_str("\x1b\\"); result.push_str(&self.link.apply(&decode_html_entities(text)).to_string()); result.push_str("\x1b]8;;\x1b\\"); result.push(' '); result.push_str(&self.link_url.apply(&format!("({})", url)).to_string()); result } fn image(&self, alt: &str, _url: &str) -> String { format!("[🖼 {}]", alt) } fn footnote(&self, text: &str) -> String { text.to_string() } fn dimmed(&self, text: &str) -> String { Style::new().dimmed().apply(text).to_string() } } impl HeadingStyler for Theme { fn h1(&self, text: &str) -> String { self.heading1.apply(text).to_string() } fn h2(&self, text: &str) -> String { self.heading2.apply(text).to_string() } fn h3(&self, text: &str) -> String { self.heading3.apply(text).to_string() } fn h4(&self, text: &str) -> String { self.heading4.apply(text).to_string() } fn h5(&self, text: &str) -> String { self.heading5.apply(text).to_string() } fn h6(&self, text: &str) -> String { self.heading6.apply(text).to_string() } } impl ListStyler for Theme { fn bullet_dash(&self, text: &str) -> String { self.bullet_dash.apply(text).to_string() } fn bullet_asterisk(&self, text: &str) -> String { self.bullet_asterisk.apply(text).to_string() } fn bullet_plus(&self, text: &str) -> String { self.bullet_plus.apply(text).to_string() } fn bullet_plus_expand(&self, text: &str) -> String { self.bullet_plus_expand.apply(text).to_string() } fn number(&self, text: &str) -> String { self.list_number.apply(text).to_string() } fn checkbox_checked(&self, text: &str) -> String { self.checkbox_checked.apply(text).to_string() } fn checkbox_unchecked(&self, text: &str) -> String { self.checkbox_unchecked.apply(text).to_string() } } impl TableStyler for Theme { fn border(&self, text: &str) -> String { self.table_border.apply(text).to_string() } fn header(&self, text: &str) -> String { self.table_header.apply(text).to_string() } } impl Theme { /// Detects the terminal theme (dark or light) and returns the appropriate /// theme. pub fn detect() -> Self { use crate::utils::{ThemeMode, detect_theme_mode}; match detect_theme_mode() { ThemeMode::Light => Self::light(), ThemeMode::Dark => Self::dark(), } } /// Dark theme (default). pub fn dark() -> Self { Self { // Inline bold: Style::new().bold(), italic: Style::new().italic(), code: Style::new().fg(Color::Yellow), strikethrough: Style::new().strikethrough().dimmed(), link: Style::new().fg(Color::Cyan).underline(), link_url: Style::new().fg(Color::Blue).dimmed(), // Headings heading1: Style::new().fg(Color::Magenta).bold(), heading2: Style::new().fg(Color::Blue).bold(), heading3: Style::new().fg(Color::Cyan).bold(), heading4: Style::new().fg(Color::Green).bold(), heading5: Style::new().fg(Color::Yellow).bold(), heading6: Style::new().fg(Color::White).bold(), // Lists bullet_dash: Style::new().fg(Color::Cyan), bullet_asterisk: Style::new().fg(Color::Green), bullet_plus: Style::new().fg(Color::Yellow), bullet_plus_expand: Style::new().fg(Color::Magenta), list_number: Style::new().fg(Color::Cyan), checkbox_checked: Style::new().fg(Color::Green), checkbox_unchecked: Style::new().fg(Color::Red), // Tables table_header: Style::new().bold(), table_border: Style::new().fg(Color::BrightBlack), table_cell: Style::new(), // Quotes blockquote: Style::new().italic().dimmed(), blockquote_border: Style::new().fg(Color::BrightBlack), think: Style::new().italic().fg(Color::BrightBlack), think_border: Style::new().fg(Color::BrightBlack), // Code block code_block_lang: Style::new().fg(Color::BrightBlack).italic(), // HR hr: Style::new().fg(Color::BrightBlack), } } /// Light theme for light terminal backgrounds. pub fn light() -> Self { Self { // Inline bold: Style::new().bold(), italic: Style::new().italic(), code: Style::new().fg(Color::Red), strikethrough: Style::new().strikethrough().dimmed(), link: Style::new().fg(Color::Blue).underline(), link_url: Style::new().fg(Color::Cyan).dimmed(), // Headings heading1: Style::new().fg(Color::Magenta).bold(), heading2: Style::new().fg(Color::Blue).bold(), heading3: Style::new().fg(Color::Cyan).bold(), heading4: Style::new().fg(Color::Green).bold(), heading5: Style::new().fg(Color::Yellow).bold(), heading6: Style::new().fg(Color::Black).bold(), // Lists bullet_dash: Style::new().fg(Color::Blue), bullet_asterisk: Style::new().fg(Color::Green), bullet_plus: Style::new().fg(Color::Magenta), bullet_plus_expand: Style::new().fg(Color::Cyan), list_number: Style::new().fg(Color::Blue), checkbox_checked: Style::new().fg(Color::Green), checkbox_unchecked: Style::new().fg(Color::Red), // Tables table_header: Style::new().bold(), table_border: Style::new().fg(Color::Black), table_cell: Style::new(), // Quotes blockquote: Style::new().italic().dimmed(), blockquote_border: Style::new().fg(Color::Black), think: Style::new().italic().fg(Color::Black), think_border: Style::new().fg(Color::Black), // Code block code_block_lang: Style::new().fg(Color::Black).italic(), // HR hr: Style::new().fg(Color::Black), } } } /// Test styler that outputs readable HTML-like tags. #[cfg(test)] pub struct TagStyler; #[cfg(test)] impl InlineStyler for TagStyler { fn text(&self, text: &str) -> String { decode_html_entities(text) } fn bold(&self, text: &str) -> String { format!("{}", decode_html_entities(text)) } fn italic(&self, text: &str) -> String { format!("{}", decode_html_entities(text)) } fn bold_italic(&self, text: &str) -> String { format!("{}", decode_html_entities(text)) } fn strikethrough(&self, text: &str) -> String { format!("{}", decode_html_entities(text)) } fn underline(&self, text: &str) -> String { format!("{}", decode_html_entities(text)) } fn code(&self, text: &str) -> String { format!("{}", text) } fn link(&self, text: &str, url: &str) -> String { format!("{}", url, decode_html_entities(text)) } fn image(&self, alt: &str, url: &str) -> String { format!("\"{}\"", alt, url) } fn footnote(&self, text: &str) -> String { format!("{}", text) } fn dimmed(&self, text: &str) -> String { format!("{}", text) } } #[cfg(test)] impl HeadingStyler for TagStyler { fn h1(&self, text: &str) -> String { format!("

{}

", text) } fn h2(&self, text: &str) -> String { format!("

{}

", text) } fn h3(&self, text: &str) -> String { format!("

{}

", text) } fn h4(&self, text: &str) -> String { format!("

{}

", text) } fn h5(&self, text: &str) -> String { format!("
{}
", text) } fn h6(&self, text: &str) -> String { format!("
{}
", text) } } #[cfg(test)] impl ListStyler for TagStyler { fn bullet_dash(&self, text: &str) -> String { format!("{}", text) } fn bullet_asterisk(&self, text: &str) -> String { format!("{}", text) } fn bullet_plus(&self, text: &str) -> String { format!("{}", text) } fn bullet_plus_expand(&self, text: &str) -> String { format!("{}", text) } fn number(&self, text: &str) -> String { format!("{}", text) } fn checkbox_checked(&self, text: &str) -> String { format!("{}", text) } fn checkbox_unchecked(&self, text: &str) -> String { format!("{}", text) } } #[cfg(test)] impl TableStyler for TagStyler { fn border(&self, text: &str) -> String { Theme::default().border(text) } fn header(&self, text: &str) -> String { Theme::default().header(text) } }