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