| |
|
|
| use streamdown_render::code::code_wrap; |
| use syntect::easy::HighlightLines; |
| use syntect::highlighting::ThemeSet; |
| use syntect::parsing::SyntaxSet; |
| use syntect::util::as_24_bit_terminal_escaped; |
|
|
| use crate::utils::{ThemeMode, detect_theme_mode}; |
|
|
| const RESET: &str = "\x1b[0m"; |
|
|
| |
| pub struct CodeHighlighter { |
| syntax_set: SyntaxSet, |
| theme_set: ThemeSet, |
| theme_mode: ThemeMode, |
| } |
|
|
| impl Default for CodeHighlighter { |
| fn default() -> Self { |
| Self { |
| syntax_set: SyntaxSet::load_defaults_newlines(), |
| theme_set: ThemeSet::load_defaults(), |
| theme_mode: detect_theme_mode(), |
| } |
| } |
| } |
|
|
| impl CodeHighlighter { |
| |
| fn highlight_line(&self, line: &str, language: Option<&str>) -> String { |
| let syntax = language |
| .and_then(|lang| self.syntax_set.find_syntax_by_token(lang)) |
| .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()); |
|
|
| let theme_name = match self.theme_mode { |
| ThemeMode::Dark => "base16-ocean.dark", |
| ThemeMode::Light => "InspiredGitHub", |
| }; |
| let theme = self.theme_set.themes.get(theme_name).unwrap_or_else(|| { |
| |
| self.theme_set |
| .themes |
| .get("base16-ocean.dark") |
| .expect("Default theme should exist") |
| }); |
| let mut highlighter = HighlightLines::new(syntax, theme); |
|
|
| match highlighter.highlight_line(line, &self.syntax_set) { |
| Ok(ranges) => as_24_bit_terminal_escaped(&ranges[..], false), |
| Err(_) => line.to_string(), |
| } |
| } |
|
|
| |
| |
| |
| pub fn render_code_line( |
| &self, |
| line: &str, |
| language: Option<&str>, |
| margin: &str, |
| width: usize, |
| ) -> Vec<String> { |
| |
| let (indent, wrapped_lines) = code_wrap(line, width, true); |
|
|
| let mut result = Vec::new(); |
|
|
| for (i, code_line) in wrapped_lines.iter().enumerate() { |
| let highlighted = self.highlight_line(code_line, language); |
|
|
| |
| let line_indent = if i == 0 { |
| "" |
| } else { |
| &" ".repeat(indent.min(4) / 2 + 1) |
| }; |
|
|
| result.push(format!("{}{}{}{}", margin, line_indent, highlighted, RESET)); |
| } |
|
|
| if result.is_empty() { |
| result.push(format!("{}{}", margin, RESET)); |
| } |
|
|
| result |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use streamdown_render::code::code_wrap; |
|
|
| #[test] |
| fn test_code_wrap_short_line() { |
| let (indent, lines) = code_wrap("let x = 1;", 80, true); |
| assert_eq!(indent, 0); |
| assert_eq!(lines.len(), 1); |
| assert_eq!(lines[0], "let x = 1;"); |
| } |
|
|
| #[test] |
| fn test_code_wrap_with_indent() { |
| let (indent, lines) = code_wrap(" let x = 1;", 80, true); |
| assert_eq!(indent, 4); |
| assert_eq!(lines.len(), 1); |
| } |
|
|
| #[test] |
| fn test_code_wrap_long_line() { |
| let long_line = "x".repeat(100); |
| let (_, lines) = code_wrap(&long_line, 40, true); |
| assert!(lines.len() > 1); |
| } |
|
|
| #[test] |
| fn test_code_wrap_empty() { |
| let (indent, lines) = code_wrap("", 80, true); |
| assert_eq!(indent, 0); |
| assert_eq!(lines.len(), 1); |
| } |
| } |
|
|