1use std::{
4 any::TypeId,
5 collections::{BTreeMap, BTreeSet, HashMap},
6 hash::BuildHasher,
7 mem::{size_of, size_of_val},
8 sync::{Arc, Mutex},
9};
10
11#[cfg(test)]
12use std::sync::atomic::{AtomicUsize, Ordering};
13
14use pulldown_cmark::{
15 BrokenLink, CodeBlockKind, Event, HeadingLevel, LinkType, Options, Parser, Tag, TagEnd,
16};
17use ratatui::{
18 style::{Color, Modifier, Style},
19 text::{Line, Span},
20};
21use syntect::{
22 easy::HighlightLines,
23 highlighting::{FontStyle, HighlightState, Style as SyntectStyle, ThemeSet},
24 parsing::{ParseState, SyntaxSet},
25 util::LinesWithEndings,
26};
27
28use crate::theme::{Theme, ThemeKey, ThemeKind};
29
30const HIGHLIGHT_CACHE_BUDGET_BYTES: usize = 8 * 1024 * 1024;
31const HIGHLIGHT_CACHE_ENTRY_OVERHEAD_BYTES: usize = 256;
32const HIGHLIGHT_CACHE_ALLOCATION_OVERHEAD_BYTES: usize = 16;
33
34#[derive(Clone, Debug, Default)]
35pub struct MarkdownDocument {
36 source: String,
37 stable_prefix_len: usize,
38 stable_blocks: Vec<MarkdownBlock>,
39 tail_blocks: Vec<MarkdownBlock>,
40 stable_reference_definitions: BTreeMap<String, ReferenceDefinition>,
41 reference_definitions: BTreeMap<String, ReferenceDefinition>,
42 stable_reference_dependencies: BTreeSet<String>,
43 parse_passes: u64,
44 parsed_bytes: u64,
45 reference_reparses: u64,
46}
47
48#[derive(Debug)]
49struct HighlightCache {
50 entries: HashMap<HighlightCacheKey, HighlightCacheEntry>,
51 budget_bytes: usize,
52 used_bytes: usize,
53 generation: u64,
54 theme: Option<ThemeKey>,
55}
56
57impl Default for HighlightCache {
58 fn default() -> Self {
59 Self {
60 entries: HashMap::new(),
61 budget_bytes: HIGHLIGHT_CACHE_BUDGET_BYTES,
62 used_bytes: 0,
63 generation: 0,
64 theme: None,
65 }
66 }
67}
68
69#[derive(Clone, Debug, Eq, Hash, PartialEq)]
70struct HighlightCacheKey {
71 content_hash: u64,
72 code_len: usize,
73 language: String,
74 theme: ThemeKey,
75 syntect_theme: &'static str,
76}
77
78#[derive(Debug)]
79struct HighlightCacheEntry {
80 source: Arc<str>,
81 lines: Arc<[Line<'static>]>,
82 bytes: usize,
83 last_used: u64,
84}
85
86impl HighlightCache {
87 fn set_theme(&mut self, theme: ThemeKey) {
88 if self.theme.is_some_and(|current| current != theme) {
89 self.entries = HashMap::new();
90 self.used_bytes = 0;
91 }
92 self.theme = Some(theme);
93 }
94
95 fn get(&mut self, key: &HighlightCacheKey, source: &str) -> Option<Arc<[Line<'static>]>> {
96 self.generation = self.generation.wrapping_add(1);
97 let generation = self.generation;
98 let entry = self.entries.get_mut(key)?;
99 if entry.source.as_ref() != source {
102 return None;
103 }
104 entry.last_used = generation;
105 Some(entry.lines.clone())
106 }
107
108 fn insert(&mut self, key: HighlightCacheKey, source: &str, lines: Arc<[Line<'static>]>) {
109 debug_assert!(self.used_bytes <= self.budget_bytes);
110 self.generation = self.generation.wrapping_add(1);
111 let bytes = highlight_entry_bytes(&key, source, &lines);
112 if bytes > self.budget_bytes {
115 return;
116 }
117 if let Some(replaced) = self.entries.remove(&key) {
118 self.used_bytes = self.used_bytes.saturating_sub(replaced.bytes);
119 }
120 let available = self.budget_bytes - bytes;
121 while self.used_bytes > available {
122 let Some(oldest) = self
123 .entries
124 .iter()
125 .min_by_key(|(_, entry)| entry.last_used)
126 .map(|(key, _)| key.clone())
127 else {
128 break;
129 };
130 if let Some(evicted) = self.entries.remove(&oldest) {
131 self.used_bytes = self.used_bytes.saturating_sub(evicted.bytes);
132 }
133 debug_assert!(self.used_bytes <= self.budget_bytes);
134 }
135 debug_assert!(self.used_bytes <= available);
136 let entry = HighlightCacheEntry {
137 source: Arc::from(source),
138 lines,
139 bytes,
140 last_used: self.generation,
141 };
142 self.used_bytes += bytes;
143 self.entries.insert(key, entry);
144 debug_assert!(self.used_bytes <= self.budget_bytes);
145 }
146}
147
148fn highlight_entry_bytes(key: &HighlightCacheKey, source: &str, lines: &[Line<'static>]) -> usize {
149 size_of::<HighlightCacheKey>()
150 + size_of::<HighlightCacheEntry>()
151 + HIGHLIGHT_CACHE_ENTRY_OVERHEAD_BYTES
152 + 3 * HIGHLIGHT_CACHE_ALLOCATION_OVERHEAD_BYTES
153 + key.language.capacity()
154 + source.len()
155 + 2 * size_of::<usize>()
156 + size_of_val(lines)
157 + lines
158 .iter()
159 .map(|line| {
160 size_of::<Span<'static>>() * line.spans.capacity()
161 + usize::from(!line.spans.is_empty())
162 * HIGHLIGHT_CACHE_ALLOCATION_OVERHEAD_BYTES
163 + line
164 .spans
165 .iter()
166 .map(|span| match &span.content {
167 std::borrow::Cow::Borrowed(content) => content.len(),
168 std::borrow::Cow::Owned(content) => {
169 content.capacity() + HIGHLIGHT_CACHE_ALLOCATION_OVERHEAD_BYTES
170 }
171 })
172 .sum::<usize>()
173 })
174 .sum::<usize>()
175}
176
177#[derive(Clone, Debug)]
178pub struct MarkdownBlock {
179 kind: MarkdownBlockKind,
180 events: Vec<Event<'static>>,
181 source: Arc<str>,
182 content_hash: u64,
183 content_len: usize,
184 rendered: Arc<Mutex<Option<RenderedBlockCacheEntry>>>,
185}
186
187#[derive(Clone, Copy, Debug, Eq, PartialEq)]
188struct RenderedBlockCacheKey {
189 content_hash: u64,
190 code_len: usize,
191 width: u16,
192 theme: ThemeKey,
193 quote_depth: usize,
194 highlighter: u64,
195}
196
197#[derive(Clone, Debug)]
198struct RenderedBlockCacheEntry {
199 key: RenderedBlockCacheKey,
200 source: Arc<str>,
201 lines: Arc<[MarkdownLine]>,
202}
203
204impl MarkdownBlock {
205 pub const fn kind(&self) -> MarkdownBlockKind {
206 self.kind
207 }
208}
209
210#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub enum MarkdownBlockKind {
212 Paragraph,
213 Heading,
214 Quote,
215 Code,
216 List,
217 Table,
218 Html,
219 ThematicBreak,
220 Other,
221}
222
223#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
224pub(crate) enum MarkdownLineKind {
225 #[default]
226 Prose,
227 ListItem {
228 continuation_indent: usize,
229 },
230 Code,
231 Table,
232}
233
234#[derive(Clone, Debug, Default, Eq, PartialEq)]
235pub(crate) struct MarkdownLine {
236 pub(crate) line: Line<'static>,
237 pub(crate) kind: MarkdownLineKind,
238}
239
240impl MarkdownDocument {
241 pub fn new(source: String) -> Self {
242 let mut document = Self {
243 source,
244 ..Self::default()
245 };
246 document.reparse_open_tail();
247 document
248 }
249
250 pub fn append(&mut self, delta: &str) {
251 self.source.push_str(delta);
252 self.reparse_open_tail();
253 }
254
255 pub fn as_str(&self) -> &str {
256 &self.source
257 }
258
259 pub fn stable_prefix_len(&self) -> usize {
260 self.stable_prefix_len
261 }
262
263 pub fn parse_passes(&self) -> u64 {
264 self.parse_passes
265 }
266
267 pub fn parsed_bytes(&self) -> u64 {
268 self.parsed_bytes
269 }
270
271 pub fn reference_reparses(&self) -> u64 {
272 self.reference_reparses
273 }
274
275 pub fn blocks(&self) -> impl Iterator<Item = &MarkdownBlock> {
276 self.stable_blocks.iter().chain(&self.tail_blocks)
277 }
278
279 fn reparse_open_tail(&mut self) {
280 let tail = self.source[self.stable_prefix_len..].to_owned();
281 let parsed = self.parse(&tail);
282 let effective_definitions = self.effective_definitions(&parsed.definitions);
283 let references_changed = self
284 .stable_reference_dependencies
285 .iter()
286 .any(|label| self.reference_definitions.get(label) != effective_definitions.get(label));
287 if references_changed {
288 self.reference_reparses = self.reference_reparses.wrapping_add(1);
289 self.stable_prefix_len = 0;
290 self.stable_blocks.clear();
291 self.tail_blocks.clear();
292 self.stable_reference_definitions.clear();
293 self.reference_definitions.clear();
294 self.stable_reference_dependencies.clear();
295 let source = self.source.clone();
296 let parsed = self.parse(&source);
297 self.reference_definitions = self.effective_definitions(&parsed.definitions);
298 self.apply_parsed(parsed);
299 return;
300 }
301 self.reference_definitions = effective_definitions;
302 self.apply_parsed(parsed);
303 }
304
305 fn parse(&mut self, source: &str) -> ParsedDocument {
306 self.parse_passes = self.parse_passes.wrapping_add(1);
307 self.parsed_bytes = self.parsed_bytes.wrapping_add(source.len() as u64);
308 parse_document(source)
309 }
310
311 fn effective_definitions(
312 &self,
313 tail_definitions: &[ParsedReferenceDefinition],
314 ) -> BTreeMap<String, ReferenceDefinition> {
315 let mut definitions = self.stable_reference_definitions.clone();
316 for definition in tail_definitions {
317 definitions
318 .entry(definition.label.clone())
319 .or_insert_with(|| definition.definition.clone());
320 }
321 definitions
322 }
323
324 fn apply_parsed(&mut self, parsed: ParsedDocument) {
325 let split = parsed.blocks.last().map_or(0, |block| block.range.start);
326 self.stable_reference_dependencies.extend(
327 parsed
328 .blocks
329 .iter()
330 .filter(|block| block.range.end <= split)
331 .flat_map(|block| block.reference_dependencies.iter().cloned()),
332 );
333 self.stable_blocks.extend(
334 parsed
335 .blocks
336 .iter()
337 .filter(|block| block.range.end <= split)
338 .map(|block| block.block.clone()),
339 );
340 self.tail_blocks = parsed
341 .blocks
342 .into_iter()
343 .filter(|block| block.range.start >= split)
344 .map(|block| block.block)
345 .collect();
346 for definition in parsed.definitions {
347 if definition.span.end <= split {
348 self.stable_reference_definitions
349 .entry(definition.label)
350 .or_insert(definition.definition);
351 }
352 }
353 self.stable_prefix_len += split;
354 }
355}
356
357fn markdown_options() -> Options {
358 Options::ENABLE_STRIKETHROUGH
359 | Options::ENABLE_TABLES
360 | Options::ENABLE_TASKLISTS
361 | Options::ENABLE_GFM
362}
363
364struct ParsedBlock {
365 block: MarkdownBlock,
366 range: std::ops::Range<usize>,
367 reference_dependencies: BTreeSet<String>,
368}
369
370struct ParsedDocument {
371 blocks: Vec<ParsedBlock>,
372 definitions: Vec<ParsedReferenceDefinition>,
373}
374
375#[derive(Clone, Debug, Eq, PartialEq)]
376struct ReferenceDefinition {
377 destination: String,
378 title: Option<String>,
379}
380
381struct ParsedReferenceDefinition {
382 label: String,
383 definition: ReferenceDefinition,
384 span: std::ops::Range<usize>,
385}
386
387fn parse_document(source: &str) -> ParsedDocument {
388 let (definitions, events, broken_references) = {
389 let mut broken_references = Vec::new();
390 let mut broken_link_callback = |broken: BrokenLink<'_>| {
391 broken_references.push((
392 broken.span,
393 normalize_reference_label(broken.reference.as_ref()),
394 ));
395 None
396 };
397 let parser = Parser::new_with_broken_link_callback(
398 source,
399 markdown_options(),
400 Some(&mut broken_link_callback),
401 );
402 let definitions = parser
403 .reference_definitions()
404 .iter()
405 .map(|(label, definition)| ParsedReferenceDefinition {
406 label: normalize_reference_label(label),
407 definition: ReferenceDefinition {
408 destination: definition.dest.to_string(),
409 title: definition.title.as_ref().map(ToString::to_string),
410 },
411 span: definition.span.clone(),
412 })
413 .collect::<Vec<_>>();
414 let events = parser.into_offset_iter().collect::<Vec<_>>();
415 (definitions, events, broken_references)
416 };
417 let mut blocks = Vec::new();
418 let mut current = Vec::new();
419 let mut current_kind = MarkdownBlockKind::Other;
420 let mut current_start = 0;
421 let mut depth = 0usize;
422
423 for (event, range) in events {
424 if depth == 0 && current.is_empty() {
425 current_start = range.start;
426 current_kind = block_kind(&event);
427 }
428 let current_end = range.end;
429 match &event {
430 Event::Start(_) => depth += 1,
431 Event::End(_) => depth = depth.saturating_sub(1),
432 _ => {}
433 }
434 current.push(event.into_static());
435 if depth == 0 {
436 let range = current_start..current_end;
437 blocks.push(ParsedBlock {
438 block: MarkdownBlock {
439 kind: current_kind,
440 events: std::mem::take(&mut current),
441 source: Arc::from(&source[range.clone()]),
442 content_hash: foldhash::fast::FixedState::default()
443 .hash_one(&source[range.clone()]),
444 content_len: range.len(),
445 rendered: Arc::new(Mutex::new(None)),
446 },
447 range,
448 reference_dependencies: BTreeSet::new(),
449 });
450 }
451 }
452 for block in &mut blocks {
453 for event in &block.block.events {
454 let Event::Start(Tag::Link { link_type, id, .. } | Tag::Image { link_type, id, .. }) =
455 event
456 else {
457 continue;
458 };
459 if matches!(
460 link_type,
461 LinkType::Reference
462 | LinkType::ReferenceUnknown
463 | LinkType::Collapsed
464 | LinkType::CollapsedUnknown
465 | LinkType::Shortcut
466 | LinkType::ShortcutUnknown
467 ) {
468 block
469 .reference_dependencies
470 .insert(normalize_reference_label(id.as_ref()));
471 }
472 }
473 block.reference_dependencies.extend(
474 broken_references
475 .iter()
476 .filter(|(span, _)| span.start >= block.range.start && span.end <= block.range.end)
477 .map(|(_, label)| label.clone()),
478 );
479 }
480 ParsedDocument {
481 blocks,
482 definitions,
483 }
484}
485
486fn normalize_reference_label(label: &str) -> String {
487 label
488 .split_whitespace()
489 .collect::<Vec<_>>()
490 .join(" ")
491 .to_lowercase()
492}
493
494fn block_kind(event: &Event<'_>) -> MarkdownBlockKind {
495 match event {
496 Event::Start(Tag::Paragraph) => MarkdownBlockKind::Paragraph,
497 Event::Start(Tag::Heading { .. }) => MarkdownBlockKind::Heading,
498 Event::Start(Tag::BlockQuote(_)) => MarkdownBlockKind::Quote,
499 Event::Start(Tag::CodeBlock(_)) => MarkdownBlockKind::Code,
500 Event::Start(Tag::List(_)) => MarkdownBlockKind::List,
501 Event::Start(Tag::Table(_)) => MarkdownBlockKind::Table,
502 Event::Start(Tag::HtmlBlock) | Event::Html(_) => MarkdownBlockKind::Html,
503 Event::Rule => MarkdownBlockKind::ThematicBreak,
504 _ => MarkdownBlockKind::Other,
505 }
506}
507
508pub trait Highlighter: Send + Sync + 'static {
509 fn highlight(&self, language: &str, code: &str, theme: &Theme) -> Arc<[Line<'static>]>;
510
511 fn highlight_stable(&self, language: &str, code: &str, theme: &Theme) -> Arc<[Line<'static>]> {
512 self.highlight(language, code, theme)
513 }
514
515 fn cache_key(&self) -> u64 {
516 foldhash::fast::FixedState::default().hash_one((
517 TypeId::of::<Self>(),
518 std::ptr::from_ref(self).cast::<()>() as usize,
519 ))
520 }
521}
522
523#[derive(Default)]
524pub struct PlainHighlighter;
525
526impl Highlighter for PlainHighlighter {
527 fn highlight(&self, _language: &str, code: &str, _theme: &Theme) -> Arc<[Line<'static>]> {
528 plain_code_lines(code).into()
529 }
530}
531
532#[derive(Clone, Debug, Eq, Hash, PartialEq)]
533struct IncrementalHighlightKey {
534 language: String,
535 theme: ThemeKey,
536 syntect_theme: &'static str,
537}
538
539#[derive(Debug)]
540struct IncrementalHighlightEntry {
541 complete_source: String,
542 lines: Vec<Line<'static>>,
543 highlight_state: HighlightState,
544 parse_state: ParseState,
545}
546
547pub struct SyntectHighlighter {
548 syntaxes: SyntaxSet,
549 themes: ThemeSet,
550 plain: PlainHighlighter,
551 cache: Mutex<HighlightCache>,
552 incremental: Mutex<HashMap<IncrementalHighlightKey, IncrementalHighlightEntry>>,
553 #[cfg(test)]
554 highlight_calls: AtomicUsize,
555 #[cfg(test)]
556 incremental_bytes: AtomicUsize,
557}
558
559impl Default for SyntectHighlighter {
560 fn default() -> Self {
561 Self {
562 syntaxes: SyntaxSet::load_defaults_newlines(),
563 themes: ThemeSet::load_defaults(),
564 plain: PlainHighlighter,
565 cache: Mutex::new(HighlightCache::default()),
566 incremental: Mutex::new(HashMap::new()),
567 #[cfg(test)]
568 highlight_calls: AtomicUsize::new(0),
569 #[cfg(test)]
570 incremental_bytes: AtomicUsize::new(0),
571 }
572 }
573}
574
575impl Highlighter for SyntectHighlighter {
576 fn highlight(&self, language: &str, code: &str, theme: &Theme) -> Arc<[Line<'static>]> {
577 #[cfg(test)]
578 self.highlight_calls.fetch_add(1, Ordering::Relaxed);
579 let language = normalized_language(language);
580 let Some(syntax) = self
581 .syntaxes
582 .find_syntax_by_token(&language)
583 .or_else(|| self.syntaxes.find_syntax_by_extension(&language))
584 else {
585 return self.plain.highlight(&language, code, theme);
586 };
587 let theme_name = syntect_theme_name(theme);
590 let Some(syntax_theme) = self.themes.themes.get(theme_name) else {
591 return self.plain.highlight(&language, code, theme);
592 };
593 let complete_len = code.rfind('\n').map_or(0, |index| index + 1);
594 let complete_source = &code[..complete_len];
595 let partial_source = &code[complete_len..];
596 let key = IncrementalHighlightKey {
597 language: language.clone(),
598 theme: theme.key(),
599 syntect_theme: theme_name,
600 };
601 let mut cache = self
602 .incremental
603 .lock()
604 .unwrap_or_else(std::sync::PoisonError::into_inner);
605 let entry = cache.entry(key).or_insert_with(|| {
606 let highlighter = HighlightLines::new(syntax, syntax_theme);
607 let (highlight_state, parse_state) = highlighter.state();
608 IncrementalHighlightEntry {
609 complete_source: String::new(),
610 lines: Vec::new(),
611 highlight_state,
612 parse_state,
613 }
614 });
615 let extends_cached = complete_source.starts_with(&entry.complete_source);
616 let mut highlighter = if extends_cached {
617 HighlightLines::from_state(
618 syntax_theme,
619 entry.highlight_state.clone(),
620 entry.parse_state.clone(),
621 )
622 } else {
623 entry.complete_source.clear();
624 entry.lines.clear();
625 HighlightLines::new(syntax, syntax_theme)
626 };
627 let appended = if extends_cached {
628 &complete_source[entry.complete_source.len()..]
629 } else {
630 complete_source
631 };
632 #[cfg(test)]
633 self.incremental_bytes
634 .fetch_add(appended.len() + partial_source.len(), Ordering::Relaxed);
635 if append_highlighted_lines(
636 &mut highlighter,
637 appended,
638 &self.syntaxes,
639 theme,
640 &mut entry.lines,
641 )
642 .is_err()
643 {
644 let reset = HighlightLines::new(syntax, syntax_theme);
645 let (highlight_state, parse_state) = reset.state();
646 entry.complete_source.clear();
647 entry.lines.clear();
648 entry.highlight_state = highlight_state;
649 entry.parse_state = parse_state;
650 return self.plain.highlight(language.as_str(), code, theme);
651 }
652 entry.complete_source.clear();
653 entry.complete_source.push_str(complete_source);
654 let (highlight_state, parse_state) = highlighter.state();
655 entry.highlight_state = highlight_state;
656 entry.parse_state = parse_state;
657
658 let mut lines = entry.lines.clone();
659 if !partial_source.is_empty() {
660 let mut partial = HighlightLines::from_state(
661 syntax_theme,
662 entry.highlight_state.clone(),
663 entry.parse_state.clone(),
664 );
665 if append_highlighted_lines(
666 &mut partial,
667 partial_source,
668 &self.syntaxes,
669 theme,
670 &mut lines,
671 )
672 .is_err()
673 {
674 return self.plain.highlight(language.as_str(), code, theme);
675 }
676 }
677 if lines.is_empty() {
678 lines.push(Line::default());
679 }
680 lines.into()
681 }
682
683 fn highlight_stable(&self, language: &str, code: &str, theme: &Theme) -> Arc<[Line<'static>]> {
684 let incremental_key = IncrementalHighlightKey {
685 language: normalized_language(language),
686 theme: theme.key(),
687 syntect_theme: syntect_theme_name(theme),
688 };
689 self.incremental
690 .lock()
691 .unwrap_or_else(std::sync::PoisonError::into_inner)
692 .remove(&incremental_key);
693 let key = HighlightCacheKey {
694 content_hash: foldhash::fast::FixedState::default().hash_one((language, code)),
695 code_len: code.len(),
696 language: language.to_owned(),
697 theme: theme.key(),
698 syntect_theme: syntect_theme_name(theme),
699 };
700 {
701 let mut cache = self
702 .cache
703 .lock()
704 .unwrap_or_else(std::sync::PoisonError::into_inner);
705 cache.set_theme(theme.key());
706 if let Some(lines) = cache.get(&key, code) {
707 return lines;
708 }
709 }
710
711 #[cfg(test)]
712 self.highlight_calls.fetch_add(1, Ordering::Relaxed);
713 let lines = self.highlight_fresh(language, code, theme);
714 let mut cache = self
715 .cache
716 .lock()
717 .unwrap_or_else(std::sync::PoisonError::into_inner);
718 cache.set_theme(theme.key());
719 cache.insert(key, code, Arc::clone(&lines));
720 lines
721 }
722}
723
724impl SyntectHighlighter {
725 fn highlight_fresh(&self, language: &str, code: &str, theme: &Theme) -> Arc<[Line<'static>]> {
726 let language = normalized_language(language);
727 let Some(syntax) = self
728 .syntaxes
729 .find_syntax_by_token(&language)
730 .or_else(|| self.syntaxes.find_syntax_by_extension(&language))
731 else {
732 return self.plain.highlight(&language, code, theme);
733 };
734 let Some(syntax_theme) = self.themes.themes.get(syntect_theme_name(theme)) else {
735 return self.plain.highlight(&language, code, theme);
736 };
737 let mut highlighter = HighlightLines::new(syntax, syntax_theme);
738 let mut lines = Vec::new();
739 if append_highlighted_lines(&mut highlighter, code, &self.syntaxes, theme, &mut lines)
740 .is_err()
741 {
742 return self.plain.highlight(&language, code, theme);
743 }
744 if lines.is_empty() {
745 lines.push(Line::default());
746 }
747 lines.into()
748 }
749}
750
751fn append_highlighted_lines(
752 highlighter: &mut HighlightLines<'_>,
753 source: &str,
754 syntaxes: &SyntaxSet,
755 theme: &Theme,
756 lines: &mut Vec<Line<'static>>,
757) -> Result<(), syntect::Error> {
758 for source_line in LinesWithEndings::from(source) {
759 let ranges = highlighter.highlight_line(source_line, syntaxes)?;
760 let spans = ranges
761 .into_iter()
762 .filter_map(|(syntect_style, content)| {
763 let content = content.strip_suffix('\n').unwrap_or(content);
764 let content = content.strip_suffix('\r').unwrap_or(content);
765 (!content.is_empty()).then(|| {
766 Span::styled(
767 content.to_owned(),
768 terminal_style_from_syntect(syntect_style, theme),
769 )
770 })
771 })
772 .collect::<Vec<_>>();
773 lines.push(Line::from(spans));
774 }
775 Ok(())
776}
777
778#[cfg(test)]
779impl SyntectHighlighter {
780 fn highlight_calls(&self) -> usize {
781 self.highlight_calls.load(Ordering::Relaxed)
782 }
783
784 fn incremental_bytes(&self) -> usize {
785 self.incremental_bytes.load(Ordering::Relaxed)
786 }
787
788 fn set_cache_budget(&self, budget_bytes: usize) {
789 let mut cache = self
790 .cache
791 .lock()
792 .unwrap_or_else(std::sync::PoisonError::into_inner);
793 cache.entries = HashMap::new();
794 cache.used_bytes = 0;
795 cache.budget_bytes = budget_bytes;
796 }
797}
798
799fn syntect_theme_name(theme: &Theme) -> &'static str {
800 match theme.key().kind {
801 ThemeKind::Dark | ThemeKind::HighContrast => "base16-eighties.dark",
802 ThemeKind::Default | ThemeKind::Mono => "InspiredGitHub",
803 }
804}
805
806fn terminal_style_from_syntect(syntect_style: SyntectStyle, theme: &Theme) -> Style {
807 let mut style = Style::default();
808 if let Some(color) = theme.quantize_rgb(
809 syntect_style.foreground.r,
810 syntect_style.foreground.g,
811 syntect_style.foreground.b,
812 ) {
813 style = style.fg(color);
814 }
815 let mut modifiers = Modifier::empty();
816 if syntect_style.font_style.contains(FontStyle::BOLD) {
817 modifiers |= Modifier::BOLD;
818 }
819 if syntect_style.font_style.contains(FontStyle::ITALIC) {
820 modifiers |= Modifier::ITALIC;
821 }
822 if syntect_style.font_style.contains(FontStyle::UNDERLINE) {
823 modifiers |= Modifier::UNDERLINED;
824 }
825 style.add_modifier(modifiers)
826}
827
828fn plain_code_lines(code: &str) -> Vec<Line<'static>> {
829 let mut lines = code
830 .lines()
831 .map(|line| Line::from(line.strip_suffix('\r').unwrap_or(line).to_owned()))
832 .collect::<Vec<_>>();
833 if lines.is_empty() {
834 lines.push(Line::default());
835 }
836 lines
837}
838
839use pulldown_cmark::Alignment;
847use unicode_segmentation::UnicodeSegmentation as _;
848use unicode_width::UnicodeWidthStr;
849
850fn cell_width(spans: &[Span<'static>]) -> usize {
852 spans
853 .iter()
854 .map(|span| UnicodeWidthStr::width(span.content.as_ref()))
855 .sum()
856}
857
858fn table_columns(natural: &[usize], available: usize) -> Option<Vec<usize>> {
862 let columns = natural.len();
863 if columns == 0 {
864 return None;
865 }
866 let borders = columns * 3 + 1; let budget = available.saturating_sub(borders);
868 let minimum = columns * 3;
869 if budget < minimum.max(8) {
870 return None;
871 }
872 let budget = budget.max(minimum);
873 let total: usize = natural.iter().sum();
874 if total <= budget {
875 return Some(natural.to_vec());
876 }
877 let mut widths: Vec<usize> = natural.to_vec();
879 let mut overflow = total - budget;
880 while overflow > 0 {
881 let Some((index, widest)) = widths
882 .iter()
883 .enumerate()
884 .max_by_key(|(_, width)| *width)
885 .map(|(index, width)| (index, *width))
886 else {
887 break;
888 };
889 if widest <= 3 {
890 break;
891 }
892 widths[index] -= 1;
893 overflow -= 1;
894 }
895 Some(widths)
896}
897
898fn wrap_cell(
901 spans: &[Span<'static>],
902 width: usize,
903 alignment: Alignment,
904) -> Vec<Vec<Span<'static>>> {
905 let width = width.max(1);
906 let mut words: Vec<Vec<Span<'static>>> = Vec::new();
907 let mut word = Vec::new();
908 for span in spans {
909 for grapheme in span.content.graphemes(true) {
910 let is_space = grapheme.chars().all(char::is_whitespace);
911 if is_space && !word.is_empty() {
912 word.push(Span::styled(grapheme.to_owned(), span.style));
913 words.push(std::mem::take(&mut word));
914 } else if let Some(last) = word.last_mut()
915 && last.style == span.style
916 {
917 last.content.to_mut().push_str(grapheme);
918 } else {
919 word.push(Span::styled(grapheme.to_owned(), span.style));
920 }
921 }
922 }
923 if !word.is_empty() {
924 words.push(word);
925 }
926
927 let mut rows: Vec<Vec<Span<'static>>> = vec![Vec::new()];
928 let mut column = 0usize;
929 for word in words {
930 let word_width = cell_width(&word);
931 if column > 0 && column + word_width > width {
932 rows.push(Vec::new());
933 column = 0;
934 }
935 if word_width <= width {
936 column += word_width;
937 rows.last_mut().expect("one row").extend(word);
938 continue;
939 }
940 for span in word {
941 for grapheme in span.content.graphemes(true) {
942 let grapheme_width = UnicodeWidthStr::width(grapheme).max(1);
943 if column > 0 && column + grapheme_width > width {
944 rows.push(Vec::new());
945 column = 0;
946 }
947 if let Some(last) = rows.last_mut().and_then(|row| row.last_mut())
948 && last.style == span.style
949 {
950 last.content.to_mut().push_str(grapheme);
951 } else {
952 rows.last_mut()
953 .expect("one row")
954 .push(Span::styled(grapheme.to_owned(), span.style));
955 }
956 column += grapheme_width;
957 }
958 }
959 }
960 let _ = alignment;
962 rows
963}
964
965fn pad_cell(
966 mut spans: Vec<Span<'static>>,
967 width: usize,
968 alignment: Alignment,
969) -> Vec<Span<'static>> {
970 let used = cell_width(&spans);
971 if used > width {
972 let mut truncated = Vec::new();
973 let mut remaining = width;
974 for span in spans {
975 let mut content = String::new();
976 for grapheme in span.content.graphemes(true) {
977 let grapheme_width = UnicodeWidthStr::width(grapheme).max(1);
978 if grapheme_width > remaining {
979 break;
980 }
981 content.push_str(grapheme);
982 remaining -= grapheme_width;
983 }
984 if !content.is_empty() {
985 truncated.push(Span::styled(content, span.style));
986 }
987 if remaining == 0 {
988 break;
989 }
990 }
991 return truncated;
992 }
993 if used >= width {
994 return spans;
995 }
996 let padding = width - used;
997 let (left, right) = match alignment {
998 Alignment::Left => (0, padding),
999 Alignment::Right => (padding, 0),
1000 Alignment::Center => (padding / 2, padding - padding / 2),
1001 Alignment::None => (0, padding),
1002 };
1003 let mut padded = Vec::new();
1004 if left > 0 {
1005 padded.push(Span::raw(" ".repeat(left)));
1006 }
1007 padded.append(&mut spans);
1008 if right > 0 {
1009 padded.push(Span::raw(" ".repeat(right)));
1010 }
1011 padded
1012}
1013
1014fn sanitize_cell(spans: Vec<Span<'static>>) -> Vec<Span<'static>> {
1018 let sanitized = spans
1019 .into_iter()
1020 .map(|span| {
1021 let content = span
1022 .content
1023 .chars()
1024 .map(|character| {
1025 if character.is_control() && character != '\t' {
1026 '\u{FFFD}'
1027 } else {
1028 character
1029 }
1030 })
1031 .collect::<String>();
1032 Span::styled(content, span.style)
1033 })
1034 .collect();
1035 expand_tabs_in_spans(sanitized)
1036}
1037
1038fn border_row(
1039 left: &str,
1040 junction: &str,
1041 right: &str,
1042 widths: &[usize],
1043 quote_prefix: &str,
1044 theme: &Theme,
1045) -> Line<'static> {
1046 let mut spans = vec![
1047 Span::styled(quote_prefix.to_owned(), theme.quote()),
1048 Span::styled(left.to_owned(), theme.code_border()),
1049 ];
1050 for (index, width) in widths.iter().enumerate() {
1051 spans.push(Span::styled("─".repeat(width + 2), theme.code_border()));
1052 spans.push(Span::styled(
1053 if index + 1 == widths.len() {
1054 right.to_owned()
1055 } else {
1056 junction.to_owned()
1057 },
1058 theme.code_border(),
1059 ));
1060 }
1061 Line::from(spans)
1062}
1063
1064fn cell_row(
1065 cells: &[Vec<Span<'static>>],
1066 widths: &[usize],
1067 alignments: &[Alignment],
1068 quote_prefix: &str,
1069 theme: &Theme,
1070 style: Style,
1071) -> Vec<Line<'static>> {
1072 let wrapped: Vec<Vec<Vec<Span<'static>>>> = cells
1073 .iter()
1074 .enumerate()
1075 .map(|(index, cell)| {
1076 wrap_cell(
1077 cell,
1078 widths.get(index).copied().unwrap_or(3),
1079 alignments.get(index).copied().unwrap_or(Alignment::None),
1080 )
1081 })
1082 .collect();
1083 let height = wrapped.iter().map(Vec::len).max().unwrap_or(1);
1084 let mut lines = Vec::new();
1085 for row in 0..height {
1086 let mut spans = vec![
1087 Span::styled(quote_prefix.to_owned(), theme.quote()),
1088 Span::styled("│ ".to_owned(), theme.code_border()),
1089 ];
1090 for (index, width) in widths.iter().enumerate() {
1091 let cell = wrapped
1092 .get(index)
1093 .and_then(|rows| rows.get(row))
1094 .cloned()
1095 .unwrap_or_default();
1096 let mut padded = pad_cell(
1097 sanitize_cell(cell),
1098 *width,
1099 alignments.get(index).copied().unwrap_or(Alignment::None),
1100 );
1101 for span in &mut padded {
1102 span.style = span.style.patch(style);
1103 }
1104 spans.append(&mut padded);
1105 spans.push(Span::styled(
1106 if index + 1 == widths.len() {
1107 " │".to_owned()
1108 } else {
1109 " │ ".to_owned()
1110 },
1111 theme.code_border(),
1112 ));
1113 }
1114 lines.push(Line::from(spans));
1115 }
1116 lines
1117}
1118
1119fn render_table(
1120 alignments: &[Alignment],
1121 header: &[Vec<Span<'static>>],
1122 rows: &[Vec<Vec<Span<'static>>>],
1123 quote_prefix: &str,
1124 available: usize,
1125 theme: &Theme,
1126) -> Vec<Line<'static>> {
1127 let columns = alignments.len().max(header.len());
1128 let mut natural = vec![3usize; columns];
1129 for (index, width) in natural.iter_mut().enumerate() {
1130 if let Some(cell) = header.get(index) {
1131 *width = (*width).max(cell_width(cell));
1132 }
1133 for row in rows {
1134 if let Some(cell) = row.get(index) {
1135 *width = (*width).max(cell_width(cell));
1136 }
1137 }
1138 *width = (*width).min(48);
1139 }
1140 let Some(widths) = table_columns(&natural, available) else {
1141 let mut lines = Vec::new();
1143 for row in rows {
1144 for (index, cell) in row.iter().enumerate() {
1145 let header_text = header
1146 .get(index)
1147 .map(|spans| {
1148 spans
1149 .iter()
1150 .map(|span| span.content.as_ref())
1151 .collect::<String>()
1152 })
1153 .filter(|text| !text.is_empty())
1154 .unwrap_or_else(|| format!("column {}", index + 1));
1155 let marker = if available >= 2 { "· " } else { "" };
1156 let mut spans = vec![
1157 Span::styled(quote_prefix.to_owned(), theme.quote()),
1158 Span::styled(marker.to_owned(), theme.code_border()),
1159 Span::styled(format!("{header_text}: "), theme.heading()),
1160 ];
1161 spans.extend(sanitize_cell(cell.clone()));
1162 let wrapped = wrap_cell(
1163 &spans[1..],
1164 available.saturating_sub(UnicodeWidthStr::width(marker)),
1165 Alignment::Left,
1166 );
1167 for row in wrapped {
1168 let mut line = vec![spans[0].clone()];
1169 line.extend(row);
1170 lines.push(Line::from(line));
1171 }
1172 }
1173 }
1174 if lines.is_empty() {
1175 let marker = if available >= 2 { "· " } else { "" };
1176 let wrapped = wrap_cell(
1177 &[Span::styled(
1178 format!("{marker}(empty table)"),
1179 theme.muted(),
1180 )],
1181 available.saturating_sub(UnicodeWidthStr::width(marker)),
1182 Alignment::Left,
1183 );
1184 for row in wrapped {
1185 let mut line = vec![Span::styled(quote_prefix.to_owned(), theme.quote())];
1186 line.extend(row);
1187 lines.push(Line::from(line));
1188 }
1189 }
1190 return lines;
1191 };
1192 let mut lines = vec![border_row("┌", "┬", "┐", &widths, quote_prefix, theme)];
1193 lines.extend(cell_row(
1194 header,
1195 &widths,
1196 alignments,
1197 quote_prefix,
1198 theme,
1199 theme.heading(),
1200 ));
1201 lines.push(border_row("├", "┼", "┤", &widths, quote_prefix, theme));
1202 for row in rows {
1203 lines.extend(cell_row(
1204 row,
1205 &widths,
1206 alignments,
1207 quote_prefix,
1208 theme,
1209 theme.body(),
1210 ));
1211 }
1212 lines.push(border_row("└", "┴", "┘", &widths, quote_prefix, theme));
1213 lines
1214}
1215
1216pub fn render_markdown(
1217 document: &MarkdownDocument,
1218 theme: &Theme,
1219 highlighter: &dyn Highlighter,
1220) -> Vec<Line<'static>> {
1221 render_markdown_width(document, theme, highlighter, u16::MAX)
1222}
1223
1224pub fn render_markdown_width(
1227 document: &MarkdownDocument,
1228 theme: &Theme,
1229 highlighter: &dyn Highlighter,
1230 width: u16,
1231) -> Vec<Line<'static>> {
1232 render_markdown_lines_width(document, theme, highlighter, width)
1233 .into_iter()
1234 .map(|line| line.line)
1235 .collect()
1236}
1237
1238pub(crate) fn render_markdown_lines_width(
1239 document: &MarkdownDocument,
1240 theme: &Theme,
1241 highlighter: &dyn Highlighter,
1242 width: u16,
1243) -> Vec<MarkdownLine> {
1244 let mut lines = Vec::new();
1245 let highlighter_key = highlighter.cache_key();
1246 for block in &document.stable_blocks {
1247 let key = RenderedBlockCacheKey {
1248 content_hash: block.content_hash,
1249 code_len: block.content_len,
1250 width,
1251 theme: theme.key(),
1252 quote_depth: 0,
1253 highlighter: highlighter_key,
1254 };
1255 let cached = block
1256 .rendered
1257 .lock()
1258 .unwrap_or_else(std::sync::PoisonError::into_inner)
1259 .as_ref()
1260 .filter(|entry| entry.key == key && entry.source == block.source)
1261 .map(|entry| Arc::clone(&entry.lines));
1262 let rendered = cached.unwrap_or_else(|| {
1263 let rendered: Arc<[MarkdownLine]> =
1264 render_block(block, true, theme, highlighter, width).into();
1265 *block
1266 .rendered
1267 .lock()
1268 .unwrap_or_else(std::sync::PoisonError::into_inner) =
1269 Some(RenderedBlockCacheEntry {
1270 key,
1271 source: Arc::clone(&block.source),
1272 lines: Arc::clone(&rendered),
1273 });
1274 rendered
1275 });
1276 push_block_lines(&mut lines, rendered.iter().cloned());
1277 }
1278 for block in &document.tail_blocks {
1281 push_block_lines(
1282 &mut lines,
1283 render_block(block, false, theme, highlighter, width),
1284 );
1285 }
1286 if lines.is_empty() {
1287 lines.push(MarkdownLine::default());
1288 }
1289 lines
1290}
1291
1292fn push_block_lines(lines: &mut Vec<MarkdownLine>, block: impl IntoIterator<Item = MarkdownLine>) {
1296 let mut block = block.into_iter().peekable();
1297 if block.peek().is_none() {
1298 return;
1299 }
1300 if !lines.is_empty() {
1301 lines.push(MarkdownLine::default());
1302 }
1303 lines.extend(block);
1304}
1305
1306fn render_block(
1307 block: &MarkdownBlock,
1308 stable: bool,
1309 theme: &Theme,
1310 highlighter: &dyn Highlighter,
1311 width: u16,
1312) -> Vec<MarkdownLine> {
1313 let mut renderer = MarkdownRenderer::new(theme, highlighter);
1314 renderer.width = usize::from(width);
1315 renderer.begin_block(stable);
1316 for event in &block.events {
1317 renderer.event(event);
1318 }
1319 renderer.flush();
1323 renderer.lines
1324}
1325
1326pub(crate) fn normalized_language(language: &str) -> String {
1327 let language = language
1328 .split_whitespace()
1329 .next()
1330 .unwrap_or_default()
1331 .trim_matches(['{', '}'])
1332 .trim_start_matches('.')
1333 .to_ascii_lowercase();
1334 match language.as_str() {
1335 "rs" => "rust".into(),
1336 "sh" | "shell" | "zsh" => "bash".into(),
1337 "js" | "jsx" => "javascript".into(),
1338 "ts" | "tsx" => "typescript".into(),
1339 "py" => "python".into(),
1340 "rb" => "ruby".into(),
1341 "yml" => "yaml".into(),
1342 "md" => "markdown".into(),
1343 "c++" => "cpp".into(),
1344 "c#" => "cs".into(),
1345 _ => language,
1346 }
1347}
1348
1349pub(crate) const INLINE_CODE_OPEN: &str = "▐";
1353pub(crate) const INLINE_CODE_CLOSE: &str = "▌";
1354
1355struct MarkdownRenderer<'a> {
1356 theme: &'a Theme,
1357 highlighter: &'a dyn Highlighter,
1358 width: usize,
1359 lines: Vec<MarkdownLine>,
1360 current: Vec<Span<'static>>,
1361 current_kind: MarkdownLineKind,
1362 styles: Vec<Style>,
1363 lists: Vec<ListState>,
1364 quote_depth: usize,
1365 code: Option<CodeCapture>,
1366 links: Vec<String>,
1367 image_depth: usize,
1368 table: Option<TableCapture<'a>>,
1369 stable_block: bool,
1370}
1371
1372struct TableCapture<'a> {
1375 alignments: Vec<pulldown_cmark::Alignment>,
1376 header: Vec<Vec<Span<'static>>>,
1377 rows: Vec<Vec<Vec<Span<'static>>>>,
1378 current_row: Vec<Vec<Span<'static>>>,
1379 cell: Option<Box<MarkdownRenderer<'a>>>,
1380}
1381
1382struct ListState {
1383 next: Option<u64>,
1384 item_content_indent: Option<usize>,
1385}
1386
1387struct CodeCapture {
1388 language: String,
1389 code: String,
1390 stable: bool,
1391}
1392
1393impl<'a> MarkdownRenderer<'a> {
1394 fn new(theme: &'a Theme, highlighter: &'a dyn Highlighter) -> Self {
1395 Self {
1396 theme,
1397 highlighter,
1398 width: usize::MAX,
1399 lines: Vec::new(),
1400 current: Vec::new(),
1401 current_kind: MarkdownLineKind::Prose,
1402 styles: vec![theme.body()],
1403 lists: Vec::new(),
1404 quote_depth: 0,
1405 code: None,
1406 links: Vec::new(),
1407 image_depth: 0,
1408 table: None,
1409 stable_block: false,
1410 }
1411 }
1412
1413 fn begin_block(&mut self, stable_block: bool) {
1414 self.stable_block = stable_block;
1415 }
1416
1417 fn event(&mut self, event: &Event<'static>) {
1418 if let Some(code) = &mut self.code {
1419 match event {
1420 Event::Text(text) => code.code.push_str(text),
1421 Event::End(TagEnd::CodeBlock) => self.finish_code(),
1422 _ => {}
1423 }
1424 return;
1425 }
1426 if self.table.is_some()
1429 && !matches!(
1430 event,
1431 Event::Start(Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell)
1432 | Event::End(
1433 TagEnd::Table | TagEnd::TableHead | TagEnd::TableRow | TagEnd::TableCell
1434 )
1435 )
1436 {
1437 if let Some(cell) = self
1438 .table
1439 .as_mut()
1440 .and_then(|table| table.cell.as_deref_mut())
1441 {
1442 cell.event(event);
1443 }
1444 return;
1445 }
1446 match event {
1447 Event::Start(tag) => self.start(tag),
1448 Event::End(tag) => self.end(*tag),
1449 Event::Text(text) | Event::Html(text) | Event::InlineHtml(text) => self.text(text),
1450 Event::Code(code) => {
1454 let style = self
1455 .styles
1456 .last()
1457 .copied()
1458 .unwrap_or_default()
1459 .patch(self.theme.inline_code());
1460 match self.theme.inline_code_cap() {
1461 Some(cap) => {
1462 self.span(INLINE_CODE_OPEN.to_owned(), cap);
1463 self.span(code.to_string(), style);
1464 self.span(INLINE_CODE_CLOSE.to_owned(), cap);
1465 }
1466 None if self.theme.inline_code_is_coloured() => {
1467 self.span(code.to_string(), style);
1468 }
1469 None => self.span(format!("`{code}`"), style),
1470 }
1471 }
1472 Event::InlineMath(math) => self.span(format!("${math}$"), self.theme.inline_code()),
1473 Event::DisplayMath(math) => {
1474 self.flush();
1475 self.span(format!("$$ {math} $$"), self.theme.inline_code());
1476 self.flush();
1477 }
1478 Event::FootnoteReference(reference) => {
1479 self.span(format!("[^{reference}]"), self.theme.link())
1480 }
1481 Event::SoftBreak => self.text(" "),
1482 Event::HardBreak => {
1483 self.flush();
1484 self.start_line_prefix();
1485 }
1486 Event::Rule => {
1487 self.flush();
1488 self.start_line_prefix();
1489 let prefix_width = cell_width(&self.current);
1490 let rule_width = if self.width == usize::from(u16::MAX) {
1491 16
1492 } else {
1493 self.width.saturating_sub(prefix_width)
1494 };
1495 self.span("─".repeat(rule_width), self.theme.muted());
1496 self.flush();
1497 }
1498 Event::TaskListMarker(done) => self.span(
1499 if *done { "[x] " } else { "[ ] " }.into(),
1500 self.theme.tool(),
1501 ),
1502 }
1503 }
1504
1505 fn start(&mut self, tag: &Tag<'static>) {
1506 match tag {
1507 Tag::Paragraph => self.start_line_prefix(),
1508 Tag::Heading { level, .. } => {
1509 self.flush();
1510 self.start_line_prefix();
1511 let style = match level {
1512 HeadingLevel::H1 => self
1513 .theme
1514 .heading()
1515 .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
1516 HeadingLevel::H2 => self.theme.heading().remove_modifier(Modifier::UNDERLINED),
1517 HeadingLevel::H3 | HeadingLevel::H4 | HeadingLevel::H5 | HeadingLevel::H6 => {
1518 self.theme
1519 .heading()
1520 .remove_modifier(Modifier::BOLD | Modifier::UNDERLINED)
1521 .add_modifier(Modifier::ITALIC)
1522 }
1523 };
1524 self.styles.push(style);
1525 }
1526 Tag::BlockQuote(kind) => {
1527 self.flush();
1528 self.quote_depth += 1;
1529 self.start_line_prefix();
1530 if let Some(kind) = kind {
1531 self.span(format!("{kind:?}: ").to_uppercase(), self.theme.quote());
1532 }
1533 }
1534 Tag::CodeBlock(kind) => {
1535 self.flush();
1536 self.code = Some(CodeCapture {
1537 language: match kind {
1538 CodeBlockKind::Indented => String::new(),
1539 CodeBlockKind::Fenced(language) => language.to_string(),
1540 },
1541 code: String::new(),
1542 stable: self.stable_block,
1543 });
1544 }
1545 Tag::List(start) => {
1546 self.flush();
1547 self.lists.push(ListState {
1548 next: *start,
1549 item_content_indent: None,
1550 });
1551 }
1552 Tag::Item => {
1553 self.flush();
1554 self.start_quote_prefix();
1555 let depth = self.lists.len().saturating_sub(1);
1556 self.span(" ".repeat(depth), self.theme.body());
1557 let marker = self.lists.last_mut().map_or_else(
1558 || "• ".to_owned(),
1559 |list| match &mut list.next {
1560 Some(next) => {
1561 let marker = format!("{next}. ");
1562 *next += 1;
1563 marker
1564 }
1565 None => "• ".to_owned(),
1566 },
1567 );
1568 self.span(marker, self.theme.tool());
1569 let continuation_indent = cell_width(&self.current);
1570 self.current_kind = MarkdownLineKind::ListItem {
1571 continuation_indent,
1572 };
1573 if let Some(list) = self.lists.last_mut() {
1574 list.item_content_indent = Some(continuation_indent);
1575 }
1576 }
1577 Tag::Table(alignments) => {
1578 self.flush();
1579 self.table = Some(TableCapture {
1580 alignments: alignments.clone(),
1581 header: Vec::new(),
1582 rows: Vec::new(),
1583 current_row: Vec::new(),
1584 cell: None,
1585 });
1586 }
1587 Tag::TableHead | Tag::TableRow => {
1588 if let Some(table) = &mut self.table {
1589 table.current_row = Vec::new();
1590 }
1591 }
1592 Tag::TableCell => {
1593 if let Some(table) = &mut self.table {
1594 table.cell = Some(Box::new(MarkdownRenderer::new(
1595 self.theme,
1596 self.highlighter,
1597 )));
1598 }
1599 }
1600 Tag::Emphasis => self.push_modifier(Modifier::ITALIC),
1601 Tag::Strong => self.push_modifier(Modifier::BOLD),
1602 Tag::Strikethrough => self.push_modifier(Modifier::CROSSED_OUT),
1603 Tag::Link { dest_url, .. } => {
1604 self.links.push(dest_url.to_string());
1605 self.styles.push(self.theme.link());
1606 }
1607 Tag::Image { dest_url, .. } => {
1608 self.image_depth += 1;
1609 self.links.push(dest_url.to_string());
1610 self.span("[image: ".into(), self.theme.link());
1611 }
1612 Tag::HtmlBlock
1613 | Tag::FootnoteDefinition(_)
1614 | Tag::MetadataBlock(_)
1615 | Tag::DefinitionList
1616 | Tag::DefinitionListTitle
1617 | Tag::DefinitionListDefinition
1618 | Tag::Superscript
1619 | Tag::Subscript => {}
1620 }
1621 }
1622
1623 fn end(&mut self, tag: TagEnd) {
1624 match tag {
1625 TagEnd::Paragraph => self.flush(),
1626 TagEnd::Item => {
1627 self.flush();
1628 if let Some(list) = self.lists.last_mut() {
1629 list.item_content_indent = None;
1630 }
1631 }
1632 TagEnd::Heading(_) => {
1633 self.styles.pop();
1634 self.flush();
1635 }
1636 TagEnd::BlockQuote(_) => {
1637 self.flush();
1638 self.quote_depth = self.quote_depth.saturating_sub(1);
1639 }
1640 TagEnd::List(_) => {
1641 self.flush();
1642 self.lists.pop();
1643 }
1644 TagEnd::Table => self.finish_table(),
1645 TagEnd::TableHead => {
1646 if let Some(table) = &mut self.table {
1647 table.header = std::mem::take(&mut table.current_row);
1648 }
1649 }
1650 TagEnd::TableRow => {
1651 if let Some(table) = &mut self.table {
1652 table.rows.push(std::mem::take(&mut table.current_row));
1653 }
1654 }
1655 TagEnd::TableCell => {
1656 if let Some(table) = &mut self.table
1657 && let Some(cell) = table.cell.take()
1658 {
1659 table.current_row.push(
1660 cell.finish()
1661 .into_iter()
1662 .next()
1663 .map(|line| line.line.spans)
1664 .unwrap_or_default(),
1665 );
1666 }
1667 }
1668 TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => {
1669 self.styles.pop();
1670 }
1671 TagEnd::Link => {
1672 self.styles.pop();
1673 if let Some(destination) = self.links.pop() {
1674 self.span(format!(" <{destination}>"), self.theme.link());
1675 }
1676 }
1677 TagEnd::Image => {
1678 if let Some(destination) = self.links.pop() {
1679 self.span(format!(" -> {destination}]"), self.theme.link());
1680 }
1681 self.image_depth = self.image_depth.saturating_sub(1);
1682 }
1683 TagEnd::CodeBlock
1684 | TagEnd::HtmlBlock
1685 | TagEnd::FootnoteDefinition
1686 | TagEnd::MetadataBlock(_)
1687 | TagEnd::DefinitionList
1688 | TagEnd::DefinitionListTitle
1689 | TagEnd::DefinitionListDefinition
1690 | TagEnd::Superscript
1691 | TagEnd::Subscript => {}
1692 }
1693 }
1694
1695 fn push_modifier(&mut self, modifier: Modifier) {
1696 let style = self
1697 .styles
1698 .last()
1699 .copied()
1700 .unwrap_or_default()
1701 .add_modifier(modifier);
1702 self.styles.push(style);
1703 }
1704
1705 fn text(&mut self, text: &str) {
1706 for (index, part) in text.split('\n').enumerate() {
1707 if index > 0 {
1708 self.flush();
1709 self.start_line_prefix();
1710 }
1711 if !part.is_empty() {
1712 self.span(
1713 part.to_owned(),
1714 self.styles.last().copied().unwrap_or_default(),
1715 );
1716 }
1717 }
1718 }
1719
1720 fn start_line_prefix(&mut self) {
1721 if !self.current.is_empty() {
1722 return;
1723 }
1724 self.start_quote_prefix();
1725 if let Some(continuation_indent) = self
1726 .lists
1727 .iter()
1728 .rev()
1729 .find_map(|list| list.item_content_indent)
1730 {
1731 let prefix_width = cell_width(&self.current);
1732 self.span(
1733 " ".repeat(continuation_indent.saturating_sub(prefix_width)),
1734 self.theme.body(),
1735 );
1736 self.current_kind = MarkdownLineKind::ListItem {
1737 continuation_indent,
1738 };
1739 }
1740 }
1741
1742 fn start_quote_prefix(&mut self) {
1743 if self.current.is_empty() && self.quote_depth > 0 {
1744 self.span("> ".repeat(self.quote_depth), self.theme.quote());
1745 }
1746 }
1747
1748 fn span(&mut self, content: String, style: Style) {
1749 if let Some(last) = self.current.last_mut()
1750 && last.style == style
1751 {
1752 last.content.to_mut().push_str(&content);
1753 } else {
1754 self.current.push(Span::styled(content, style));
1755 }
1756 }
1757
1758 fn flush(&mut self) {
1759 if !self.current.is_empty() {
1760 self.lines.push(MarkdownLine {
1761 line: Line::from(std::mem::take(&mut self.current)),
1762 kind: self.current_kind,
1763 });
1764 self.current_kind = MarkdownLineKind::Prose;
1765 }
1766 }
1767
1768 fn finish_code(&mut self) {
1769 let Some(code) = self.code.take() else {
1770 return;
1771 };
1772 let label = code.language.split_whitespace().next().unwrap_or_default();
1773 let highlighted = if code.stable {
1776 self.highlighter
1777 .highlight_stable(label, &code.code, self.theme)
1778 } else {
1779 self.highlighter.highlight(label, &code.code, self.theme)
1780 };
1781 match self.theme.code_background() {
1783 Some(background) => self.finish_banded_code(&highlighted, background),
1784 None => self.finish_framed_code(&highlighted),
1785 }
1786 }
1787
1788 fn finish_banded_code(&mut self, highlighted: &[Line<'static>], background: Color) {
1793 let quote_prefix = "> ".repeat(self.quote_depth);
1794 let band = Style::default().bg(background);
1795 let marker = self.theme.code_border().patch(band);
1796 let quote_width = UnicodeWidthStr::width(quote_prefix.as_str());
1797 let bounded = self.width != usize::from(u16::MAX);
1798 let content_width = self.width.saturating_sub(quote_width + 1).max(1);
1799 let mut rows = Vec::new();
1800 for line in highlighted {
1801 for (index, content) in wrap_code_spans(
1802 expand_tabs_in_spans(line.spans.clone()),
1803 content_width,
1804 content_width,
1805 )
1806 .into_iter()
1807 .enumerate()
1808 {
1809 let mut spans = vec![Span::styled(if index == 0 { " " } else { "↪" }, marker)];
1810 spans.extend(content.into_iter().map(|mut span| {
1811 span.style = span.style.patch(band);
1812 span
1813 }));
1814 rows.push(spans);
1815 }
1816 }
1817 let band_width = if bounded {
1818 self.width.saturating_sub(quote_width)
1819 } else {
1820 rows.iter()
1821 .map(|row| row.iter().map(Span::width).sum::<usize>())
1822 .max()
1823 .unwrap_or(0)
1824 + 1
1825 };
1826 for mut row in rows {
1827 let used = row.iter().map(Span::width).sum::<usize>();
1828 if used < band_width {
1829 row.push(Span::styled(" ".repeat(band_width - used), band));
1830 }
1831 let mut spans = vec![Span::styled(quote_prefix.clone(), self.theme.quote())];
1832 spans.extend(row);
1833 self.push_code_line(Line::from(spans));
1834 }
1835 }
1836
1837 fn finish_framed_code(&mut self, highlighted: &[Line<'static>]) {
1840 let quote_prefix = "> ".repeat(self.quote_depth);
1841 let border = self.theme.code_border();
1842 self.push_code_line(Line::from(vec![
1843 Span::styled(quote_prefix.clone(), self.theme.quote()),
1844 Span::styled("┌─ code".to_owned(), border),
1845 ]));
1846 let quote_width = UnicodeWidthStr::width(quote_prefix.as_str());
1847 let first_width = self.width.saturating_sub(quote_width + 2).max(1);
1848 let continuation_width = self.width.saturating_sub(quote_width + 1).max(1);
1849 for line in highlighted {
1850 let line_style = line.style;
1851 for (index, content) in wrap_code_spans(
1852 expand_tabs_in_spans(line.spans.clone()),
1853 first_width,
1854 continuation_width,
1855 )
1856 .into_iter()
1857 .enumerate()
1858 {
1859 let mut spans = vec![
1860 Span::styled(quote_prefix.clone(), self.theme.quote()),
1861 Span::styled(if index == 0 { "│ " } else { "│" }, border),
1862 ];
1863 spans.extend(content);
1864 self.push_code_line(Line::from(spans).style(line_style));
1865 }
1866 }
1867 self.push_code_line(Line::from(vec![
1868 Span::styled(quote_prefix, self.theme.quote()),
1869 Span::styled("└─".to_owned(), border),
1870 ]));
1871 }
1872
1873 fn push_code_line(&mut self, line: Line<'static>) {
1874 self.lines.push(MarkdownLine {
1875 line,
1876 kind: MarkdownLineKind::Code,
1877 });
1878 }
1879
1880 fn finish_table(&mut self) {
1885 let Some(table) = self.table.take() else {
1886 return;
1887 };
1888 let quote_prefix = "> ".repeat(self.quote_depth);
1889 let prefix_width = UnicodeWidthStr::width(quote_prefix.as_str());
1890 let available = self.width.saturating_sub(prefix_width).max(1);
1891 for line in render_table(
1892 &table.alignments,
1893 &table.header,
1894 &table.rows,
1895 "e_prefix,
1896 available,
1897 self.theme,
1898 ) {
1899 self.lines.push(MarkdownLine {
1900 line,
1901 kind: MarkdownLineKind::Table,
1902 });
1903 }
1904 }
1905
1906 fn finish(mut self) -> Vec<MarkdownLine> {
1907 self.flush();
1908 if self.lines.is_empty() {
1909 self.lines.push(MarkdownLine::default());
1910 }
1911 self.lines
1912 }
1913}
1914
1915pub(crate) const TAB_WIDTH: usize = 8;
1917
1918pub(crate) fn tab_advance(column: usize) -> usize {
1920 TAB_WIDTH - column % TAB_WIDTH
1921}
1922
1923pub(crate) fn expand_tabs_in_spans(spans: Vec<Span<'static>>) -> Vec<Span<'static>> {
1928 let mut column = 0;
1929 spans
1930 .into_iter()
1931 .map(|span| {
1932 if !span.content.contains('\t') {
1933 column += span.width();
1934 return span;
1935 }
1936 let mut content = String::with_capacity(span.content.len());
1937 for character in span.content.chars() {
1938 if character == '\t' {
1939 let advance = tab_advance(column);
1940 content.extend(std::iter::repeat_n(' ', advance));
1941 column += advance;
1942 } else {
1943 content.push(character);
1944 column += unicode_width::UnicodeWidthChar::width(character).unwrap_or(0);
1945 }
1946 }
1947 Span::styled(content, span.style)
1948 })
1949 .collect()
1950}
1951
1952pub(crate) fn wrap_code_spans(
1953 spans: Vec<Span<'static>>,
1954 first_width: usize,
1955 continuation_width: usize,
1956) -> Vec<Vec<Span<'static>>> {
1957 let mut lines = Vec::new();
1958 let mut current: Vec<Span<'static>> = Vec::new();
1959 let mut current_width = 0;
1960 let mut available = first_width.max(1);
1961
1962 for span in spans {
1963 for grapheme in span.content.graphemes(true) {
1964 let mut grapheme_width = UnicodeWidthStr::width(grapheme);
1965 if current_width > 0 && current_width + grapheme_width > available {
1966 lines.push(std::mem::take(&mut current));
1967 current_width = 0;
1968 available = continuation_width.max(1);
1969 }
1970 let grapheme = if grapheme_width > available {
1971 grapheme_width = 1;
1972 "�"
1973 } else {
1974 grapheme
1975 };
1976 if let Some(last) = current.last_mut()
1977 && last.style == span.style
1978 {
1979 last.content.to_mut().push_str(grapheme);
1980 } else {
1981 current.push(Span::styled(grapheme.to_owned(), span.style));
1982 }
1983 current_width += grapheme_width;
1984 }
1985 }
1986 if !current.is_empty() || lines.is_empty() {
1987 lines.push(current);
1988 }
1989 lines
1990}
1991
1992#[cfg(test)]
1993mod tests;