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