Skip to main content

markdown_syntax/
serialize.rs

1//! AST to canonical Markdown. The verbs live on [`Document`]
2//! ([`to_markdown`](Document::to_markdown) /
3//! [`to_markdown_with`](Document::to_markdown_with)); [`SerializeOptions`] tunes
4//! the output style. The document is validated first, so serialization can fail
5//! with a [`SerializeError`].
6//!
7//! The serializer only renders: each node is written in the spelling the AST
8//! records for it, or else by one fixed rule. Text, escapes, and character
9//! references are written as recorded, and the output is never parsed back.
10
11use alloc::{
12    format,
13    string::{String, ToString},
14    vec::Vec,
15};
16
17use crate::{ast::*, diagnostic::Diagnostic, validate::validate_document};
18
19/// The newline style emitted by the serializer.
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum LineEnding {
22    /// Unix `\n`.
23    Lf,
24    /// Windows `\r\n`.
25    CrLf,
26}
27
28impl LineEnding {
29    fn as_str(self) -> &'static str {
30        match self {
31            Self::Lf => "\n",
32            Self::CrLf => "\r\n",
33        }
34    }
35}
36
37/// The bullet char [`SerializeOptions::bullet`] writes unordered lists with.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum BulletMarker {
40    /// `-`.
41    Dash,
42    /// `*`.
43    Asterisk,
44    /// `+`.
45    Plus,
46}
47
48/// The delimiter [`SerializeOptions::ordered_delimiter`] writes ordered-list
49/// markers with.
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum OrderedDelimiter {
52    /// `.`, as in `1.`.
53    Period,
54    /// `)`, as in `1)`.
55    Paren,
56}
57
58/// Output-style options for serialization. Defaults: LF, trailing newline, and
59/// the list markers and code fences the AST records.
60#[derive(Clone, Debug, Eq, PartialEq)]
61#[non_exhaustive]
62pub struct SerializeOptions {
63    /// Newline style to emit.
64    pub line_ending: LineEnding,
65    /// Whether to end the output with a trailing newline.
66    pub final_newline: bool,
67    /// The bullet for unordered lists: `None`, the default, keeps the marker
68    /// each list records; `Some` writes every unordered list with it.
69    pub bullet: Option<BulletMarker>,
70    /// The delimiter for ordered-list markers: `None`, the default, keeps the
71    /// delimiter each list records; `Some` writes every ordered list with it.
72    pub ordered_delimiter: Option<OrderedDelimiter>,
73    /// The fence char for fenced code blocks: `None`, the default, keeps the
74    /// fence each block records; `Some` writes every fenced block with it,
75    /// except that an info string holding a backtick always takes tildes.
76    pub fence_marker: Option<FenceMarker>,
77}
78
79impl Default for SerializeOptions {
80    fn default() -> Self {
81        Self {
82            line_ending: LineEnding::Lf,
83            final_newline: true,
84            bullet: None,
85            ordered_delimiter: None,
86            fence_marker: None,
87        }
88    }
89}
90
91/// Why serialization failed.
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum SerializeError {
94    /// The AST failed validation; carries the validation diagnostics.
95    InvalidDocument(Vec<Diagnostic>),
96}
97
98impl Document {
99    /// Serialize this document to canonical Markdown with default options.
100    pub fn to_markdown(&self) -> Result<String, SerializeError> {
101        self.to_markdown_with(&SerializeOptions::default())
102    }
103
104    /// Serialize this document to canonical Markdown with explicit options.
105    pub fn to_markdown_with(&self, options: &SerializeOptions) -> Result<String, SerializeError> {
106        let diagnostics = validate_document(self);
107        if !diagnostics.is_empty() {
108            return Err(SerializeError::InvalidDocument(diagnostics));
109        }
110
111        let mut output = write_blocks(&self.children, options, Join::Gap, true)?;
112        if options.line_ending == LineEnding::CrLf {
113            output = lf_to_crlf(&output);
114        }
115        // Blocks end without a line ending, except an HTML block whose last
116        // line is empty (the final newline ends that line too) and an indented
117        // code block that keeps its value's `\r` or `\r\n` ending.
118        if options.final_newline && !output.is_empty() && !ends_with_carriage_return_ending(&output)
119        {
120            output.push_str(options.line_ending.as_str());
121        }
122        Ok(output)
123    }
124}
125
126/// Rewrites each bare `\n` as `\r\n`; existing `\r\n` and `\r` endings that
127/// verbatim values carry stay as they are.
128fn lf_to_crlf(input: &str) -> String {
129    let mut output = String::with_capacity(input.len());
130    let mut previous = '\0';
131    for ch in input.chars() {
132        if ch == '\n' && previous != '\r' {
133            output.push('\r');
134        }
135        output.push(ch);
136        previous = ch;
137    }
138    output
139}
140
141fn ends_with_carriage_return_ending(input: &str) -> bool {
142    input.ends_with('\r') || input.ends_with("\r\n")
143}
144
145/// Separates two blocks with one blank line. A block that already ends its
146/// last line with `\r` or `\r\n` gets only the blank line, written so that it
147/// cannot merge into that ending.
148fn push_block_gap(output: &mut String) {
149    if output.ends_with('\r') {
150        output.push('\r');
151    } else if output.ends_with("\r\n") {
152        output.push('\n');
153    } else {
154        output.push_str("\n\n");
155    }
156}
157
158/// How sibling blocks are separated.
159#[derive(Clone, Copy, Eq, PartialEq)]
160enum Join {
161    /// By a blank line.
162    Gap,
163    /// By a line ending alone, as the blocks of a tight list item are.
164    Line,
165}
166
167/// Writes a sequence of sibling blocks. `document_start` is true only for the
168/// top-level document body, where the first block sits at byte 0 and a
169/// contiguous `---` would open frontmatter.
170fn write_blocks(
171    blocks: &[Block],
172    options: &SerializeOptions,
173    join: Join,
174    document_start: bool,
175) -> Result<String, SerializeError> {
176    let mut output = String::new();
177    // The marker of the list written right before the current block; ordered
178    // and unordered markers never share a char.
179    let mut previous_list: Option<char> = None;
180    for (index, block) in blocks.iter().enumerate() {
181        if index > 0 {
182            match join {
183                Join::Gap => push_block_gap(&mut output),
184                Join::Line => output.push('\n'),
185            }
186        }
187        let after_paragraph_line =
188            join == Join::Line && index > 0 && matches!(blocks[index - 1], Block::Paragraph(_));
189        let written = match block {
190            Block::List(list) => {
191                let marker = list_marker(list, options, previous_list);
192                previous_list = Some(marker);
193                write_list(list, options, marker)?
194            }
195            Block::ThematicBreak(node) => {
196                previous_list = None;
197                write_thematic_break(node, document_start && index == 0, after_paragraph_line)
198            }
199            _ => {
200                previous_list = None;
201                write_block(block, options)?
202            }
203        };
204        output.push_str(&written);
205    }
206    Ok(output)
207}
208
209fn write_block(block: &Block, options: &SerializeOptions) -> Result<String, SerializeError> {
210    match block {
211        Block::Paragraph(node) => write_inlines(&node.children, Context::BLOCK),
212        Block::Heading(node) => write_heading(node),
213        Block::ThematicBreak(node) => Ok(write_thematic_break(node, false, false)),
214        Block::BlockQuote(node) => {
215            let inner = write_blocks(&node.children, options, Join::Gap, false)?;
216            Ok(if inner.is_empty() {
217                ">".into()
218            } else {
219                quote_lines(&inner)
220            })
221        }
222        Block::Alert(node) => write_alert(node, options),
223        Block::List(node) => write_list(node, options, list_marker(node, options, None)),
224        Block::CodeBlock(node) => Ok(write_code_block(node, options)),
225        // An HTML block's lines are joined with `\n`, so a value ending in one
226        // ends with an empty line that belongs to the block (an unclosed
227        // comment, say); it is written as it is.
228        Block::HtmlBlock(node) => Ok(node.value.clone()),
229        Block::HtmlContainer(node) => write_html_container(node, options),
230        Block::Definition(node) => Ok(write_definition(node)),
231        Block::FootnoteDefinition(node) => {
232            let inner = write_blocks(&node.children, options, Join::Gap, false)?;
233            // Content opening with a space or a tab (indented code, say)
234            // starts on the line after the label, as in a list item, where
235            // the label's line would take that whitespace as its separator.
236            Ok(if inner.starts_with([' ', '\t']) {
237                format!("[^{}]:\n{}", node.label, prefix_lines(&inner, "    "))
238            } else {
239                format!("[^{}]: {}", node.label, indent_continuation(&inner))
240            })
241        }
242        Block::Table(node) => write_table(node),
243        Block::MathBlock(node) => {
244            let fence = block_math_fence(&node.value);
245            Ok(fenced_body(&fence, &node.value, &fence))
246        }
247        Block::Frontmatter(node) => {
248            let fence = match node.kind {
249                FrontmatterKind::Yaml => "---",
250                FrontmatterKind::Toml => "+++",
251            };
252            // The value holds the lines between the fences joined with `\n`,
253            // so a final `\n` is an empty last line.
254            Ok(format!("{fence}\n{}\n{fence}", node.value))
255        }
256        Block::LeafDirective(node) => Ok(format!(
257            "::{}{}{}",
258            node.name,
259            write_directive_label(&node.label)?,
260            write_attributes(&node.attributes)
261        )),
262        Block::ContainerDirective(node) => {
263            let mut inner = write_blocks(&node.children, options, Join::Gap, false)?;
264            let fence = directive_fence(&inner);
265            // Content ends with a line ending before the closing fence; an
266            // empty directive takes no blank line, which would loosen a list
267            // holding it.
268            if !inner.is_empty() {
269                inner.push('\n');
270            }
271            Ok(format!(
272                "{fence}{}{}{}\n{inner}{fence}",
273                node.name,
274                write_directive_label(&node.label)?,
275                write_attributes(&node.attributes)
276            ))
277        }
278    }
279}
280
281/// A thematic break. A dash break is written spaced where a contiguous `---`
282/// would read otherwise: at the document start, where it opens frontmatter,
283/// and on the line after a paragraph's, where it underlines a setext heading.
284fn write_thematic_break(
285    node: &ThematicBreak,
286    at_document_start: bool,
287    after_paragraph_line: bool,
288) -> String {
289    match node.marker {
290        ThematicBreakMarker::Dash if at_document_start || after_paragraph_line => "- - -".into(),
291        ThematicBreakMarker::Dash => "---".into(),
292        ThematicBreakMarker::Asterisk => "***".into(),
293        ThematicBreakMarker::Underscore => "___".into(),
294    }
295}
296
297/// Whether `node` is written as a setext heading: a setext underline can only
298/// express depth 1 (`=`) or 2 (`-`), and it underlines content.
299pub(crate) fn writes_setext(node: &Heading) -> bool {
300    node.kind == HeadingKind::Setext && matches!(node.depth, 1 | 2) && !node.children.is_empty()
301}
302
303fn write_heading(node: &Heading) -> Result<String, SerializeError> {
304    let content = write_inlines(&node.children, Context::HEADING)?;
305    if writes_setext(node) {
306        let marker = if node.depth == 1 { "=" } else { "-" };
307        return Ok(format!(
308            "{content}\n{}",
309            marker.repeat(content.len().max(3))
310        ));
311    }
312    let hashes = "#".repeat(usize::from(node.depth));
313    Ok(if content.is_empty() {
314        hashes
315    } else if reads_as_closing_sequence(&content) {
316        // Content ending in what would read as the closing sequence keeps it
317        // by a closing sequence of its own.
318        format!("{hashes} {content} #")
319    } else {
320        format!("{hashes} {content}")
321    })
322}
323
324/// Whether written ATX content ends in a `#` run that is all of it or follows
325/// a space or a tab, which a reader strips as the closing sequence.
326fn reads_as_closing_sequence(content: &str) -> bool {
327    let before = content.trim_end_matches('#');
328    before.len() < content.len() && (before.is_empty() || before.ends_with([' ', '\t']))
329}
330
331fn write_definition(node: &Definition) -> String {
332    let destination = write_destination(&node.destination, node.destination_kind);
333    let mut output = format!("[{}]: {}", node.label, destination);
334    if let Some(title) = &node.title {
335        output.push(' ');
336        output.push_str(&write_title(title));
337    }
338    output
339}
340
341fn write_html_container(
342    node: &HtmlContainer,
343    options: &SerializeOptions,
344) -> Result<String, SerializeError> {
345    match &node.content {
346        HtmlContainerContent::Blocks(children) => {
347            let inner = write_blocks(children, options, Join::Gap, false)?;
348            Ok(if inner.is_empty() {
349                format!("{}\n{}", node.opening.raw, node.closing.raw)
350            } else {
351                format!("{}\n{}\n\n{}", node.opening.raw, inner, node.closing.raw)
352            })
353        }
354        HtmlContainerContent::Inlines(children) => Ok(format!(
355            "{}{}{}",
356            node.opening.raw,
357            write_inlines(children, Context::BLOCK)?,
358            node.closing.raw
359        )),
360    }
361}
362
363fn write_alert(node: &Alert, options: &SerializeOptions) -> Result<String, SerializeError> {
364    let mut output = String::from("> [!");
365    output.push_str(alert_kind_name(node.kind));
366    output.push(']');
367    if let Some(title) = &node.title {
368        if !title.is_empty() {
369            output.push(' ');
370            // A line ending would end the marker's line.
371            output.push_str(&title.replace(['\n', '\r'], " "));
372        }
373    }
374    let inner = write_blocks(&node.children, options, Join::Gap, false)?;
375    if !inner.is_empty() {
376        output.push('\n');
377        output.push_str(&quote_lines(&inner));
378    }
379    Ok(output)
380}
381
382/// `inner` with every line in the quote: an empty last line, which a value
383/// ending in a line ending holds, takes `>` too.
384fn quote_lines(inner: &str) -> String {
385    let mut output = prefix_lines(inner, "> ");
386    if inner.ends_with(['\n', '\r']) {
387        output.push('>');
388    }
389    output
390}
391
392fn alert_kind_name(kind: AlertKind) -> &'static str {
393    match kind {
394        AlertKind::Note => "NOTE",
395        AlertKind::Tip => "TIP",
396        AlertKind::Important => "IMPORTANT",
397        AlertKind::Warning => "WARNING",
398        AlertKind::Caution => "CAUTION",
399    }
400}
401
402/// The marker char a list is written with: the one the options replace it
403/// with, or else the one the AST records. A marker that the list written right
404/// before it in the same container, `previous`, also uses yields to the next
405/// one in the order `-`, `*`, `+`, or `.`, `)`; validation keeps two adjacent
406/// recorded markers apart, so only a replaced marker can meet this.
407fn list_marker(list: &List, options: &SerializeOptions, previous: Option<char>) -> char {
408    let marker = if list.ordered {
409        options
410            .ordered_delimiter
411            .map_or_else(|| written_marker(list), ordered_delimiter_char)
412    } else {
413        options
414            .bullet
415            .map_or_else(|| written_marker(list), bullet_char)
416    };
417    if previous == Some(marker) {
418        let order: &[char] = if list.ordered {
419            &['.', ')']
420        } else {
421            &['-', '*', '+']
422        };
423        let at = order.iter().position(|char| *char == marker).unwrap_or(0);
424        return order[(at + 1) % order.len()];
425    }
426    marker
427}
428
429/// The marker char the AST records for a list: its bullet, or its ordered
430/// delimiter. A delimiter of the other list kind is written `-` or `.`.
431/// Serialization and validation's adjacent-list rule both read it.
432pub(crate) fn written_marker(list: &List) -> char {
433    match (list.ordered, list.delimiter) {
434        (false, ListDelimiter::Asterisk) => '*',
435        (false, ListDelimiter::Plus) => '+',
436        (false, _) => '-',
437        (true, ListDelimiter::Paren) => ')',
438        (true, _) => '.',
439    }
440}
441
442fn bullet_char(bullet: BulletMarker) -> char {
443    match bullet {
444        BulletMarker::Dash => '-',
445        BulletMarker::Asterisk => '*',
446        BulletMarker::Plus => '+',
447    }
448}
449
450fn ordered_delimiter_char(delimiter: OrderedDelimiter) -> char {
451    match delimiter {
452        OrderedDelimiter::Period => '.',
453        OrderedDelimiter::Paren => ')',
454    }
455}
456
457fn write_list(
458    node: &List,
459    options: &SerializeOptions,
460    marker_char: char,
461) -> Result<String, SerializeError> {
462    let mut output = String::new();
463    for (index, item) in node.children.iter().enumerate() {
464        if index > 0 {
465            output.push_str(if node.tight { "\n" } else { "\n\n" });
466        }
467        let marker = if node.ordered {
468            // Only the first number sets the start, so later items stop
469            // counting at the largest number a marker can hold.
470            let number = node
471                .start
472                .unwrap_or(1)
473                .saturating_add(index as u64)
474                .min(crate::parse::MAX_ORDERED_NUMBER);
475            format!("{number}{marker_char} ")
476        } else {
477            format!("{marker_char} ")
478        };
479        let inner = write_item_blocks(&item.children, options, node.tight, item.checked)?;
480        let break_after_bullet = breaks_after_bullet(node, item, marker_char);
481        let opens_with_whitespace = opens_with_padding(&inner);
482        if break_after_bullet || opens_with_whitespace {
483            output.push_str(marker.trim_end());
484            output.push('\n');
485            output.push_str(&prefix_lines(&inner, &" ".repeat(marker.len())));
486        } else {
487            output.push_str(&marker);
488            output.push_str(&indent_after_first_line(&inner, marker.len()));
489        }
490    }
491    Ok(output)
492}
493
494fn thematic_break_char(marker: ThematicBreakMarker) -> char {
495    match marker {
496        ThematicBreakMarker::Dash => '-',
497        ThematicBreakMarker::Asterisk => '*',
498        ThematicBreakMarker::Underscore => '_',
499    }
500}
501
502/// An item's blocks; a task item's, `task` holding whether it is checked,
503/// open with its checkbox: at the start of the first paragraph after the
504/// definitions the item starts with, or else of the item.
505/// A thematic break of the bullet's own char right after the bullet would
506/// read as one longer break, so it starts on the next line.
507fn breaks_after_bullet(list: &List, item: &ListItem, marker_char: char) -> bool {
508    !list.ordered
509        && item.checked.is_none()
510        && matches!(
511            item.children.first(),
512            Some(Block::ThematicBreak(ThematicBreak { marker, .. }))
513                if thematic_break_char(*marker) == marker_char
514        )
515}
516
517/// Spaces or a tab opening an item's written content would read as padding
518/// after the marker, so that content starts on the next line. Four spaces or
519/// more open indented code, which the marker line holds after one space: an
520/// item that starts empty cannot interrupt a paragraph.
521fn opens_with_padding(inner: &str) -> bool {
522    !inner.starts_with("    ") && inner.starts_with([' ', '\t'])
523}
524
525/// Whether, with default options, the serializer starts `list`'s first item
526/// on a line after its bullet, so that the list, written after a paragraph's
527/// line, cannot interrupt the paragraph. Only a leaf block can open with
528/// whitespace, so only a leading leaf block is written to tell.
529pub(crate) fn first_item_starts_below_its_bullet(list: &List) -> bool {
530    let Some(item) = list.children.first() else {
531        return false;
532    };
533    let options = SerializeOptions::default();
534    if breaks_after_bullet(list, item, list_marker(list, &options, None)) {
535        return true;
536    }
537    if item.checked.is_some() {
538        return false;
539    }
540    match item.children.first() {
541        Some(
542            block @ (Block::Paragraph(_)
543            | Block::Heading(_)
544            | Block::HtmlBlock(_)
545            | Block::CodeBlock(_)),
546        ) => write_block(block, &options).is_ok_and(|written| opens_with_padding(&written)),
547        _ => false,
548    }
549}
550
551fn write_item_blocks(
552    blocks: &[Block],
553    options: &SerializeOptions,
554    tight: bool,
555    task: Option<bool>,
556) -> Result<String, SerializeError> {
557    let join = if tight { Join::Line } else { Join::Gap };
558    let Some(checked) = task else {
559        return write_blocks(blocks, options, join, false);
560    };
561    let checkbox = if checked { "[x]" } else { "[ ]" };
562    let definitions = blocks
563        .iter()
564        .take_while(|block| matches!(block, Block::Definition(_)))
565        .count();
566    if matches!(blocks.get(definitions), Some(Block::Paragraph(_))) {
567        let before = write_blocks(&blocks[..definitions], options, join, false)?;
568        let rest = write_blocks(&blocks[definitions..], options, join, false)?;
569        let mut output = before;
570        if !output.is_empty() {
571            match join {
572                Join::Gap => push_block_gap(&mut output),
573                Join::Line => output.push('\n'),
574            }
575        }
576        output.push_str(checkbox);
577        output.push(' ');
578        output.push_str(&rest);
579        return Ok(output);
580    }
581    let written = write_blocks(blocks, options, join, false)?;
582    Ok(if written.is_empty() {
583        checkbox.into()
584    } else {
585        format!("{checkbox} {written}")
586    })
587}
588
589fn write_code_block(node: &CodeBlock, options: &SerializeOptions) -> String {
590    match node.kind {
591        CodeBlockKind::Indented => {
592            // Each value line ends with a line ending; the block gap or the
593            // final newline writes a last `\n`, so only `\r` and `\r\n` stay.
594            let body = trim_trailing_newline(&node.value);
595            let mut output = prefix_lines(body, "    ");
596            let ending = &node.value[body.len()..];
597            if matches!(ending, "\r" | "\r\n") {
598                output.push_str(ending);
599            }
600            output
601        }
602        CodeBlockKind::Fenced { marker, length } => {
603            let marker = code_block_fence_marker(node, marker, options);
604            let (fence, indent) = code_block_fence(&node.value, marker, length.max(3));
605            let mut opener = fence.clone();
606            if let Some(info) = &node.info {
607                opener.push(' ');
608                opener.push_str(&escape_code_info(info));
609            }
610            let body = fenced_body(&opener, &node.value, &fence);
611            if indent == 0 {
612                body
613            } else {
614                prefix_lines(&body, &" ".repeat(indent))
615            }
616        }
617    }
618}
619
620/// A fenced block: `opener`, then `value` (whose lines each keep their line
621/// ending, the last one optionally), then `closer`. An empty value writes no
622/// line between the fences.
623fn fenced_body(opener: &str, value: &str, closer: &str) -> String {
624    let mut output = String::with_capacity(opener.len() + value.len() + closer.len() + 2);
625    output.push_str(opener);
626    output.push('\n');
627    output.push_str(value);
628    if !value.is_empty() && !ends_with_line_ending(value) {
629        output.push('\n');
630    }
631    output.push_str(closer);
632    output
633}
634
635/// The fence char a code block is written with: tildes when its info string
636/// holds a backtick, which a backtick fence's info string cannot, or else the
637/// char the AST records or the options replace it with.
638fn code_block_fence_marker(
639    node: &CodeBlock,
640    marker: FenceMarker,
641    options: &SerializeOptions,
642) -> FenceMarker {
643    if node.info.as_deref().is_some_and(|info| info.contains('`')) {
644        return FenceMarker::Tilde;
645    }
646    options.fence_marker.unwrap_or(marker)
647}
648
649fn escape_code_info(input: &str) -> String {
650    let mut output = String::new();
651    // The parser trims the info string, so whitespace at either end is
652    // written as a character reference.
653    let inner_start = input.len() - input.trim_start_matches([' ', '\t']).len();
654    let inner_end = input.trim_end_matches([' ', '\t']).len().max(inner_start);
655    for (offset, char) in input.char_indices() {
656        match char {
657            ' ' | '\t' if offset < inner_start || offset >= inner_end => {
658                output.push_str(&char_reference(char));
659            }
660            '\t' => output.push(char),
661            char if char.is_control() => output.push_str(&char_reference(char)),
662            '\\' | '&' => {
663                output.push('\\');
664                output.push(char);
665            }
666            _ => output.push(char),
667        }
668    }
669    output
670}
671
672/// A hexadecimal character reference for `char`.
673fn char_reference(char: char) -> String {
674    format!("&#x{:X};", char as u32)
675}
676
677fn write_table(node: &Table) -> Result<String, SerializeError> {
678    let delimiter_row = node
679        .alignments
680        .iter()
681        .map(|alignment| match alignment {
682            TableAlignment::None => "---",
683            TableAlignment::Left => ":---",
684            TableAlignment::Center => ":---:",
685            TableAlignment::Right => "---:",
686        })
687        .collect::<Vec<_>>()
688        .join(" | ");
689    let mut output = String::new();
690    for (row_index, row) in node.rows.iter().enumerate() {
691        if row_index > 0 {
692            output.push('\n');
693        }
694        let cells = row
695            .cells
696            .iter()
697            .map(|cell| Ok(encode_cell(&write_inlines(&cell.children, Context::BLOCK)?)))
698            .collect::<Result<Vec<_>, SerializeError>>()?;
699        output.push_str(&format!("| {} |", cells.join(" | ")));
700        if row_index == 0 {
701            output.push_str(&format!("\n| {delimiter_row} |"));
702        }
703    }
704    Ok(output)
705}
706
707/// A cell's inline content, written as anywhere else, encoded as cell
708/// source. The parser splits a row at each `|` after no backslash or an even
709/// run of them, and reads an odd run before a `|` with one backslash less, so
710/// a `\` added before each `|` of the first kind reads back as written. A `|`
711/// that the content already puts after an odd run, an escaped `|` in text,
712/// reads with one backslash less, still as an escaped `|`; validation rejects
713/// the cell values whose meaning that would change.
714fn encode_cell(written: &str) -> String {
715    let mut output = String::with_capacity(written.len());
716    let mut backslashes = 0usize;
717    for char in written.chars() {
718        if char == '|' && backslashes % 2 == 0 {
719            output.push('\\');
720        }
721        backslashes = if char == '\\' { backslashes + 1 } else { 0 };
722        output.push(char);
723    }
724    output
725}
726
727/// Where inline content is written, as the value encodings of its nodes read
728/// it.
729#[derive(Clone, Copy, Debug, Eq, PartialEq)]
730struct Context {
731    /// In a heading, which a line ending would end.
732    heading: bool,
733}
734
735impl Context {
736    const BLOCK: Self = Self { heading: false };
737    const HEADING: Self = Self { heading: true };
738}
739
740fn write_inlines(inlines: &[Inline], context: Context) -> Result<String, SerializeError> {
741    let mut output = InlineOut::default();
742    for inline in inlines {
743        write_inline(inline, context, &mut output)?;
744    }
745    Ok(output.text)
746}
747
748/// The Markdown written so far for the inline content of one block, cell, or
749/// label, which the parser reads in one inline pass, and the backtick runs in
750/// it that a code span's fence must not close.
751#[derive(Default)]
752struct InlineOut {
753    text: String,
754    /// `open_runs[n]`: a backtick run of length `n` that opens no code span
755    /// was written, outside every code span: a later fence of that length
756    /// would close it.
757    open_runs: Vec<bool>,
758    /// How far `text` has been read for `open_runs`.
759    scanned: usize,
760}
761
762impl InlineOut {
763    /// Reads the text written since the last code span for backtick runs. A
764    /// backslash escapes the first backtick of a run, which then opens with
765    /// the rest of the run.
766    fn scan_open_runs(&mut self) {
767        let mut escaped = false;
768        let mut run = 0;
769        let mut run_escaped = false;
770        for byte in self.text.as_bytes()[self.scanned..]
771            .iter()
772            .copied()
773            .chain(core::iter::once(b' '))
774        {
775            if byte == b'`' {
776                if run == 0 {
777                    run_escaped = escaped;
778                }
779                run += 1;
780                escaped = false;
781                continue;
782            }
783            let opening = run - usize::from(run_escaped && run > 0);
784            if opening > 0 {
785                if self.open_runs.len() <= opening {
786                    self.open_runs.resize(opening + 1, false);
787                }
788                self.open_runs[opening] = true;
789            }
790            run = 0;
791            escaped = byte == b'\\' && !escaped;
792        }
793        self.scanned = self.text.len();
794    }
795
796    fn is_open_run(&self, length: usize) -> bool {
797        self.open_runs.get(length).copied().unwrap_or(false)
798    }
799
800    /// Writes, with `write`, content that the parser reads whole from its
801    /// first char, so no backtick run in it opens a code span.
802    fn push_opaque(&mut self, write: impl FnOnce(&mut String)) {
803        self.scan_open_runs();
804        write(&mut self.text);
805        self.scanned = self.text.len();
806    }
807}
808
809impl core::ops::Deref for InlineOut {
810    type Target = String;
811
812    fn deref(&self) -> &String {
813        &self.text
814    }
815}
816
817impl core::ops::DerefMut for InlineOut {
818    fn deref_mut(&mut self) -> &mut String {
819        &mut self.text
820    }
821}
822
823fn write_inline(
824    inline: &Inline,
825    context: Context,
826    out: &mut InlineOut,
827) -> Result<(), SerializeError> {
828    match inline {
829        Inline::Text(node) => out.push_str(&node.value),
830        Inline::Escape(node) => {
831            out.push('\\');
832            out.push(node.value);
833        }
834        Inline::CharacterReference(node) => out.push_str(&node.reference),
835        Inline::Emphasis(node) => {
836            let delimiter = emphasis_delimiter(node.delimiter);
837            write_span(out, delimiter, &node.children, delimiter, context)?;
838        }
839        Inline::Strong(node) => {
840            let delimiter = emphasis_delimiter(node.delimiter).repeat(2);
841            write_span(out, &delimiter, &node.children, &delimiter, context)?;
842        }
843        Inline::Delete(node) => write_span(out, "~~", &node.children, "~~", context)?,
844        Inline::Mark(node) => write_span(out, "==", &node.children, "==", context)?,
845        Inline::InlineFootnote(node) => write_span(out, "^[", &node.children, "]", context)?,
846        Inline::Shortcode(node) => {
847            out.push(':');
848            out.push_str(&node.name);
849            out.push(':');
850        }
851        Inline::Code(node) => write_code_span(node, out),
852        Inline::Link(node) => {
853            write_span(out, "[", &node.children, "](", context)?;
854            out.push_opaque(|out| {
855                write_resource(
856                    out,
857                    &node.destination,
858                    node.destination_kind,
859                    node.title.as_ref(),
860                );
861            });
862        }
863        Inline::Autolink(node) => out.push_opaque(|out| match node.form {
864            AutolinkForm::Literal => out.push_str(&node.text),
865            AutolinkForm::Angle => {
866                out.push('<');
867                out.push_str(&node.text);
868                out.push('>');
869            }
870        }),
871        Inline::Image(node) => {
872            write_span(out, "![", &node.alt, "](", context)?;
873            out.push_opaque(|out| {
874                write_resource(
875                    out,
876                    &node.destination,
877                    node.destination_kind,
878                    node.title.as_ref(),
879                );
880            });
881        }
882        Inline::LinkReference(node) => {
883            write_reference(out, "[", &node.children, node.kind, &node.label, context)?;
884        }
885        Inline::ImageReference(node) => {
886            write_reference(out, "![", &node.alt, node.kind, &node.label, context)?;
887        }
888        Inline::Html(node) => out.push_opaque(|out| out.push_str(&node.value)),
889        Inline::SoftBreak(_) if context.heading => out.push(' '),
890        Inline::SoftBreak(_) => out.push('\n'),
891        Inline::LineBreak(node) => match node.kind {
892            LineBreakKind::Backslash => out.push_str("\\\n"),
893            LineBreakKind::Spaces => out.push_str("  \n"),
894        },
895        Inline::Math(node) => {
896            let math = write_inline_math(node);
897            out.push_opaque(|out| out.push_str(&math));
898        }
899        Inline::FootnoteReference(node) => {
900            out.push_str("[^");
901            out.push_str(&node.label);
902            out.push(']');
903        }
904        Inline::WikiLink(node) => out.push_opaque(|out| {
905            if node.embed {
906                out.push('!');
907            }
908            out.push_str("[[");
909            out.push_str(&node.target);
910            if node.target != node.label {
911                out.push('|');
912                out.push_str(&node.label);
913            }
914            out.push_str("]]");
915        }),
916        Inline::TextDirective(node) => {
917            // The label is read in an inline pass of its own.
918            let label = if node.label.is_empty() {
919                String::new()
920            } else {
921                format!("[{}]", write_inlines(&node.label, context)?)
922            };
923            out.push_opaque(|out| {
924                out.push(':');
925                out.push_str(&node.name);
926                out.push_str(&label);
927                out.push_str(&write_attributes(&node.attributes));
928            });
929        }
930    }
931    Ok(())
932}
933
934fn emphasis_delimiter(delimiter: EmphasisDelimiter) -> &'static str {
935    match delimiter {
936        EmphasisDelimiter::Asterisk => "*",
937        EmphasisDelimiter::Underscore => "_",
938    }
939}
940
941/// Writes `open`, `children`, and `close`.
942fn write_span(
943    out: &mut InlineOut,
944    open: &str,
945    children: &[Inline],
946    close: &str,
947    context: Context,
948) -> Result<(), SerializeError> {
949    out.push_str(open);
950    for child in children {
951        write_inline(child, context, out)?;
952    }
953    out.push_str(close);
954    Ok(())
955}
956
957/// A link's or image's `destination "title")`, after its `](`.
958fn write_resource(
959    out: &mut String,
960    destination: &str,
961    kind: LinkDestinationKind,
962    title: Option<&Title>,
963) {
964    out.push_str(&write_destination(destination, kind));
965    if let Some(title) = title {
966        out.push(' ');
967        out.push_str(&write_title(title));
968    }
969    out.push(')');
970}
971
972/// What follows a reference's text: nothing, `[]`, or `[label]`.
973/// A reference link or image. A full reference writes its text and then its
974/// label; a shortcut or collapsed one writes its label as written, which is
975/// both its text and its key, so the text it reads back as matches the
976/// definition it names.
977fn write_reference(
978    out: &mut InlineOut,
979    open: &str,
980    children: &[Inline],
981    kind: ReferenceKind,
982    label: &str,
983    context: Context,
984) -> Result<(), SerializeError> {
985    match kind {
986        ReferenceKind::Full => write_span(out, open, children, "]", context)?,
987        ReferenceKind::Shortcut | ReferenceKind::Collapsed => {
988            out.push_str(open);
989            out.push_str(label);
990            out.push(']');
991        }
992    }
993    out.push_opaque(|out| write_reference_kind(out, kind, label));
994    Ok(())
995}
996
997fn write_reference_kind(out: &mut String, kind: ReferenceKind, label: &str) {
998    match kind {
999        ReferenceKind::Shortcut => {}
1000        ReferenceKind::Collapsed => out.push_str("[]"),
1001        ReferenceKind::Full => {
1002            out.push('[');
1003            // A label is matched as written.
1004            out.push_str(label);
1005            out.push(']');
1006        }
1007    }
1008}
1009
1010/// Writes a code span from its value: fenced by the shortest backtick run
1011/// that neither the value holds nor a run written before it in the same
1012/// inline content opens, and padded with a space at each end when the value
1013/// would otherwise lose an end to the fence or to the space stripping.
1014fn write_code_span(node: &CodeInline, out: &mut InlineOut) {
1015    let value = &node.value;
1016    out.scan_open_runs();
1017    let held = backtick_runs(value);
1018    // An escaped backtick written just before joins the opening fence into
1019    // one longer run, which an earlier open run of that length would take as
1020    // its close.
1021    let before = out.len() - out.trim_end_matches('`').len();
1022    let length = (1..)
1023        .find(|length| {
1024            !held.get(*length).copied().unwrap_or(false)
1025                && !out.is_open_run(*length)
1026                && (before == 0 || !out.is_open_run(before + *length))
1027        })
1028        .unwrap_or(1);
1029    let fence = "`".repeat(length);
1030    out.push_str(&fence);
1031    if code_span_needs_padding(value) {
1032        out.push(' ');
1033        out.push_str(value);
1034        out.push(' ');
1035    } else {
1036        out.push_str(value);
1037    }
1038    out.push_str(&fence);
1039    // The span's own runs are matched; what follows is read afresh.
1040    out.scanned = out.text.len();
1041}
1042
1043/// `runs[n]`: whether `value` holds a maximal run of `n` backticks.
1044fn backtick_runs(value: &str) -> Vec<bool> {
1045    // A value of `len` bytes holds runs of at most `len` backticks.
1046    let mut runs = alloc::vec![false; value.len() + 1];
1047    let mut run = 0;
1048    for byte in value.bytes() {
1049        if byte == b'`' {
1050            run += 1;
1051        } else {
1052            runs[run] = true;
1053            run = 0;
1054        }
1055    }
1056    runs[run] = true;
1057    runs
1058}
1059
1060fn code_span_needs_padding(input: &str) -> bool {
1061    input.starts_with('`')
1062        || input.ends_with('`')
1063        || (input.starts_with(' ') && input.ends_with(' ') && input.chars().any(|char| char != ' '))
1064}
1065
1066pub(crate) fn write_inline_math(node: &MathInline) -> String {
1067    match node.kind {
1068        MathInlineKind::Code => format!("$`{}`$", node.value),
1069        // Dollar math is written verbatim behind its exact-length fence.
1070        MathInlineKind::Dollar { dollars } => {
1071            let fence = "$".repeat(usize::from(dollars));
1072            format!("{fence}{}{fence}", node.value)
1073        }
1074    }
1075}
1076
1077/// A directive's `[label]`, or nothing for an empty label.
1078fn write_directive_label(label: &[Inline]) -> Result<String, SerializeError> {
1079    if label.is_empty() {
1080        Ok(String::new())
1081    } else {
1082        Ok(format!("[{}]", write_inlines(label, Context::BLOCK)?))
1083    }
1084}
1085
1086fn write_attributes(attributes: &[DirectiveAttribute]) -> String {
1087    if attributes.is_empty() {
1088        return String::new();
1089    }
1090    let mut output = String::from("{");
1091    for (index, attribute) in attributes.iter().enumerate() {
1092        if index > 0 {
1093            output.push(' ');
1094        }
1095        match (&*attribute.name, &attribute.value) {
1096            ("id", Some(value)) if is_directive_shorthand_value(value) => {
1097                output.push('#');
1098                output.push_str(value);
1099            }
1100            ("class", Some(value)) if is_directive_shorthand_value(value) => {
1101                output.push('.');
1102                output.push_str(value);
1103            }
1104            (_, Some(value)) => {
1105                output.push_str(&attribute.name);
1106                output.push_str("=\"");
1107                output.push_str(&escape_title(value, LinkTitleKind::DoubleQuote));
1108                output.push('"');
1109            }
1110            (_, None) => output.push_str(&attribute.name),
1111        }
1112    }
1113    output.push('}');
1114    output
1115}
1116
1117fn is_directive_shorthand_value(input: &str) -> bool {
1118    !input.is_empty()
1119        && input
1120            .chars()
1121            .all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
1122}
1123
1124fn write_destination(input: &str, kind: LinkDestinationKind) -> String {
1125    match kind {
1126        LinkDestinationKind::Omitted if input.is_empty() => String::new(),
1127        LinkDestinationKind::Angle => {
1128            let mut output = String::from("<");
1129            for char in input.chars() {
1130                match char {
1131                    char if char.is_control() => output.push_str(&char_reference(char)),
1132                    '\\' | '<' | '>' => {
1133                        output.push('\\');
1134                        output.push(char);
1135                    }
1136                    _ => output.push(char),
1137                }
1138            }
1139            output.push('>');
1140            output
1141        }
1142        LinkDestinationKind::Bare | LinkDestinationKind::Omitted if input.is_empty() => "<>".into(),
1143        LinkDestinationKind::Bare | LinkDestinationKind::Omitted => {
1144            let mut output = String::new();
1145            for char in input.chars() {
1146                match char {
1147                    // A space would end the destination, and `\ ` is no escape.
1148                    char if char.is_control() || char == ' ' => {
1149                        output.push_str(&char_reference(char));
1150                    }
1151                    '(' | ')' | '\\' | '<' | '>' | '&' => {
1152                        output.push('\\');
1153                        output.push(char);
1154                    }
1155                    _ => output.push(char),
1156                }
1157            }
1158            output
1159        }
1160    }
1161}
1162
1163fn write_title(title: &Title) -> String {
1164    let (open, close) = match title.kind {
1165        LinkTitleKind::DoubleQuote => ('"', '"'),
1166        LinkTitleKind::SingleQuote => ('\'', '\''),
1167        LinkTitleKind::Paren => ('(', ')'),
1168    };
1169    let mut output = String::new();
1170    output.push(open);
1171    output.push_str(&escape_title(&title.value, title.kind));
1172    output.push(close);
1173    output
1174}
1175
1176fn escape_title(input: &str, kind: LinkTitleKind) -> String {
1177    let mut output = String::new();
1178    for char in input.chars() {
1179        match char {
1180            char if char.is_control() => output.push_str(&char_reference(char)),
1181            '\\' | '&' => {
1182                output.push('\\');
1183                output.push(char);
1184            }
1185            '"' if kind == LinkTitleKind::DoubleQuote => output.push_str("\\\""),
1186            '\'' if kind == LinkTitleKind::SingleQuote => output.push_str("\\'"),
1187            '(' | ')' if kind == LinkTitleKind::Paren => {
1188                output.push('\\');
1189                output.push(char);
1190            }
1191            _ => output.push(char),
1192        }
1193    }
1194    output
1195}
1196
1197/// Prefixes every line of `input` with `prefix`, keeping each line's ending.
1198fn prefix_lines(input: &str, prefix: &str) -> String {
1199    if input.is_empty() {
1200        return String::new();
1201    }
1202    let bytes = input.as_bytes();
1203    let mut output = String::new();
1204    let mut line_start = 0;
1205    let mut cursor = 0;
1206    while cursor < input.len() {
1207        let eol_end = match bytes[cursor] {
1208            b'\n' => Some(cursor + 1),
1209            b'\r' if bytes.get(cursor + 1) == Some(&b'\n') => Some(cursor + 2),
1210            b'\r' => Some(cursor + 1),
1211            _ => None,
1212        };
1213        if let Some(end) = eol_end {
1214            output.push_str(prefix);
1215            output.push_str(&input[line_start..end]);
1216            cursor = end;
1217            line_start = cursor;
1218        } else {
1219            cursor += 1;
1220        }
1221    }
1222    if line_start < input.len() {
1223        output.push_str(prefix);
1224        output.push_str(&input[line_start..]);
1225    }
1226    output
1227}
1228
1229fn indent_after_first_line(input: &str, width: usize) -> String {
1230    indent_lines_of(input, width)
1231}
1232
1233fn indent_continuation(input: &str) -> String {
1234    indent_lines_of(input, 4)
1235}
1236
1237/// Indents every line of `input` after its first by `width` spaces. Each
1238/// line keeps its line ending, a final one included.
1239fn indent_lines_of(input: &str, width: usize) -> String {
1240    let mut output = String::with_capacity(input.len());
1241    let bytes = input.as_bytes();
1242    let mut line_start = 0;
1243    let mut index = 0;
1244    let push = |output: &mut String, line: &str, number: usize| {
1245        if number > 0 {
1246            output.extend(core::iter::repeat_n(' ', width));
1247        }
1248        output.push_str(line);
1249    };
1250    let mut number = 0;
1251    while index < bytes.len() {
1252        let end = match bytes[index] {
1253            b'\n' => Some(index + 1),
1254            b'\r' if bytes.get(index + 1) == Some(&b'\n') => Some(index + 2),
1255            b'\r' => Some(index + 1),
1256            _ => None,
1257        };
1258        match end {
1259            Some(end) => {
1260                push(&mut output, &input[line_start..end], number);
1261                number += 1;
1262                index = end;
1263                line_start = end;
1264            }
1265            None => index += 1,
1266        }
1267    }
1268    if line_start < input.len() {
1269        push(&mut output, &input[line_start..], number);
1270    }
1271    output
1272}
1273
1274fn trim_trailing_newline(input: &str) -> &str {
1275    input.trim_end_matches('\n').trim_end_matches('\r')
1276}
1277
1278fn ends_with_line_ending(input: &str) -> bool {
1279    input.ends_with('\n') || input.ends_with('\r')
1280}
1281
1282/// The fence for a code block's `value` and the columns the block is indented
1283/// by, so that no line of `value` closes it. A closing fence is up to three
1284/// spaces, at least as many marker chars, and nothing else but spaces and
1285/// tabs. The fence keeps `min_len` chars when indenting the block (which the
1286/// value's lines lose again) moves every closing-like line past three spaces;
1287/// otherwise it grows past the longest of them.
1288fn code_block_fence(value: &str, marker: FenceMarker, min_len: usize) -> (String, usize) {
1289    let char = match marker {
1290        FenceMarker::Backtick => '`',
1291        FenceMarker::Tilde => '~',
1292    };
1293    let closing_like = |line: &str, length: usize| {
1294        let indent = line.len() - line.trim_start_matches(' ').len();
1295        let rest = &line[indent..];
1296        let run = rest.len() - rest.trim_start_matches(char).len();
1297        (indent <= 3 && run >= length && rest[run..].trim_matches([' ', '\t']).is_empty())
1298            .then_some((indent, run))
1299    };
1300    let lines = || value.split(['\n', '\r']);
1301    let least_indent = lines()
1302        .filter_map(|line| closing_like(line, min_len))
1303        .map(|(indent, _)| indent)
1304        .min();
1305    match least_indent {
1306        None => (char.to_string().repeat(min_len), 0),
1307        Some(indent) if indent > 0 => (char.to_string().repeat(min_len), 4 - indent),
1308        Some(_) => {
1309            let mut length = min_len;
1310            for line in lines() {
1311                if let Some((_, run)) = closing_like(line, length) {
1312                    length = run + 1;
1313                }
1314            }
1315            (char.to_string().repeat(length), 0)
1316        }
1317    }
1318}
1319
1320fn block_math_fence(input: &str) -> String {
1321    let mut length = 2;
1322    for line in trim_trailing_newline(input).lines() {
1323        let trimmed = line.trim();
1324        if trimmed.len() >= 2 && trimmed.chars().all(|char| char == '$') {
1325            length = length.max(trimmed.len() + 1);
1326        }
1327    }
1328    "$".repeat(length)
1329}
1330
1331fn directive_fence(inner: &str) -> String {
1332    let mut length = 3;
1333    for line in inner.lines() {
1334        if let Some(closing) = directive_closing_fence_len(line) {
1335            length = length.max(closing + 1);
1336        }
1337    }
1338    ":".repeat(length)
1339}
1340
1341fn directive_closing_fence_len(line: &str) -> Option<usize> {
1342    let trimmed = trim_up_to_three_indent_columns(line)?;
1343    let length = trimmed
1344        .as_bytes()
1345        .iter()
1346        .take_while(|byte| **byte == b':')
1347        .count();
1348    if length >= 3 && trimmed[length..].trim().is_empty() {
1349        Some(length)
1350    } else {
1351        None
1352    }
1353}
1354
1355fn trim_up_to_three_indent_columns(input: &str) -> Option<&str> {
1356    let mut columns = 0usize;
1357    let mut bytes = 0usize;
1358    for byte in input.as_bytes() {
1359        match *byte {
1360            b' ' => columns += 1,
1361            b'\t' => columns += 4 - (columns % 4),
1362            _ => break,
1363        }
1364        bytes += 1;
1365    }
1366    (columns <= 3).then_some(&input[bytes..])
1367}