diff --git a/src/code.rs b/src/code.rs index c572fce..2747a28 100644 --- a/src/code.rs +++ b/src/code.rs @@ -75,3 +75,58 @@ pub fn justify_blocks(lines: &mut [FmtLine<'_>]) { b.justify(lines); } } + +/// If the skin has a `code_syntax_highlighter`, replace all `FmtLine::Normal(Code)` blocks +/// with `FmtLine::HighlightedCode` lines. +/// +/// This must be called *after* `justify_blocks` so that block widths are already set. +pub fn highlight_blocks(lines: &mut Vec>, skin: &MadSkin) { + let Some(ref highlighter) = skin.code_syntax_highlighter else { + return; + }; + + let blocks = find_blocks(lines); + // Process in reverse so that splice indices remain valid. + for block in blocks.into_iter().rev() { + // Collect source text and metadata from the block's lines. + let mut lang: Option = None; + let mut block_width: usize = 0; + let code_lines: Vec = lines + .iter() + .skip(block.start) + .take(block.height) + .filter_map(|l| { + if let FmtLine::Normal(fc) = l { + if lang.is_none() { + lang = fc.code_lang.clone(); + } + block_width = block_width + .max(fc.spacing.map(|s| s.width).unwrap_or(fc.visible_length)); + Some(fc.compounds.iter().map(|c| c.src).collect::()) + } else { + None + } + }) + .collect(); + + if code_lines.is_empty() { + continue; + } + + let highlighted = highlighter.highlight(&code_lines.join("\n"), lang.as_deref()); + + let new_lines: Vec> = highlighted + .into_iter() + .zip(code_lines.iter()) + .map(|(ansi_line, raw_line)| { + FmtLine::HighlightedCode(HighlightedCodeLine { + content: ansi_line, + visible_len: raw_line.chars().count(), + block_width, + }) + }) + .collect(); + + lines.splice(block.start..block.start + block.height, new_lines); + } +} diff --git a/src/composite.rs b/src/composite.rs index c1aab4b..e495ff5 100644 --- a/src/composite.rs +++ b/src/composite.rs @@ -19,6 +19,10 @@ pub struct FmtComposite<'s> { pub visible_length: usize, pub spacing: Option, + + /// Language tag from a preceding fenced code block (e.g. "lua"). + /// Only set for composites with `CompositeKind::Code`. + pub code_lang: Option, } impl<'s> FmtComposite<'s> { @@ -28,6 +32,7 @@ impl<'s> FmtComposite<'s> { compounds: Vec::new(), visible_length: 0, spacing: None, + code_lang: None, } } pub fn from(composite: Composite<'s>, skin: &MadSkin) -> Self { @@ -37,6 +42,7 @@ impl<'s> FmtComposite<'s> { kind, compounds: composite.compounds, spacing: None, + code_lang: None, } } pub fn from_compound(compound: Compound<'s>) -> Self { diff --git a/src/fit/wrap.rs b/src/fit/wrap.rs index b382570..b983117 100644 --- a/src/fit/wrap.rs +++ b/src/fit/wrap.rs @@ -27,6 +27,7 @@ fn follow_up_composite<'s>(fc: &FmtComposite<'s>, skin: &MadSkin) -> FmtComposit compounds: Vec::new(), visible_length, spacing: fc.spacing, + code_lang: None, } } @@ -77,6 +78,7 @@ pub fn hard_wrap_composite<'s, 'c>( compounds: Vec::new(), visible_length: first_width, spacing: src_composite.spacing, + code_lang: None, }; // Strategy 1: diff --git a/src/highlighter.rs b/src/highlighter.rs new file mode 100644 index 0000000..46b1566 --- /dev/null +++ b/src/highlighter.rs @@ -0,0 +1,32 @@ +use std::fmt; +use std::sync::Arc; + +/// A wrapper around a syntax-highlighting function. +/// +/// The inner function takes a code string and an optional language token, +/// and returns one highlighted (ANSI-escaped) string per input line. +pub struct CodeHighlighter(pub Arc) -> Vec + Send + Sync>); + +impl CodeHighlighter { + pub fn highlight(&self, code: &str, lang: Option<&str>) -> Vec { + (self.0)(code, lang) + } +} + +impl Clone for CodeHighlighter { + fn clone(&self) -> Self { + CodeHighlighter(Arc::clone(&self.0)) + } +} + +impl fmt::Debug for CodeHighlighter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "CodeHighlighter(..)") + } +} + +impl PartialEq for CodeHighlighter { + fn eq(&self, _: &Self) -> bool { + false + } +} diff --git a/src/lib.rs b/src/lib.rs index 1861881..2e2f4b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,6 +107,7 @@ mod area; mod ask; mod code; mod color; +mod highlighter; mod composite; mod composite_kind; mod compound_style; @@ -138,6 +139,7 @@ pub use { terminal_size, Area, }, + highlighter::CodeHighlighter, ask::*, color::*, composite::FmtComposite, @@ -157,7 +159,7 @@ pub use { }, fit::*, inline::FmtInline, - line::FmtLine, + line::{FmtLine, HighlightedCodeLine}, line_style::LineStyle, list_indentation::*, minimad::{ diff --git a/src/line.rs b/src/line.rs index 20e3f72..0c7e05f 100644 --- a/src/line.rs +++ b/src/line.rs @@ -13,6 +13,17 @@ use crate::{ }, }; +/// A syntax-highlighted code line, produced after the highlighting pass. +#[derive(Debug)] +pub struct HighlightedCodeLine { + /// ANSI-escaped highlighted content for this line + pub content: String, + /// Visible character width (without ANSI codes) + pub visible_len: usize, + /// Width of the containing code block (used for right-padding) + pub block_width: usize, +} + /// A line in a text. This structure should normally not be /// used outside of the lib. #[derive(Debug)] @@ -21,6 +32,8 @@ pub enum FmtLine<'s> { TableRow(FmtTableRow<'s>), TableRule(FmtTableRule), HorizontalRule, + /// A code line that has been replaced by syntax-highlighted output. + HighlightedCode(HighlightedCodeLine), } impl<'s> FmtLine<'s> { @@ -46,6 +59,7 @@ impl<'s> FmtLine<'s> { FmtLine::TableRow(row) => row.cells.iter().fold(0, |s, c| s + c.visible_length), // Is that right ? no spacing ? FmtLine::TableRule(rule) => 1 + rule.widths.iter().fold(0, |s, w| s + w + 1), FmtLine::HorizontalRule => 0, // No intrinsic width + FmtLine::HighlightedCode(l) => l.visible_len, } } } diff --git a/src/skin.rs b/src/skin.rs index 8cf2576..b2d0bbe 100644 --- a/src/skin.rs +++ b/src/skin.rs @@ -50,6 +50,40 @@ pub struct MadSkin { pub table_border_chars: &'static TableBorderChars, pub list_items_indentation_mode: ListItemsIndentationMode, + /// Optional syntax highlighter for fenced code blocks. + /// + /// When set, fenced code blocks are rendered using the highlighter's output + /// instead of the default code style. + /// + /// # Example with syntect + /// + /// ```rust,ignore + /// use std::sync::Arc; + /// use syntect::easy::HighlightLines; + /// use syntect::highlighting::ThemeSet; + /// use syntect::parsing::SyntaxSet; + /// use syntect::util::as_24_bit_terminal_escaped; + /// use termimad::{CodeHighlighter, MadSkin}; + /// + /// let ps = SyntaxSet::load_defaults_newlines(); + /// let theme = ThemeSet::load_defaults().themes["base16-ocean.dark"].clone(); + /// + /// let mut skin = MadSkin::default(); + /// skin.code_syntax_highlighter = Some(CodeHighlighter(Arc::new(move |code, lang| { + /// let syntax = lang + /// .and_then(|l| ps.find_syntax_by_token(l)) + /// .unwrap_or_else(|| ps.find_syntax_plain_text()); + /// let mut h = HighlightLines::new(syntax, &theme); + /// code.lines() + /// .map(|line| { + /// let ranges = h.highlight_line(line, &ps).unwrap_or_default(); + /// as_24_bit_terminal_escaped(&ranges, false) + /// }) + /// .collect() + /// }))); + /// ``` + pub code_syntax_highlighter: Option, + /// compounds which should be replaced with special /// renders. /// Experimental. This API will probably change @@ -88,6 +122,7 @@ impl Default for MadSkin { ellipsis: CompoundStyle::default(), table_border_chars: STANDARD_TABLE_BORDER_CHARS, list_items_indentation_mode: Default::default(), + code_syntax_highlighter: None, #[cfg(feature = "special-renders")] special_chars: std::collections::HashMap::new(), @@ -125,6 +160,7 @@ impl MadSkin { horizontal_rule: StyledChar::nude('―'), ellipsis: CompoundStyle::default(), list_items_indentation_mode: Default::default(), + code_syntax_highlighter: None, #[cfg(feature = "special-renders")] special_chars: std::collections::HashMap::new(), table_border_chars: STANDARD_TABLE_BORDER_CHARS, @@ -696,6 +732,16 @@ impl MadSkin { write!(f, "{}", self.horizontal_rule.repeated(w))?; } } + FmtLine::HighlightedCode(hl) => { + // Write the ANSI-highlighted content then reset terminal colors. + write!(f, "{}", hl.content)?; + write!(f, "\x1b[0m")?; + // Right-pad to block_width using the code_block background. + if with_right_completion { + let padding = hl.block_width.saturating_sub(hl.visible_len); + self.code_block.compound_style.repeat_space(f, padding)?; + } + } } Ok(()) } diff --git a/src/text.rs b/src/text.rs index fce441a..12a919d 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,12 +1,14 @@ use { crate::{ code, + composite_kind::CompositeKind, fit::wrap, line::FmtLine, skin::MadSkin, tbl, }, minimad::{ + Line, parse_text, Options, Text, @@ -39,7 +41,7 @@ impl<'k, 's> FmtText<'k, 's> { /// This can be called directly or using one of the skin helper /// method. pub fn from(skin: &'k MadSkin, src: &'s str, width: Option) -> FmtText<'k, 's> { - let mt = parse_text(src, Options::default()); + let mt = parse_text(src, Options::default().keep_code_fences(true)); Self::from_text(skin, mt, width) } /// build a text as raw (with no markdown interpretation) @@ -54,13 +56,37 @@ impl<'k, 's> FmtText<'k, 's> { mut text: Text<'s>, width: Option, ) -> FmtText<'k, 's> { - let mut lines = text - .lines - .drain(..) - .map(|mline| FmtLine::from(mline, skin)) - .collect(); + // Manual loop (instead of drain().map()) to track the current code-fence language tag. + let mut lines: Vec> = Vec::with_capacity(text.lines.len()); + let mut current_code_lang: Option = None; + for mline in text.lines.drain(..) { + match mline { + Line::CodeFence(ref composite) => { + // Opening fence carries the language; closing fence has no compounds → None. + current_code_lang = composite + .compounds + .first() + .map(|c| c.src.to_string()) + .filter(|s| !s.is_empty()); + // CodeFence lines are not pushed to the output. + } + other => { + let mut fmt_line = FmtLine::from(other, skin); + // Attach the language tag to Code composites. + if let FmtLine::Normal(ref mut fc) = fmt_line { + if fc.kind == CompositeKind::Code { + fc.code_lang = current_code_lang.clone(); + } + } + lines.push(fmt_line); + } + } + } tbl::fix_all_tables(&mut lines, width.unwrap_or(usize::MAX), skin); code::justify_blocks(&mut lines); + // Syntax highlighting replaces Normal(Code) lines with HighlightedCode lines. + // This must happen after justify_blocks so block widths are already set. + code::highlight_blocks(&mut lines, skin); if let Some(width) = width { if width >= 3 { lines =