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
37/// The extensions this crate reads. [`crate::source`] colours with the same set.
38pub(crate) const OPTIONS: Options = Options::ENABLE_TABLES
39    .union(Options::ENABLE_STRIKETHROUGH)
40    .union(Options::ENABLE_TASKLISTS)
41    .union(Options::ENABLE_GFM);
42
43impl From<BlockQuoteKind> for QuoteKind {
44    fn from(kind: BlockQuoteKind) -> Self {
45        match kind {
46            BlockQuoteKind::Note => Self::Note,
47            BlockQuoteKind::Tip => Self::Tip,
48            BlockQuoteKind::Important => Self::Important,
49            BlockQuoteKind::Warning => Self::Warning,
50            BlockQuoteKind::Caution => Self::Caution,
51        }
52    }
53}
54
55/// Parse a markdown document.
56pub fn parse(source: &str) -> Doc {
57    parse_plain(source)
58}
59
60/// A parse, and where in the source each block came from.
61pub struct ParsedDoc {
62    pub doc: Doc,
63    /// One range per `doc.blocks` entry, in document order.
64    ///
65    /// The ranges partition the source: the first starts at 0, each one ends
66    /// where the next begins, and the last ends at `source.len()`. Splicing
67    /// them back in order reproduces the source byte for byte.
68    ///
69    /// A block the source spells inside another's bytes — the empty bullet of
70    /// `- ![](cover.png)` — takes an empty range. The one per entry holds
71    /// either way.
72    pub block_ranges: Vec<Range<usize>>,
73}
74
75/// [`parse`], keeping the source range each block was parsed from.
76pub fn parse_ranges(source: &str) -> ParsedDoc {
77    let (doc, starts) = parse_spanned(source);
78    ParsedDoc {
79        block_ranges: ranges(&starts, source.len()),
80        doc,
81    }
82}
83
84impl From<&str> for Doc {
85    fn from(source: &str) -> Self {
86        parse(source)
87    }
88}
89
90impl From<&str> for ParsedDoc {
91    fn from(source: &str) -> Self {
92        parse_ranges(source)
93    }
94}
95
96impl From<(&str, &Marks)> for Doc {
97    fn from((source, marks): (&str, &Marks)) -> Self {
98        parse_with(source, marks)
99    }
100}
101
102/// Block starts, in document order, to one range each.
103///
104/// Each block runs to where the next one starts, so the partition is the
105/// shape of the loop rather than something the parser has to get right: the
106/// first range opens at 0, the last closes at `len`, and a start that arrives
107/// behind the one before it takes an empty range instead of a backwards one.
108fn ranges(starts: &[usize], len: usize) -> Vec<Range<usize>> {
109    let mut out = Vec::with_capacity(starts.len());
110    let mut at = 0;
111    for &start in starts.iter().skip(1) {
112        let start = start.clamp(at, len);
113        out.push(at..start);
114        at = start;
115    }
116    if !starts.is_empty() {
117        out.push(at..len);
118    }
119    out
120}
121
122/// [`parse`] with the app's own marks — see [`crate::Marks`].
123///
124/// Registered delimiters are lifted out of the source *before* CommonMark sees
125/// it, which is the only place the difference between `==` and `\=\=` still
126/// exists: a backslash escape is gone by the time there is a [`Text`] to scan,
127/// and a pass over one would read an escaped delimiter back as a mark and move
128/// the document on every save.
129pub fn parse_with(source: &str, marks: &Marks) -> Doc {
130    if marks.is_empty() {
131        return parse_plain(source);
132    }
133    let mut doc = parse_plain(&lift(source, marks));
134    for block in &mut doc.blocks {
135        for part in block.parts() {
136            if let Some(text) = block.text_at_mut(part) {
137                settle(text, marks);
138            }
139        }
140    }
141    doc
142}
143
144fn parse_plain(source: &str) -> Doc {
145    parse_spanned(source).0
146}
147
148/// The parse every entry point runs, with the offset each block started at.
149///
150/// `renumber` rewrites numbers and adds no block, so the starts stay one per
151/// block — the invariant [`ParsedDoc::block_ranges`] rests on.
152fn parse_spanned(source: &str) -> (Doc, Vec<usize>) {
153    let mut state = ParseState::default();
154    for (event, range) in Parser::new_ext(source, OPTIONS).into_offset_iter() {
155        state.event(event, range);
156    }
157    state.doc.renumber();
158    (state.doc, state.starts)
159}
160
161/// Accumulates one run of inline content and the marks over it.
162#[derive(Default)]
163struct TextBuilder {
164    text: String,
165    marks: Vec<MarkSpan>,
166    /// Indices into `marks` for the marks still open, innermost last.
167    open: Vec<usize>,
168}
169
170impl TextBuilder {
171    /// Open a mark at the cursor. Marks land in the list in the order they
172    /// open, which is outermost first — the ordering [`crate::serialize`] reads
173    /// back to reproduce the nesting.
174    fn open(&mut self, mark: Mark) {
175        let ix = self.marks.len();
176        let at = self.text.len();
177        self.marks.push(MarkSpan {
178            range: at..at,
179            mark,
180        });
181        self.open.push(ix);
182    }
183
184    /// Whether anything at all has accumulated — an image with no alt text is
185    /// a mark and no text, and still has to close as a block.
186    fn is_empty(&self) -> bool {
187        self.text.is_empty() && self.marks.is_empty()
188    }
189
190    fn close(&mut self) {
191        if let Some(ix) = self.open.pop() {
192            self.marks[ix].range.end = self.text.len();
193        }
194    }
195
196    /// A mark that opens and closes around `s` in one event (inline code).
197    fn wrap(&mut self, mark: Mark, s: &str) {
198        let start = self.text.len();
199        self.text.push_str(s);
200        self.marks.push(MarkSpan {
201            range: start..self.text.len(),
202            mark,
203        });
204    }
205
206    fn take(&mut self) -> Text {
207        self.open.clear();
208        let mut text = normalize(
209            &std::mem::take(&mut self.text),
210            &std::mem::take(&mut self.marks),
211        );
212        settle_mentions(&mut text);
213        linkify(&mut text);
214        text
215    }
216}
217
218/// A mention the shorthand cannot spell says its name instead.
219///
220/// [`Form::Auto`] records that `<url>` was written. Where the angles cannot be
221/// written back — a `mailto:`, a boundary inside a span emitted whole — the
222/// form settles here, so the document already holds what the next parse would
223/// produce. A mention alone in its paragraph passes and stays `Auto`, which is
224/// what leaves it to become a card.
225fn settle_mentions(text: &mut Text) {
226    let settled: Vec<usize> = (0..text.marks.len())
227        .filter(|ix| {
228            matches!(
229                text.marks[*ix].mark,
230                Mark::Mention {
231                    form: Form::Auto,
232                    ..
233                }
234            ) && !is_shorthand(text, *ix)
235        })
236        .collect();
237    for ix in settled {
238        if let Mark::Mention { form, .. } = &mut text.marks[ix].mark {
239            *form = Form::Chip;
240        }
241    }
242}
243
244/// Whether the mark at `ix` can be written with the `<url>` shorthand.
245///
246/// The angles hold a bare URL and nothing else, so a mention has to *be* its
247/// URL: `<mailto:x>` is an autolink this cannot spell that way, and
248/// `**<https://x>**` has a boundary inside a span that is written whole and so
249/// has nowhere to put it. Everything that fails here still has the explicit
250/// spelling to fall back on, which is why nothing ever has to stop being a
251/// mention.
252pub(crate) fn is_shorthand(text: &Text, ix: usize) -> bool {
253    let span = &text.marks[ix];
254    let Mark::Mention { url, form } = &span.mark else {
255        return false;
256    };
257    *form == Form::Auto
258        && text.text.get(span.range.clone()) == Some(url.as_str())
259        && is_url(url)
260        && text.alone(ix)
261}
262
263/// The schemes a bare URL may carry. Narrow on purpose: a scheme and no
264/// whitespace. Anything cleverer starts linking text that merely contains a dot.
265const SCHEMES: [&str; 2] = ["https://", "http://"];
266
267/// Every bare URL in `text`, as byte ranges.
268///
269/// One scan answers two questions that have to agree: what [`linkify`] marks,
270/// and what [`crate::serialize`] may write without brackets. Split them and the
271/// round trip drifts the first time the two disagree about a trailing bracket.
272pub(crate) fn urls(text: &str) -> Vec<Range<usize>> {
273    let mut found = Vec::new();
274    let mut at = 0;
275    while at < text.len() {
276        let Some((start, scheme)) = SCHEMES
277            .iter()
278            .filter_map(|scheme| text[at..].find(scheme).map(|ix| (at + ix, *scheme)))
279            .min_by_key(|(ix, _)| *ix)
280        else {
281            break;
282        };
283        let stop = text[start..]
284            .find(char::is_whitespace)
285            .map_or(text.len(), |ix| start + ix);
286        let end = start + trim_url(&text[start..stop]);
287        // A scheme mid-word belongs to the word, and a scheme with no host
288        // behind it is not a URL.
289        let opens = text[..start]
290            .chars()
291            .next_back()
292            .is_none_or(|c| !c.is_alphanumeric());
293        if opens && end > start + scheme.len() {
294            found.push(start..end);
295        }
296        at = stop.max(start + 1);
297    }
298    found
299}
300
301/// Whether `source` is exactly one bare URL, and nothing else.
302///
303/// The question an editor asks of a paste, and the one [`crate::serialize`]
304/// asks before writing a link bare — the same question, so it is one function.
305pub fn is_url(source: &str) -> bool {
306    matches!(urls(source).as_slice(), [only] if *only == (0..source.len()))
307}
308
309/// Whether a URL or a path names a picture, by the only thing either says
310/// about itself without being fetched — its extension, against what gpui can
311/// decode.
312///
313/// What decides whether a paste or a drop is worth offering as an image. A
314/// server is free to disagree; the answer is a guess about a name, and the
315/// alternative is a menu row that paints a broken box.
316pub fn is_image(source: &str) -> bool {
317    let path = source.split(['?', '#']).next().unwrap_or(source);
318    let Some((_, extension)) = path.rsplit_once('.') else {
319        return false;
320    };
321    matches!(
322        extension.to_ascii_lowercase().as_str(),
323        "png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "ico" | "tif" | "tiff" | "avif"
324    )
325}
326
327/// How much of a run the URL is. Closing punctuation belongs to the sentence,
328/// and a bracket only belongs to the URL when the URL opened it.
329fn trim_url(run: &str) -> usize {
330    let mut end = run.len();
331    while let Some(last) = run[..end].chars().next_back() {
332        let keep = match last {
333            '.' | ',' | ';' | ':' | '!' | '?' | '\'' | '"' => false,
334            ')' => run[..end].matches('(').count() >= run[..end].matches(')').count(),
335            ']' => run[..end].matches('[').count() >= run[..end].matches(']').count(),
336            _ => true,
337        };
338        if keep {
339            break;
340        }
341        end -= last.len_utf8();
342    }
343    end
344}
345
346/// Mark the bare URLs in a run.
347///
348/// CommonMark links `<http://x>` and nothing else, so a URL typed on its own
349/// arrives as text. Marking it here is what lets a reader click it, what lets
350/// [`crate::serialize`] write it back without brackets, and what makes a URL
351/// alone in a block a [`BlockKind::Bookmark`].
352fn linkify(text: &mut Text) {
353    let fresh: Vec<Range<usize>> = urls(&text.text)
354        .into_iter()
355        .filter(|range| {
356            // A URL already inside a link, an image target or a code span is
357            // spelled by that mark, not by this one.
358            !text.marks.iter().any(|span| {
359                matches!(
360                    span.mark,
361                    Mark::Link(_) | Mark::Mention { .. } | Mark::Image(_) | Mark::Code
362                ) && span.range.start < range.end
363                    && range.start < span.range.end
364            })
365        })
366        .collect();
367    for range in fresh {
368        let url = text.text[range.clone()].to_string();
369        text.marks.push(MarkSpan {
370            range,
371            mark: Mark::Link(url),
372        });
373    }
374}
375
376/// Drop the whitespace markdown itself drops, and move the marks with it.
377///
378/// Leading and trailing spaces on a line are not content — one trailing space
379/// is insignificant, two are a hard break, and a continuation line's indent
380/// belongs to block structure. Keeping them would mean writing out whitespace
381/// that the next parse discards, so the document would change every time it was
382/// saved. Blank lines at either end of a block go the same way.
383pub(crate) fn normalize(text: &str, marks: &[MarkSpan]) -> Text {
384    let bytes = text.as_bytes();
385    let mut keep = vec![true; text.len()];
386
387    let mut line_begin = 0;
388    for offset in memchr_newlines(text).chain([text.len()]) {
389        let line = &text[line_begin..offset];
390        let lead = line.len() - line.trim_start_matches([' ', '\t']).len();
391        let trail = line.len() - line.trim_end_matches([' ', '\t']).len();
392        keep[line_begin..line_begin + lead].fill(false);
393        keep[offset - trail..offset].fill(false);
394        line_begin = offset + 1;
395    }
396
397    let mut head = 0;
398    while head < text.len() && (!keep[head] || bytes[head] == b'\n') {
399        keep[head] = false;
400        head += 1;
401    }
402    let mut tail = text.len();
403    while tail > 0 && (!keep[tail - 1] || bytes[tail - 1] == b'\n') {
404        keep[tail - 1] = false;
405        tail -= 1;
406    }
407
408    let mut out = String::with_capacity(text.len());
409    let mut map = vec![0; text.len() + 1];
410    for (offset, ch) in text.char_indices() {
411        map[offset] = out.len();
412        if keep[offset] {
413            out.push(ch);
414        }
415    }
416    map[text.len()] = out.len();
417
418    let marks = marks
419        .iter()
420        .map(|span| MarkSpan {
421            range: map[span.range.start]..map[span.range.end],
422            mark: span.mark.clone(),
423        })
424        // A mark left covering nothing has no spelling that survives a
425        // round trip — `****` is literal text, not empty bold. An image is the
426        // exception: `![](url)` is exactly a mark over no alt text.
427        .filter(|span| !span.range.is_empty() || matches!(span.mark, Mark::Image(_)))
428        .collect();
429
430    Text {
431        text: out,
432        marks: merge_same_mark(marks),
433    }
434}
435
436/// Fuse spans of the same mark that overlap or nest.
437///
438/// Emphasis inside the same emphasis is redundant — `_a _b_ c_` is italic
439/// either way — and two spans of one mark have no unambiguous spelling: written
440/// back out, the delimiters pair up differently than they came in. Collapsing
441/// them here means the parse produces the one form that survives being written
442/// and read again.
443fn merge_same_mark(mut marks: Vec<MarkSpan>) -> Vec<MarkSpan> {
444    let mut ix = 0;
445    while ix < marks.len() {
446        let mut fused = None;
447        for other in ix + 1..marks.len() {
448            let (a, b) = (&marks[ix], &marks[other]);
449            if a.mark == b.mark
450                && !matches!(a.mark, Mark::Image(_) | Mark::Mention { .. })
451                && a.range.start <= b.range.end
452                && b.range.start <= a.range.end
453            {
454                fused = Some((
455                    other,
456                    a.range.start.min(b.range.start),
457                    a.range.end.max(b.range.end),
458                ));
459                break;
460            }
461        }
462        match fused {
463            Some((other, start, end)) => {
464                marks[ix].range = start..end;
465                marks.remove(other);
466            }
467            None => ix += 1,
468        }
469    }
470    marks
471}
472
473/// Flatten a block whose serialized form is one line.
474///
475/// A setext heading (`Title\n=====`) and a table cell can both hold a line
476/// break that has nowhere to go in the output — an ATX `#` heading ends at its
477/// newline, and a second line in a cell would end the row. Both are single-line
478/// blocks in this model, and since a newline and a space are each one byte, the
479/// marks over them do not move.
480pub(crate) fn collapse_to_one_line(text: &mut Text) {
481    if text.text.contains('\n') {
482        text.text = text.text.replace('\n', " ");
483    }
484}
485
486/// Split a trailing `|480` off an image's alt text, which is where a width is
487/// written down.
488///
489/// Obsidian's spelling, and the only one the parser leaves intact: `{width=480}`
490/// trails as literal text and breaks the paragraph out of being an image at all,
491/// and `=480x` is not an image to begin with. The last `|` wins, so a caption
492/// may hold its own — but one *ending* in `|123` gives that tail up, because the
493/// escape that tells them apart on disk is gone by the time this reads it.
494fn split_width(alt: &str) -> (&str, Option<u32>) {
495    let Some((caption, tail)) = alt.rsplit_once('|') else {
496        return (alt, None);
497    };
498    // A zero would paint a picture no pixels wide, and nothing that writes one
499    // can produce it — the drag floors at `MIN_IMAGE_WIDTH`.
500    match tail.parse().ok().filter(|width| *width > 0) {
501        Some(width) => (caption, Some(width)),
502        None => (alt, None),
503    }
504}
505
506fn memchr_newlines(text: &str) -> impl Iterator<Item = usize> + '_ {
507    text.bytes()
508        .enumerate()
509        .filter_map(|(ix, b)| (b == b'\n').then_some(ix))
510}
511
512/// A list item's marker, held until the item's first paragraph arrives.
513#[derive(Clone, Copy)]
514enum Marker {
515    Bullet,
516    Ordered(u64),
517    Task(bool),
518}
519
520impl Marker {
521    fn into_kind(self, text: Text) -> BlockKind {
522        match self {
523            Self::Bullet => BlockKind::Bullet(text),
524            Self::Ordered(number) => BlockKind::Ordered { number, text },
525            Self::Task(checked) => BlockKind::Task { checked, text },
526        }
527    }
528}
529
530#[derive(Default)]
531struct TableBuild {
532    align: Vec<Align>,
533    header: Vec<Text>,
534    rows: Vec<Vec<Text>>,
535    row: Vec<Text>,
536    in_head: bool,
537}
538
539/// An open blockquote, and how many blocks the document held when it opened.
540struct OpenQuote {
541    kind: Option<QuoteKind>,
542    at: usize,
543}
544
545#[derive(Default)]
546struct ParseState {
547    doc: Doc,
548    builder: TextBuilder,
549    /// One entry per open list; `Some` counts an ordered list's next number.
550    lists: Vec<Option<u64>>,
551    /// One entry per open blockquote, innermost last.
552    quotes: Vec<OpenQuote>,
553    pending_marker: Option<Marker>,
554    heading: Option<u8>,
555    code: Option<(Option<String>, String)>,
556    table: Option<TableBuild>,
557    /// The event being handled. What a block that owns no inline run — an
558    /// empty marker, a rule — is placed from.
559    at: Range<usize>,
560    /// Where the run being accumulated started: the first event since the last
561    /// block was pushed. A paragraph is pushed by whatever *follows* it, so
562    /// the event in hand at that point is the next block's, not this one's.
563    span: Option<usize>,
564    /// Where each block started, in the order they were pushed.
565    starts: Vec<usize>,
566}
567
568impl ParseState {
569    /// Indent level for a block that is not a list marker.
570    ///
571    /// Only list nesting counts. A blockquote decides a block's *kind*, not how
572    /// deep it sits — so a code block inside a quote stays at the quote's own
573    /// level rather than acquiring an indent that nothing in the serialized
574    /// output could reproduce.
575    fn indent(&self) -> u8 {
576        self.lists.len() as u8
577    }
578
579    /// The alert kind a quoted block inherits — the innermost open blockquote's.
580    fn quote_kind(&self) -> Option<QuoteKind> {
581        self.quotes.last().and_then(|open| open.kind)
582    }
583
584    /// Append a block, clamping its indent so the document invariant holds
585    /// (first block at 0, never more than one deeper than its predecessor).
586    fn push(&mut self, kind: BlockKind, indent: u8) {
587        self.starts.push(self.span.take().unwrap_or(self.at.start));
588        let max = self.doc.blocks.last().map_or(0, |b| b.indent + 1);
589        self.doc.blocks.push(Block {
590            kind,
591            indent: indent.min(max),
592        });
593    }
594
595    /// Emit a pending marker as an empty block so a non-paragraph leaf (a code
596    /// block, a table) nests *under* its bullet instead of replacing it.
597    fn flush_marker(&mut self) {
598        let Some(marker) = self.pending_marker.take() else {
599            return;
600        };
601        let indent = self.indent().saturating_sub(1);
602        self.push(marker.into_kind(Text::default()), indent);
603    }
604
605    /// Close any inline content still open as a block.
606    ///
607    /// A *tight* list item carries no `Paragraph` tags — pulldown-cmark emits
608    /// its text directly between `Item` tags — so every block boundary has to
609    /// close the run itself rather than waiting for an end tag that never
610    /// comes. Table cells are exempt: their builder is per-cell, and closing it
611    /// here would push a block out of the middle of a table.
612    fn flush_inline(&mut self) {
613        if self.table.is_none() && !self.builder.is_empty() {
614            self.finish_paragraph();
615        }
616    }
617
618    /// Close the current run of inline content as a block.
619    fn finish_paragraph(&mut self) {
620        let text = self.builder.take();
621
622        // A paragraph that is nothing but one image is an image block — the
623        // `![](media://…)`-on-its-own-line shape. Anything else keeps the image
624        // inline, where it stays an image rather than decaying to a link.
625        if let [
626            MarkSpan {
627                range,
628                mark: Mark::Image(url),
629            },
630        ] = text.marks.as_slice()
631            && range.start == 0
632            && range.end == text.text.len()
633        {
634            let (caption, width) = split_width(&text.text);
635            let (url, alt) = (url.clone(), Text::plain(caption.to_string()));
636            self.flush_marker();
637            let indent = self.indent();
638            self.push(BlockKind::Image { url, alt, width }, indent);
639            return;
640        }
641
642        // A paragraph that is nothing but a mention is a bookmark — the same
643        // `<https://x>` that paints as a chip inside a sentence, given a line
644        // of its own. A bare URL is what someone types when they mean a link
645        // and `[Title](url)` is what a sentence spells, so carding either would
646        // leave no way to write a link that stays one — and it is the paste
647        // menu's `Dismiss` that has to write that down.
648        //
649        // A chip promotes too: off the text flow it can be a real element, and
650        // that is the only place a favicon has room to sit.
651        //
652        // The text has to *be* the URL. `[Example Site](url "chip")` alone on a
653        // line keeps its title and stays a paragraph, because promoting it
654        // would drop words someone wrote — a block shows only what the preview
655        // gave it.
656        if let [
657            MarkSpan {
658                range,
659                mark: Mark::Mention { url, form },
660            },
661        ] = text.marks.as_slice()
662            && range.start == 0
663            && range.end == text.text.len()
664            && text.text == *url
665            && is_url(url)
666        {
667            let (url, form) = (url.clone(), *form);
668            self.flush_marker();
669            let indent = self.indent();
670            self.push(BlockKind::Bookmark { url, form }, indent);
671            return;
672        }
673
674        if !self.quotes.is_empty() {
675            // The bullet comes first so the quote reads as its child rather
676            // than replacing it.
677            self.flush_marker();
678            let kind = self.quote_kind();
679            let indent = self.indent();
680            self.push(BlockKind::Quote { kind, text }, indent);
681        } else if let Some(marker) = self.pending_marker.take() {
682            let indent = self.indent().saturating_sub(1);
683            self.push(marker.into_kind(text), indent);
684        } else {
685            let indent = self.indent();
686            self.push(BlockKind::Paragraph(text), indent);
687        }
688    }
689
690    fn event(&mut self, event: Event<'_>, range: Range<usize>) {
691        // An `End` carries the range of the whole element it closes, which for
692        // a list or a quote opens well before the block that just went in.
693        // Only something that starts content can start a run.
694        if !matches!(event, Event::End(_)) {
695            self.span.get_or_insert(range.start);
696        }
697        self.at = range;
698        match event {
699            Event::Start(tag) => self.start(tag),
700            Event::End(tag) => self.end(tag),
701
702            Event::Text(t) => match &mut self.code {
703                Some((_, code)) => code.push_str(&t),
704                None => self.builder.text.push_str(&t),
705            },
706            Event::Code(t) => self.builder.wrap(Mark::Code, &t),
707            // Raw HTML is content, not structure: this model has no HTML node,
708            // so it survives as the literal text the author typed.
709            Event::Html(t) | Event::InlineHtml(t) => self.builder.text.push_str(&t),
710            // Soft and hard breaks are both just a line break in a block —
711            // the distinction has no meaning in this model, or in Notion.
712            Event::SoftBreak | Event::HardBreak => match &mut self.code {
713                Some((_, code)) => code.push('\n'),
714                None => self.builder.text.push('\n'),
715            },
716            Event::Rule => {
717                self.flush_inline();
718                self.flush_marker();
719                let indent = self.indent();
720                self.push(BlockKind::Rule, indent);
721            }
722            Event::TaskListMarker(checked) => {
723                self.pending_marker = Some(Marker::Task(checked));
724            }
725            Event::FootnoteReference(label) => {
726                self.builder.text.push_str(&format!("[^{label}]"));
727            }
728            _ => {}
729        }
730    }
731
732    fn start(&mut self, tag: Tag<'_>) {
733        match tag {
734            Tag::Heading { level, .. } => {
735                self.flush_inline();
736                self.heading = Some(level as u8);
737            }
738            Tag::BlockQuote(kind) => {
739                self.flush_inline();
740                let at = self.doc.blocks.len();
741                self.quotes.push(OpenQuote {
742                    kind: kind.map(QuoteKind::from),
743                    at,
744                });
745            }
746            Tag::CodeBlock(kind) => {
747                self.flush_inline();
748                self.flush_marker();
749                let language = match kind {
750                    CodeBlockKind::Fenced(info) => {
751                        let tag = info.split_whitespace().next().unwrap_or("");
752                        (!tag.is_empty()).then(|| tag.to_string())
753                    }
754                    CodeBlockKind::Indented => None,
755                };
756                self.code = Some((language, String::new()));
757            }
758            Tag::List(start) => {
759                self.flush_inline();
760                // An item whose content is only a nested list still has to emit
761                // its own marker first. `flush_inline` covers the item that had
762                // text; this covers the empty one, whose pending marker the
763                // nested `Start(Item)` would otherwise overwrite — losing a
764                // level of nesting. It runs before the push so the marker is
765                // numbered at the outer list's depth.
766                self.flush_marker();
767                self.lists.push(start);
768            }
769            Tag::Item => {
770                self.flush_inline();
771                self.pending_marker = Some(match self.lists.last_mut() {
772                    Some(Some(number)) => {
773                        let n = *number;
774                        *number += 1;
775                        Marker::Ordered(n)
776                    }
777                    _ => Marker::Bullet,
778                });
779            }
780            Tag::Table(aligns) => {
781                self.flush_inline();
782                self.flush_marker();
783                self.table = Some(TableBuild {
784                    align: aligns.iter().map(align_of).collect(),
785                    ..TableBuild::default()
786                });
787            }
788            Tag::TableHead => {
789                if let Some(table) = &mut self.table {
790                    table.in_head = true;
791                }
792            }
793            Tag::Emphasis => {
794                self.builder.open(Mark::Italic);
795            }
796            Tag::Strong => {
797                self.builder.open(Mark::Bold);
798            }
799            Tag::Strikethrough => {
800                self.builder.open(Mark::Strike);
801            }
802            // A rich link is its own mark rather than a flag on a link: where
803            // the spelling came from is what decides the painting, and a flag
804            // beside the mark is a second place for that to be recorded.
805            Tag::Link {
806                link_type,
807                dest_url,
808                title,
809                ..
810            } => {
811                let url = dest_url.into_string();
812                let form = match link_type {
813                    LinkType::Autolink => Some(Form::Auto),
814                    _ => Form::from_title(&title),
815                };
816                self.builder.open(match form {
817                    Some(form) => Mark::Mention { url, form },
818                    None => Mark::Link(url),
819                });
820            }
821            Tag::Image { dest_url, .. } => {
822                self.builder.open(Mark::Image(dest_url.into_string()));
823            }
824            _ => {}
825        }
826    }
827
828    fn end(&mut self, tag: TagEnd) {
829        match tag {
830            TagEnd::Paragraph | TagEnd::HtmlBlock => self.flush_inline(),
831            TagEnd::Heading(_) => {
832                self.flush_marker();
833                let level = self.heading.take().unwrap_or(1);
834                let mut text = self.builder.take();
835                collapse_to_one_line(&mut text);
836                let indent = self.indent();
837                self.push(BlockKind::Heading { level, text }, indent);
838            }
839            // Flushed before the depth changes, so trailing text still lands
840            // as a quote rather than as a paragraph after it.
841            TagEnd::BlockQuote(_) => {
842                self.flush_inline();
843                // pulldown-cmark takes the marker line out of the text, so a
844                // blockquote that held nothing else arrives here empty.
845                if let Some(open) = self.quotes.pop()
846                    && open.kind.is_some()
847                    && self.doc.blocks.len() == open.at
848                {
849                    let indent = self.indent();
850                    self.push(
851                        BlockKind::Quote {
852                            kind: open.kind,
853                            text: Text::default(),
854                        },
855                        indent,
856                    );
857                }
858            }
859            TagEnd::CodeBlock => {
860                if let Some((language, code)) = self.code.take() {
861                    let indent = self.indent();
862                    // The fence swallows the final newline; storing it would
863                    // grow the block by one blank line on every round trip.
864                    let code = code.strip_suffix('\n').map_or(code.clone(), str::to_string);
865                    self.push(
866                        BlockKind::Code {
867                            language,
868                            code: Text::plain(code),
869                        },
870                        indent,
871                    );
872                }
873            }
874            TagEnd::List(_) => {
875                self.flush_inline();
876                self.lists.pop();
877            }
878            // A tight item's text arrives with no `Paragraph` tag to close it,
879            // so the item's end is what turns it into the marker block. Only an
880            // item that produced nothing at all falls through to an empty one.
881            TagEnd::Item => {
882                self.flush_inline();
883                self.flush_marker();
884            }
885            TagEnd::Table => {
886                if let Some(table) = self.table.take() {
887                    let indent = self.indent();
888                    self.push(
889                        BlockKind::Table {
890                            align: table.align,
891                            header: table.header,
892                            rows: table.rows,
893                        },
894                        indent,
895                    );
896                }
897            }
898            TagEnd::TableHead => {
899                if let Some(table) = &mut self.table {
900                    table.header = std::mem::take(&mut table.row);
901                    table.in_head = false;
902                }
903            }
904            TagEnd::TableRow => {
905                if let Some(table) = &mut self.table {
906                    let row = std::mem::take(&mut table.row);
907                    table.rows.push(row);
908                }
909            }
910            TagEnd::TableCell => {
911                let mut cell = self.builder.take();
912                collapse_to_one_line(&mut cell);
913                if let Some(table) = &mut self.table {
914                    table.row.push(cell);
915                }
916            }
917            TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough | TagEnd::Link => {
918                self.builder.close();
919            }
920            TagEnd::Image => self.builder.close(),
921            _ => {}
922        }
923    }
924}
925
926fn align_of(alignment: &Alignment) -> Align {
927    match alignment {
928        Alignment::Center => Align::Center,
929        Alignment::Right => Align::Right,
930        Alignment::Left | Alignment::None => Align::Left,
931    }
932}
933
934/// Parse markdown, and say where `offset` in it landed in the document.
935///
936/// The inverse of [`crate::serialize_at`] and the same trick: a sentinel goes
937/// into the source at the offset, the source is parsed, and the text holding
938/// the sentinel is the caret's. The document comes back without it.
939///
940/// The caret is the start of the document where the sentinel would have
941/// changed what the source *means* — between a `#` and its space, inside a
942/// fence's delimiter — because a caret in the right place is worth less than a
943/// document that is still the one you were editing.
944pub fn parse_at(source: &str, offset: usize, marks: &Marks) -> (Doc, Cursor) {
945    let plain = parse_with(source, marks);
946    let start = || (plain.clone(), Cursor::default().clamp(&plain));
947    if source.contains(crate::serialize::SENTINEL) {
948        return start();
949    }
950    let mut marked = String::with_capacity(source.len() + 3);
951    let offset = offset.min(source.len());
952    if !source.is_char_boundary(offset) {
953        return start();
954    }
955    marked.push_str(&source[..offset]);
956    marked.push(crate::serialize::SENTINEL);
957    marked.push_str(&source[offset..]);
958
959    let mut doc = parse_with(&marked, marks);
960    let Some(at) = find(&doc) else { return start() };
961    let Some(text) = doc
962        .blocks
963        .get_mut(at.block)
964        .and_then(|block| block.text_at_mut(at.part))
965    else {
966        return start();
967    };
968    text.remove(at.offset..at.offset + crate::serialize::SENTINEL.len_utf8());
969    // The sentinel is a character like any other to the parser, so a document
970    // it changed the shape of is not the one the caller handed in.
971    if doc != plain { start() } else { (doc, at) }
972}
973
974/// Where the sentinel sits, in document order.
975fn find(doc: &Doc) -> Option<Cursor> {
976    doc.blocks.iter().enumerate().find_map(|(ix, block)| {
977        block.parts().into_iter().find_map(|part| {
978            let at = block.text_at(part)?.text.find(crate::serialize::SENTINEL)?;
979            Some(Cursor::new(ix, part, at))
980        })
981    })
982}
983
984/// A registered mark, lifted out of the source and into two private-use
985/// characters CommonMark carries through as ordinary text.
986///
987/// The pair rather than the delimiter itself, because the delimiter is what the
988/// escape question is about: by the time pulldown has finished, `\=\=` and `==`
989/// are the same two bytes, and only the source still knows which was written.
990const OPEN: char = '\u{E010}';
991const CLOSE: char = '\u{E011}';
992
993/// Which registered mark an [`OPEN`] belongs to, as a character of its own so
994/// the pair needs no length prefix.
995fn tag(ix: usize) -> Option<char> {
996    char::from_u32(0xE020 + u32::try_from(ix).ok()?).filter(|_| ix < 0x100)
997}
998
999fn tag_index(c: char) -> Option<usize> {
1000    (0xE020..0xE120)
1001        .contains(&(c as u32))
1002        .then(|| c as usize - 0xE020)
1003}
1004
1005/// The source with every registered delimiter pair replaced by its sentinels.
1006fn lift(source: &str, marks: &Marks) -> String {
1007    let skipped = literal(source);
1008    let entries = marks.sorted();
1009    let mut out = String::with_capacity(source.len());
1010    let mut open: Vec<(usize, &str)> = Vec::new();
1011    let mut at = 0usize;
1012
1013    while at < source.len() {
1014        // Inside a fence, a code span or a link's destination the delimiter is
1015        // not markup and never was.
1016        if let Some(range) = skipped.iter().find(|range| range.contains(&at)) {
1017            out.push_str(&source[at..range.end]);
1018            at = range.end;
1019            continue;
1020        }
1021        let rest = &source[at..];
1022        // A backslash takes the next character with it, delimiter or not.
1023        if let Some(escaped) = rest.strip_prefix('\\') {
1024            let width = escaped.chars().next().map_or(1, |c| 1 + c.len_utf8());
1025            out.push_str(&rest[..width.min(rest.len())]);
1026            at += width.min(rest.len());
1027            continue;
1028        }
1029        let found = entries
1030            .iter()
1031            .find(|entry| rest.starts_with(entry.delimiter.as_ref()));
1032        if let Some(entry) = found {
1033            let delimiter: &str = entry.delimiter.as_ref();
1034            let closes = open.last().is_some_and(|(_, open)| *open == delimiter);
1035            if closes && !source[..at].ends_with(char::is_whitespace) {
1036                out.push(CLOSE);
1037                open.pop();
1038                at += delimiter.len();
1039                continue;
1040            }
1041            if !closes
1042                && let Some(ix) = marks.position(entry)
1043                && let Some(tag) = tag(ix)
1044                && closing(
1045                    source,
1046                    at + delimiter.len(),
1047                    delimiter,
1048                    &skipped,
1049                    line_end(source, at),
1050                )
1051            {
1052                out.push(OPEN);
1053                out.push(tag);
1054                open.push((ix, delimiter));
1055                at += delimiter.len();
1056                continue;
1057            }
1058        }
1059        let c = rest.chars().next().unwrap_or_default();
1060        out.push(c);
1061        at += c.len_utf8();
1062    }
1063    out
1064}
1065
1066/// Whether a delimiter opened at `from` has a partner to close against: an
1067/// unescaped one, on the same line, outside everything literal, with something
1068/// between them that neither opens nor closes on a space — the rule emphasis
1069/// already follows.
1070///
1071/// The same line, and only ever the same line. Emphasis may reach across a soft
1072/// break; a mark this crate does not know the meaning of may not, because the
1073/// next line may belong to another block — a lazy continuation out of a quote,
1074/// a list item's second paragraph — and no mark can span two of those. An open
1075/// with no close on its own line stays the text it was written as.
1076fn closing(
1077    source: &str,
1078    from: usize,
1079    delimiter: &str,
1080    skipped: &[Range<usize>],
1081    line_end: usize,
1082) -> bool {
1083    if source[from..].starts_with(char::is_whitespace) {
1084        return false;
1085    }
1086    let mut at = from;
1087    while let Some(found) = source[at..line_end.max(at)].find(delimiter) {
1088        let found = at + found;
1089        let escaped = source[..found].ends_with('\\');
1090        let literal = skipped.iter().any(|range| range.contains(&found));
1091        let spaced = source[..found].ends_with(char::is_whitespace);
1092        if !escaped && !literal && !spaced && found > from {
1093            return true;
1094        }
1095        at = found + delimiter.len();
1096    }
1097    false
1098}
1099
1100/// Where the line `at` sits on ends.
1101fn line_end(source: &str, at: usize) -> usize {
1102    source[at..].find('\n').map_or(source.len(), |ix| at + ix)
1103}
1104
1105/// The source ranges a delimiter means nothing in: a fence, a code span, raw
1106/// HTML, and a link's destination.
1107fn literal(source: &str) -> Vec<Range<usize>> {
1108    let mut out = Vec::new();
1109    for (event, range) in Parser::new_ext(source, OPTIONS).into_offset_iter() {
1110        match event {
1111            Event::Code(_) | Event::Html(_) | Event::InlineHtml(_) => out.push(range),
1112            Event::Start(Tag::CodeBlock(_)) => out.push(range),
1113            // A link's destination only: its label is prose, and a mark is
1114            // welcome in it. An autolink has no `](` and is a destination all
1115            // through.
1116            Event::Start(Tag::Link { .. }) => {
1117                let at = source[range.clone()]
1118                    .rfind("](")
1119                    .map_or(range.start, |ix| range.start + ix);
1120                out.push(at..range.end);
1121            }
1122            // A picture whole: its label is alt text, which markdown writes as
1123            // a plain string — a mark placed there would have nowhere to go on
1124            // the way out.
1125            Event::Start(Tag::Image { .. }) => out.push(range),
1126            _ => {}
1127        }
1128    }
1129    out
1130}
1131
1132/// Take the sentinels back out of a parsed text, leaving the marks they stood
1133/// for — and move every mark the ordinary parse produced, whose offsets were
1134/// measured with the sentinels still in.
1135fn settle(text: &mut Text, marks: &Marks) {
1136    if !text.text.contains(OPEN) {
1137        return;
1138    }
1139    let mut settled = String::with_capacity(text.text.len());
1140    // Where a sentinel was, and how many bytes it took with it.
1141    let mut cut: Vec<(usize, usize)> = Vec::new();
1142    // Each open takes a number, because a mark is closed inner first and the
1143    // list is read outermost first — `++==x==++` is underline over highlight,
1144    // and writing it the other way round is a different document.
1145    let mut open: Vec<(usize, usize, usize)> = Vec::new();
1146    let mut found: Vec<(usize, MarkSpan)> = Vec::new();
1147    let mut opened = 0usize;
1148    let mut chars = text.text.char_indices();
1149
1150    while let Some((at, c)) = chars.next() {
1151        match c {
1152            OPEN => {
1153                let width = match chars.next() {
1154                    Some((_, tag)) => {
1155                        if let Some(ix) = tag_index(tag) {
1156                            open.push((ix, settled.len(), opened));
1157                            opened += 1;
1158                        }
1159                        OPEN.len_utf8() + tag.len_utf8()
1160                    }
1161                    None => OPEN.len_utf8(),
1162                };
1163                cut.push((at, width));
1164            }
1165            CLOSE => {
1166                if let Some((ix, from, seq)) = open.pop()
1167                    && let Some(entry) = marks.index(ix)
1168                {
1169                    found.push((
1170                        seq,
1171                        MarkSpan {
1172                            range: from..settled.len(),
1173                            mark: Mark::Custom(entry.name.to_string()),
1174                        },
1175                    ));
1176                }
1177                cut.push((at, CLOSE.len_utf8()));
1178            }
1179            _ => settled.push(c),
1180        }
1181    }
1182
1183    let moved = |offset: usize| {
1184        offset
1185            - cut
1186                .iter()
1187                .filter(|(at, _)| *at < offset)
1188                .map(|(_, width)| width)
1189                .sum::<usize>()
1190    };
1191    for span in &mut text.marks {
1192        span.range = moved(span.range.start)..moved(span.range.end);
1193    }
1194    text.text = settled;
1195    found.sort_by_key(|(seq, _)| *seq);
1196    text.marks.extend(found.into_iter().map(|(_, span)| span));
1197    // Outermost first is what the serializer writes the nesting from. A stable
1198    // sort leaves the ordinary marks in the order the parse put them.
1199    text.marks
1200        .sort_by_key(|span| (span.range.start, std::cmp::Reverse(span.range.end)));
1201    text.marks.retain(|span| !span.range.is_empty());
1202}