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