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: `` 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 // ``-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}