Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FmtLine<'_>>, 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<String> = None;
let mut block_width: usize = 0;
let code_lines: Vec<String> = 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::<String>())
} else {
None
}
})
.collect();

if code_lines.is_empty() {
continue;
}

let highlighted = highlighter.highlight(&code_lines.join("\n"), lang.as_deref());

let new_lines: Vec<FmtLine<'_>> = 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);
}
}
6 changes: 6 additions & 0 deletions src/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ pub struct FmtComposite<'s> {
pub visible_length: usize,

pub spacing: Option<Spacing>,

/// Language tag from a preceding fenced code block (e.g. "lua").
/// Only set for composites with `CompositeKind::Code`.
pub code_lang: Option<String>,
}

impl<'s> FmtComposite<'s> {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/fit/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions src/highlighter.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Fn(&str, Option<&str>) -> Vec<String> + Send + Sync>);

impl CodeHighlighter {
pub fn highlight(&self, code: &str, lang: Option<&str>) -> Vec<String> {
(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
}
}
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ mod area;
mod ask;
mod code;
mod color;
mod highlighter;
mod composite;
mod composite_kind;
mod compound_style;
Expand Down Expand Up @@ -138,6 +139,7 @@ pub use {
terminal_size,
Area,
},
highlighter::CodeHighlighter,
ask::*,
color::*,
composite::FmtComposite,
Expand All @@ -157,7 +159,7 @@ pub use {
},
fit::*,
inline::FmtInline,
line::FmtLine,
line::{FmtLine, HighlightedCodeLine},
line_style::LineStyle,
list_indentation::*,
minimad::{
Expand Down
14 changes: 14 additions & 0 deletions src/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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> {
Expand All @@ -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,
}
}
}
46 changes: 46 additions & 0 deletions src/skin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodeHighlighter>,

/// compounds which should be replaced with special
/// renders.
/// Experimental. This API will probably change
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(())
}
Expand Down
38 changes: 32 additions & 6 deletions src/text.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use {
crate::{
code,
composite_kind::CompositeKind,
fit::wrap,
line::FmtLine,
skin::MadSkin,
tbl,
},
minimad::{
Line,
parse_text,
Options,
Text,
Expand Down Expand Up @@ -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<usize>) -> 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)
Expand All @@ -54,13 +56,37 @@ impl<'k, 's> FmtText<'k, 's> {
mut text: Text<'s>,
width: Option<usize>,
) -> 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<FmtLine<'s>> = Vec::with_capacity(text.lines.len());
let mut current_code_lang: Option<String> = 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 =
Expand Down