Skip to main content

markdown/
source.rs

1//! Colouring markdown *source*.
2//!
3//! The one language this crate can classify without help: it already parses
4//! markdown, and a source view needs colour on the platforms
5//! [`crate::highlight`] cannot reach — tree-sitter is C, and a browser has no
6//! libc to build it against.
7//!
8//! Spans are painted into a per-byte map rather than pushed as they arrive,
9//! because markdown nests — a link inside a heading inside a quote — and the
10//! renderer needs them disjoint and in order. Inner events land last and win.
11
12use std::ops::Range;
13
14use pulldown_cmark::{Event, Parser, Tag};
15use theme::HighlightKind;
16
17/// The fence tags that mean "this is markdown".
18pub const LANGUAGES: [&str; 2] = ["md", "markdown"];
19
20/// Whether a fence tag names markdown.
21pub fn is_markdown(language: &str) -> bool {
22    LANGUAGES.contains(&language)
23}
24
25/// Colour `source` as markdown, in bytes and in document order.
26pub fn spans(source: &str) -> Vec<(Range<usize>, HighlightKind)> {
27    let mut map: Vec<Option<HighlightKind>> = vec![None; source.len()];
28    let mut paint = |range: Range<usize>, kind: HighlightKind| {
29        for slot in &mut map[range.start.min(source.len())..range.end.min(source.len())] {
30            *slot = Some(kind);
31        }
32    };
33
34    for (event, range) in Parser::new_ext(source, crate::parse::OPTIONS).into_offset_iter() {
35        match event {
36            Event::Start(Tag::Heading { .. }) => paint(range, HighlightKind::Keyword),
37            Event::Start(Tag::BlockQuote(_)) => paint(range, HighlightKind::Comment),
38            Event::Start(Tag::CodeBlock(_)) | Event::Code(_) => paint(range, HighlightKind::String),
39            Event::Start(Tag::Strong | Tag::Emphasis | Tag::Strikethrough) => {
40                paint(range, HighlightKind::Constant);
41            }
42            // The destination only: a link's label is prose and reads as prose.
43            // An autolink has no `](` and is a destination all through.
44            Event::Start(Tag::Link { .. } | Tag::Image { .. }) => {
45                let at = source[range.clone()]
46                    .rfind("](")
47                    .map_or(range.start, |ix| range.start + ix);
48                paint(at..range.end, HighlightKind::Attribute);
49            }
50            // The marker, not the item: the text of a list is prose too.
51            Event::Start(Tag::Item) => paint(marker(source, range), HighlightKind::Punctuation),
52            Event::Start(Tag::Table(_)) => {
53                for (ix, _) in source[range.clone()].match_indices('|') {
54                    paint(
55                        range.start + ix..range.start + ix + 1,
56                        HighlightKind::Punctuation,
57                    );
58                }
59            }
60            Event::TaskListMarker(_) => paint(range, HighlightKind::Boolean),
61            Event::Rule => paint(range, HighlightKind::Punctuation),
62            Event::Html(_) | Event::InlineHtml(_) => paint(range, HighlightKind::Tag),
63            _ => {}
64        }
65    }
66
67    // Run-length encoded back out, which is what makes the result disjoint and
68    // ordered however deeply the source nested.
69    let mut spans: Vec<(Range<usize>, HighlightKind)> = Vec::new();
70    for (at, kind) in map.into_iter().enumerate() {
71        let Some(kind) = kind else { continue };
72        match spans.last_mut() {
73            Some((range, last)) if *last == kind && range.end == at => range.end = at + 1,
74            _ => spans.push((at..at + 1, kind)),
75        }
76    }
77    spans
78}
79
80/// A list item's marker: the indent, the bullet or number, and the space after
81/// it. Everything the item's own range holds before its text starts.
82fn marker(source: &str, range: Range<usize>) -> Range<usize> {
83    let item = &source[range.clone()];
84    let text = item.trim_start();
85    let start = range.start + (item.len() - text.len());
86    let width = text
87        .find(char::is_whitespace)
88        .map_or(text.len(), |ix| ix + 1);
89    start..start + width
90}