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
367fn memchr_newlines(text: &str) -> impl Iterator<Item = usize> + '_ {
368    text.bytes()
369        .enumerate()
370        .filter_map(|(ix, b)| (b == b'\n').then_some(ix))
371}
372
373/// A list item's marker, held until the item's first paragraph arrives.
374#[derive(Clone, Copy)]
375enum Marker {
376    Bullet,
377    Ordered(u64),
378    Task(bool),
379}
380
381impl Marker {
382    fn into_kind(self, text: Text) -> BlockKind {
383        match self {
384            Self::Bullet => BlockKind::Bullet(text),
385            Self::Ordered(number) => BlockKind::Ordered { number, text },
386            Self::Task(checked) => BlockKind::Task { checked, text },
387        }
388    }
389}
390
391#[derive(Default)]
392struct TableBuild {
393    align: Vec<Align>,
394    header: Vec<Text>,
395    rows: Vec<Vec<Text>>,
396    row: Vec<Text>,
397    in_head: bool,
398}
399
400#[derive(Default)]
401struct ParseState {
402    doc: Doc,
403    builder: TextBuilder,
404    /// One entry per open list; `Some` counts an ordered list's next number.
405    lists: Vec<Option<u64>>,
406    quote_depth: u8,
407    pending_marker: Option<Marker>,
408    heading: Option<u8>,
409    code: Option<(Option<String>, String)>,
410    table: Option<TableBuild>,
411}
412
413impl ParseState {
414    /// Indent level for a block that is not a list marker.
415    ///
416    /// Only list nesting counts. A blockquote decides a block's *kind*, not how
417    /// deep it sits — so a code block inside a quote stays at the quote's own
418    /// level rather than acquiring an indent that nothing in the serialized
419    /// output could reproduce.
420    fn indent(&self) -> u8 {
421        self.lists.len() as u8
422    }
423
424    /// Append a block, clamping its indent so the document invariant holds
425    /// (first block at 0, never more than one deeper than its predecessor).
426    fn push(&mut self, kind: BlockKind, indent: u8) {
427        let max = self.doc.blocks.last().map_or(0, |b| b.indent + 1);
428        self.doc.blocks.push(Block {
429            kind,
430            indent: indent.min(max),
431        });
432    }
433
434    /// Emit a pending marker as an empty block so a non-paragraph leaf (a code
435    /// block, a table) nests *under* its bullet instead of replacing it.
436    fn flush_marker(&mut self) {
437        let Some(marker) = self.pending_marker.take() else {
438            return;
439        };
440        let indent = self.indent().saturating_sub(1);
441        self.push(marker.into_kind(Text::default()), indent);
442    }
443
444    /// Close any inline content still open as a block.
445    ///
446    /// A *tight* list item carries no `Paragraph` tags — pulldown-cmark emits
447    /// its text directly between `Item` tags — so every block boundary has to
448    /// close the run itself rather than waiting for an end tag that never
449    /// comes. Table cells are exempt: their builder is per-cell, and closing it
450    /// here would push a block out of the middle of a table.
451    fn flush_inline(&mut self) {
452        if self.table.is_none() && !self.builder.is_empty() {
453            self.finish_paragraph();
454        }
455    }
456
457    /// Close the current run of inline content as a block.
458    fn finish_paragraph(&mut self) {
459        let text = self.builder.take();
460
461        // A paragraph that is nothing but one image is an image block — the
462        // `![](media://…)`-on-its-own-line shape. Anything else keeps the image
463        // inline, where it stays an image rather than decaying to a link.
464        if let [
465            MarkSpan {
466                range,
467                mark: Mark::Image(url),
468            },
469        ] = text.marks.as_slice()
470            && range.start == 0
471            && range.end == text.text.len()
472        {
473            let (url, alt) = (url.clone(), Text::plain(text.text));
474            self.flush_marker();
475            let indent = self.indent();
476            self.push(BlockKind::Image { url, alt }, indent);
477            return;
478        }
479
480        // A paragraph that is nothing but a mention is a bookmark — the same
481        // `<https://x>` that paints as a chip inside a sentence, given a line
482        // of its own. A bare URL is what someone types when they mean a link
483        // and `[Title](url)` is what a sentence spells, so carding either would
484        // leave no way to write a link that stays one — and it is the paste
485        // menu's `Dismiss` that has to write that down.
486        //
487        // A chip promotes too: off the text flow it can be a real element, and
488        // that is the only place a favicon has room to sit.
489        //
490        // The text has to *be* the URL. `[Example Site](url "chip")` alone on a
491        // line keeps its title and stays a paragraph, because promoting it
492        // would drop words someone wrote — a block shows only what the preview
493        // gave it.
494        if let [
495            MarkSpan {
496                range,
497                mark: Mark::Mention { url, form },
498            },
499        ] = text.marks.as_slice()
500            && range.start == 0
501            && range.end == text.text.len()
502            && text.text == *url
503            && is_url(url)
504        {
505            let (url, form) = (url.clone(), *form);
506            self.flush_marker();
507            let indent = self.indent();
508            self.push(BlockKind::Bookmark { url, form }, indent);
509            return;
510        }
511
512        if self.quote_depth > 0 {
513            // The bullet comes first so the quote reads as its child rather
514            // than replacing it.
515            self.flush_marker();
516            let indent = self.indent();
517            self.push(BlockKind::Quote(text), indent);
518        } else if let Some(marker) = self.pending_marker.take() {
519            let indent = self.indent().saturating_sub(1);
520            self.push(marker.into_kind(text), indent);
521        } else {
522            let indent = self.indent();
523            self.push(BlockKind::Paragraph(text), indent);
524        }
525    }
526
527    fn event(&mut self, event: Event<'_>) {
528        match event {
529            Event::Start(tag) => self.start(tag),
530            Event::End(tag) => self.end(tag),
531
532            Event::Text(t) => match &mut self.code {
533                Some((_, code)) => code.push_str(&t),
534                None => self.builder.text.push_str(&t),
535            },
536            Event::Code(t) => self.builder.wrap(Mark::Code, &t),
537            // Raw HTML is content, not structure: this model has no HTML node,
538            // so it survives as the literal text the author typed.
539            Event::Html(t) | Event::InlineHtml(t) => self.builder.text.push_str(&t),
540            // Soft and hard breaks are both just a line break in a block —
541            // the distinction has no meaning in this model, or in Notion.
542            Event::SoftBreak | Event::HardBreak => match &mut self.code {
543                Some((_, code)) => code.push('\n'),
544                None => self.builder.text.push('\n'),
545            },
546            Event::Rule => {
547                self.flush_inline();
548                self.flush_marker();
549                let indent = self.indent();
550                self.push(BlockKind::Rule, indent);
551            }
552            Event::TaskListMarker(checked) => {
553                self.pending_marker = Some(Marker::Task(checked));
554            }
555            Event::FootnoteReference(label) => {
556                self.builder.text.push_str(&format!("[^{label}]"));
557            }
558            _ => {}
559        }
560    }
561
562    fn start(&mut self, tag: Tag<'_>) {
563        match tag {
564            Tag::Heading { level, .. } => {
565                self.flush_inline();
566                self.heading = Some(level as u8);
567            }
568            Tag::BlockQuote(_) => {
569                self.flush_inline();
570                self.quote_depth += 1;
571            }
572            Tag::CodeBlock(kind) => {
573                self.flush_inline();
574                self.flush_marker();
575                let language = match kind {
576                    CodeBlockKind::Fenced(info) => {
577                        let tag = info.split_whitespace().next().unwrap_or("");
578                        (!tag.is_empty()).then(|| tag.to_string())
579                    }
580                    CodeBlockKind::Indented => None,
581                };
582                self.code = Some((language, String::new()));
583            }
584            Tag::List(start) => {
585                self.flush_inline();
586                // An item whose content is only a nested list still has to emit
587                // its own marker first. `flush_inline` covers the item that had
588                // text; this covers the empty one, whose pending marker the
589                // nested `Start(Item)` would otherwise overwrite — losing a
590                // level of nesting. It runs before the push so the marker is
591                // numbered at the outer list's depth.
592                self.flush_marker();
593                self.lists.push(start);
594            }
595            Tag::Item => {
596                self.flush_inline();
597                self.pending_marker = Some(match self.lists.last_mut() {
598                    Some(Some(number)) => {
599                        let n = *number;
600                        *number += 1;
601                        Marker::Ordered(n)
602                    }
603                    _ => Marker::Bullet,
604                });
605            }
606            Tag::Table(aligns) => {
607                self.flush_inline();
608                self.flush_marker();
609                self.table = Some(TableBuild {
610                    align: aligns.iter().map(align_of).collect(),
611                    ..TableBuild::default()
612                });
613            }
614            Tag::TableHead => {
615                if let Some(table) = &mut self.table {
616                    table.in_head = true;
617                }
618            }
619            Tag::Emphasis => {
620                self.builder.open(Mark::Italic);
621            }
622            Tag::Strong => {
623                self.builder.open(Mark::Bold);
624            }
625            Tag::Strikethrough => {
626                self.builder.open(Mark::Strike);
627            }
628            // A rich link is its own mark rather than a flag on a link: where
629            // the spelling came from is what decides the painting, and a flag
630            // beside the mark is a second place for that to be recorded.
631            Tag::Link {
632                link_type,
633                dest_url,
634                title,
635                ..
636            } => {
637                let url = dest_url.into_string();
638                let form = match link_type {
639                    LinkType::Autolink => Some(Form::Auto),
640                    _ => Form::from_title(&title),
641                };
642                self.builder.open(match form {
643                    Some(form) => Mark::Mention { url, form },
644                    None => Mark::Link(url),
645                });
646            }
647            Tag::Image { dest_url, .. } => {
648                self.builder.open(Mark::Image(dest_url.into_string()));
649            }
650            _ => {}
651        }
652    }
653
654    fn end(&mut self, tag: TagEnd) {
655        match tag {
656            TagEnd::Paragraph | TagEnd::HtmlBlock => self.flush_inline(),
657            TagEnd::Heading(_) => {
658                self.flush_marker();
659                let level = self.heading.take().unwrap_or(1);
660                let mut text = self.builder.take();
661                collapse_to_one_line(&mut text);
662                let indent = self.indent();
663                self.push(BlockKind::Heading { level, text }, indent);
664            }
665            // Flushed before the depth changes, so trailing text still lands
666            // as a quote rather than as a paragraph after it.
667            TagEnd::BlockQuote(_) => {
668                self.flush_inline();
669                self.quote_depth = self.quote_depth.saturating_sub(1);
670            }
671            TagEnd::CodeBlock => {
672                if let Some((language, code)) = self.code.take() {
673                    let indent = self.indent();
674                    // The fence swallows the final newline; storing it would
675                    // grow the block by one blank line on every round trip.
676                    let code = code.strip_suffix('\n').map_or(code.clone(), str::to_string);
677                    self.push(
678                        BlockKind::Code {
679                            language,
680                            code: Text::plain(code),
681                        },
682                        indent,
683                    );
684                }
685            }
686            TagEnd::List(_) => {
687                self.flush_inline();
688                self.lists.pop();
689            }
690            // A tight item's text arrives with no `Paragraph` tag to close it,
691            // so the item's end is what turns it into the marker block. Only an
692            // item that produced nothing at all falls through to an empty one.
693            TagEnd::Item => {
694                self.flush_inline();
695                self.flush_marker();
696            }
697            TagEnd::Table => {
698                if let Some(table) = self.table.take() {
699                    let indent = self.indent();
700                    self.push(
701                        BlockKind::Table {
702                            align: table.align,
703                            header: table.header,
704                            rows: table.rows,
705                        },
706                        indent,
707                    );
708                }
709            }
710            TagEnd::TableHead => {
711                if let Some(table) = &mut self.table {
712                    table.header = std::mem::take(&mut table.row);
713                    table.in_head = false;
714                }
715            }
716            TagEnd::TableRow => {
717                if let Some(table) = &mut self.table {
718                    let row = std::mem::take(&mut table.row);
719                    table.rows.push(row);
720                }
721            }
722            TagEnd::TableCell => {
723                let mut cell = self.builder.take();
724                collapse_to_one_line(&mut cell);
725                if let Some(table) = &mut self.table {
726                    table.row.push(cell);
727                }
728            }
729            TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough | TagEnd::Link => {
730                self.builder.close();
731            }
732            TagEnd::Image => self.builder.close(),
733            _ => {}
734        }
735    }
736}
737
738fn align_of(alignment: &Alignment) -> Align {
739    match alignment {
740        Alignment::Center => Align::Center,
741        Alignment::Right => Align::Right,
742        Alignment::Left | Alignment::None => Align::Left,
743    }
744}