Skip to main content

markdown_syntax/
serialize.rs

1//! AST to 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//! A document with its source is written partly from it (`reuse`, decision
8//! 0017): each node that corresponds to the node its source parses to is
9//! written as its source, an edited one with its own syntax copied around
10//! what changed, and the rest is rendered. Rendering only renders: each node
11//! is written in the spelling the AST records for it, or else by one fixed
12//! rule, with text, escapes, and character references as recorded. The output
13//! is parsed back only to check the blocks written partly from the source.
14
15use alloc::{
16    format,
17    string::{String, ToString},
18    vec::Vec,
19};
20
21use alloc::collections::BTreeMap;
22
23use crate::{
24    ast::*, diagnostic::Diagnostic, leaves, parse::Definitions, parse::InlineSource, span::Span,
25    validate::validate_document,
26};
27
28mod reuse;
29
30/// The newline style emitted by the serializer.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum LineEnding {
33    /// Unix `\n`.
34    Lf,
35    /// Windows `\r\n`.
36    CrLf,
37}
38
39impl LineEnding {
40    fn as_str(self) -> &'static str {
41        match self {
42            Self::Lf => "\n",
43            Self::CrLf => "\r\n",
44        }
45    }
46}
47
48/// The bullet char [`SerializeOptions::bullet`] writes unordered lists with.
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum BulletMarker {
51    /// `-`.
52    Dash,
53    /// `*`.
54    Asterisk,
55    /// `+`.
56    Plus,
57}
58
59/// The delimiter [`SerializeOptions::ordered_delimiter`] writes ordered-list
60/// markers with.
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub enum OrderedDelimiter {
63    /// `.`, as in `1.`.
64    Period,
65    /// `)`, as in `1)`.
66    Paren,
67}
68
69/// Output-style options for serialization. Defaults: LF, trailing newline, and
70/// the list markers and code fences the AST records.
71#[derive(Clone, Debug, Eq, PartialEq)]
72#[non_exhaustive]
73pub struct SerializeOptions {
74    /// Newline style to emit.
75    pub line_ending: LineEnding,
76    /// Whether to end the output with a trailing newline.
77    pub final_newline: bool,
78    /// The bullet for unordered lists: `None`, the default, keeps the marker
79    /// each list records; `Some` writes every unordered list with it.
80    pub bullet: Option<BulletMarker>,
81    /// The delimiter for ordered-list markers: `None`, the default, keeps the
82    /// delimiter each list records; `Some` writes every ordered list with it.
83    pub ordered_delimiter: Option<OrderedDelimiter>,
84    /// The fence char for fenced code blocks: `None`, the default, keeps the
85    /// fence each block records; `Some` writes every fenced block with it,
86    /// except that an info string holding a backtick always takes tildes.
87    pub fence_marker: Option<FenceMarker>,
88}
89
90impl Default for SerializeOptions {
91    fn default() -> Self {
92        Self {
93            line_ending: LineEnding::Lf,
94            final_newline: true,
95            bullet: None,
96            ordered_delimiter: None,
97            fence_marker: None,
98        }
99    }
100}
101
102/// Why serialization failed.
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub enum SerializeError {
105    /// The AST failed validation; carries the validation diagnostics.
106    InvalidDocument(Vec<Diagnostic>),
107}
108
109impl Document {
110    /// Write this document as Markdown with default options: the source of
111    /// every unchanged node, the source of its own syntax around a node whose
112    /// children changed, and canonical Markdown for the rest.
113    pub fn to_markdown(&self) -> Result<String, SerializeError> {
114        self.to_markdown_with(&SerializeOptions::default())
115    }
116
117    /// Write this document as Markdown with explicit options. A document
118    /// with a source reuses it for each node that corresponds to the node
119    /// the source parses to at the same span, and for the own syntax of one
120    /// whose children changed; the options apply to the rest.
121    pub fn to_markdown_with(&self, options: &SerializeOptions) -> Result<String, SerializeError> {
122        Ok(self.write(options)?.0)
123    }
124
125    /// The text `to_markdown_with` writes, and how many top-level blocks it
126    /// wrote canonically because, written partly from the source, they read
127    /// back as something else.
128    pub(crate) fn write(
129        &self,
130        options: &SerializeOptions,
131    ) -> Result<(String, usize), SerializeError> {
132        let diagnostics = validate_document(self);
133        if !diagnostics.is_empty() {
134            return Err(SerializeError::InvalidDocument(diagnostics));
135        }
136        // A leaf keeps its text only while its inlines are the ones the text
137        // reads as.
138        let current;
139        let definitions = Definitions::of(self);
140        let blocks = if leaves::any_kept(&self.children) {
141            let mut blocks = self.children.clone();
142            leaves::visit_mut(&mut blocks, &mut |leaf| {
143                if let Some(text) = leaf.source.as_deref() {
144                    if crate::parse::parse_inline_tree(text, &definitions).inlines != *leaf.inlines
145                    {
146                        *leaf.source = None;
147                    }
148                }
149            });
150            current = blocks;
151            &current
152        } else {
153            &self.children
154        };
155        let Some(source) = self.source() else {
156            return Ok((write_canonical(blocks, options)?, 0));
157        };
158        // A block written partly from the source stays so only where the
159        // output reads back as it at its place.
160        let base = crate::parse(source).document;
161        let write =
162            |whole: &[bool]| write_reusing(blocks, &base, source, options, &definitions, whole);
163        // A `---` break kept opening the document stays only while no later
164        // line closes it as frontmatter, which every write is checked for.
165        let opens_with_break = matches!(blocks.first(), Some(Block::ThematicBreak(_)));
166        let mut whole = alloc::vec![false; blocks.len()];
167        let (mut output, mut partly) = write(&whole)?;
168        if opens_with_break && opens_frontmatter(&output) {
169            whole[0] = true;
170            (output, partly) = write(&whole)?;
171        }
172        if partly.is_empty() {
173            return Ok((output, 0));
174        }
175        let misread = misread(blocks, &output, &partly);
176        if misread.is_empty() {
177            return Ok((output, 0));
178        }
179        for index in &misread {
180            whole[*index] = true;
181        }
182        output = write(&whole)?.0;
183        if opens_with_break && !whole[0] && opens_frontmatter(&output) {
184            whole[0] = true;
185            output = write(&whole)?.0;
186        }
187        Ok((output, misread.len()))
188    }
189}
190
191/// Whether `output` opens with frontmatter: its first line is a fence that
192/// a later line closes.
193fn opens_frontmatter(output: &str) -> bool {
194    let mut lines = output.split(['\n', '\r']);
195    let opening = lines.next().and_then(crate::parse::frontmatter_fence_kind);
196    opening.is_some() && lines.any(|line| crate::parse::frontmatter_fence_kind(line) == opening)
197}
198
199/// The canonical Markdown of a document's top-level blocks.
200fn write_canonical(blocks: &[Block], options: &SerializeOptions) -> Result<String, SerializeError> {
201    let mut output = write_blocks(blocks, options, Join::Gap, true, None)?;
202    if options.line_ending == LineEnding::CrLf {
203        output = lf_to_crlf(&output);
204    }
205    // Blocks end without a line ending, except an HTML block whose last
206    // line is empty (the final newline ends that line too) and an indented
207    // code block that keeps its value's `\r` or `\r\n` ending.
208    // A last blank line always takes its line ending.
209    let ends_with_blank_line = matches!(blocks.last(), Some(Block::BlankLine(_)));
210    if options.final_newline
211        && !blocks.is_empty()
212        && (ends_with_blank_line || !ends_with_carriage_return_ending(&output))
213    {
214        output.push_str(options.line_ending.as_str());
215    }
216    Ok(output)
217}
218
219/// Writes `blocks` against `base_document`, the parse of `source`: each
220/// block matched to the base block at its span as its source when it
221/// corresponds to it, partly from the source when only what it holds
222/// changed (`reuse`), and canonically otherwise; returns the output and the
223/// indices of the blocks written partly from the source, which `whole`
224/// writes canonically instead. Between two blocks written from the source
225/// and adjacent there the source's own bytes are kept where they read the
226/// same (`reuse::keeps_gap`), as are the bytes before the source's first
227/// block and after its last when those blocks are written from it there;
228/// every other boundary is a blank line.
229///
230/// A block is written canonically instead where its source would read
231/// differently in its new place: one a blank line and a paragraph after it
232/// would change, as they extend an unclosed fence, when it comes before a
233/// block other than its successor in the source; one the block before it
234/// would take in (`moves_into_previous`); and a thematic break that comes to
235/// open the document, where it would open frontmatter.
236fn write_reusing(
237    blocks: &[Block],
238    base_document: &Document,
239    source: &str,
240    options: &SerializeOptions,
241    definitions: &Definitions,
242    whole: &[bool],
243) -> Result<(String, Vec<usize>), SerializeError> {
244    let base = &base_document.children;
245    let mut correspondence = Correspondence {
246        base: base_document,
247        sources: None,
248    };
249    let base_definitions = Definitions::of(base_document);
250    let mut reuse = reuse::Reuse {
251        source,
252        options,
253        correspondence: &mut correspondence,
254        definitions,
255        base_definitions: &base_definitions,
256    };
257    let span_of = |block: &Block| block.span().expect("a reused block has a span");
258    let mut outs = Vec::with_capacity(blocks.len());
259    // Whether each block written partly from the source is an open list.
260    let mut open_lists = alloc::vec![false; blocks.len()];
261    for (index, block) in blocks.iter().enumerate() {
262        let found = block.span().and_then(|span| {
263            let at = base
264                .binary_search_by_key(&span.start, |base| {
265                    base.span().map_or(usize::MAX, |span| span.start)
266                })
267                .ok()?;
268            (base[at].span() == Some(span)
269                && core::mem::discriminant(&base[at]) == core::mem::discriminant(block))
270            .then_some(at)
271        });
272        outs.push(match found {
273            Some(at) => match reuse.block(block, &base[at])? {
274                reuse::Out::Kept(_) => (Some(at), None),
275                reuse::Out::Lines(_) | reuse::Out::OpenList(_) if whole[index] => (None, None),
276                reuse::Out::Lines(lines) => (None, Some((at, lines))),
277                reuse::Out::OpenList(lines) => {
278                    open_lists[index] = true;
279                    (None, Some((at, lines)))
280                }
281                reuse::Out::Render => (None, None),
282            },
283            None => (None, None),
284        });
285    }
286    let mut reused: Vec<Option<usize>> = outs.iter().map(|(at, _)| *at).collect();
287    // The blocks written partly from the source, with their place in it.
288    let mut partly: Vec<Option<(usize, Vec<reuse::Line>)>> =
289        outs.into_iter().map(|(_, lines)| lines).collect();
290    // Where each block written from the source, whole or partly, stands in
291    // the source.
292    let placed =
293        |reused: &[Option<usize>], partly: &[Option<(usize, Vec<reuse::Line>)>], index: usize| {
294            reused[index].or(partly[index].as_ref().map(|(at, _)| *at))
295        };
296    if blocks.len() == base.len()
297        && reused
298            .iter()
299            .enumerate()
300            .all(|(index, at)| *at == Some(index))
301    {
302        return Ok((source.into(), Vec::new()));
303    }
304    // Whether the boundary before each block holds the blank line that
305    // separates two blocks; the source between two blocks adjacent in the
306    // source is kept only where it still does or does not.
307    let separated = separated_boundaries(blocks);
308    let base_separated = separated_boundaries(base);
309    // Whether the source between the block at `index`, placed at `at`, and
310    // the one before it, placed at `at - 1`, is kept: the boundary holds a
311    // separating blank line exactly when it did, and the source reads the
312    // same between them (`reuse::keeps_gap`).
313    let keeps_gap = |partly: &[Option<(usize, Vec<reuse::Line>)>], index: usize, at: usize| {
314        if index == 0 || at == 0 || separated[index] != base_separated[at] {
315            return false;
316        }
317        let left = base[at - 1].span().expect("a parsed block has a span");
318        let right = base[at].span().expect("a parsed block has a span");
319        let left_end = left.start + strip_line_ending(&source[left.start..left.end]).len();
320        reuse::keeps_gap(
321            source,
322            &source[left_end..right.start],
323            (
324                &blocks[index - 1],
325                left,
326                partly[index - 1]
327                    .as_ref()
328                    .map(|(_, lines)| lines.as_slice()),
329            ),
330            (
331                &blocks[index],
332                right,
333                partly[index].as_ref().map(|(_, lines)| lines.as_slice()),
334            ),
335            open_lists[index - 1],
336            true,
337        )
338    };
339    // Backward, so that each block knows whether its successor stays reused,
340    // with the source between them.
341    for index in (0..blocks.len().saturating_sub(1)).rev() {
342        let Some(at) = placed(&reused, &partly, index) else {
343            continue;
344        };
345        if placed(&reused, &partly, index + 1) == Some(at + 1)
346            && keeps_gap(&partly, index + 1, at + 1)
347        {
348            continue;
349        }
350        match &partly[index] {
351            Some((_, lines)) => {
352                let mut text = reuse::join(lines, "\n");
353                text.push('\n');
354                if !closed_by_a_blank_line(&text) {
355                    partly[index] = None;
356                }
357            }
358            None => {
359                let span = span_of(&blocks[index]);
360                if !closed_by_a_blank_line(&source[span.start..span.end]) {
361                    reused[index] = None;
362                }
363            }
364        }
365    }
366
367    let mut output = String::with_capacity(source.len());
368    // The block written before the current one: its index in the source's
369    // parse, where its text ends there, and its line ending there when it was
370    // reused, and its marker when it is a list.
371    let mut previous_at: Option<usize> = None;
372    let mut placement = reuse::Placement::default();
373    let mut previous_ending = "";
374    let mut previous_body_end = 0;
375    // Whether the output ends with the source's own trailing bytes, which
376    // take the place of the final newline.
377    let mut ends_with_source_tail = false;
378    // Whether every block since the last one other than a blank line follows
379    // its predecessor in the source.
380    let mut run_follows = true;
381    for (index, block) in blocks.iter().enumerate() {
382        // A list written partly from the source keeps its recorded marker,
383        // which a list before it may already use.
384        if let (Block::List(list), Some(_)) = (block, &partly[index]) {
385            if placement.previous_list() == Some(written_marker(list)) {
386                partly[index] = None;
387            }
388        }
389        let follows_predecessor = matches!(
390            (previous_at, placed(&reused, &partly, index)),
391            (Some(previous), Some(at)) if previous + 1 == at && keeps_gap(&partly, index, at)
392        );
393        // Blank lines between them do not end a list or a footnote
394        // definition before an indented line, so a block follows its
395        // predecessor only where every block since the last other than a
396        // blank line did.
397        let follows_run = follows_predecessor && run_follows;
398        run_follows = index == 0 || !matches!(block, Block::BlankLine(_)) || follows_run;
399        // A block whose source the block before it could take in stays only
400        // after its predecessor in the source, written as it was there.
401        let taken_in = |text: &str| placement.takes_in(block, text, follows_run);
402        if let Some((_, lines)) = &partly[index] {
403            if taken_in(&lines[0].body) {
404                partly[index] = None;
405            }
406        }
407        let mut at = reused[index];
408        if let Some(position) = at {
409            let start = span_of(block).start;
410            // A break that comes to open the document could open
411            // frontmatter; one that opened it there stays a break while no
412            // later line closes it as frontmatter (`Document::write`), which
413            // `whole` marks.
414            let rewritten = matches!(block, Block::ThematicBreak(_))
415                && index == 0
416                && (position != 0 || whole[0] && source[start..].starts_with("---"))
417                || taken_in(&source[start..]);
418            if rewritten {
419                at = None;
420            }
421        }
422        if index > 0 {
423            if follows_predecessor {
424                let start = span_of(block).start;
425                output.push_str(&source[previous_body_end..start]);
426            } else if separated[index] {
427                // A reused block keeps its own line ending; one more makes
428                // the blank line.
429                output.push_str(previous_ending);
430                match previous_ending {
431                    "" => push_gap(&mut output, options),
432                    "\r" => output.push('\r'),
433                    _ => output.push_str(options.line_ending.as_str()),
434                }
435            } else {
436                // A blank line is a line of its own, without a separator.
437                output.push_str(match previous_ending {
438                    "" => options.line_ending.as_str(),
439                    own => own,
440                });
441            }
442        }
443        previous_ending = "";
444        match at {
445            Some(position) => {
446                let span = span_of(block);
447                let body = strip_line_ending(&source[span.start..span.end]);
448                let body_end = span.start + body.len();
449                if index == 0 && position == 0 {
450                    output.push_str(&source[..span.start]);
451                }
452                output.push_str(body);
453                if index + 1 == blocks.len() && position + 1 == base.len() {
454                    output.push_str(&source[body_end..]);
455                    ends_with_source_tail = true;
456                }
457                previous_ending = &source[body_end..span.end];
458                previous_body_end = body_end;
459                let marker = match block {
460                    Block::List(list) => Some(written_marker(list)),
461                    _ => None,
462                };
463                placement.wrote(block, marker, false);
464            }
465            None if partly[index].is_some() => {
466                let (position, lines) = partly[index].as_ref().expect("written partly");
467                let span = base[*position].span().expect("a parsed block has a span");
468                let body_end = span.start + strip_line_ending(&source[span.start..span.end]).len();
469                if index == 0 && *position == 0 {
470                    output.push_str(&source[..span.start]);
471                }
472                output.push_str(&reuse::join(lines, options.line_ending.as_str()));
473                if index + 1 == blocks.len() && *position + 1 == base.len() {
474                    output.push_str(&source[body_end..]);
475                    ends_with_source_tail = true;
476                }
477                previous_body_end = body_end;
478                let marker = match block {
479                    Block::List(list) => Some(written_marker(list)),
480                    _ => None,
481                };
482                placement.wrote(block, marker, open_lists[index]);
483                at = Some(*position);
484            }
485            None => {
486                let mut text = write_blocks(
487                    core::slice::from_ref(block),
488                    options,
489                    Join::Gap,
490                    index == 0,
491                    placement.previous_list(),
492                )?;
493                if options.line_ending == LineEnding::CrLf {
494                    text = lf_to_crlf(&text);
495                }
496                output.push_str(&text);
497                let marker = match block {
498                    Block::List(list) => {
499                        Some(list_marker(list, options, placement.previous_list()))
500                    }
501                    _ => None,
502                };
503                placement.wrote(block, marker, false);
504            }
505        }
506        previous_at = at;
507    }
508    let ends_with_blank_line = matches!(blocks.last(), Some(Block::BlankLine(_)));
509    if !ends_with_source_tail
510        && options.final_newline
511        && !blocks.is_empty()
512        && (ends_with_blank_line || !ends_with_carriage_return_ending(&output))
513    {
514        // A reused last block keeps its own line ending, which can be part
515        // of its value, as in indented code.
516        output.push_str(match previous_ending {
517            "" => options.line_ending.as_str(),
518            own => own,
519        });
520    }
521    let partly = partly
522        .iter()
523        .enumerate()
524        .filter_map(|(index, lines)| lines.as_ref().map(|_| index))
525        .collect();
526    Ok((output, partly))
527}
528
529/// The blocks among `partly`, indices of `blocks` written partly from the
530/// source, that `output` reads back as something else in their place: each
531/// one that differs, or every one when the output holds a different number
532/// of top-level blocks.
533fn misread(blocks: &[Block], output: &str, partly: &[usize]) -> Vec<usize> {
534    let read = crate::parse(output).document.children;
535    if read.len() != blocks.len() {
536        return partly.to_vec();
537    }
538    partly
539        .iter()
540        .copied()
541        .filter(|index| !reads_as(&read[*index], &blocks[*index]))
542        .collect()
543}
544
545/// Whether `read`, a block of the parsed output, reads as `block` as
546/// "Round-trip stability" compares them, an ordered item without a number
547/// reading as the number it is written with.
548fn reads_as(read: &Block, block: &Block) -> bool {
549    let mut read = crate::compare::normalized_blocks(core::slice::from_ref(read));
550    let mut block = crate::compare::normalized_blocks(core::slice::from_ref(block));
551    number_items(&mut read);
552    number_items(&mut block);
553    read == block
554}
555
556/// Gives each ordered item without a number the one it is written with.
557fn number_items(blocks: &mut [Block]) {
558    for block in blocks {
559        match block {
560            Block::List(list) => {
561                let mut previous = None;
562                for item in &mut list.children {
563                    if list.ordered {
564                        item.number = Some(item_number(item, &mut previous));
565                    }
566                    number_items(&mut item.children);
567                }
568            }
569            Block::BlockQuote(node) => number_items(&mut node.children),
570            Block::Alert(node) => number_items(&mut node.children),
571            Block::FootnoteDefinition(node) => number_items(&mut node.children),
572            Block::ContainerDirective(node) => number_items(&mut node.children),
573            Block::HtmlContainer(node) => {
574                if let HtmlContainerContent::Blocks(children) = &mut node.content {
575                    number_items(children);
576                }
577            }
578            Block::Paragraph(_)
579            | Block::Heading(_)
580            | Block::ThematicBreak(_)
581            | Block::CodeBlock(_)
582            | Block::HtmlBlock(_)
583            | Block::Definition(_)
584            | Block::Table(_)
585            | Block::MathBlock(_)
586            | Block::Frontmatter(_)
587            | Block::LeafDirective(_)
588            | Block::BlankLine(_) => {}
589        }
590    }
591}
592
593/// Whether a block corresponds to a block of the document its source parses
594/// to: their spans and own fields are equal and their children correspond,
595/// where a leaf that keeps its text corresponds to the leaf at its span whose
596/// inline source is that text.
597struct Correspondence<'a> {
598    base: &'a Document,
599    /// The inline source of each leaf of `base`, by span, read once the
600    /// first leaf that keeps its text needs it.
601    sources: Option<BTreeMap<(usize, usize), InlineSource>>,
602}
603
604impl Correspondence<'_> {
605    /// The inline source of each leaf of the base, by span.
606    fn sources(&mut self) -> &BTreeMap<(usize, usize), InlineSource> {
607        let base_document = self.base;
608        self.sources.get_or_insert_with(|| {
609            let mut sources = BTreeMap::new();
610            base_document.each_leaf_source(|_, span, source| {
611                sources.insert((span.start, span.end), source);
612            });
613            sources
614        })
615    }
616
617    /// The inline source of the base leaf at `span`.
618    fn source_of(&mut self, span: Span) -> Option<&InlineSource> {
619        self.sources().get(&(span.start, span.end))
620    }
621
622    fn holds(&mut self, block: &Block, base: &Block) -> bool {
623        if !leaves::any_kept(core::slice::from_ref(block)) {
624            return block == base;
625        }
626        let sources = self.sources();
627        // Each leaf whose text is its base leaf's inline source takes the
628        // inlines that text reads as there.
629        let mut block = block.clone();
630        leaves::visit_mut(core::slice::from_mut(&mut block), &mut |leaf| {
631            let (Some(text), Some(span)) = (leaf.source.as_deref(), leaf.span) else {
632                return;
633            };
634            if let Some(source) = sources
635                .get(&(span.start, span.end))
636                .filter(|source| source.text == text)
637            {
638                crate::parse::inline_spans_to_source(source, leaf.inlines);
639                *leaf.source = None;
640            }
641        });
642        block == *base
643    }
644}
645
646/// For each block, whether the boundary before it holds the blank line that
647/// separates two blocks: one written after a block other than a blank line,
648/// before a block or before the blank lines that precede one.
649fn separated_boundaries(blocks: &[Block]) -> Vec<bool> {
650    let mut separated = alloc::vec![false; blocks.len()];
651    let mut block_follows = false;
652    for index in (1..blocks.len()).rev() {
653        block_follows |= !matches!(blocks[index], Block::BlankLine(_));
654        separated[index] = block_follows && !matches!(blocks[index - 1], Block::BlankLine(_));
655    }
656    separated
657}
658
659/// Whether a block written from the source, whose text opens with `text`,
660/// reads differently where it does not follow its predecessor in the
661/// source, after `last`, the last block written before it other than a blank
662/// line, and `previous_list`, the marker of a list written just before it:
663/// indentation after a list or a footnote definition, which blank lines do
664/// not end, continues them; four columns of it on a block other than
665/// indented code read as indented code, as a paragraph opened by definitions
666/// took them as a continuation; and a list after a list written with its
667/// marker continues that list. A blank line reads as one wherever it stands.
668/// `rewritten_after_an_edit` in `tests/support/edits.rs` states the same rule
669/// for the simulated edits.
670fn moves_into_previous(
671    block: &Block,
672    text: &str,
673    last: Option<&Block>,
674    previous_list: Option<char>,
675) -> bool {
676    match block {
677        Block::BlankLine(_) => false,
678        Block::List(list) if previous_list == Some(written_marker(list)) => true,
679        _ if matches!(last, Some(Block::List(_) | Block::FootnoteDefinition(_))) => {
680            text.starts_with([' ', '\t'])
681        }
682        Block::CodeBlock(_) => false,
683        _ => opens_with_code_indent(text),
684    }
685}
686
687/// Whether `text` opens with four columns of indentation, which starts
688/// indented code where no paragraph continues.
689fn opens_with_code_indent(text: &str) -> bool {
690    let mut column = 0;
691    for char in text.chars() {
692        match char {
693            ' ' => column += 1,
694            '\t' => column += 4 - column % 4,
695            _ => break,
696        }
697        if column >= 4 {
698            return true;
699        }
700    }
701    false
702}
703
704/// Whether the block parsed from `text`, its source with its line ending,
705/// keeps that text when a blank line and a paragraph follow it, as a closed
706/// fence does and an unclosed one does not. `tests/support/edits.rs` holds a
707/// copy.
708fn closed_by_a_blank_line(text: &str) -> bool {
709    let gap = if text.ends_with('\n') {
710        "\n"
711    } else if text.ends_with('\r') {
712        "\r"
713    } else {
714        "\n\n"
715    };
716    let probe = format!("{text}{gap}x");
717    let children = crate::parse(&probe).document.children;
718    let Some((Block::Paragraph(last), rest)) = children.split_last() else {
719        return false;
720    };
721    last.meta
722        .span
723        .is_some_and(|span| span.start + 1 == probe.len())
724        && rest.iter().all(|block| {
725            matches!(block, Block::BlankLine(_))
726                || block.span().is_some_and(|span| span.end <= text.len())
727        })
728}
729
730/// Pushes the gap between two top-level blocks in the options' line ending.
731fn push_gap(output: &mut String, options: &SerializeOptions) {
732    let from = output.len();
733    push_block_gap(output);
734    if options.line_ending == LineEnding::CrLf {
735        let gap = lf_to_crlf(&output[from..]);
736        output.truncate(from);
737        output.push_str(&gap);
738    }
739}
740
741/// `text` without one trailing `\n`, `\r\n`, or `\r`.
742fn strip_line_ending(text: &str) -> &str {
743    text.strip_suffix("\r\n")
744        .or_else(|| text.strip_suffix('\n'))
745        .or_else(|| text.strip_suffix('\r'))
746        .unwrap_or(text)
747}
748
749/// Rewrites each bare `\n` as `\r\n`; existing `\r\n` and `\r` endings that
750/// verbatim values carry stay as they are.
751fn lf_to_crlf(input: &str) -> String {
752    let mut output = String::with_capacity(input.len());
753    let mut previous = '\0';
754    for ch in input.chars() {
755        if ch == '\n' && previous != '\r' {
756            output.push('\r');
757        }
758        output.push(ch);
759        previous = ch;
760    }
761    output
762}
763
764fn ends_with_carriage_return_ending(input: &str) -> bool {
765    input.ends_with('\r') || input.ends_with("\r\n")
766}
767
768/// Separates two blocks with one blank line. A block that already ends its
769/// last line with `\r` or `\r\n` gets only the blank line, written so that it
770/// cannot merge into that ending.
771fn push_block_gap(output: &mut String) {
772    if output.ends_with('\r') {
773        output.push('\r');
774    } else if output.ends_with("\r\n") {
775        output.push('\n');
776    } else {
777        output.push_str("\n\n");
778    }
779}
780
781/// How sibling blocks are separated.
782#[derive(Clone, Copy, Eq, PartialEq)]
783enum Join {
784    /// By a blank line.
785    Gap,
786    /// By a line ending alone, as the blocks of a tight list item are.
787    Line,
788}
789
790/// Writes a sequence of sibling blocks. `document_start` is true only for the
791/// top-level document body, where the first block sits at byte 0 and a
792/// contiguous `---` would open frontmatter.
793///
794/// Two blocks are separated by `join`, and each `BlankLine` is one more empty
795/// line: between two blocks after their separator, and before the first
796/// block or after the last on its own.
797fn write_blocks(
798    blocks: &[Block],
799    options: &SerializeOptions,
800    join: Join,
801    document_start: bool,
802    previous_list: Option<char>,
803) -> Result<String, SerializeError> {
804    let mut output = String::new();
805    // The marker of the list written right before the current block; ordered
806    // and unordered markers never share a char.
807    let mut previous_list = previous_list;
808    // Whether a block other than a blank line was written, and the blank
809    // lines since the last one.
810    let mut wrote_block = false;
811    let mut blank_lines = 0;
812    for (index, block) in blocks.iter().enumerate() {
813        if let Block::BlankLine(_) = block {
814            blank_lines += 1;
815            continue;
816        }
817        if wrote_block {
818            match join {
819                Join::Gap => push_block_gap(&mut output),
820                Join::Line => output.push('\n'),
821            }
822        }
823        output.extend(core::iter::repeat_n('\n', blank_lines));
824        blank_lines = 0;
825        let after_paragraph_line =
826            join == Join::Line && index > 0 && matches!(blocks[index - 1], Block::Paragraph(_));
827        let written = match block {
828            Block::List(list) => {
829                let marker = list_marker(list, options, previous_list);
830                previous_list = Some(marker);
831                write_list(list, options, marker)?
832            }
833            Block::ThematicBreak(node) => {
834                previous_list = None;
835                write_thematic_break(node, document_start && index == 0, after_paragraph_line)
836            }
837            _ => {
838                previous_list = None;
839                write_block(block, options)?
840            }
841        };
842        output.push_str(&written);
843        wrote_block = true;
844    }
845    // Trailing blank lines each end the line before them; with no block,
846    // the first of them starts the output.
847    let trailing = if wrote_block {
848        blank_lines
849    } else {
850        blank_lines.saturating_sub(1)
851    };
852    output.extend(core::iter::repeat_n('\n', trailing));
853    Ok(output)
854}
855
856/// Writes `block` as canonical Markdown, and a leaf that keeps its text as
857/// that text: `to_markdown_with` has cleared every kept text its leaf's
858/// inlines no longer read as.
859fn write_block(block: &Block, options: &SerializeOptions) -> Result<String, SerializeError> {
860    match block {
861        Block::BlankLine(_) => Ok(String::new()),
862        Block::Paragraph(node) => match &node.source {
863            Some(text) => Ok(text.clone()),
864            None => write_inlines(&node.children, Context::BLOCK),
865        },
866        Block::Heading(node) => write_heading(node),
867        Block::ThematicBreak(node) => Ok(write_thematic_break(node, false, false)),
868        Block::BlockQuote(node) => {
869            let inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
870            // No content, or one blank line, is one `>` line.
871            Ok(if inner.is_empty() {
872                ">".into()
873            } else {
874                quote_lines(&inner)
875            })
876        }
877        Block::Alert(node) => write_alert(node, options),
878        Block::List(node) => write_list(node, options, list_marker(node, options, None)),
879        Block::CodeBlock(node) => Ok(write_code_block(node, options)),
880        // An HTML block's lines are joined with `\n`, so a value ending in one
881        // ends with an empty line that belongs to the block (an unclosed
882        // comment, say); it is written as it is.
883        Block::HtmlBlock(node) => Ok(node.value.clone()),
884        Block::HtmlContainer(node) => write_html_container(node, options),
885        Block::Definition(node) => Ok(write_definition(node)),
886        Block::FootnoteDefinition(node) => {
887            let inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
888            // Content opening with a space or a tab (indented code, say)
889            // starts on the line after the label, as in a list item, where
890            // the label's line would take that whitespace as its separator.
891            Ok(if inner.starts_with([' ', '\t']) {
892                format!(
893                    "[^{}]:\n{}",
894                    node.label,
895                    prefix_container_lines(&inner, "    ")
896                )
897            } else {
898                format!("[^{}]: {}", node.label, indent_continuation(&inner))
899            })
900        }
901        Block::Table(node) => write_table(node),
902        Block::MathBlock(node) => {
903            let fence = block_math_fence(&node.value);
904            Ok(fenced_body(&fence, &node.value, &fence))
905        }
906        Block::Frontmatter(node) => {
907            let fence = match node.kind {
908                FrontmatterKind::Yaml => "---",
909                FrontmatterKind::Toml => "+++",
910            };
911            // The value holds the lines between the fences joined with `\n`,
912            // so a final `\n` is an empty last line.
913            Ok(format!("{fence}\n{}\n{fence}", node.value))
914        }
915        Block::LeafDirective(node) => Ok(format!(
916            "::{}{}{}",
917            node.name,
918            write_directive_label(&node.label, node.label_source.as_deref())?,
919            write_attributes(&node.attributes)
920        )),
921        Block::ContainerDirective(node) => {
922            let mut inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
923            let fence = directive_fence(&inner);
924            // Content ends with a line ending before the closing fence; an
925            // empty directive takes no blank line, which would loosen a list
926            // holding it.
927            if !node.children.is_empty() {
928                inner.push('\n');
929            }
930            Ok(format!(
931                "{fence}{}{}{}\n{inner}{fence}",
932                node.name,
933                write_directive_label(&node.label, node.label_source.as_deref())?,
934                write_attributes(&node.attributes)
935            ))
936        }
937    }
938}
939
940/// A thematic break. A dash break is written spaced where a contiguous `---`
941/// would read otherwise: at the document start, where it opens frontmatter,
942/// and on the line after a paragraph's, where it underlines a setext heading.
943fn write_thematic_break(
944    node: &ThematicBreak,
945    at_document_start: bool,
946    after_paragraph_line: bool,
947) -> String {
948    match node.marker {
949        ThematicBreakMarker::Dash if at_document_start || after_paragraph_line => "- - -".into(),
950        ThematicBreakMarker::Dash => "---".into(),
951        ThematicBreakMarker::Asterisk => "***".into(),
952        ThematicBreakMarker::Underscore => "___".into(),
953    }
954}
955
956/// Whether `node` is written as a setext heading: a setext underline can only
957/// express depth 1 (`=`) or 2 (`-`), and it underlines content.
958pub(crate) fn writes_setext(node: &Heading) -> bool {
959    node.kind == HeadingKind::Setext && matches!(node.depth, 1 | 2) && !node.children.is_empty()
960}
961
962fn write_heading(node: &Heading) -> Result<String, SerializeError> {
963    let setext = writes_setext(node);
964    let content = match one_line(node.source.as_deref(), !setext) {
965        Some(text) => text.into(),
966        None => write_inlines(&node.children, Context::HEADING)?,
967    };
968    if setext {
969        let marker = if node.depth == 1 { "=" } else { "-" };
970        return Ok(format!(
971            "{content}\n{}",
972            marker.repeat(content.len().max(3))
973        ));
974    }
975    let hashes = "#".repeat(usize::from(node.depth));
976    Ok(if content.is_empty() {
977        hashes
978    } else if reads_as_closing_sequence(&content) {
979        // Content ending in what would read as the closing sequence keeps it
980        // by a closing sequence of its own.
981        format!("{hashes} {content} #")
982    } else {
983        format!("{hashes} {content}")
984    })
985}
986
987/// Whether written ATX content ends in a `#` run that is all of it or follows
988/// a space or a tab, which a reader strips as the closing sequence.
989fn reads_as_closing_sequence(content: &str) -> bool {
990    let before = content.trim_end_matches('#');
991    before.len() < content.len() && (before.is_empty() || before.ends_with([' ', '\t']))
992}
993
994fn write_definition(node: &Definition) -> String {
995    let destination = write_destination(&node.destination, node.destination_kind);
996    let mut output = format!("[{}]: {}", node.label, destination);
997    if let Some(title) = &node.title {
998        output.push(' ');
999        output.push_str(&write_title(title));
1000    }
1001    output
1002}
1003
1004fn write_html_container(
1005    node: &HtmlContainer,
1006    options: &SerializeOptions,
1007) -> Result<String, SerializeError> {
1008    match &node.content {
1009        HtmlContainerContent::Blocks(children) => {
1010            let inner = write_blocks(children, options, Join::Gap, false, None)?;
1011            // Content takes a blank line before the closing tag; blank lines
1012            // alone stand between the tags.
1013            let blank_lines = children
1014                .iter()
1015                .all(|block| matches!(block, Block::BlankLine(_)));
1016            Ok(if blank_lines {
1017                format!(
1018                    "{}\n{}{}",
1019                    node.opening.raw,
1020                    "\n".repeat(children.len()),
1021                    node.closing.raw
1022                )
1023            } else {
1024                format!("{}\n{}\n\n{}", node.opening.raw, inner, node.closing.raw)
1025            })
1026        }
1027        HtmlContainerContent::Inlines(children) => Ok(format!(
1028            "{}{}{}",
1029            node.opening.raw,
1030            write_inlines(children, Context::BLOCK)?,
1031            node.closing.raw
1032        )),
1033    }
1034}
1035
1036fn write_alert(node: &Alert, options: &SerializeOptions) -> Result<String, SerializeError> {
1037    let mut output = String::from("> [!");
1038    output.push_str(alert_kind_name(node.kind));
1039    output.push(']');
1040    if let Some(title) = &node.title {
1041        if !title.is_empty() {
1042            output.push(' ');
1043            // A line ending would end the marker's line.
1044            output.push_str(&title.replace(['\n', '\r'], " "));
1045        }
1046    }
1047    let inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
1048    if !inner.is_empty() {
1049        output.push('\n');
1050        output.push_str(&quote_lines(&inner));
1051    } else if !node.children.is_empty() {
1052        // One blank line.
1053        output.push_str("\n>");
1054    }
1055    Ok(output)
1056}
1057
1058/// `inner` with every line in the quote: an empty last line, which a value
1059/// ending in a line ending holds, takes `>` too.
1060fn quote_lines(inner: &str) -> String {
1061    let mut output = prefix_container_lines(inner, "> ");
1062    if inner.ends_with(['\n', '\r']) {
1063        output.push('>');
1064    }
1065    output
1066}
1067
1068fn alert_kind_name(kind: AlertKind) -> &'static str {
1069    match kind {
1070        AlertKind::Note => "NOTE",
1071        AlertKind::Tip => "TIP",
1072        AlertKind::Important => "IMPORTANT",
1073        AlertKind::Warning => "WARNING",
1074        AlertKind::Caution => "CAUTION",
1075    }
1076}
1077
1078/// The marker char a list is written with: the one the options replace it
1079/// with, or else the one the AST records. A marker that the list written right
1080/// before it in the same container, `previous`, also uses yields to the next
1081/// one in the order `-`, `*`, `+`, or `.`, `)`; validation keeps two adjacent
1082/// recorded markers apart, so only a replaced marker can meet this.
1083fn list_marker(list: &List, options: &SerializeOptions, previous: Option<char>) -> char {
1084    let marker = if list.ordered {
1085        options
1086            .ordered_delimiter
1087            .map_or_else(|| written_marker(list), ordered_delimiter_char)
1088    } else {
1089        options
1090            .bullet
1091            .map_or_else(|| written_marker(list), bullet_char)
1092    };
1093    if previous == Some(marker) {
1094        let order: &[char] = if list.ordered {
1095            &['.', ')']
1096        } else {
1097            &['-', '*', '+']
1098        };
1099        let at = order.iter().position(|char| *char == marker).unwrap_or(0);
1100        return order[(at + 1) % order.len()];
1101    }
1102    marker
1103}
1104
1105/// The marker char the AST records for a list: its bullet, or its ordered
1106/// delimiter. A delimiter of the other list kind is written `-` or `.`.
1107/// Serialization and validation's adjacent-list rule both read it.
1108pub(crate) fn written_marker(list: &List) -> char {
1109    match (list.ordered, list.delimiter) {
1110        (false, ListDelimiter::Asterisk) => '*',
1111        (false, ListDelimiter::Plus) => '+',
1112        (false, _) => '-',
1113        (true, ListDelimiter::Paren) => ')',
1114        (true, _) => '.',
1115    }
1116}
1117
1118fn bullet_char(bullet: BulletMarker) -> char {
1119    match bullet {
1120        BulletMarker::Dash => '-',
1121        BulletMarker::Asterisk => '*',
1122        BulletMarker::Plus => '+',
1123    }
1124}
1125
1126fn ordered_delimiter_char(delimiter: OrderedDelimiter) -> char {
1127    match delimiter {
1128        OrderedDelimiter::Period => '.',
1129        OrderedDelimiter::Paren => ')',
1130    }
1131}
1132
1133fn write_list(
1134    node: &List,
1135    options: &SerializeOptions,
1136    marker_char: char,
1137) -> Result<String, SerializeError> {
1138    let mut output = String::new();
1139    // The number the previous item was written with.
1140    let mut previous: Option<u64> = None;
1141    for (index, item) in node.children.iter().enumerate() {
1142        if index > 0 {
1143            output.push_str(if node.tight { "\n" } else { "\n\n" });
1144        }
1145        output.push_str(&write_item(
1146            node,
1147            item,
1148            options,
1149            marker_char,
1150            &mut previous,
1151        )?);
1152    }
1153    Ok(output)
1154}
1155
1156/// The number an ordered item is written with: its own, or else one more than
1157/// the previous item's, `previous`, which it then becomes.
1158fn item_number(item: &ListItem, previous: &mut Option<u64>) -> u64 {
1159    // An item without a number counts on from the previous one, and stops at
1160    // the largest number a marker can hold.
1161    let number = item.number.unwrap_or_else(|| {
1162        previous.map_or(1, |previous| {
1163            previous
1164                .saturating_add(1)
1165                .min(crate::parse::MAX_ORDERED_NUMBER)
1166        })
1167    });
1168    *previous = Some(number);
1169    number
1170}
1171
1172/// One item of `list`, written with `marker_char`; `previous` is the number
1173/// the item before it was written with.
1174fn write_item(
1175    list: &List,
1176    item: &ListItem,
1177    options: &SerializeOptions,
1178    marker_char: char,
1179    previous: &mut Option<u64>,
1180) -> Result<String, SerializeError> {
1181    let marker = if list.ordered {
1182        format!("{}{marker_char} ", item_number(item, previous))
1183    } else {
1184        format!("{marker_char} ")
1185    };
1186    let inner = write_item_blocks(&item.children, options, list.tight, item.checked)?;
1187    let break_after_bullet = breaks_after_bullet(list, item, marker_char);
1188    let opens_with_whitespace = opens_with_padding(&inner);
1189    Ok(if break_after_bullet || opens_with_whitespace {
1190        format!(
1191            "{}\n{}",
1192            marker.trim_end(),
1193            prefix_container_lines(&inner, &" ".repeat(marker.len()))
1194        )
1195    } else {
1196        format!("{marker}{}", indent_after_first_line(&inner, marker.len()))
1197    })
1198}
1199
1200fn thematic_break_char(marker: ThematicBreakMarker) -> char {
1201    match marker {
1202        ThematicBreakMarker::Dash => '-',
1203        ThematicBreakMarker::Asterisk => '*',
1204        ThematicBreakMarker::Underscore => '_',
1205    }
1206}
1207
1208/// An item's blocks; a task item's, `task` holding whether it is checked,
1209/// open with its checkbox: at the start of the first paragraph after the
1210/// definitions the item starts with, or else of the item.
1211/// A thematic break of the bullet's own char right after the bullet would
1212/// read as one longer break, so it starts on the next line.
1213fn breaks_after_bullet(list: &List, item: &ListItem, marker_char: char) -> bool {
1214    !list.ordered
1215        && item.checked.is_none()
1216        && matches!(
1217            item.children.first(),
1218            Some(Block::ThematicBreak(ThematicBreak { marker, .. }))
1219                if thematic_break_char(*marker) == marker_char
1220        )
1221}
1222
1223/// Spaces or a tab opening an item's written content would read as padding
1224/// after the marker, so that content starts on the next line. Four spaces or
1225/// more open indented code, which the marker line holds after one space: an
1226/// item that starts empty cannot interrupt a paragraph.
1227fn opens_with_padding(inner: &str) -> bool {
1228    !inner.starts_with("    ") && inner.starts_with([' ', '\t'])
1229}
1230
1231/// Whether `block`, written with default options on the line after a
1232/// paragraph's, starts there instead of continuing the paragraph: the
1233/// parser's rule (`interrupts_paragraph`) for the block as the serializer
1234/// writes it, a setext heading and a list whose first item starts below its
1235/// bullet continuing the paragraph.
1236pub(crate) fn written_after_paragraph_interrupts(block: &Block) -> bool {
1237    match block {
1238        Block::Heading(heading) => !writes_setext(heading),
1239        Block::List(list) => {
1240            crate::parse::interrupts_paragraph(block) && !first_item_starts_below_its_bullet(list)
1241        }
1242        _ => crate::parse::interrupts_paragraph(block),
1243    }
1244}
1245
1246/// Whether, with default options, the serializer starts `list`'s first item
1247/// on a line after its bullet, so that the list, written after a paragraph's
1248/// line, cannot interrupt the paragraph. Only a leaf block can open with
1249/// whitespace, so only a leading leaf block is written to tell.
1250pub(crate) fn first_item_starts_below_its_bullet(list: &List) -> bool {
1251    let Some(item) = list.children.first() else {
1252        return false;
1253    };
1254    let options = SerializeOptions::default();
1255    if breaks_after_bullet(list, item, list_marker(list, &options, None)) {
1256        return true;
1257    }
1258    if item.checked.is_some() {
1259        return false;
1260    }
1261    match item.children.first() {
1262        Some(
1263            block @ (Block::Paragraph(_)
1264            | Block::Heading(_)
1265            | Block::HtmlBlock(_)
1266            | Block::CodeBlock(_)),
1267        ) => write_block(block, &options).is_ok_and(|written| opens_with_padding(&written)),
1268        _ => false,
1269    }
1270}
1271
1272fn write_item_blocks(
1273    blocks: &[Block],
1274    options: &SerializeOptions,
1275    tight: bool,
1276    task: Option<bool>,
1277) -> Result<String, SerializeError> {
1278    let join = if tight { Join::Line } else { Join::Gap };
1279    let Some(checked) = task else {
1280        return write_blocks(blocks, options, join, false, None);
1281    };
1282    let checkbox = if checked { "[x]" } else { "[ ]" };
1283    let definitions = blocks
1284        .iter()
1285        .take_while(|block| matches!(block, Block::Definition(_)))
1286        .count();
1287    if matches!(blocks.get(definitions), Some(Block::Paragraph(_))) {
1288        let before = write_blocks(&blocks[..definitions], options, join, false, None)?;
1289        let rest = write_blocks(&blocks[definitions..], options, join, false, None)?;
1290        let mut output = before;
1291        if !output.is_empty() {
1292            match join {
1293                Join::Gap => push_block_gap(&mut output),
1294                Join::Line => output.push('\n'),
1295            }
1296        }
1297        output.push_str(checkbox);
1298        output.push(' ');
1299        output.push_str(&rest);
1300        return Ok(output);
1301    }
1302    let written = write_blocks(blocks, options, join, false, None)?;
1303    Ok(if written.is_empty() {
1304        checkbox.into()
1305    } else {
1306        format!("{checkbox} {written}")
1307    })
1308}
1309
1310fn write_code_block(node: &CodeBlock, options: &SerializeOptions) -> String {
1311    match node.kind {
1312        CodeBlockKind::Indented => {
1313            // Each value line ends with a line ending; the block gap or the
1314            // final newline writes a last `\n`, so only `\r` and `\r\n` stay.
1315            let body = trim_trailing_newline(&node.value);
1316            let mut output = prefix_lines(body, "    ");
1317            let ending = &node.value[body.len()..];
1318            if matches!(ending, "\r" | "\r\n") {
1319                output.push_str(ending);
1320            }
1321            output
1322        }
1323        CodeBlockKind::Fenced { marker, length } => {
1324            let marker = code_block_fence_marker(node, marker, options);
1325            let (fence, indent) = code_block_fence(&node.value, marker, length.max(3));
1326            let mut opener = fence.clone();
1327            if let Some(info) = &node.info {
1328                opener.push(' ');
1329                opener.push_str(&escape_code_info(info));
1330            }
1331            let body = fenced_body(&opener, &node.value, &fence);
1332            if indent == 0 {
1333                body
1334            } else {
1335                prefix_lines(&body, &" ".repeat(indent))
1336            }
1337        }
1338    }
1339}
1340
1341/// A fenced block: `opener`, then `value` (whose lines each keep their line
1342/// ending, the last one optionally), then `closer`. An empty value writes no
1343/// line between the fences.
1344fn fenced_body(opener: &str, value: &str, closer: &str) -> String {
1345    let mut output = String::with_capacity(opener.len() + value.len() + closer.len() + 2);
1346    output.push_str(opener);
1347    output.push('\n');
1348    output.push_str(value);
1349    if !value.is_empty() && !ends_with_line_ending(value) {
1350        output.push('\n');
1351    }
1352    output.push_str(closer);
1353    output
1354}
1355
1356/// The fence char a code block is written with: tildes when its info string
1357/// holds a backtick, which a backtick fence's info string cannot, or else the
1358/// char the AST records or the options replace it with.
1359fn code_block_fence_marker(
1360    node: &CodeBlock,
1361    marker: FenceMarker,
1362    options: &SerializeOptions,
1363) -> FenceMarker {
1364    if node.info.as_deref().is_some_and(|info| info.contains('`')) {
1365        return FenceMarker::Tilde;
1366    }
1367    options.fence_marker.unwrap_or(marker)
1368}
1369
1370fn escape_code_info(input: &str) -> String {
1371    let mut output = String::new();
1372    // The parser trims the info string, so whitespace at either end is
1373    // written as a character reference.
1374    let inner_start = input.len() - input.trim_start_matches([' ', '\t']).len();
1375    let inner_end = input.trim_end_matches([' ', '\t']).len().max(inner_start);
1376    for (offset, char) in input.char_indices() {
1377        match char {
1378            ' ' | '\t' if offset < inner_start || offset >= inner_end => {
1379                output.push_str(&char_reference(char));
1380            }
1381            '\t' => output.push(char),
1382            char if char.is_control() => output.push_str(&char_reference(char)),
1383            '\\' | '&' => {
1384                output.push('\\');
1385                output.push(char);
1386            }
1387            _ => output.push(char),
1388        }
1389    }
1390    output
1391}
1392
1393/// A hexadecimal character reference for `char`.
1394fn char_reference(char: char) -> String {
1395    format!("&#x{:X};", char as u32)
1396}
1397
1398fn write_table(node: &Table) -> Result<String, SerializeError> {
1399    let mut output = String::new();
1400    for (row_index, row) in node.rows.iter().enumerate() {
1401        if row_index > 0 {
1402            output.push('\n');
1403        }
1404        output.push_str(&write_row(row)?);
1405        if row_index == 0 {
1406            output.push('\n');
1407            output.push_str(&write_delimiter_row(&node.alignments));
1408        }
1409    }
1410    Ok(output)
1411}
1412
1413fn write_delimiter_row(alignments: &[TableAlignment]) -> String {
1414    let cells = alignments
1415        .iter()
1416        .map(|alignment| match alignment {
1417            TableAlignment::None => "---",
1418            TableAlignment::Left => ":---",
1419            TableAlignment::Center => ":---:",
1420            TableAlignment::Right => "---:",
1421        })
1422        .collect::<Vec<_>>();
1423    format!("| {} |", cells.join(" | "))
1424}
1425
1426fn write_row(row: &TableRow) -> Result<String, SerializeError> {
1427    let cells = row
1428        .cells
1429        .iter()
1430        .map(|cell| {
1431            Ok(encode_cell(&match one_line(cell.source.as_deref(), true) {
1432                Some(text) => text.into(),
1433                None => write_inlines(&cell.children, Context::BLOCK)?,
1434            }))
1435        })
1436        .collect::<Result<Vec<_>, SerializeError>>()?;
1437    Ok(format!("| {} |", cells.join(" | ")))
1438}
1439
1440/// A cell's inline content, written as anywhere else, encoded as cell
1441/// source. The parser splits a row at each `|` after no backslash or an even
1442/// run of them, and reads an odd run before a `|` with one backslash less, so
1443/// a `\` added before each `|` of the first kind reads back as written. A `|`
1444/// that the content already puts after an odd run, an escaped `|` in text,
1445/// reads with one backslash less, still as an escaped `|`; validation rejects
1446/// the cell values whose meaning that would change.
1447fn encode_cell(written: &str) -> String {
1448    let mut output = String::with_capacity(written.len());
1449    let mut backslashes = 0usize;
1450    for char in written.chars() {
1451        if char == '|' && backslashes % 2 == 0 {
1452            output.push('\\');
1453        }
1454        backslashes = if char == '\\' { backslashes + 1 } else { 0 };
1455        output.push(char);
1456    }
1457    output
1458}
1459
1460/// Where inline content is written, as the value encodings of its nodes read
1461/// it.
1462#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1463struct Context {
1464    /// In a heading, which a line ending would end.
1465    heading: bool,
1466}
1467
1468impl Context {
1469    const BLOCK: Self = Self { heading: false };
1470    const HEADING: Self = Self { heading: true };
1471}
1472
1473fn write_inlines(inlines: &[Inline], context: Context) -> Result<String, SerializeError> {
1474    let mut output = InlineOut::default();
1475    for inline in inlines {
1476        write_inline(inline, context, &mut output)?;
1477    }
1478    Ok(output.text)
1479}
1480
1481/// The Markdown written so far for the inline content of one block, cell, or
1482/// label, which the parser reads in one inline pass, and the backtick runs in
1483/// it that a code span's fence must not close.
1484#[derive(Default)]
1485struct InlineOut {
1486    text: String,
1487    /// `open_runs[n]`: a backtick run of length `n` that opens no code span
1488    /// was written, outside every code span: a later fence of that length
1489    /// would close it.
1490    open_runs: Vec<bool>,
1491    /// How far `text` has been read for `open_runs`.
1492    scanned: usize,
1493}
1494
1495impl InlineOut {
1496    /// Reads the text written since the last code span for backtick runs. A
1497    /// backslash escapes the first backtick of a run, which then opens with
1498    /// the rest of the run.
1499    fn scan_open_runs(&mut self) {
1500        let mut escaped = false;
1501        let mut run = 0;
1502        let mut run_escaped = false;
1503        for byte in self.text.as_bytes()[self.scanned..]
1504            .iter()
1505            .copied()
1506            .chain(core::iter::once(b' '))
1507        {
1508            if byte == b'`' {
1509                if run == 0 {
1510                    run_escaped = escaped;
1511                }
1512                run += 1;
1513                escaped = false;
1514                continue;
1515            }
1516            let opening = run - usize::from(run_escaped && run > 0);
1517            if opening > 0 {
1518                if self.open_runs.len() <= opening {
1519                    self.open_runs.resize(opening + 1, false);
1520                }
1521                self.open_runs[opening] = true;
1522            }
1523            run = 0;
1524            escaped = byte == b'\\' && !escaped;
1525        }
1526        self.scanned = self.text.len();
1527    }
1528
1529    fn is_open_run(&self, length: usize) -> bool {
1530        self.open_runs.get(length).copied().unwrap_or(false)
1531    }
1532
1533    /// Writes, with `write`, content that the parser reads whole from its
1534    /// first char, so no backtick run in it opens a code span.
1535    fn push_opaque(&mut self, write: impl FnOnce(&mut String)) {
1536        self.scan_open_runs();
1537        write(&mut self.text);
1538        self.scanned = self.text.len();
1539    }
1540}
1541
1542impl core::ops::Deref for InlineOut {
1543    type Target = String;
1544
1545    fn deref(&self) -> &String {
1546        &self.text
1547    }
1548}
1549
1550impl core::ops::DerefMut for InlineOut {
1551    fn deref_mut(&mut self) -> &mut String {
1552        &mut self.text
1553    }
1554}
1555
1556fn write_inline(
1557    inline: &Inline,
1558    context: Context,
1559    out: &mut InlineOut,
1560) -> Result<(), SerializeError> {
1561    match inline {
1562        Inline::Text(node) => out.push_str(&node.value),
1563        Inline::Escape(node) => {
1564            out.push('\\');
1565            out.push(node.value);
1566        }
1567        Inline::CharacterReference(node) => out.push_str(&node.reference),
1568        Inline::Emphasis(node) => {
1569            let delimiter = emphasis_delimiter(node.delimiter);
1570            write_span(out, delimiter, &node.children, delimiter, context)?;
1571        }
1572        Inline::Strong(node) => {
1573            let delimiter = emphasis_delimiter(node.delimiter).repeat(2);
1574            write_span(out, &delimiter, &node.children, &delimiter, context)?;
1575        }
1576        Inline::Delete(node) => write_span(out, "~~", &node.children, "~~", context)?,
1577        Inline::Mark(node) => write_span(out, "==", &node.children, "==", context)?,
1578        Inline::InlineFootnote(node) => write_span(out, "^[", &node.children, "]", context)?,
1579        Inline::Shortcode(node) => {
1580            out.push(':');
1581            out.push_str(&node.name);
1582            out.push(':');
1583        }
1584        Inline::Tag(node) => {
1585            out.push('#');
1586            out.push_str(&node.name);
1587        }
1588        Inline::Code(node) => write_code_span(node, out),
1589        Inline::Link(node) => {
1590            write_span(out, "[", &node.children, "](", context)?;
1591            out.push_opaque(|out| {
1592                write_resource(
1593                    out,
1594                    &node.destination,
1595                    node.destination_kind,
1596                    node.title.as_ref(),
1597                );
1598            });
1599        }
1600        Inline::Autolink(node) => out.push_opaque(|out| match node.form {
1601            AutolinkForm::Literal => out.push_str(&node.text),
1602            AutolinkForm::Angle => {
1603                out.push('<');
1604                out.push_str(&node.text);
1605                out.push('>');
1606            }
1607        }),
1608        Inline::Image(node) => {
1609            write_span(out, "![", &node.alt, "](", context)?;
1610            out.push_opaque(|out| {
1611                write_resource(
1612                    out,
1613                    &node.destination,
1614                    node.destination_kind,
1615                    node.title.as_ref(),
1616                );
1617            });
1618        }
1619        Inline::LinkReference(node) => {
1620            write_reference(out, "[", &node.children, node.kind, &node.label, context)?;
1621        }
1622        Inline::ImageReference(node) => {
1623            write_reference(out, "![", &node.alt, node.kind, &node.label, context)?;
1624        }
1625        Inline::Html(node) => out.push_opaque(|out| out.push_str(&node.value)),
1626        Inline::SoftBreak(_) if context.heading => out.push(' '),
1627        Inline::SoftBreak(_) => out.push('\n'),
1628        Inline::LineBreak(node) => match node.kind {
1629            LineBreakKind::Backslash => out.push_str("\\\n"),
1630            LineBreakKind::Spaces => out.push_str("  \n"),
1631        },
1632        Inline::Math(node) => {
1633            let math = write_inline_math(node);
1634            out.push_opaque(|out| out.push_str(&math));
1635        }
1636        Inline::FootnoteReference(node) => {
1637            out.push_str("[^");
1638            out.push_str(&node.label);
1639            out.push(']');
1640        }
1641        Inline::WikiLink(node) => out.push_opaque(|out| {
1642            if node.embed {
1643                out.push('!');
1644            }
1645            out.push_str("[[");
1646            out.push_str(&node.target);
1647            if node.target != node.label {
1648                out.push('|');
1649                out.push_str(&node.label);
1650            }
1651            out.push_str("]]");
1652        }),
1653        Inline::TextDirective(node) => {
1654            // The label is read in an inline pass of its own.
1655            let label = if node.label.is_empty() {
1656                String::new()
1657            } else {
1658                format!("[{}]", write_inlines(&node.label, context)?)
1659            };
1660            out.push_opaque(|out| {
1661                out.push(':');
1662                out.push_str(&node.name);
1663                out.push_str(&label);
1664                out.push_str(&write_attributes(&node.attributes));
1665            });
1666        }
1667    }
1668    Ok(())
1669}
1670
1671fn emphasis_delimiter(delimiter: EmphasisDelimiter) -> &'static str {
1672    match delimiter {
1673        EmphasisDelimiter::Asterisk => "*",
1674        EmphasisDelimiter::Underscore => "_",
1675    }
1676}
1677
1678/// Writes `open`, `children`, and `close`.
1679fn write_span(
1680    out: &mut InlineOut,
1681    open: &str,
1682    children: &[Inline],
1683    close: &str,
1684    context: Context,
1685) -> Result<(), SerializeError> {
1686    out.push_str(open);
1687    for child in children {
1688        write_inline(child, context, out)?;
1689    }
1690    out.push_str(close);
1691    Ok(())
1692}
1693
1694/// A link's or image's `destination "title")`, after its `](`.
1695fn write_resource(
1696    out: &mut String,
1697    destination: &str,
1698    kind: LinkDestinationKind,
1699    title: Option<&Title>,
1700) {
1701    out.push_str(&write_destination(destination, kind));
1702    if let Some(title) = title {
1703        out.push(' ');
1704        out.push_str(&write_title(title));
1705    }
1706    out.push(')');
1707}
1708
1709/// What follows a reference's text: nothing, `[]`, or `[label]`.
1710/// A reference link or image. A full reference writes its text and then its
1711/// label; a shortcut or collapsed one writes its label as written, which is
1712/// both its text and its key, so the text it reads back as matches the
1713/// definition it names.
1714fn write_reference(
1715    out: &mut InlineOut,
1716    open: &str,
1717    children: &[Inline],
1718    kind: ReferenceKind,
1719    label: &str,
1720    context: Context,
1721) -> Result<(), SerializeError> {
1722    match kind {
1723        ReferenceKind::Full => write_span(out, open, children, "]", context)?,
1724        ReferenceKind::Shortcut | ReferenceKind::Collapsed => {
1725            out.push_str(open);
1726            out.push_str(label);
1727            out.push(']');
1728        }
1729    }
1730    out.push_opaque(|out| write_reference_kind(out, kind, label));
1731    Ok(())
1732}
1733
1734fn write_reference_kind(out: &mut String, kind: ReferenceKind, label: &str) {
1735    match kind {
1736        ReferenceKind::Shortcut => {}
1737        ReferenceKind::Collapsed => out.push_str("[]"),
1738        ReferenceKind::Full => {
1739            out.push('[');
1740            // A label is matched as written.
1741            out.push_str(label);
1742            out.push(']');
1743        }
1744    }
1745}
1746
1747/// Writes a code span from its value: fenced by the shortest backtick run
1748/// that neither the value holds nor a run written before it in the same
1749/// inline content opens, and padded with a space at each end when the value
1750/// would otherwise lose an end to the fence or to the space stripping.
1751fn write_code_span(node: &CodeInline, out: &mut InlineOut) {
1752    let value = &node.value;
1753    out.scan_open_runs();
1754    let held = backtick_runs(value);
1755    // An escaped backtick written just before joins the opening fence into
1756    // one longer run, which an earlier open run of that length would take as
1757    // its close.
1758    let before = out.len() - out.trim_end_matches('`').len();
1759    let length = (1..)
1760        .find(|length| {
1761            !held.get(*length).copied().unwrap_or(false)
1762                && !out.is_open_run(*length)
1763                && (before == 0 || !out.is_open_run(before + *length))
1764        })
1765        .unwrap_or(1);
1766    let fence = "`".repeat(length);
1767    out.push_str(&fence);
1768    if code_span_needs_padding(value) {
1769        out.push(' ');
1770        out.push_str(value);
1771        out.push(' ');
1772    } else {
1773        out.push_str(value);
1774    }
1775    out.push_str(&fence);
1776    // The span's own runs are matched; what follows is read afresh.
1777    out.scanned = out.text.len();
1778}
1779
1780/// `runs[n]`: whether `value` holds a maximal run of `n` backticks.
1781fn backtick_runs(value: &str) -> Vec<bool> {
1782    // A value of `len` bytes holds runs of at most `len` backticks.
1783    let mut runs = alloc::vec![false; value.len() + 1];
1784    let mut run = 0;
1785    for byte in value.bytes() {
1786        if byte == b'`' {
1787            run += 1;
1788        } else {
1789            runs[run] = true;
1790            run = 0;
1791        }
1792    }
1793    runs[run] = true;
1794    runs
1795}
1796
1797fn code_span_needs_padding(input: &str) -> bool {
1798    input.starts_with('`')
1799        || input.ends_with('`')
1800        || (input.starts_with(' ') && input.ends_with(' ') && input.chars().any(|char| char != ' '))
1801}
1802
1803pub(crate) fn write_inline_math(node: &MathInline) -> String {
1804    match node.kind {
1805        MathInlineKind::Code => format!("$`{}`$", node.value),
1806        // Dollar math is written verbatim behind its exact-length fence.
1807        MathInlineKind::Dollar { dollars } => {
1808            let fence = "$".repeat(usize::from(dollars));
1809            format!("{fence}{}{fence}", node.value)
1810        }
1811    }
1812}
1813
1814/// A directive's `[label]`, written as the text it keeps if any, or nothing
1815/// for an empty label.
1816fn write_directive_label(label: &[Inline], source: Option<&str>) -> Result<String, SerializeError> {
1817    if let Some(text) = one_line(source, true) {
1818        return Ok(if text.is_empty() {
1819            String::new()
1820        } else {
1821            format!("[{text}]")
1822        });
1823    }
1824    if label.is_empty() {
1825        Ok(String::new())
1826    } else {
1827        Ok(format!("[{}]", write_inlines(label, Context::BLOCK)?))
1828    }
1829}
1830
1831/// The text a leaf keeps, where it can be written in place of its inlines:
1832/// on one line when `single` holds, as in a cell, a label, or an ATX heading,
1833/// which a line ending would end.
1834fn one_line(source: Option<&str>, single: bool) -> Option<&str> {
1835    source.filter(|text| !single || !text.contains(['\n', '\r']))
1836}
1837
1838fn write_attributes(attributes: &[DirectiveAttribute]) -> String {
1839    if attributes.is_empty() {
1840        return String::new();
1841    }
1842    let mut output = String::from("{");
1843    for (index, attribute) in attributes.iter().enumerate() {
1844        if index > 0 {
1845            output.push(' ');
1846        }
1847        match (&*attribute.name, &attribute.value) {
1848            ("id", Some(value)) if is_directive_shorthand_value(value) => {
1849                output.push('#');
1850                output.push_str(value);
1851            }
1852            ("class", Some(value)) if is_directive_shorthand_value(value) => {
1853                output.push('.');
1854                output.push_str(value);
1855            }
1856            (_, Some(value)) => {
1857                output.push_str(&attribute.name);
1858                output.push_str("=\"");
1859                output.push_str(&escape_title(value, LinkTitleKind::DoubleQuote));
1860                output.push('"');
1861            }
1862            (_, None) => output.push_str(&attribute.name),
1863        }
1864    }
1865    output.push('}');
1866    output
1867}
1868
1869fn is_directive_shorthand_value(input: &str) -> bool {
1870    !input.is_empty()
1871        && input
1872            .chars()
1873            .all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
1874}
1875
1876fn write_destination(input: &str, kind: LinkDestinationKind) -> String {
1877    match kind {
1878        LinkDestinationKind::Omitted if input.is_empty() => String::new(),
1879        LinkDestinationKind::Angle => {
1880            let mut output = String::from("<");
1881            for char in input.chars() {
1882                match char {
1883                    char if char.is_control() => output.push_str(&char_reference(char)),
1884                    '\\' | '<' | '>' => {
1885                        output.push('\\');
1886                        output.push(char);
1887                    }
1888                    _ => output.push(char),
1889                }
1890            }
1891            output.push('>');
1892            output
1893        }
1894        LinkDestinationKind::Bare | LinkDestinationKind::Omitted if input.is_empty() => "<>".into(),
1895        LinkDestinationKind::Bare | LinkDestinationKind::Omitted => {
1896            let mut output = String::new();
1897            for char in input.chars() {
1898                match char {
1899                    // A space would end the destination, and `\ ` is no escape.
1900                    char if char.is_control() || char == ' ' => {
1901                        output.push_str(&char_reference(char));
1902                    }
1903                    '(' | ')' | '\\' | '<' | '>' | '&' => {
1904                        output.push('\\');
1905                        output.push(char);
1906                    }
1907                    _ => output.push(char),
1908                }
1909            }
1910            output
1911        }
1912    }
1913}
1914
1915fn write_title(title: &Title) -> String {
1916    let (open, close) = match title.kind {
1917        LinkTitleKind::DoubleQuote => ('"', '"'),
1918        LinkTitleKind::SingleQuote => ('\'', '\''),
1919        LinkTitleKind::Paren => ('(', ')'),
1920    };
1921    let mut output = String::new();
1922    output.push(open);
1923    output.push_str(&escape_title(&title.value, title.kind));
1924    output.push(close);
1925    output
1926}
1927
1928fn escape_title(input: &str, kind: LinkTitleKind) -> String {
1929    let mut output = String::new();
1930    for char in input.chars() {
1931        match char {
1932            char if char.is_control() => output.push_str(&char_reference(char)),
1933            '\\' | '&' => {
1934                output.push('\\');
1935                output.push(char);
1936            }
1937            '"' if kind == LinkTitleKind::DoubleQuote => output.push_str("\\\""),
1938            '\'' if kind == LinkTitleKind::SingleQuote => output.push_str("\\'"),
1939            '(' | ')' if kind == LinkTitleKind::Paren => {
1940                output.push('\\');
1941                output.push(char);
1942            }
1943            _ => output.push(char),
1944        }
1945    }
1946    output
1947}
1948
1949/// Prefixes every line of `input` with a container's `prefix`, keeping each
1950/// line's ending; an empty line takes the prefix without its trailing
1951/// whitespace, so that no blank line the writer writes ends in whitespace.
1952fn prefix_container_lines(input: &str, prefix: &str) -> String {
1953    prefix_lines_with(input, prefix, prefix.trim_end_matches([' ', '\t']))
1954}
1955
1956/// Prefixes every line of `input` with `prefix`, keeping each line's ending.
1957fn prefix_lines(input: &str, prefix: &str) -> String {
1958    prefix_lines_with(input, prefix, prefix)
1959}
1960
1961/// Prefixes every line of `input` with `prefix`, or an empty one with
1962/// `empty_prefix`, keeping each line's ending.
1963fn prefix_lines_with(input: &str, prefix: &str, empty_prefix: &str) -> String {
1964    if input.is_empty() {
1965        return String::new();
1966    }
1967    let bytes = input.as_bytes();
1968    let mut output = String::new();
1969    let mut line_start = 0;
1970    let mut cursor = 0;
1971    while cursor < input.len() {
1972        let eol_end = match bytes[cursor] {
1973            b'\n' => Some(cursor + 1),
1974            b'\r' if bytes.get(cursor + 1) == Some(&b'\n') => Some(cursor + 2),
1975            b'\r' => Some(cursor + 1),
1976            _ => None,
1977        };
1978        if let Some(end) = eol_end {
1979            output.push_str(if cursor == line_start {
1980                empty_prefix
1981            } else {
1982                prefix
1983            });
1984            output.push_str(&input[line_start..end]);
1985            cursor = end;
1986            line_start = cursor;
1987        } else {
1988            cursor += 1;
1989        }
1990    }
1991    if line_start < input.len() {
1992        output.push_str(prefix);
1993        output.push_str(&input[line_start..]);
1994    }
1995    output
1996}
1997
1998fn indent_after_first_line(input: &str, width: usize) -> String {
1999    indent_lines_of(input, width)
2000}
2001
2002fn indent_continuation(input: &str) -> String {
2003    indent_lines_of(input, 4)
2004}
2005
2006/// Indents every line of `input` after its first by `width` spaces, as a
2007/// container's continuation; an empty line stays empty. Each line keeps its
2008/// line ending, a final one included.
2009fn indent_lines_of(input: &str, width: usize) -> String {
2010    let mut output = String::with_capacity(input.len());
2011    let bytes = input.as_bytes();
2012    let mut line_start = 0;
2013    let mut index = 0;
2014    let push = |output: &mut String, line: &str, number: usize| {
2015        if number > 0 && !line.starts_with(['\n', '\r']) {
2016            output.extend(core::iter::repeat_n(' ', width));
2017        }
2018        output.push_str(line);
2019    };
2020    let mut number = 0;
2021    while index < bytes.len() {
2022        let end = match bytes[index] {
2023            b'\n' => Some(index + 1),
2024            b'\r' if bytes.get(index + 1) == Some(&b'\n') => Some(index + 2),
2025            b'\r' => Some(index + 1),
2026            _ => None,
2027        };
2028        match end {
2029            Some(end) => {
2030                push(&mut output, &input[line_start..end], number);
2031                number += 1;
2032                index = end;
2033                line_start = end;
2034            }
2035            None => index += 1,
2036        }
2037    }
2038    if line_start < input.len() {
2039        push(&mut output, &input[line_start..], number);
2040    }
2041    output
2042}
2043
2044fn trim_trailing_newline(input: &str) -> &str {
2045    input.trim_end_matches('\n').trim_end_matches('\r')
2046}
2047
2048fn ends_with_line_ending(input: &str) -> bool {
2049    input.ends_with('\n') || input.ends_with('\r')
2050}
2051
2052/// The fence for a code block's `value` and the columns the block is indented
2053/// by, so that no line of `value` closes it. A closing fence is up to three
2054/// spaces, at least as many marker chars, and nothing else but spaces and
2055/// tabs. The fence keeps `min_len` chars when indenting the block (which the
2056/// value's lines lose again) moves every closing-like line past three spaces;
2057/// otherwise it grows past the longest of them.
2058fn code_block_fence(value: &str, marker: FenceMarker, min_len: usize) -> (String, usize) {
2059    let char = match marker {
2060        FenceMarker::Backtick => '`',
2061        FenceMarker::Tilde => '~',
2062    };
2063    let closing_like = |line: &str, length: usize| {
2064        let indent = line.len() - line.trim_start_matches(' ').len();
2065        let rest = &line[indent..];
2066        let run = rest.len() - rest.trim_start_matches(char).len();
2067        (indent <= 3 && run >= length && rest[run..].trim_matches([' ', '\t']).is_empty())
2068            .then_some((indent, run))
2069    };
2070    let lines = || value.split(['\n', '\r']);
2071    let least_indent = lines()
2072        .filter_map(|line| closing_like(line, min_len))
2073        .map(|(indent, _)| indent)
2074        .min();
2075    match least_indent {
2076        None => (char.to_string().repeat(min_len), 0),
2077        Some(indent) if indent > 0 => (char.to_string().repeat(min_len), 4 - indent),
2078        Some(_) => {
2079            let mut length = min_len;
2080            for line in lines() {
2081                if let Some((_, run)) = closing_like(line, length) {
2082                    length = run + 1;
2083                }
2084            }
2085            (char.to_string().repeat(length), 0)
2086        }
2087    }
2088}
2089
2090fn block_math_fence(input: &str) -> String {
2091    let mut length = 2;
2092    for line in trim_trailing_newline(input).lines() {
2093        let trimmed = line.trim();
2094        if trimmed.len() >= 2 && trimmed.chars().all(|char| char == '$') {
2095            length = length.max(trimmed.len() + 1);
2096        }
2097    }
2098    "$".repeat(length)
2099}
2100
2101fn directive_fence(inner: &str) -> String {
2102    let mut length = 3;
2103    for line in inner.lines() {
2104        if let Some(closing) = directive_closing_fence_len(line) {
2105            length = length.max(closing + 1);
2106        }
2107    }
2108    ":".repeat(length)
2109}
2110
2111fn directive_closing_fence_len(line: &str) -> Option<usize> {
2112    let trimmed = trim_up_to_three_indent_columns(line)?;
2113    let length = trimmed
2114        .as_bytes()
2115        .iter()
2116        .take_while(|byte| **byte == b':')
2117        .count();
2118    if length >= 3 && trimmed[length..].trim().is_empty() {
2119        Some(length)
2120    } else {
2121        None
2122    }
2123}
2124
2125fn trim_up_to_three_indent_columns(input: &str) -> Option<&str> {
2126    let mut columns = 0usize;
2127    let mut bytes = 0usize;
2128    for byte in input.as_bytes() {
2129        match *byte {
2130            b' ' => columns += 1,
2131            b'\t' => columns += 4 - (columns % 4),
2132            _ => break,
2133        }
2134        bytes += 1;
2135    }
2136    (columns <= 3).then_some(&input[bytes..])
2137}
2138
2139#[cfg(test)]
2140mod tests {
2141    use super::*;
2142
2143    // serialization: Unchanged source is kept
2144    #[test]
2145    fn the_read_back_check_names_each_block_that_reads_differently() {
2146        let blocks = crate::parse("> x\n\n# y\n\nz").document.children;
2147        assert_eq!(misread(&blocks, "> x\n\n# y\n\nz", &[0, 2]), [0usize; 0]);
2148        // A block read differently in its place.
2149        assert_eq!(misread(&blocks, "> w\n\n# y\n\nz", &[0, 2]), [0]);
2150        assert_eq!(misread(&blocks, "> w\n\n# y\n\nz", &[2]), [0usize; 0]);
2151        // `z` read into the quote: the block count differs.
2152        assert_eq!(misread(&blocks, "> x\nz\n\n# y", &[0, 2]), [0, 2]);
2153    }
2154}