File size: 3,698 Bytes
1851bae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | //! Code block rendering with syntax highlighting and line wrapping.
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";
/// Code block highlighter using syntect.
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 {
/// Highlight a single line of code.
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(|| {
// Fallback to base16-ocean.dark if theme not found
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(),
}
}
/// Render a code line with margin, wrapping if needed.
///
/// Returns multiple lines if the code exceeds the available width.
pub fn render_code_line(
&self,
line: &str,
language: Option<&str>,
margin: &str,
width: usize,
) -> Vec<String> {
// Use code_wrap with pretty_broken=true for line wrapping
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);
// Add continuation indent for wrapped lines
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);
}
}
|