Skip to main content

markdown/
parse.rs

1//! Markdown → [`Doc`].
2//!
3//! CommonMark nests; a [`Doc`] does not. Indent counts **list nesting only**:
4//!
5//! - a list item's first paragraph becomes its marker block (bullet, ordered,
6//!   task) at one level shallower than the open list count, and anything else
7//!   in that item becomes a child at the list count itself;
8//! - everything else keeps its kind at the open list count.
9//!
10//! A block inside a blockquote carries a [`Block::quote`] with the GFM alert
11//! the blockquote opened with. A blockquote never adds to the indent.
12//! Blockquotes nested in one another flatten to one, and the fixed-point test
13//! in [`crate::serialize`] keeps whatever the first parse decides.
14//!
15//! The parse also normalizes what markdown itself would not preserve: leading
16//! and trailing whitespace per line, blank lines at a block's edges, headings
17//! and table cells flattened to one line, and ordered runs renumbered
18//! consecutively. Each of those is a place where writing the document back out
19//! and reading it again would otherwise land somewhere new.
20
21use pulldown_cmark::{
22    Alignment, BlockQuoteKind, CodeBlockKind, Event, LinkType, Options, Parser, Tag, TagEnd,
23};
24use std::ops::Range;
25
26use crate::{
27    doc::{Align, Block, BlockKind, Doc, Form, Mark, MarkSpan, QuoteKind, Quoted, Text},
28    marks::Marks,
29    select::Cursor,
30};
31
32mod inline;
33mod lift;
34mod state;
35
36pub use inline::*;
37pub use lift::*;
38use state::*;
39
40/// A word and a size after it, as a fence's info string and a link's title
41/// spell them: `mermaid 360`, `embed 360`. A last word that is not a positive
42/// whole number is part of the first.
43pub(crate) fn split_size(spelled: &str) -> (&str, Option<u32>) {
44    let Some((name, tail)) = spelled.rsplit_once(char::is_whitespace) else {
45        return (spelled, None);
46    };
47    match tail.parse().ok().filter(|size| *size > 0) {
48        Some(size) => (name.trim_end(), Some(size)),
49        None => (spelled, None),
50    }
51}
52
53/// The extensions this crate reads. [`crate::source`] colours with the same set.
54pub(crate) const OPTIONS: Options = Options::ENABLE_TABLES
55    .union(Options::ENABLE_STRIKETHROUGH)
56    .union(Options::ENABLE_TASKLISTS)
57    .union(Options::ENABLE_GFM);
58
59impl From<BlockQuoteKind> for QuoteKind {
60    fn from(kind: BlockQuoteKind) -> Self {
61        match kind {
62            BlockQuoteKind::Note => Self::Note,
63            BlockQuoteKind::Tip => Self::Tip,
64            BlockQuoteKind::Important => Self::Important,
65            BlockQuoteKind::Warning => Self::Warning,
66            BlockQuoteKind::Caution => Self::Caution,
67        }
68    }
69}
70
71/// Parse a markdown document.
72pub fn parse(source: &str) -> Doc {
73    parse_plain(source)
74}
75
76/// A parse, and where in the source each block came from.
77pub struct ParsedDoc {
78    pub doc: Doc,
79    /// One range per `doc.blocks` entry, in document order.
80    ///
81    /// The ranges partition the source: the first starts at 0, each one ends
82    /// where the next begins, and the last ends at `source.len()`. Splicing
83    /// them back in order reproduces the source byte for byte.
84    ///
85    /// A block the source spells inside another's bytes — the empty bullet of
86    /// `- ![](cover.png)` — takes an empty range. The one per entry holds
87    /// either way.
88    pub block_ranges: Vec<Range<usize>>,
89}
90
91/// [`parse`], keeping the source range each block was parsed from.
92pub fn parse_ranges(source: &str) -> ParsedDoc {
93    let (doc, starts) = parse_spanned(source);
94    ParsedDoc {
95        block_ranges: ranges(&starts, source.len()),
96        doc,
97    }
98}
99
100impl From<&str> for Doc {
101    fn from(source: &str) -> Self {
102        parse(source)
103    }
104}
105
106impl From<&str> for ParsedDoc {
107    fn from(source: &str) -> Self {
108        parse_ranges(source)
109    }
110}
111
112impl From<(&str, &Marks)> for Doc {
113    fn from((source, marks): (&str, &Marks)) -> Self {
114        parse_with(source, marks)
115    }
116}
117
118/// Block starts, in document order, to one range each.
119///
120/// Each block runs to where the next one starts, so the partition is the
121/// shape of the loop rather than something the parser has to get right: the
122/// first range opens at 0, the last closes at `len`, and a start that arrives
123/// behind the one before it takes an empty range instead of a backwards one.
124fn ranges(starts: &[usize], len: usize) -> Vec<Range<usize>> {
125    let mut out = Vec::with_capacity(starts.len());
126    let mut at = 0;
127    for &start in starts.iter().skip(1) {
128        let start = start.clamp(at, len);
129        out.push(at..start);
130        at = start;
131    }
132    if !starts.is_empty() {
133        out.push(at..len);
134    }
135    out
136}
137
138/// [`parse`] with the app's own marks — see [`crate::Marks`].
139///
140/// Registered delimiters are lifted out of the source *before* CommonMark sees
141/// it, which is the only place the difference between `==` and `\=\=` still
142/// exists: a backslash escape is gone by the time there is a [`Text`] to scan,
143/// and a pass over one would read an escaped delimiter back as a mark and move
144/// the document on every save.
145pub fn parse_with(source: &str, marks: &Marks) -> Doc {
146    if marks.is_empty() {
147        return parse_plain(source);
148    }
149    let mut doc = parse_plain(&lift(source, marks));
150    for block in &mut doc.blocks {
151        for part in block.parts() {
152            if let Some(text) = block.text_at_mut(part) {
153                settle(text, marks);
154            }
155        }
156    }
157    doc
158}
159
160fn parse_plain(source: &str) -> Doc {
161    parse_spanned(source).0
162}
163
164/// The parse every entry point runs, with the offset each block started at.
165///
166/// `renumber` rewrites numbers and adds no block, so the starts stay one per
167/// block — the invariant [`ParsedDoc::block_ranges`] rests on.
168fn parse_spanned(source: &str) -> (Doc, Vec<usize>) {
169    let mut state = ParseState::default();
170    for (event, range) in Parser::new_ext(source, OPTIONS).into_offset_iter() {
171        state.event(event, range);
172    }
173    state.doc.renumber();
174    (state.doc, state.starts)
175}
176
177/// Accumulates one run of inline content and the marks over it.
178#[derive(Default)]
179struct TextBuilder {
180    text: String,
181    marks: Vec<MarkSpan>,
182    /// Indices into `marks` for the marks still open, innermost last.
183    open: Vec<usize>,
184}
185
186impl TextBuilder {
187    /// Open a mark at the cursor. Marks land in the list in the order they
188    /// open, which is outermost first — the ordering [`crate::serialize`] reads
189    /// back to reproduce the nesting.
190    fn open(&mut self, mark: Mark) {
191        let ix = self.marks.len();
192        let at = self.text.len();
193        self.marks.push(MarkSpan {
194            range: at..at,
195            mark,
196        });
197        self.open.push(ix);
198    }
199
200    /// Whether anything at all has accumulated — an image with no alt text is
201    /// a mark and no text, and still has to close as a block.
202    fn is_empty(&self) -> bool {
203        self.text.is_empty() && self.marks.is_empty()
204    }
205
206    fn close(&mut self) {
207        if let Some(ix) = self.open.pop() {
208            self.marks[ix].range.end = self.text.len();
209        }
210    }
211
212    /// A mark that opens and closes around `s` in one event (inline code).
213    fn wrap(&mut self, mark: Mark, s: &str) {
214        let start = self.text.len();
215        self.text.push_str(s);
216        self.marks.push(MarkSpan {
217            range: start..self.text.len(),
218            mark,
219        });
220    }
221
222    fn take(&mut self) -> Text {
223        self.open.clear();
224        let mut text = normalize(
225            &std::mem::take(&mut self.text),
226            &std::mem::take(&mut self.marks),
227        );
228        settle_mentions(&mut text);
229        linkify(&mut text);
230        text
231    }
232}