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