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::doc::{Align, Block, BlockKind, Doc, Form, Mark, MarkSpan, Text};
29
30/// Parse a markdown document.
31pub fn parse(source: &str) -> Doc {
32    let options =
33        Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TASKLISTS;
34    let mut state = ParseState::default();
35    for event in Parser::new_ext(source, options) {
36        state.event(event);
37    }
38    state.doc.renumber();
39    state.doc
40}
41
42/// Accumulates one run of inline content and the marks over it.
43#[derive(Default)]
44struct TextBuilder {
45    text: String,
46    marks: Vec<MarkSpan>,
47    /// Indices into `marks` for the marks still open, innermost last.
48    open: Vec<usize>,
49}
50
51impl TextBuilder {
52    /// Open a mark at the cursor. Marks land in the list in the order they
53    /// open, which is outermost first — the ordering [`crate::serialize`] reads
54    /// back to reproduce the nesting.
55    fn open(&mut self, mark: Mark) {
56        let ix = self.marks.len();
57        let at = self.text.len();
58        self.marks.push(MarkSpan {
59            range: at..at,
60            mark,
61        });
62        self.open.push(ix);
63    }
64
65    /// Whether anything at all has accumulated — an image with no alt text is
66    /// a mark and no text, and still has to close as a block.
67    fn is_empty(&self) -> bool {
68        self.text.is_empty() && self.marks.is_empty()
69    }
70
71    fn close(&mut self) {
72        if let Some(ix) = self.open.pop() {
73            self.marks[ix].range.end = self.text.len();
74        }
75    }
76
77    /// A mark that opens and closes around `s` in one event (inline code).
78    fn wrap(&mut self, mark: Mark, s: &str) {
79        let start = self.text.len();
80        self.text.push_str(s);
81        self.marks.push(MarkSpan {
82            range: start..self.text.len(),
83            mark,
84        });
85    }
86
87    fn take(&mut self) -> Text {
88        self.open.clear();
89        let mut text = normalize(
90            &std::mem::take(&mut self.text),
91            &std::mem::take(&mut self.marks),
92        );
93        settle_mentions(&mut text);
94        linkify(&mut text);
95        text
96    }
97}
98
99/// A mention the shorthand cannot spell says its name instead.
100///
101/// [`Form::Auto`] records that `<url>` was written. Where the angles cannot be
102/// written back — a `mailto:`, a boundary inside a span emitted whole — the
103/// form settles here, so the document already holds what the next parse would
104/// produce. A mention alone in its paragraph passes and stays `Auto`, which is
105/// what leaves it to become a card.
106fn settle_mentions(text: &mut Text) {
107    let settled: Vec<usize> = (0..text.marks.len())
108        .filter(|ix| {
109            matches!(
110                text.marks[*ix].mark,
111                Mark::Mention {
112                    form: Form::Auto,
113                    ..
114                }
115            ) && !is_shorthand(text, *ix)
116        })
117        .collect();
118    for ix in settled {
119        if let Mark::Mention { form, .. } = &mut text.marks[ix].mark {
120            *form = Form::Chip;
121        }
122    }
123}
124
125/// Whether the mark at `ix` can be written with the `<url>` shorthand.
126///
127/// The angles hold a bare URL and nothing else, so a mention has to *be* its
128/// URL: `<mailto:x>` is an autolink this cannot spell that way, and
129/// `**<https://x>**` has a boundary inside a span that is written whole and so
130/// has nowhere to put it. Everything that fails here still has the explicit
131/// spelling to fall back on, which is why nothing ever has to stop being a
132/// mention.
133pub(crate) fn is_shorthand(text: &Text, ix: usize) -> bool {
134    let span = &text.marks[ix];
135    let Mark::Mention { url, form } = &span.mark else {
136        return false;
137    };
138    *form == Form::Auto
139        && text.text.get(span.range.clone()) == Some(url.as_str())
140        && is_url(url)
141        && text.alone(ix)
142}
143
144/// The schemes a bare URL may carry. Narrow on purpose: a scheme and no
145/// whitespace. Anything cleverer starts linking text that merely contains a dot.
146const SCHEMES: [&str; 2] = ["https://", "http://"];
147
148/// Every bare URL in `text`, as byte ranges.
149///
150/// One scan answers two questions that have to agree: what [`linkify`] marks,
151/// and what [`crate::serialize`] may write without brackets. Split them and the
152/// round trip drifts the first time the two disagree about a trailing bracket.
153pub(crate) fn urls(text: &str) -> Vec<Range<usize>> {
154    let mut found = Vec::new();
155    let mut at = 0;
156    while at < text.len() {
157        let Some((start, scheme)) = SCHEMES
158            .iter()
159            .filter_map(|scheme| text[at..].find(scheme).map(|ix| (at + ix, *scheme)))
160            .min_by_key(|(ix, _)| *ix)
161        else {
162            break;
163        };
164        let stop = text[start..]
165            .find(char::is_whitespace)
166            .map_or(text.len(), |ix| start + ix);
167        let end = start + trim_url(&text[start..stop]);
168        // A scheme mid-word belongs to the word, and a scheme with no host
169        // behind it is not a URL.
170        let opens = text[..start]
171            .chars()
172            .next_back()
173            .is_none_or(|c| !c.is_alphanumeric());
174        if opens && end > start + scheme.len() {
175            found.push(start..end);
176        }
177        at = stop.max(start + 1);
178    }
179    found
180}
181
182/// Whether `source` is exactly one bare URL, and nothing else.
183///
184/// The question an editor asks of a paste, and the one [`crate::serialize`]
185/// asks before writing a link bare — the same question, so it is one function.
186pub fn is_url(source: &str) -> bool {
187    matches!(urls(source).as_slice(), [only] if *only == (0..source.len()))
188}
189
190/// Whether a URL or a path names a picture, by the only thing either says
191/// about itself without being fetched — its extension, against what gpui can
192/// decode.
193///
194/// What decides whether a paste or a drop is worth offering as an image. A
195/// server is free to disagree; the answer is a guess about a name, and the
196/// alternative is a menu row that paints a broken box.
197pub fn is_image(source: &str) -> bool {
198    let path = source.split(['?', '#']).next().unwrap_or(source);
199    let Some((_, extension)) = path.rsplit_once('.') else {
200        return false;
201    };
202    matches!(
203        extension.to_ascii_lowercase().as_str(),
204        "png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "ico" | "tif" | "tiff" | "avif"
205    )
206}
207
208/// How much of a run the URL is. Closing punctuation belongs to the sentence,
209/// and a bracket only belongs to the URL when the URL opened it.
210fn trim_url(run: &str) -> usize {
211    let mut end = run.len();
212    while let Some(last) = run[..end].chars().next_back() {
213        let keep = match last {
214            '.' | ',' | ';' | ':' | '!' | '?' | '\'' | '"' => false,
215            ')' => run[..end].matches('(').count() >= run[..end].matches(')').count(),
216            ']' => run[..end].matches('[').count() >= run[..end].matches(']').count(),
217            _ => true,
218        };
219        if keep {
220            break;
221        }
222        end -= last.len_utf8();
223    }
224    end
225}
226
227/// Mark the bare URLs in a run.
228///
229/// CommonMark links `<http://x>` and nothing else, so a URL typed on its own
230/// arrives as text. Marking it here is what lets a reader click it, what lets
231/// [`crate::serialize`] write it back without brackets, and what makes a URL
232/// alone in a block a [`BlockKind::Bookmark`].
233fn linkify(text: &mut Text) {
234    let fresh: Vec<Range<usize>> = urls(&text.text)
235        .into_iter()
236        .filter(|range| {
237            // A URL already inside a link, an image target or a code span is
238            // spelled by that mark, not by this one.
239            !text.marks.iter().any(|span| {
240                matches!(
241                    span.mark,
242                    Mark::Link(_) | Mark::Mention { .. } | Mark::Image(_) | Mark::Code
243                ) && span.range.start < range.end
244                    && range.start < span.range.end
245            })
246        })
247        .collect();
248    for range in fresh {
249        let url = text.text[range.clone()].to_string();
250        text.marks.push(MarkSpan {
251            range,
252            mark: Mark::Link(url),
253        });
254    }
255}
256
257/// Drop the whitespace markdown itself drops, and move the marks with it.
258///
259/// Leading and trailing spaces on a line are not content — one trailing space
260/// is insignificant, two are a hard break, and a continuation line's indent
261/// belongs to block structure. Keeping them would mean writing out whitespace
262/// that the next parse discards, so the document would change every time it was
263/// saved. Blank lines at either end of a block go the same way.
264pub(crate) fn normalize(text: &str, marks: &[MarkSpan]) -> Text {
265    let bytes = text.as_bytes();
266    let mut keep = vec![true; text.len()];
267
268    let mut line_begin = 0;
269    for offset in memchr_newlines(text).chain([text.len()]) {
270        let line = &text[line_begin..offset];
271        let lead = line.len() - line.trim_start_matches([' ', '\t']).len();
272        let trail = line.len() - line.trim_end_matches([' ', '\t']).len();
273        keep[line_begin..line_begin + lead].fill(false);
274        keep[offset - trail..offset].fill(false);
275        line_begin = offset + 1;
276    }
277
278    let mut head = 0;
279    while head < text.len() && (!keep[head] || bytes[head] == b'\n') {
280        keep[head] = false;
281        head += 1;
282    }
283    let mut tail = text.len();
284    while tail > 0 && (!keep[tail - 1] || bytes[tail - 1] == b'\n') {
285        keep[tail - 1] = false;
286        tail -= 1;
287    }
288
289    let mut out = String::with_capacity(text.len());
290    let mut map = vec![0; text.len() + 1];
291    for (offset, ch) in text.char_indices() {
292        map[offset] = out.len();
293        if keep[offset] {
294            out.push(ch);
295        }
296    }
297    map[text.len()] = out.len();
298
299    let marks = marks
300        .iter()
301        .map(|span| MarkSpan {
302            range: map[span.range.start]..map[span.range.end],
303            mark: span.mark.clone(),
304        })
305        // A mark left covering nothing has no spelling that survives a
306        // round trip — `****` is literal text, not empty bold. An image is the
307        // exception: `![](url)` is exactly a mark over no alt text.
308        .filter(|span| !span.range.is_empty() || matches!(span.mark, Mark::Image(_)))
309        .collect();
310
311    Text {
312        text: out,
313        marks: merge_same_mark(marks),
314    }
315}
316
317/// Fuse spans of the same mark that overlap or nest.
318///
319/// Emphasis inside the same emphasis is redundant — `_a _b_ c_` is italic
320/// either way — and two spans of one mark have no unambiguous spelling: written
321/// back out, the delimiters pair up differently than they came in. Collapsing
322/// them here means the parse produces the one form that survives being written
323/// and read again.
324fn merge_same_mark(mut marks: Vec<MarkSpan>) -> Vec<MarkSpan> {
325    let mut ix = 0;
326    while ix < marks.len() {
327        let mut fused = None;
328        for other in ix + 1..marks.len() {
329            let (a, b) = (&marks[ix], &marks[other]);
330            if a.mark == b.mark
331                && !matches!(a.mark, Mark::Image(_) | Mark::Mention { .. })
332                && a.range.start <= b.range.end
333                && b.range.start <= a.range.end
334            {
335                fused = Some((
336                    other,
337                    a.range.start.min(b.range.start),
338                    a.range.end.max(b.range.end),
339                ));
340                break;
341            }
342        }
343        match fused {
344            Some((other, start, end)) => {
345                marks[ix].range = start..end;
346                marks.remove(other);
347            }
348            None => ix += 1,
349        }
350    }
351    marks
352}
353
354/// Flatten a block whose serialized form is one line.
355///
356/// A setext heading (`Title\n=====`) and a table cell can both hold a line
357/// break that has nowhere to go in the output — an ATX `#` heading ends at its
358/// newline, and a second line in a cell would end the row. Both are single-line
359/// blocks in this model, and since a newline and a space are each one byte, the
360/// marks over them do not move.
361pub(crate) fn collapse_to_one_line(text: &mut Text) {
362    if text.text.contains('\n') {
363        text.text = text.text.replace('\n', " ");
364    }
365}
366
367/// Split a trailing `|480` off an image's alt text, which is where a width is
368/// written down.
369///
370/// Obsidian's spelling, and the only one the parser leaves intact: `{width=480}`
371/// trails as literal text and breaks the paragraph out of being an image at all,
372/// and `=480x` is not an image to begin with. The last `|` wins, so a caption
373/// may hold its own — but one *ending* in `|123` gives that tail up, because the
374/// escape that tells them apart on disk is gone by the time this reads it.
375fn split_width(alt: &str) -> (&str, Option<u32>) {
376    let Some((caption, tail)) = alt.rsplit_once('|') else {
377        return (alt, None);
378    };
379    // A zero would paint a picture no pixels wide, and nothing that writes one
380    // can produce it — the drag floors at `MIN_IMAGE_WIDTH`.
381    match tail.parse().ok().filter(|width| *width > 0) {
382        Some(width) => (caption, Some(width)),
383        None => (alt, None),
384    }
385}
386
387fn memchr_newlines(text: &str) -> impl Iterator<Item = usize> + '_ {
388    text.bytes()
389        .enumerate()
390        .filter_map(|(ix, b)| (b == b'\n').then_some(ix))
391}
392
393/// A list item's marker, held until the item's first paragraph arrives.
394#[derive(Clone, Copy)]
395enum Marker {
396    Bullet,
397    Ordered(u64),
398    Task(bool),
399}
400
401impl Marker {
402    fn into_kind(self, text: Text) -> BlockKind {
403        match self {
404            Self::Bullet => BlockKind::Bullet(text),
405            Self::Ordered(number) => BlockKind::Ordered { number, text },
406            Self::Task(checked) => BlockKind::Task { checked, text },
407        }
408    }
409}
410
411#[derive(Default)]
412struct TableBuild {
413    align: Vec<Align>,
414    header: Vec<Text>,
415    rows: Vec<Vec<Text>>,
416    row: Vec<Text>,
417    in_head: bool,
418}
419
420#[derive(Default)]
421struct ParseState {
422    doc: Doc,
423    builder: TextBuilder,
424    /// One entry per open list; `Some` counts an ordered list's next number.
425    lists: Vec<Option<u64>>,
426    quote_depth: u8,
427    pending_marker: Option<Marker>,
428    heading: Option<u8>,
429    code: Option<(Option<String>, String)>,
430    table: Option<TableBuild>,
431}
432
433impl ParseState {
434    /// Indent level for a block that is not a list marker.
435    ///
436    /// Only list nesting counts. A blockquote decides a block's *kind*, not how
437    /// deep it sits — so a code block inside a quote stays at the quote's own
438    /// level rather than acquiring an indent that nothing in the serialized
439    /// output could reproduce.
440    fn indent(&self) -> u8 {
441        self.lists.len() as u8
442    }
443
444    /// Append a block, clamping its indent so the document invariant holds
445    /// (first block at 0, never more than one deeper than its predecessor).
446    fn push(&mut self, kind: BlockKind, indent: u8) {
447        let max = self.doc.blocks.last().map_or(0, |b| b.indent + 1);
448        self.doc.blocks.push(Block {
449            kind,
450            indent: indent.min(max),
451        });
452    }
453
454    /// Emit a pending marker as an empty block so a non-paragraph leaf (a code
455    /// block, a table) nests *under* its bullet instead of replacing it.
456    fn flush_marker(&mut self) {
457        let Some(marker) = self.pending_marker.take() else {
458            return;
459        };
460        let indent = self.indent().saturating_sub(1);
461        self.push(marker.into_kind(Text::default()), indent);
462    }
463
464    /// Close any inline content still open as a block.
465    ///
466    /// A *tight* list item carries no `Paragraph` tags — pulldown-cmark emits
467    /// its text directly between `Item` tags — so every block boundary has to
468    /// close the run itself rather than waiting for an end tag that never
469    /// comes. Table cells are exempt: their builder is per-cell, and closing it
470    /// here would push a block out of the middle of a table.
471    fn flush_inline(&mut self) {
472        if self.table.is_none() && !self.builder.is_empty() {
473            self.finish_paragraph();
474        }
475    }
476
477    /// Close the current run of inline content as a block.
478    fn finish_paragraph(&mut self) {
479        let text = self.builder.take();
480
481        // A paragraph that is nothing but one image is an image block — the
482        // `![](media://…)`-on-its-own-line shape. Anything else keeps the image
483        // inline, where it stays an image rather than decaying to a link.
484        if let [
485            MarkSpan {
486                range,
487                mark: Mark::Image(url),
488            },
489        ] = text.marks.as_slice()
490            && range.start == 0
491            && range.end == text.text.len()
492        {
493            let (caption, width) = split_width(&text.text);
494            let (url, alt) = (url.clone(), Text::plain(caption.to_string()));
495            self.flush_marker();
496            let indent = self.indent();
497            self.push(BlockKind::Image { url, alt, width }, indent);
498            return;
499        }
500
501        // A paragraph that is nothing but a mention is a bookmark — the same
502        // `<https://x>` that paints as a chip inside a sentence, given a line
503        // of its own. A bare URL is what someone types when they mean a link
504        // and `[Title](url)` is what a sentence spells, so carding either would
505        // leave no way to write a link that stays one — and it is the paste
506        // menu's `Dismiss` that has to write that down.
507        //
508        // A chip promotes too: off the text flow it can be a real element, and
509        // that is the only place a favicon has room to sit.
510        //
511        // The text has to *be* the URL. `[Example Site](url "chip")` alone on a
512        // line keeps its title and stays a paragraph, because promoting it
513        // would drop words someone wrote — a block shows only what the preview
514        // gave it.
515        if let [
516            MarkSpan {
517                range,
518                mark: Mark::Mention { url, form },
519            },
520        ] = text.marks.as_slice()
521            && range.start == 0
522            && range.end == text.text.len()
523            && text.text == *url
524            && is_url(url)
525        {
526            let (url, form) = (url.clone(), *form);
527            self.flush_marker();
528            let indent = self.indent();
529            self.push(BlockKind::Bookmark { url, form }, indent);
530            return;
531        }
532
533        if self.quote_depth > 0 {
534            // The bullet comes first so the quote reads as its child rather
535            // than replacing it.
536            self.flush_marker();
537            let indent = self.indent();
538            self.push(BlockKind::Quote(text), indent);
539        } else if let Some(marker) = self.pending_marker.take() {
540            let indent = self.indent().saturating_sub(1);
541            self.push(marker.into_kind(text), indent);
542        } else {
543            let indent = self.indent();
544            self.push(BlockKind::Paragraph(text), indent);
545        }
546    }
547
548    fn event(&mut self, event: Event<'_>) {
549        match event {
550            Event::Start(tag) => self.start(tag),
551            Event::End(tag) => self.end(tag),
552
553            Event::Text(t) => match &mut self.code {
554                Some((_, code)) => code.push_str(&t),
555                None => self.builder.text.push_str(&t),
556            },
557            Event::Code(t) => self.builder.wrap(Mark::Code, &t),
558            // Raw HTML is content, not structure: this model has no HTML node,
559            // so it survives as the literal text the author typed.
560            Event::Html(t) | Event::InlineHtml(t) => self.builder.text.push_str(&t),
561            // Soft and hard breaks are both just a line break in a block —
562            // the distinction has no meaning in this model, or in Notion.
563            Event::SoftBreak | Event::HardBreak => match &mut self.code {
564                Some((_, code)) => code.push('\n'),
565                None => self.builder.text.push('\n'),
566            },
567            Event::Rule => {
568                self.flush_inline();
569                self.flush_marker();
570                let indent = self.indent();
571                self.push(BlockKind::Rule, indent);
572            }
573            Event::TaskListMarker(checked) => {
574                self.pending_marker = Some(Marker::Task(checked));
575            }
576            Event::FootnoteReference(label) => {
577                self.builder.text.push_str(&format!("[^{label}]"));
578            }
579            _ => {}
580        }
581    }
582
583    fn start(&mut self, tag: Tag<'_>) {
584        match tag {
585            Tag::Heading { level, .. } => {
586                self.flush_inline();
587                self.heading = Some(level as u8);
588            }
589            Tag::BlockQuote(_) => {
590                self.flush_inline();
591                self.quote_depth += 1;
592            }
593            Tag::CodeBlock(kind) => {
594                self.flush_inline();
595                self.flush_marker();
596                let language = match kind {
597                    CodeBlockKind::Fenced(info) => {
598                        let tag = info.split_whitespace().next().unwrap_or("");
599                        (!tag.is_empty()).then(|| tag.to_string())
600                    }
601                    CodeBlockKind::Indented => None,
602                };
603                self.code = Some((language, String::new()));
604            }
605            Tag::List(start) => {
606                self.flush_inline();
607                // An item whose content is only a nested list still has to emit
608                // its own marker first. `flush_inline` covers the item that had
609                // text; this covers the empty one, whose pending marker the
610                // nested `Start(Item)` would otherwise overwrite — losing a
611                // level of nesting. It runs before the push so the marker is
612                // numbered at the outer list's depth.
613                self.flush_marker();
614                self.lists.push(start);
615            }
616            Tag::Item => {
617                self.flush_inline();
618                self.pending_marker = Some(match self.lists.last_mut() {
619                    Some(Some(number)) => {
620                        let n = *number;
621                        *number += 1;
622                        Marker::Ordered(n)
623                    }
624                    _ => Marker::Bullet,
625                });
626            }
627            Tag::Table(aligns) => {
628                self.flush_inline();
629                self.flush_marker();
630                self.table = Some(TableBuild {
631                    align: aligns.iter().map(align_of).collect(),
632                    ..TableBuild::default()
633                });
634            }
635            Tag::TableHead => {
636                if let Some(table) = &mut self.table {
637                    table.in_head = true;
638                }
639            }
640            Tag::Emphasis => {
641                self.builder.open(Mark::Italic);
642            }
643            Tag::Strong => {
644                self.builder.open(Mark::Bold);
645            }
646            Tag::Strikethrough => {
647                self.builder.open(Mark::Strike);
648            }
649            // A rich link is its own mark rather than a flag on a link: where
650            // the spelling came from is what decides the painting, and a flag
651            // beside the mark is a second place for that to be recorded.
652            Tag::Link {
653                link_type,
654                dest_url,
655                title,
656                ..
657            } => {
658                let url = dest_url.into_string();
659                let form = match link_type {
660                    LinkType::Autolink => Some(Form::Auto),
661                    _ => Form::from_title(&title),
662                };
663                self.builder.open(match form {
664                    Some(form) => Mark::Mention { url, form },
665                    None => Mark::Link(url),
666                });
667            }
668            Tag::Image { dest_url, .. } => {
669                self.builder.open(Mark::Image(dest_url.into_string()));
670            }
671            _ => {}
672        }
673    }
674
675    fn end(&mut self, tag: TagEnd) {
676        match tag {
677            TagEnd::Paragraph | TagEnd::HtmlBlock => self.flush_inline(),
678            TagEnd::Heading(_) => {
679                self.flush_marker();
680                let level = self.heading.take().unwrap_or(1);
681                let mut text = self.builder.take();
682                collapse_to_one_line(&mut text);
683                let indent = self.indent();
684                self.push(BlockKind::Heading { level, text }, indent);
685            }
686            // Flushed before the depth changes, so trailing text still lands
687            // as a quote rather than as a paragraph after it.
688            TagEnd::BlockQuote(_) => {
689                self.flush_inline();
690                self.quote_depth = self.quote_depth.saturating_sub(1);
691            }
692            TagEnd::CodeBlock => {
693                if let Some((language, code)) = self.code.take() {
694                    let indent = self.indent();
695                    // The fence swallows the final newline; storing it would
696                    // grow the block by one blank line on every round trip.
697                    let code = code.strip_suffix('\n').map_or(code.clone(), str::to_string);
698                    self.push(
699                        BlockKind::Code {
700                            language,
701                            code: Text::plain(code),
702                        },
703                        indent,
704                    );
705                }
706            }
707            TagEnd::List(_) => {
708                self.flush_inline();
709                self.lists.pop();
710            }
711            // A tight item's text arrives with no `Paragraph` tag to close it,
712            // so the item's end is what turns it into the marker block. Only an
713            // item that produced nothing at all falls through to an empty one.
714            TagEnd::Item => {
715                self.flush_inline();
716                self.flush_marker();
717            }
718            TagEnd::Table => {
719                if let Some(table) = self.table.take() {
720                    let indent = self.indent();
721                    self.push(
722                        BlockKind::Table {
723                            align: table.align,
724                            header: table.header,
725                            rows: table.rows,
726                        },
727                        indent,
728                    );
729                }
730            }
731            TagEnd::TableHead => {
732                if let Some(table) = &mut self.table {
733                    table.header = std::mem::take(&mut table.row);
734                    table.in_head = false;
735                }
736            }
737            TagEnd::TableRow => {
738                if let Some(table) = &mut self.table {
739                    let row = std::mem::take(&mut table.row);
740                    table.rows.push(row);
741                }
742            }
743            TagEnd::TableCell => {
744                let mut cell = self.builder.take();
745                collapse_to_one_line(&mut cell);
746                if let Some(table) = &mut self.table {
747                    table.row.push(cell);
748                }
749            }
750            TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough | TagEnd::Link => {
751                self.builder.close();
752            }
753            TagEnd::Image => self.builder.close(),
754            _ => {}
755        }
756    }
757}
758
759fn align_of(alignment: &Alignment) -> Align {
760    match alignment {
761        Alignment::Center => Align::Center,
762        Alignment::Right => Align::Right,
763        Alignment::Left | Alignment::None => Align::Left,
764    }
765}