| |
| |
| |
| |
|
|
| use colored::{Color, ColoredString, Colorize}; |
| use streamdown_parser::decode_html_entities; |
|
|
| use crate::style::{HeadingStyler, InlineStyler, ListStyler, TableStyler}; |
|
|
| |
| #[derive(Clone, Debug, Default)] |
| pub struct Style { |
| pub fg: Option<Color>, |
| pub bg: Option<Color>, |
| 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 |
| } |
|
|
| |
| 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 |
| } |
| } |
|
|
| |
| #[derive(Clone, Debug)] |
| pub struct Theme { |
| |
| pub bold: Style, |
| pub italic: Style, |
| pub code: Style, |
| pub strikethrough: Style, |
| pub link: Style, |
| pub link_url: Style, |
|
|
| |
| pub heading1: Style, |
| pub heading2: Style, |
| pub heading3: Style, |
| pub heading4: Style, |
| pub heading5: Style, |
| pub heading6: Style, |
|
|
| |
| 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, |
|
|
| |
| pub table_header: Style, |
| pub table_border: Style, |
| pub table_cell: Style, |
|
|
| |
| pub blockquote: Style, |
| pub blockquote_border: Style, |
| pub think: Style, |
| pub think_border: Style, |
|
|
| |
| pub code_block_lang: Style, |
|
|
| |
| pub hr: Style, |
| } |
|
|
| impl Default for Theme { |
| fn default() -> Self { |
| Self::detect() |
| } |
| } |
|
|
| |
| 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 { |
| |
| |
| pub fn detect() -> Self { |
| use crate::utils::{ThemeMode, detect_theme_mode}; |
|
|
| match detect_theme_mode() { |
| ThemeMode::Light => Self::light(), |
| ThemeMode::Dark => Self::dark(), |
| } |
| } |
|
|
| |
| pub fn dark() -> Self { |
| Self { |
| |
| 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(), |
|
|
| |
| 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(), |
|
|
| |
| 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), |
|
|
| |
| table_header: Style::new().bold(), |
| table_border: Style::new().fg(Color::BrightBlack), |
| table_cell: Style::new(), |
|
|
| |
| 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_lang: Style::new().fg(Color::BrightBlack).italic(), |
|
|
| |
| hr: Style::new().fg(Color::BrightBlack), |
| } |
| } |
|
|
| |
| pub fn light() -> Self { |
| Self { |
| |
| 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(), |
|
|
| |
| 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(), |
|
|
| |
| 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), |
|
|
| |
| table_header: Style::new().bold(), |
| table_border: Style::new().fg(Color::Black), |
| table_cell: Style::new(), |
|
|
| |
| 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_lang: Style::new().fg(Color::Black).italic(), |
|
|
| |
| hr: Style::new().fg(Color::Black), |
| } |
| } |
| } |
|
|
| |
| #[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!("<b>{}</b>", decode_html_entities(text)) |
| } |
|
|
| fn italic(&self, text: &str) -> String { |
| format!("<i>{}</i>", decode_html_entities(text)) |
| } |
|
|
| fn bold_italic(&self, text: &str) -> String { |
| format!("<b><i>{}</i></b>", decode_html_entities(text)) |
| } |
|
|
| fn strikethrough(&self, text: &str) -> String { |
| format!("<s>{}</s>", decode_html_entities(text)) |
| } |
|
|
| fn underline(&self, text: &str) -> String { |
| format!("<u>{}</u>", decode_html_entities(text)) |
| } |
|
|
| fn code(&self, text: &str) -> String { |
| format!("<code>{}</code>", text) |
| } |
|
|
| fn link(&self, text: &str, url: &str) -> String { |
| format!("<a href=\"{}\">{}</a>", url, decode_html_entities(text)) |
| } |
|
|
| fn image(&self, alt: &str, url: &str) -> String { |
| format!("<img alt=\"{}\" src=\"{}\"/>", alt, url) |
| } |
|
|
| fn footnote(&self, text: &str) -> String { |
| format!("<footnote>{}</footnote>", text) |
| } |
|
|
| fn dimmed(&self, text: &str) -> String { |
| format!("<dim>{}</dim>", text) |
| } |
| } |
|
|
| #[cfg(test)] |
| impl HeadingStyler for TagStyler { |
| fn h1(&self, text: &str) -> String { |
| format!("<h1>{}</h1>", text) |
| } |
|
|
| fn h2(&self, text: &str) -> String { |
| format!("<h2>{}</h2>", text) |
| } |
|
|
| fn h3(&self, text: &str) -> String { |
| format!("<h3>{}</h3>", text) |
| } |
|
|
| fn h4(&self, text: &str) -> String { |
| format!("<h4>{}</h4>", text) |
| } |
|
|
| fn h5(&self, text: &str) -> String { |
| format!("<h5>{}</h5>", text) |
| } |
|
|
| fn h6(&self, text: &str) -> String { |
| format!("<h6>{}</h6>", text) |
| } |
| } |
|
|
| #[cfg(test)] |
| impl ListStyler for TagStyler { |
| fn bullet_dash(&self, text: &str) -> String { |
| format!("<dash>{}</dash>", text) |
| } |
|
|
| fn bullet_asterisk(&self, text: &str) -> String { |
| format!("<asterisk>{}</asterisk>", text) |
| } |
|
|
| fn bullet_plus(&self, text: &str) -> String { |
| format!("<plus>{}</plus>", text) |
| } |
|
|
| fn bullet_plus_expand(&self, text: &str) -> String { |
| format!("<expand>{}</expand>", text) |
| } |
|
|
| fn number(&self, text: &str) -> String { |
| format!("<num>{}</num>", text) |
| } |
|
|
| fn checkbox_checked(&self, text: &str) -> String { |
| format!("<checked>{}</checked>", text) |
| } |
|
|
| fn checkbox_unchecked(&self, text: &str) -> String { |
| format!("<unchecked>{}</unchecked>", 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) |
| } |
| } |
|
|