Skip to main content

iced_markdown_editor/
markdown.rs

1//! CommonMark import and export.
2//!
3//! Import uses `pulldown-cmark`, a CommonMark 0.31.2 parser, with two GFM
4//! extensions: `~~strikethrough~~` and task lists (`- [ ]` / `- [x]`).
5//! Export writes Markdown that parses back to the same document; the round
6//! trip is tested against all 652 examples of the CommonMark 0.31.2
7//! specification and against random documents (see the tests at the end of
8//! this file).
9//!
10//! Inline formatting is written with Markdown delimiters (`**`, `*`, `~~`)
11//! when they survive a round trip. CommonMark's delimiter rules cannot
12//! express every combination (e.g. bold text that starts with punctuation
13//! directly after a letter), so such paragraphs fall back to
14//! `<strong>`, `<em>` and `<del>` tags, which the importer recognises.
15
16use crate::doc::{Block, Container, Document, Flag, Leaf, Link, ListInfo, OBJ, SOFT, Style};
17use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, LinkType, Options, Parser, Tag};
18use std::collections::HashMap;
19use std::sync::Arc;
20
21fn options() -> Options {
22    Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TASKLISTS
23}
24
25fn is_break(c: char) -> bool {
26    c == '\n' || c == SOFT
27}
28
29// ====================================================================
30// Import
31// ====================================================================
32
33enum Ctx {
34    Paragraph,
35    Heading,
36    Quote,
37    List,
38    Item,
39    Code,
40    Html,
41    Emphasis,
42    Strong,
43    Strike,
44    Link,
45    Image,
46    Other,
47}
48
49/// Inline HTML tags that map to formatting (the export fallback).
50fn html_flag(tag: &str) -> Option<(Flag, bool)> {
51    match tag {
52        "<strong>" => Some((Flag::Bold, false)),
53        "</strong>" => Some((Flag::Bold, true)),
54        "<em>" => Some((Flag::Italic, false)),
55        "</em>" => Some((Flag::Italic, true)),
56        "<del>" => Some((Flag::Strike, false)),
57        "</del>" => Some((Flag::Strike, true)),
58        _ => None,
59    }
60}
61
62fn flag_bit(flag: Flag) -> u8 {
63    match flag {
64        Flag::Bold => 1,
65        Flag::Italic => 2,
66        Flag::Strike => 4,
67        Flag::Code => 8,
68    }
69}
70
71fn flag_tag(flag: Flag, closing: bool) -> &'static str {
72    match (flag, closing) {
73        (Flag::Bold, false) => "<strong>",
74        (Flag::Bold, true) => "</strong>",
75        (Flag::Italic, false) => "<em>",
76        (Flag::Italic, true) => "</em>",
77        (Flag::Strike, false) => "<del>",
78        (Flag::Strike, true) => "</del>",
79        (Flag::Code, false) => "<code>",
80        (Flag::Code, true) => "</code>",
81    }
82}
83
84#[derive(Default)]
85struct Builder {
86    blocks: Vec<Block>,
87    current: Option<Block>,
88    stack: Vec<Ctx>,
89    containers: Vec<Container>,
90    lists: Vec<(ListInfo, usize)>,
91    starts: Vec<usize>,
92    bold: u32,
93    italic: u32,
94    strike: u32,
95    /// Formatting from inline HTML tags: (flag, start index in the block).
96    html_open: Vec<(Flag, usize)>,
97    /// Per character of the current block: flags provided by HTML tags.
98    html_bits: Vec<u8>,
99    /// Open links: (target, start index).
100    links: Vec<(Arc<Link>, usize)>,
101    /// Open image: (target, start index).
102    image: Option<(Arc<Link>, usize)>,
103}
104
105impl Builder {
106    fn html_has(&self, flag: Flag) -> bool {
107        self.html_open.iter().any(|(open, _)| *open == flag)
108    }
109
110    fn style(&self, code: bool) -> Style {
111        Style {
112            bold: self.bold > 0 || self.html_has(Flag::Bold),
113            italic: self.italic > 0 || self.html_has(Flag::Italic),
114            strike: self.strike > 0 || self.html_has(Flag::Strike),
115            code,
116            link: self.links.last().map(|(link, _)| link.clone()),
117            image: self.image.as_ref().map(|(link, _)| link.clone()),
118            html: false,
119        }
120    }
121
122    fn len(&self) -> usize {
123        self.current.as_ref().map(Block::len).unwrap_or(0)
124    }
125
126    /// The leaf block receiving inline content. Tight list items have no
127    /// paragraph events, so a paragraph is created implicitly.
128    fn ensure(&mut self) -> &mut Block {
129        if self.current.is_none() {
130            self.current = Some(Block::new(self.containers.clone(), Leaf::Paragraph));
131            self.html_bits.clear();
132        }
133        self.current.as_mut().expect("current block")
134    }
135
136    fn push_styled(&mut self, text: &str, style: Style) {
137        let bits = self
138            .html_open
139            .iter()
140            .fold(0u8, |bits, (flag, _)| bits | flag_bit(*flag));
141        let block = self.ensure();
142        let mut count = 0;
143        for c in text.chars() {
144            block.push(c, style.clone());
145            count += 1;
146        }
147        self.html_bits.extend(std::iter::repeat(bits).take(count));
148    }
149
150    fn push_text(&mut self, text: &str, code: bool) {
151        if let Some(block) = self.current.as_mut() {
152            if block.leaf.is_raw() {
153                for c in text.chars() {
154                    if c != '\r' {
155                        block.push(c, Style::default());
156                    }
157                }
158                return;
159            }
160        }
161        let style = self.style(code);
162        if self.image.is_some() {
163            let text: String = text
164                .chars()
165                .map(|c| if is_break(c) { ' ' } else { c })
166                .collect();
167            self.push_styled(&text, style);
168        } else {
169            let text: String = text
170                .chars()
171                .map(|c| if c == '\n' { SOFT } else { c })
172                .collect();
173            self.push_styled(&text, style);
174        }
175    }
176
177    fn push_break(&mut self, c: char) {
178        if self.image.is_some() {
179            self.push_styled(" ", self.style(false));
180        } else {
181            let style = self.style(false);
182            self.push_styled(&c.to_string(), style);
183        }
184    }
185
186    /// `[ ]` / `[x]` at the start of a list item.
187    fn task(&mut self, checked: bool) {
188        let Some(depth) = self
189            .containers
190            .iter()
191            .rposition(|c| matches!(c, Container::Item { .. }))
192        else {
193            return;
194        };
195        if let Container::Item { task, .. } = &mut self.containers[depth] {
196            *task = Some(checked);
197        }
198        if let Some(block) = self.current.as_mut() {
199            if let Some(Container::Item { task, .. }) = block.containers.get_mut(depth) {
200                *task = Some(checked);
201            }
202        }
203    }
204
205    fn inline_html(&mut self, tag: &str) {
206        if self.image.is_some() {
207            return;
208        }
209        match html_flag(tag) {
210            Some((flag, false)) => {
211                let start = self.len();
212                self.ensure();
213                self.html_open.push((flag, start));
214            }
215            Some((flag, true)) if self.html_has(flag) => {
216                if let Some(index) = self.html_open.iter().rposition(|(open, _)| *open == flag) {
217                    self.html_open.remove(index);
218                }
219            }
220            _ => {
221                let raw: String = tag
222                    .chars()
223                    .map(|c| if c == '\n' { SOFT } else { c })
224                    .collect();
225                let style = Style {
226                    html: true,
227                    link: self.links.last().map(|(link, _)| link.clone()),
228                    ..Style::default()
229                };
230                self.push_styled(&raw, style);
231            }
232        }
233    }
234
235    fn flush(&mut self) {
236        let Some(mut block) = self.current.take() else {
237            return;
238        };
239
240        // Unmatched formatting tags become raw HTML again.
241        for (flag, start) in std::mem::take(&mut self.html_open).into_iter().rev() {
242            let bit = flag_bit(flag);
243            for j in start..block.len() {
244                if self.html_bits.get(j).is_some_and(|bits| bits & bit != 0) {
245                    self.html_bits[j] &= !bit;
246                    block.styles[j].set(flag, false);
247                }
248            }
249            let tag = flag_tag(flag, false);
250            let count = tag.chars().count();
251            let raw = Style {
252                html: true,
253                ..Style::default()
254            };
255            block.chars.splice(start..start, tag.chars());
256            block
257                .styles
258                .splice(start..start, std::iter::repeat(raw).take(count));
259            self.html_bits
260                .splice(start..start, std::iter::repeat(0).take(count));
261        }
262        self.html_bits.clear();
263
264        match block.leaf {
265            Leaf::Code => {
266                if block.chars.last() == Some(&'\n') {
267                    block.chars.pop();
268                    block.styles.pop();
269                }
270            }
271            Leaf::Html => {
272                while block.chars.last() == Some(&'\n') {
273                    block.chars.pop();
274                    block.styles.pop();
275                }
276            }
277            _ => {}
278        }
279        self.blocks.push(block);
280    }
281
282    fn begin(&mut self, leaf: Leaf) {
283        self.flush();
284        self.current = Some(Block::new(self.containers.clone(), leaf));
285        self.html_bits.clear();
286    }
287
288    fn start(&mut self, tag: Tag<'_>) {
289        if self.image.is_some() {
290            // Inside an image description only the text matters.
291            self.stack.push(Ctx::Other);
292            return;
293        }
294        match tag {
295            Tag::Paragraph => {
296                if matches!(self.containers.last(), Some(Container::Item { .. })) {
297                    if let Some((list, _)) = self.lists.last_mut() {
298                        list.tight = false;
299                    }
300                }
301                self.begin(Leaf::Paragraph);
302                self.stack.push(Ctx::Paragraph);
303            }
304            Tag::Heading { level, .. } => {
305                let level = match level {
306                    HeadingLevel::H1 => 1,
307                    HeadingLevel::H2 => 2,
308                    HeadingLevel::H3 => 3,
309                    HeadingLevel::H4 => 4,
310                    HeadingLevel::H5 => 5,
311                    HeadingLevel::H6 => 6,
312                };
313                self.begin(Leaf::Heading(level));
314                self.stack.push(Ctx::Heading);
315            }
316            Tag::BlockQuote(_) => {
317                self.flush();
318                self.containers.push(Container::quote());
319                self.starts.push(self.blocks.len());
320                self.stack.push(Ctx::Quote);
321            }
322            Tag::CodeBlock(kind) => {
323                self.begin(Leaf::Code);
324                if let CodeBlockKind::Fenced(info) = kind {
325                    let info = info.trim();
326                    if !info.is_empty() {
327                        if let Some(block) = self.current.as_mut() {
328                            block.info = Some(Arc::from(info));
329                        }
330                    }
331                }
332                self.stack.push(Ctx::Code);
333            }
334            Tag::HtmlBlock => {
335                self.begin(Leaf::Html);
336                self.stack.push(Ctx::Html);
337            }
338            Tag::List(start) => {
339                self.flush();
340                let list = ListInfo::new(start.is_some(), start.unwrap_or(1));
341                self.lists.push((list, self.blocks.len()));
342                self.stack.push(Ctx::List);
343            }
344            Tag::Item => {
345                self.flush();
346                let list = self
347                    .lists
348                    .last()
349                    .map(|(list, _)| *list)
350                    .unwrap_or(ListInfo::new(false, 1));
351                self.containers.push(Container::item(list));
352                self.starts.push(self.blocks.len());
353                self.stack.push(Ctx::Item);
354            }
355            Tag::Emphasis => {
356                self.italic += 1;
357                self.stack.push(Ctx::Emphasis);
358            }
359            Tag::Strong => {
360                self.bold += 1;
361                self.stack.push(Ctx::Strong);
362            }
363            Tag::Strikethrough => {
364                self.strike += 1;
365                self.stack.push(Ctx::Strike);
366            }
367            Tag::Link {
368                link_type,
369                dest_url,
370                title,
371                ..
372            } => {
373                let url = if link_type == LinkType::Email {
374                    format!("mailto:{dest_url}")
375                } else {
376                    dest_url.to_string()
377                };
378                let start = self.len();
379                self.ensure();
380                self.links
381                    .push((Link::new(url, Some(title.to_string())), start));
382                self.stack.push(Ctx::Link);
383            }
384            Tag::Image {
385                dest_url, title, ..
386            } => {
387                let start = self.len();
388                self.ensure();
389                self.image = Some((
390                    Link::new(dest_url.to_string(), Some(title.to_string())),
391                    start,
392                ));
393                self.stack.push(Ctx::Image);
394            }
395            _ => self.stack.push(Ctx::Other),
396        }
397    }
398
399    fn end(&mut self) {
400        match self.stack.pop() {
401            Some(Ctx::Paragraph) | Some(Ctx::Heading) | Some(Ctx::Code) | Some(Ctx::Html) => {
402                self.flush()
403            }
404            Some(Ctx::Quote) | Some(Ctx::Item) => {
405                self.flush();
406                let start = self.starts.pop().unwrap_or(0);
407                if self.blocks.len() == start {
408                    // Empty block quote / empty list item.
409                    self.blocks
410                        .push(Block::new(self.containers.clone(), Leaf::Paragraph));
411                }
412                self.containers.pop();
413            }
414            Some(Ctx::List) => {
415                self.flush();
416                if let Some((list, first)) = self.lists.pop() {
417                    if !list.tight {
418                        for block in &mut self.blocks[first..] {
419                            for container in &mut block.containers {
420                                if let Container::Item { list: info, .. } = container {
421                                    if info.id == list.id {
422                                        info.tight = false;
423                                    }
424                                }
425                            }
426                        }
427                    }
428                }
429            }
430            Some(Ctx::Emphasis) => self.italic = self.italic.saturating_sub(1),
431            Some(Ctx::Strong) => self.bold = self.bold.saturating_sub(1),
432            Some(Ctx::Strike) => self.strike = self.strike.saturating_sub(1),
433            Some(Ctx::Link) => {
434                if let Some((link, start)) = self.links.last().cloned() {
435                    if self.len() == start {
436                        // Empty link text keeps a placeholder.
437                        let style = self.style(false);
438                        self.push_styled(
439                            &OBJ.to_string(),
440                            Style {
441                                link: Some(link),
442                                ..style
443                            },
444                        );
445                    }
446                }
447                self.links.pop();
448            }
449            Some(Ctx::Image) => {
450                if let Some((_, start)) = self.image.clone() {
451                    if self.len() == start {
452                        let style = self.style(false);
453                        self.push_styled(&OBJ.to_string(), style);
454                    }
455                }
456                self.image = None;
457            }
458            Some(Ctx::Other) | None => {}
459        }
460    }
461}
462
463/// Parses CommonMark (with `~~strikethrough~~` and task lists).
464pub fn parse(source: &str) -> Document {
465    let mut builder = Builder::default();
466
467    for event in Parser::new_ext(source, options()) {
468        match event {
469            Event::Start(tag) => builder.start(tag),
470            Event::End(_) => builder.end(),
471            Event::Text(text) => builder.push_text(&text, false),
472            Event::Code(text) => {
473                if builder.image.is_some() {
474                    builder.push_text(&text, false);
475                } else {
476                    builder.push_text(&text, true);
477                }
478            }
479            Event::Html(text) => {
480                if !matches!(builder.current.as_ref().map(|b| b.leaf), Some(Leaf::Html)) {
481                    builder.begin(Leaf::Html);
482                }
483                builder.push_text(&text, false);
484            }
485            Event::InlineHtml(text) => builder.inline_html(&text),
486            Event::SoftBreak => builder.push_break(SOFT),
487            Event::HardBreak => builder.push_break('\n'),
488            Event::TaskListMarker(checked) => builder.task(checked),
489            Event::Rule => {
490                builder.flush();
491                builder
492                    .blocks
493                    .push(Block::new(builder.containers.clone(), Leaf::Rule));
494            }
495            _ => {}
496        }
497    }
498
499    builder.flush();
500
501    if builder.blocks.is_empty() {
502        builder.blocks.push(Block::new(Vec::new(), Leaf::Paragraph));
503    }
504
505    Document {
506        blocks: builder.blocks,
507    }
508}
509
510// ====================================================================
511// Export: inline content
512// ====================================================================
513
514const MARKS: [Flag; 3] = [Flag::Bold, Flag::Italic, Flag::Strike];
515
516fn mark_token(flag: Flag, use_html: bool, closing: bool) -> &'static str {
517    if use_html {
518        flag_tag(flag, closing)
519    } else {
520        match flag {
521            Flag::Bold => "**",
522            Flag::Italic => "*",
523            Flag::Strike => "~~",
524            Flag::Code => "`",
525        }
526    }
527}
528
529fn esc_text(c: char, line_start: bool, heading: bool) -> String {
530    if heading && c == '#' {
531        return "\\#".into();
532    }
533    if "\\`*_[]<>~&|!".contains(c) {
534        return format!("\\{c}");
535    }
536    if line_start && "#>+-=".contains(c) {
537        return format!("\\{c}");
538    }
539    c.to_string()
540}
541
542fn esc_dest(url: &str) -> String {
543    // Plain form when nothing needs escaping, `<...>` otherwise.
544    let plain = !url.is_empty()
545        && !url
546            .chars()
547            .any(|c| c.is_whitespace() || c.is_control() || "<>()\\&".contains(c));
548    if plain {
549        return url.to_string();
550    }
551    let mut out = String::from("<");
552    for c in url.chars() {
553        match c {
554            '<' | '>' | '\\' | '&' => {
555                out.push('\\');
556                out.push(c);
557            }
558            '\n' | '\r' => out.push(' '),
559            c => out.push(c),
560        }
561    }
562    out.push('>');
563    out
564}
565
566fn esc_title(title: &str) -> String {
567    let mut out = String::from("\"");
568    for c in title.chars() {
569        if matches!(c, '"' | '\\' | '&') {
570            out.push('\\');
571        }
572        out.push(c);
573    }
574    out.push('"');
575    out
576}
577
578fn code_span(text: &str) -> String {
579    let mut longest = 0;
580    let mut run = 0;
581    for c in text.chars() {
582        run = if c == '`' { run + 1 } else { 0 };
583        longest = longest.max(run);
584    }
585    let ticks = "`".repeat(longest + 1);
586    let pad = text.starts_with('`')
587        || text.ends_with('`')
588        || (text.starts_with(' ') && text.ends_with(' ') && !text.trim_matches(' ').is_empty());
589    if pad {
590        format!("{ticks} {text} {ticks}")
591    } else {
592        format!("{ticks}{text}{ticks}")
593    }
594}
595
596/// `scheme:...` usable as an autolink.
597fn is_autolink(url: &str) -> bool {
598    let Some((scheme, rest)) = url.split_once(':') else {
599        return false;
600    };
601    let mut chars = scheme.chars();
602    let first_ok = chars.next().is_some_and(|c| c.is_ascii_alphabetic());
603    let len_ok = (2..=32).contains(&scheme.len());
604    let scheme_ok = scheme
605        .chars()
606        .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '.' | '-'));
607    let rest_ok = rest
608        .chars()
609        .all(|c| !c.is_whitespace() && !c.is_control() && c != '<' && c != '>');
610    first_ok && len_ok && scheme_ok && rest_ok
611}
612
613fn is_email(text: &str) -> bool {
614    let Some((local, domain)) = text.split_once('@') else {
615        return false;
616    };
617    let local_ok = !local.is_empty()
618        && local
619            .chars()
620            .all(|c| c.is_ascii_alphanumeric() || ".!#$%&'*+/=?^_`{|}~-".contains(c));
621    let domain_ok = !domain.is_empty()
622        && domain.split('.').all(|label| {
623            !label.is_empty()
624                && label.len() <= 63
625                && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
626                && !label.starts_with('-')
627                && !label.ends_with('-')
628        });
629    local_ok && domain_ok
630}
631
632fn image_md(alt: &[char], target: &Link) -> String {
633    let mut text = String::new();
634    for c in alt {
635        match *c {
636            OBJ => {}
637            SOFT | '\n' => text.push(' '),
638            c => text.push_str(&esc_text(c, false, false)),
639        }
640    }
641    let title = target
642        .title
643        .as_deref()
644        .map(|title| format!(" {}", esc_title(title)))
645        .unwrap_or_default();
646    format!("![{text}]({}{title})", esc_dest(&target.url))
647}
648
649/// `![alt](url)` with escaping, for inserting images as Markdown text.
650pub(crate) fn image_markdown(alt: &str, url: &str) -> String {
651    let alt: Vec<char> = alt.chars().collect();
652    image_md(
653        &alt,
654        &Link {
655            url: url.to_string(),
656            title: None,
657        },
658    )
659}
660
661struct Inline<'a> {
662    chars: &'a [char],
663    styles: &'a [Style],
664    heading: bool,
665    use_html: bool,
666    para_start: bool,
667    para_end: bool,
668    out: String,
669    open: Vec<Flag>,
670    line_start: bool,
671}
672
673impl<'a> Inline<'a> {
674    fn has(&self, k: usize, flag: Flag) -> bool {
675        self.styles[k].get(flag)
676    }
677
678    fn wanted(&self, k: usize) -> Vec<Flag> {
679        MARKS
680            .into_iter()
681            .filter(|flag| self.has(k, *flag))
682            .collect()
683    }
684
685    fn mark_end(&self, flag: Flag, mut k: usize) -> usize {
686        while k < self.chars.len() && self.has(k, flag) {
687            k += 1;
688        }
689        k
690    }
691
692    /// Closes every open mark not in `keep` (and everything opened after it).
693    fn close_to(&mut self, keep: &[Flag]) {
694        while self.open.iter().any(|flag| !keep.contains(flag)) {
695            if let Some(flag) = self.open.pop() {
696                self.out.push_str(mark_token(flag, self.use_html, true));
697            }
698        }
699    }
700
701    /// Opens `marks` at `k`; the longest-running mark becomes outermost.
702    fn open_at(&mut self, k: usize, marks: &[Flag]) {
703        let mut new: Vec<(Flag, usize)> = marks
704            .iter()
705            .filter(|flag| !self.open.contains(flag))
706            .map(|flag| (*flag, self.mark_end(*flag, k)))
707            .collect();
708        new.sort_by(|a, b| b.1.cmp(&a.1));
709        for (flag, _) in new {
710            self.open.push(flag);
711            self.out.push_str(mark_token(flag, self.use_html, false));
712        }
713    }
714
715    fn emit_text(&mut self, text: &[char], at_end: bool) {
716        let mut text: Vec<char> = text.to_vec();
717        let mut tail = String::new();
718        if at_end {
719            while let Some(c) = text.last().copied() {
720                if c == ' ' || c == '\t' {
721                    tail.insert_str(0, if c == ' ' { "&#32;" } else { "&#9;" });
722                    text.pop();
723                } else {
724                    break;
725                }
726            }
727        }
728
729        let mut k = 0;
730        while k < text.len() {
731            let c = text[k];
732            if c == OBJ {
733                k += 1;
734                continue;
735            }
736            if self.line_start && (c == ' ' || c == '\t') {
737                self.out.push_str(if c == ' ' { "&#32;" } else { "&#9;" });
738                k += 1;
739                continue;
740            }
741            if self.line_start && c.is_ascii_digit() {
742                let mut m = k;
743                while m < text.len() && text[m].is_ascii_digit() {
744                    m += 1;
745                }
746                if m < text.len() && (text[m] == '.' || text[m] == ')') {
747                    self.out.extend(&text[k..m]);
748                    self.out.push('\\');
749                    self.out.push(text[m]);
750                    k = m + 1;
751                    self.line_start = false;
752                    continue;
753                }
754            }
755            if c == '\u{a0}' {
756                self.out.push_str("&nbsp;");
757            } else {
758                let escaped = esc_text(c, self.line_start, self.heading);
759                self.out.push_str(&escaped);
760            }
761            self.line_start = false;
762            k += 1;
763        }
764
765        if !tail.is_empty() {
766            self.out.push_str(&tail);
767            self.line_start = false;
768        }
769    }
770
771    fn run(mut self) -> String {
772        let n = self.chars.len();
773        let mut i = 0;
774
775        while i < n {
776            let c = self.chars[i];
777            let style = self.styles[i].clone();
778
779            if is_break(c) {
780                if style.html && c == SOFT {
781                    // A line ending inside raw HTML.
782                    self.out.push('\n');
783                    self.line_start = true;
784                    i += 1;
785                    continue;
786                }
787                let trailing = self.chars[i + 1..]
788                    .iter()
789                    .all(|ch| *ch == '\n' || *ch == OBJ);
790                if c == '\n' && ((i == 0 && self.para_start) || (self.para_end && trailing)) {
791                    // A hard break at the edge of a paragraph cannot be expressed.
792                    i += 1;
793                    continue;
794                }
795                let prev = if i > 0 { Some(self.chars[i - 1]) } else { None };
796                let next = self.chars.get(i + 1).copied();
797                let edge = (prev.is_none() && self.para_start)
798                    || (next.is_none() && self.para_end)
799                    || prev.is_some_and(is_break)
800                    || next.is_some_and(is_break);
801                if c == SOFT && (self.heading || edge) {
802                    self.out.push_str("&#10;");
803                    self.line_start = false;
804                } else if self.heading {
805                    self.out.push('\n');
806                    self.line_start = true;
807                } else {
808                    self.out.push_str(if c == '\n' { "\\\n" } else { "\n" });
809                    self.line_start = true;
810                }
811                i += 1;
812                continue;
813            }
814
815            if let Some(link) = style.link.clone() {
816                let mut j = i;
817                while j < n && self.styles[j].link.as_ref() == Some(&link) {
818                    j += 1;
819                }
820                // Marks covering the whole link and continuing outside it
821                // are written around the link, others inside.
822                let after = if j < n { self.wanted(j) } else { Vec::new() };
823                let outside: Vec<Flag> = self
824                    .wanted(i)
825                    .into_iter()
826                    .filter(|flag| {
827                        (self.open.contains(flag) || after.contains(flag))
828                            && (i..j).all(|k| self.has(k, *flag))
829                    })
830                    .collect();
831                self.close_to(&outside);
832                self.open_at(i, &outside);
833
834                let text: String = self.chars[i..j].iter().collect();
835                let plain_style = Style {
836                    bold: outside.contains(&Flag::Bold),
837                    italic: outside.contains(&Flag::Italic),
838                    strike: outside.contains(&Flag::Strike),
839                    link: Some(link.clone()),
840                    ..Style::default()
841                };
842                let plain = self.styles[i..j].iter().all(|st| *st == plain_style);
843                if plain && link.title.is_none() && link.url == text && is_autolink(&link.url) {
844                    self.out.push_str(&format!("<{}>", link.url));
845                } else if plain
846                    && link.title.is_none()
847                    && link.url == format!("mailto:{text}")
848                    && is_email(&text)
849                {
850                    self.out.push_str(&format!("<{text}>"));
851                } else {
852                    let inner: Vec<Style> = self.styles[i..j]
853                        .iter()
854                        .map(|st| {
855                            let mut st = st.clone();
856                            st.link = None;
857                            for flag in &outside {
858                                st.set(*flag, false);
859                            }
860                            st
861                        })
862                        .collect();
863                    let body = Inline {
864                        chars: &self.chars[i..j],
865                        styles: &inner,
866                        heading: self.heading,
867                        use_html: self.use_html,
868                        para_start: false,
869                        para_end: false,
870                        out: String::new(),
871                        open: Vec::new(),
872                        line_start: true,
873                    }
874                    .run();
875                    let title = link
876                        .title
877                        .as_deref()
878                        .map(|title| format!(" {}", esc_title(title)))
879                        .unwrap_or_default();
880                    self.out
881                        .push_str(&format!("[{body}]({}{title})", esc_dest(&link.url)));
882                }
883                self.line_start = false;
884                i = j;
885                continue;
886            }
887
888            let wanted = self.wanted(i);
889            self.close_to(&wanted);
890            self.open_at(i, &wanted);
891
892            let mut j = i;
893            if let Some(image) = style.image.clone() {
894                while j < n && self.styles[j] == style {
895                    j += 1;
896                }
897                self.out.push_str(&image_md(&self.chars[i..j], &image));
898                self.line_start = false;
899            } else if style.html {
900                while j < n && self.styles[j] == style && self.chars[j] != SOFT {
901                    j += 1;
902                }
903                self.out.extend(&self.chars[i..j]);
904                self.line_start = false;
905            } else if style.code {
906                while j < n && self.styles[j] == style && !is_break(self.chars[j]) {
907                    j += 1;
908                }
909                let text: String = self.chars[i..j].iter().collect();
910                self.out.push_str(&code_span(&text));
911                self.line_start = false;
912            } else {
913                while j < n && self.styles[j] == style && !is_break(self.chars[j]) {
914                    j += 1;
915                }
916                let rest = &self.chars[j..];
917                let at_end = rest.iter().all(|ch| *ch == OBJ)
918                    || rest.first().is_some_and(|ch| is_break(*ch));
919                let chars = self.chars;
920                self.emit_text(&chars[i..j], at_end);
921            }
922            i = j;
923        }
924
925        self.close_to(&[]);
926        self.out
927    }
928}
929
930fn inline_md(chars: &[char], styles: &[Style], heading: bool, use_html: bool) -> String {
931    Inline {
932        chars,
933        styles,
934        heading,
935        use_html,
936        para_start: true,
937        para_end: true,
938        out: String::new(),
939        open: Vec::new(),
940        line_start: true,
941    }
942    .run()
943}
944
945/// Markdown delimiters when they survive a round trip, otherwise HTML tags.
946fn inline_checked(block: &Block, heading: bool, wrap: impl Fn(&str) -> String) -> String {
947    let mut text = String::new();
948    for use_html in [false, true] {
949        text = inline_md(&block.chars, &block.styles, heading, use_html);
950        if block.chars.is_empty() {
951            return text;
952        }
953        let back = parse(&wrap(&text));
954        if back.blocks.len() == 1
955            && back.blocks[0].chars == block.chars
956            && back.blocks[0].styles == block.styles
957        {
958            return text;
959        }
960    }
961    text
962}
963
964// ====================================================================
965// Export: blocks
966// ====================================================================
967
968/// Hard breaks at the start or end of a paragraph cannot be expressed.
969fn strip_edges(block: &Block) -> Block {
970    let mut block = block.clone();
971    while block.chars.first() == Some(&'\n') {
972        block.chars.remove(0);
973        block.styles.remove(0);
974    }
975    while block.chars.last() == Some(&'\n') {
976        block.chars.pop();
977        block.styles.pop();
978    }
979    block
980}
981
982/// Drops empty paragraphs unless they are the only content of their
983/// container (an empty list item or block quote).
984fn expressible(blocks: &[Block]) -> Vec<Block> {
985    let mut out = Vec::new();
986    for (index, block) in blocks.iter().enumerate() {
987        let empty = match block.leaf {
988            Leaf::Paragraph => strip_edges(block).chars.is_empty(),
989            Leaf::Html => block.chars.is_empty(),
990            _ => false,
991        };
992        if empty {
993            let Some(inner) = block.containers.last() else {
994                continue;
995            };
996            let shared = blocks
997                .iter()
998                .enumerate()
999                .any(|(other, b)| other != index && b.containers.iter().any(|c| c.same(inner)));
1000            if shared {
1001                continue;
1002            }
1003            out.push(Block::new(block.containers.clone(), Leaf::Paragraph));
1004        } else {
1005            out.push(block.clone());
1006        }
1007    }
1008    if out.is_empty() {
1009        out.push(Block::new(Vec::new(), Leaf::Paragraph));
1010    }
1011    out
1012}
1013
1014const HTML_BLOCK_TAGS: &[&str] = &[
1015    "address",
1016    "article",
1017    "aside",
1018    "base",
1019    "basefont",
1020    "blockquote",
1021    "body",
1022    "caption",
1023    "center",
1024    "col",
1025    "colgroup",
1026    "dd",
1027    "details",
1028    "dialog",
1029    "dir",
1030    "div",
1031    "dl",
1032    "dt",
1033    "fieldset",
1034    "figcaption",
1035    "figure",
1036    "footer",
1037    "form",
1038    "frame",
1039    "frameset",
1040    "h1",
1041    "h2",
1042    "h3",
1043    "h4",
1044    "h5",
1045    "h6",
1046    "head",
1047    "header",
1048    "hr",
1049    "html",
1050    "iframe",
1051    "legend",
1052    "li",
1053    "link",
1054    "main",
1055    "menu",
1056    "menuitem",
1057    "nav",
1058    "noframes",
1059    "ol",
1060    "optgroup",
1061    "option",
1062    "p",
1063    "param",
1064    "search",
1065    "section",
1066    "summary",
1067    "table",
1068    "tbody",
1069    "td",
1070    "tfoot",
1071    "th",
1072    "thead",
1073    "title",
1074    "tr",
1075    "track",
1076    "ul",
1077];
1078
1079/// HTML block types 1-6 can interrupt a paragraph, type 7 cannot.
1080fn html_interrupts(first_line: &str) -> bool {
1081    let s = first_line.trim_start_matches(' ');
1082    let lower = s.to_ascii_lowercase();
1083    for tag in ["<script", "<pre", "<style", "<textarea"] {
1084        if let Some(rest) = lower.strip_prefix(tag) {
1085            if rest.is_empty() || rest.starts_with(|c: char| c.is_whitespace() || c == '>') {
1086                return true;
1087            }
1088        }
1089    }
1090    if s.starts_with("<!--") || s.starts_with("<?") || s.starts_with("<![CDATA[") {
1091        return true;
1092    }
1093    if s.starts_with("<!") && s[2..].starts_with(|c: char| c.is_ascii_alphabetic()) {
1094        return true;
1095    }
1096    let name_start = if s.starts_with("</") {
1097        2
1098    } else if s.starts_with('<') {
1099        1
1100    } else {
1101        return false;
1102    };
1103    let name: String = s[name_start..]
1104        .chars()
1105        .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
1106        .collect();
1107    if name.is_empty() {
1108        return false;
1109    }
1110    let rest = &s[name_start + name.len()..];
1111    let ends_ok = rest.is_empty()
1112        || rest.starts_with(|c: char| c.is_whitespace())
1113        || rest.starts_with('>')
1114        || rest.starts_with("/>");
1115    ends_ok && HTML_BLOCK_TAGS.contains(&name.to_ascii_lowercase().as_str())
1116}
1117
1118/// Written as a setext heading (content with line breaks).
1119fn is_setext(block: &Block) -> bool {
1120    matches!(block.leaf, Leaf::Heading(1 | 2))
1121        && strip_edges(block).chars.iter().any(|c| is_break(*c))
1122}
1123
1124/// Can `b` start right after `a` without a blank line?
1125fn can_interrupt(a: &Block, b: &Block, common: usize) -> bool {
1126    if b.containers.len() > common
1127        && b.containers[common].is_quote()
1128        && a.containers.get(common).is_some_and(Container::is_quote)
1129    {
1130        return false; // two different quotes at the same depth would merge
1131    }
1132    if a.leaf == Leaf::Html {
1133        // An HTML block ends when its container closes.
1134        return common < a.containers.len();
1135    }
1136    if a.leaf != Leaf::Paragraph || strip_edges(a).chars.is_empty() {
1137        return true;
1138    }
1139    if b.containers.len() > common {
1140        let container = b.containers[common];
1141        let Container::Item { list, .. } = container else {
1142            return true;
1143        };
1144        let first_of_list = !a.containers.get(common).is_some_and(
1145            |other| matches!(other, Container::Item { list: l, .. } if l.id == list.id),
1146        );
1147        if first_of_list {
1148            if list.ordered && list.start != 1 {
1149                return false;
1150            }
1151            let empty_item = b.leaf == Leaf::Paragraph
1152                && strip_edges(b).chars.is_empty()
1153                && b.containers.len() == common + 1;
1154            if empty_item {
1155                return false;
1156            }
1157        }
1158        return true;
1159    }
1160    if is_setext(b) {
1161        return false; // the first line of a setext heading is paragraph text
1162    }
1163    match b.leaf {
1164        Leaf::Heading(_) | Leaf::Code | Leaf::Rule => true,
1165        Leaf::Html => {
1166            let text = b.text();
1167            html_interrupts(text.split('\n').next().unwrap_or(""))
1168        }
1169        Leaf::Paragraph | Leaf::Line => false,
1170    }
1171}
1172
1173fn needs_blank(a: &Block, b: &Block, common: usize) -> bool {
1174    // Deepest list both blocks are in (same list, possibly different items).
1175    let mut shared: Option<ListInfo> = None;
1176    for d in 0..a.containers.len().min(b.containers.len()) {
1177        match (a.containers[d], b.containers[d]) {
1178            (
1179                Container::Item {
1180                    list: la, id: ia, ..
1181                },
1182                Container::Item {
1183                    list: lb, id: ib, ..
1184                },
1185            ) if la.id == lb.id => {
1186                shared = Some(la);
1187                if ia != ib {
1188                    break;
1189                }
1190            }
1191            (ca, cb) if ca.same(&cb) => {}
1192            _ => break,
1193        }
1194    }
1195    !(shared.is_some_and(|list| list.tight) && can_interrupt(a, b, common))
1196}
1197
1198fn block_lines(block: &Block) -> Option<String> {
1199    match block.leaf {
1200        Leaf::Rule => Some("___".into()), // never confused with a list marker
1201        Leaf::Html | Leaf::Line => Some(block.text()),
1202        Leaf::Paragraph => {
1203            let block = strip_edges(block);
1204            if block.chars.is_empty() {
1205                None
1206            } else {
1207                Some(inline_checked(&block, false, |text| text.to_string()))
1208            }
1209        }
1210        Leaf::Heading(level) => {
1211            let level = level.clamp(1, 6) as usize;
1212            let block = strip_edges(block);
1213            if is_setext(&block) {
1214                let under = format!("\n{}", if level == 1 { "===" } else { "---" });
1215                let text = inline_checked(&block, false, |text| format!("{text}{under}"));
1216                return Some(format!("{text}{under}"));
1217            }
1218            let mut atx = block.clone();
1219            for c in &mut atx.chars {
1220                if *c == '\n' {
1221                    *c = ' ';
1222                }
1223            }
1224            let hashes = "#".repeat(level);
1225            let text = inline_checked(&atx, true, |text| format!("{hashes} {text}"));
1226            if text.is_empty() {
1227                Some(hashes)
1228            } else {
1229                Some(format!("{hashes} {text}"))
1230            }
1231        }
1232        Leaf::Code => {
1233            let text = block.text();
1234            let info = block.info.as_deref().unwrap_or("");
1235            let fence_char = if info.contains('`') { '~' } else { '`' };
1236            let mut longest = 0;
1237            let mut run = 0;
1238            for c in text.chars() {
1239                run = if c == fence_char { run + 1 } else { 0 };
1240                longest = longest.max(run);
1241            }
1242            let fence: String = std::iter::repeat(fence_char)
1243                .take((longest + 1).max(3))
1244                .collect();
1245            if text.is_empty() {
1246                Some(format!("{fence}{info}\n{fence}"))
1247            } else {
1248                Some(format!("{fence}{info}\n{text}\n{fence}"))
1249            }
1250        }
1251    }
1252}
1253
1254fn first_prefix(containers: &[Container], common: usize, markers: &HashMap<u64, String>) -> String {
1255    let mut prefix = String::new();
1256    for (d, container) in containers.iter().enumerate() {
1257        match container {
1258            Container::Quote { .. } => prefix.push_str("> "),
1259            Container::Item { id, .. } => {
1260                let marker = markers.get(id).map(String::as_str).unwrap_or("- ");
1261                if d >= common {
1262                    prefix.push_str(marker);
1263                } else {
1264                    prefix.push_str(&" ".repeat(marker.len()));
1265                }
1266            }
1267        }
1268    }
1269    prefix
1270}
1271
1272fn cont_prefix(containers: &[Container], markers: &HashMap<u64, String>) -> String {
1273    let mut prefix = String::new();
1274    for container in containers {
1275        match container {
1276            Container::Quote { .. } => prefix.push_str("> "),
1277            Container::Item { id, .. } => {
1278                let width = markers.get(id).map(String::len).unwrap_or(2);
1279                prefix.push_str(&" ".repeat(width));
1280            }
1281        }
1282    }
1283    prefix
1284}
1285
1286/// The task marker written before `block`: only for the first paragraph of
1287/// a task item (GFM puts the marker at the start of that paragraph).
1288fn task_marker(block: &Block, common: usize) -> Option<&'static str> {
1289    let depth = block.containers.len().checked_sub(1)?;
1290    if depth < common || block.leaf != Leaf::Paragraph || strip_edges(block).chars.is_empty() {
1291        return None;
1292    }
1293    match block.containers[depth].task()? {
1294        true => Some("[x] "),
1295        false => Some("[ ] "),
1296    }
1297}
1298
1299/// Writes the document as Markdown.
1300pub fn serialize(doc: &Document) -> String {
1301    let blocks = expressible(&doc.blocks);
1302    let mut lines: Vec<String> = Vec::new();
1303    let mut markers: HashMap<u64, String> = HashMap::new();
1304    let mut list_marker: HashMap<u64, char> = HashMap::new();
1305    let mut list_counter: HashMap<u64, u64> = HashMap::new();
1306    let mut previous: Option<&Block> = None;
1307
1308    for block in &blocks {
1309        let containers = &block.containers;
1310        let mut common = 0;
1311        if let Some(prev) = previous {
1312            while common < containers.len().min(prev.containers.len())
1313                && containers[common].same(&prev.containers[common])
1314            {
1315                common += 1;
1316            }
1317        }
1318
1319        // Markers for items starting here.
1320        for d in common..containers.len() {
1321            let Container::Item { list, id, .. } = containers[d] else {
1322                continue;
1323            };
1324            if markers.contains_key(&id) {
1325                continue;
1326            }
1327            if !list_marker.contains_key(&list.id) {
1328                // A list right after another list at the same depth needs a
1329                // different marker, otherwise both would merge.
1330                let other = previous.and_then(|prev| {
1331                    let sibling = prev.containers.get(d)?;
1332                    let Container::Item { list: other, .. } = sibling else {
1333                        return None;
1334                    };
1335                    let same_parent = (0..d).all(|k| prev.containers[k].same(&containers[k]));
1336                    (other.id != list.id && same_parent)
1337                        .then(|| list_marker.get(&other.id).copied())
1338                        .flatten()
1339                });
1340                let marker = if list.ordered {
1341                    if other == Some('.') { ')' } else { '.' }
1342                } else if other == Some('-') {
1343                    '*'
1344                } else {
1345                    '-'
1346                };
1347                list_marker.insert(list.id, marker);
1348                list_counter.insert(list.id, list.start.min(999_999_999));
1349            }
1350            let marker = list_marker[&list.id];
1351            let text = if list.ordered {
1352                let number = list_counter[&list.id];
1353                list_counter.insert(list.id, (number + 1).min(999_999_999));
1354                format!("{number}{marker} ")
1355            } else {
1356                format!("{marker} ")
1357            };
1358            markers.insert(id, text);
1359        }
1360
1361        if let Some(prev) = previous {
1362            if needs_blank(prev, block, common) {
1363                lines.push(
1364                    cont_prefix(&containers[..common], &markers)
1365                        .trim_end()
1366                        .to_string(),
1367                );
1368            }
1369        }
1370
1371        let mut first = first_prefix(containers, common, &markers);
1372        if let Some(task) = task_marker(block, common) {
1373            first.push_str(task);
1374        }
1375        let cont = cont_prefix(containers, &markers);
1376        match block_lines(block) {
1377            None => lines.push(first.trim_end().to_string()),
1378            Some(body) => {
1379                for (k, line) in body.split('\n').enumerate() {
1380                    let prefix = if k == 0 { &first } else { &cont };
1381                    if line.is_empty() {
1382                        lines.push(prefix.trim_end().to_string());
1383                    } else {
1384                        lines.push(format!("{prefix}{line}"));
1385                    }
1386                }
1387            }
1388        }
1389
1390        previous = Some(block);
1391    }
1392
1393    let mut out = lines.join("\n");
1394    out.push('\n');
1395    out
1396}
1397
1398/// Parses and writes Markdown again, in the form the editor saves it.
1399pub fn normalize(markdown: &str) -> String {
1400    serialize(&parse(markdown))
1401}
1402
1403// ====================================================================
1404// Tests
1405// ====================================================================
1406
1407#[cfg(test)]
1408mod tests {
1409    use super::*;
1410
1411    fn html(markdown: &str) -> String {
1412        let mut out = String::new();
1413        pulldown_cmark::html::push_html(&mut out, Parser::new_ext(markdown, options()));
1414        out
1415    }
1416
1417    #[derive(serde::Deserialize)]
1418    struct Example {
1419        markdown: String,
1420        example: u32,
1421        section: String,
1422    }
1423
1424    /// Examples the editor cannot reproduce exactly because its model has
1425    /// no "emphasis inside the same emphasis" (e.g. `*(*foo*)*`). The
1426    /// rendering looks the same; only the redundant nesting is lost.
1427    const NESTED_EMPHASIS: &[u32] = &[
1428        368, 372, 388, 406, 407, 408, 416, 417, 418, 424, 425, 426, 431, 460, 462, 463, 464, 465,
1429        467,
1430    ];
1431
1432    /// Different formats on the same text whose nesting order changes,
1433    /// e.g. `<del>*foo*</del>` is written as `*~~foo~~*`. The model stores
1434    /// formats per character, not their order; the rendering looks the same.
1435    const FORMAT_ORDER: &[u32] = &[168];
1436
1437    fn examples() -> Vec<Example> {
1438        serde_json::from_str(include_str!("../tests/commonmark-0.31.2.json")).unwrap()
1439    }
1440
1441    /// Every CommonMark 0.31.2 example produces the same HTML after the
1442    /// editor has read and written it.
1443    #[test]
1444    fn commonmark_spec_round_trip() {
1445        let examples = examples();
1446        assert_eq!(examples.len(), 652);
1447
1448        let mut unexpected = Vec::new();
1449        let mut known = 0;
1450        for example in &examples {
1451            let written = normalize(&example.markdown);
1452            let expected = html(&example.markdown);
1453            let actual = html(&written);
1454            if expected != actual {
1455                let is_known = NESTED_EMPHASIS.contains(&example.example)
1456                    || FORMAT_ORDER.contains(&example.example);
1457                if is_known {
1458                    known += 1;
1459                } else {
1460                    unexpected.push(format!(
1461                        "example {} ({})\n--- markdown\n{}--- written\n{}--- expected html\n{}--- actual html\n{}",
1462                        example.example, example.section, example.markdown, written, expected, actual
1463                    ));
1464                }
1465            }
1466        }
1467
1468        println!(
1469            "round trip: {} of 652 identical, {known} known differences",
1470            652 - known - unexpected.len()
1471        );
1472        if !unexpected.is_empty() {
1473            panic!(
1474                "{} unexpected differences:\n\n{}",
1475                unexpected.len(),
1476                unexpected.join("\n\n")
1477            );
1478        }
1479    }
1480
1481    /// Writing is stable: saving an opened file twice gives the same text.
1482    #[test]
1483    fn commonmark_spec_stable() {
1484        for example in &examples() {
1485            let once = normalize(&example.markdown);
1486            let twice = normalize(&once);
1487            assert_eq!(once, twice, "example {} is not stable", example.example);
1488        }
1489    }
1490
1491    #[test]
1492    fn task_lists() {
1493        for source in [
1494            "- [ ] asdf\n",
1495            "- [ ] open\n- [x] done\n",
1496            "1. [x] first\n2. [ ] second\n",
1497            "- [ ] parent\n  - [x] child\n  - plain child\n",
1498            "- [x] item\n\n  with a second paragraph\n",
1499            "> - [ ] in a quote\n",
1500            "- [ ] **bold** and [link](https://x.org)\n",
1501        ] {
1502            assert_eq!(normalize(source), source, "task list changed");
1503        }
1504
1505        let doc = parse("- [ ] open\n- [X] done\n* [x] other list\n");
1506        let tasks: Vec<Option<bool>> = doc
1507            .blocks
1508            .iter()
1509            .map(|block| block.containers.last().and_then(Container::task))
1510            .collect();
1511        assert_eq!(tasks, vec![Some(false), Some(true), Some(true)]);
1512        assert_eq!(doc.blocks[0].text(), "open");
1513        assert_eq!(normalize("- [X] done\n"), "- [x] done\n");
1514
1515        assert_eq!(
1516            normalize("- \\[ \\] not a task\n"),
1517            "- \\[ \\] not a task\n"
1518        );
1519        let doc = parse("- \\[ \\] not a task\n");
1520        assert_eq!(doc.blocks[0].containers[0].task(), None);
1521        assert_eq!(doc.blocks[0].text(), "[ ] not a task");
1522    }
1523
1524    #[test]
1525    fn image_markdown_round_trips() {
1526        let text = image_markdown("a [b]", "assets/my image.png");
1527        assert_eq!(text, "![a \\[b\\]](<assets/my image.png>)");
1528        let doc = parse(&text);
1529        let style = &doc.blocks[0].styles[0];
1530        assert_eq!(style.image.as_ref().unwrap().url, "assets/my image.png");
1531        assert_eq!(doc.blocks[0].text(), "a [b]");
1532    }
1533
1534    // ---- random documents as the editor can create them ---------------
1535
1536    struct Rng(u64);
1537
1538    impl Rng {
1539        fn next(&mut self) -> u64 {
1540            self.0 ^= self.0 << 13;
1541            self.0 ^= self.0 >> 7;
1542            self.0 ^= self.0 << 17;
1543            self.0
1544        }
1545        fn below(&mut self, n: u64) -> u64 {
1546            self.next() % n
1547        }
1548        fn chance(&mut self, p: f64) -> bool {
1549            (self.next() % 10_000) as f64 / 10_000.0 < p
1550        }
1551        fn pick<'a, T>(&mut self, items: &'a [T]) -> &'a T {
1552            &items[self.below(items.len() as u64) as usize]
1553        }
1554    }
1555
1556    const PIECES: &[&str] = &[
1557        "a", "b", "c", " ", "x", "y", "z", "1", "2", "3", "*", "_", "[", "]", "#", "<", ">", "!",
1558        "-", "+", "=", ".", ")", "`", "\\", "&", "~", "|", "  ", "\t", "\u{2028}", "\n", "\u{a0}",
1559        "[ ] ", "[x] ",
1560    ];
1561
1562    fn random_inline(rng: &mut Rng, len: usize) -> (Vec<char>, Vec<Style>) {
1563        let links = [
1564            Link::new("https://x.org/a", None),
1565            Link::new("/u", Some("T \"q\"".into())),
1566            Link::new("mailto:a@b.co", None),
1567        ];
1568        let mut chars = Vec::new();
1569        let mut styles = Vec::new();
1570        let mut style = Style::default();
1571        while chars.len() < len {
1572            if rng.chance(0.25) {
1573                style = Style {
1574                    bold: rng.chance(0.3),
1575                    italic: rng.chance(0.3),
1576                    strike: rng.chance(0.2),
1577                    code: rng.chance(0.15),
1578                    link: rng.chance(0.15).then(|| rng.pick(&links).clone()),
1579                    ..Style::default()
1580                };
1581            }
1582            for mut c in rng.pick(PIECES).chars() {
1583                if style.code && (is_break(c) || c == '\t') {
1584                    c = 'q';
1585                }
1586                chars.push(c);
1587                styles.push(style.clone());
1588            }
1589        }
1590        (chars, styles)
1591    }
1592
1593    fn random_blocks(rng: &mut Rng, path: Vec<Container>, depth: usize, out: &mut Vec<Block>) {
1594        for _ in 0..1 + rng.below(3) {
1595            let roll = rng.below(100);
1596            if depth < 3 && roll < 20 {
1597                let mut inner = path.clone();
1598                inner.push(Container::quote());
1599                random_blocks(rng, inner, depth + 1, out);
1600            } else if depth < 3 && roll < 40 {
1601                let mut list = ListInfo::new(rng.chance(0.5), *rng.pick(&[1, 1, 3, 10]));
1602                list.tight = rng.chance(0.6);
1603                for _ in 0..1 + rng.below(3) {
1604                    let task = if rng.chance(0.3) {
1605                        Some(rng.chance(0.5))
1606                    } else {
1607                        None
1608                    };
1609                    let mut inner = path.clone();
1610                    inner.push(Container::task_item(list, task));
1611                    random_blocks(rng, inner, depth + 1, out);
1612                }
1613            } else {
1614                let leaf = *rng.pick(&[
1615                    Leaf::Paragraph,
1616                    Leaf::Paragraph,
1617                    Leaf::Paragraph,
1618                    Leaf::Heading(1),
1619                    Leaf::Heading(2),
1620                    Leaf::Heading(3),
1621                    Leaf::Code,
1622                    Leaf::Rule,
1623                    Leaf::Html,
1624                ]);
1625                let mut block = Block::new(path.clone(), leaf);
1626                match leaf {
1627                    Leaf::Rule => {}
1628                    Leaf::Code => {
1629                        for _ in 0..rng.below(13) {
1630                            block.push(
1631                                *rng.pick(&['x', ' ', '\n', '`', '~', '\t']),
1632                                Style::default(),
1633                            );
1634                        }
1635                        block.info = (*rng.pick(&[None, Some("rust"), Some("a b")])).map(Arc::from);
1636                    }
1637                    Leaf::Html => {
1638                        let text = format!("<div>\n{}\n</div>", rng.pick(&["x", "*y*", "z"]));
1639                        for c in text.chars() {
1640                            block.push(c, Style::default());
1641                        }
1642                    }
1643                    _ => {
1644                        let len = 1 + rng.below(14) as usize;
1645                        let (chars, styles) = random_inline(rng, len);
1646                        block.chars = chars;
1647                        block.styles = styles;
1648                    }
1649                }
1650                out.push(block);
1651            }
1652        }
1653    }
1654
1655    /// What Markdown cannot express, applied to the expected document.
1656    fn expected(blocks: &[Block]) -> Vec<Block> {
1657        let mut out: Vec<Block> = Vec::new();
1658        for block in expressible(blocks) {
1659            let mut block = if block.leaf.is_text() {
1660                strip_edges(&block)
1661            } else {
1662                block
1663            };
1664            if matches!(block.leaf, Leaf::Heading(3..=6)) {
1665                for c in &mut block.chars {
1666                    if *c == '\n' {
1667                        *c = ' ';
1668                    }
1669                }
1670            }
1671            out.push(block);
1672        }
1673        for index in 0..out.len() {
1674            for depth in 0..out[index].containers.len() {
1675                let Container::Item { task: Some(_), .. } = out[index].containers[depth] else {
1676                    continue;
1677                };
1678                let container = out[index].containers[depth];
1679                let first = (0..out.len())
1680                    .find(|k| {
1681                        out[*k]
1682                            .containers
1683                            .get(depth)
1684                            .is_some_and(|c| c.same(&container))
1685                    })
1686                    .unwrap();
1687                let ok = out[first].leaf == Leaf::Paragraph
1688                    && out[first].containers.len() == depth + 1
1689                    && !out[first].chars.is_empty();
1690                if !ok {
1691                    if let Container::Item { task, .. } = &mut out[index].containers[depth] {
1692                        *task = None;
1693                    }
1694                }
1695            }
1696        }
1697        out
1698    }
1699
1700    type Canon = Vec<(
1701        Vec<(char, usize, usize, bool, u64, Option<bool>)>,
1702        String,
1703        String,
1704        Vec<Style>,
1705        Option<String>,
1706    )>;
1707
1708    fn canon(blocks: &[Block]) -> Canon {
1709        let mut ids: HashMap<(char, u64), usize> = HashMap::new();
1710        let mut id = |kind: char, value: u64| {
1711            let len = ids.len();
1712            *ids.entry((kind, value)).or_insert(len)
1713        };
1714        blocks
1715            .iter()
1716            .map(|block| {
1717                let containers: Vec<(char, usize, usize, bool, u64, Option<bool>)> = block
1718                    .containers
1719                    .iter()
1720                    .map(|c| match c {
1721                        Container::Quote { id: q } => ('q', id('q', *q), 0, false, 0, None),
1722                        Container::Item {
1723                            list,
1724                            id: item,
1725                            task,
1726                        } => (
1727                            'i',
1728                            id('l', list.id),
1729                            id('i', *item),
1730                            list.ordered,
1731                            if list.ordered { list.start } else { 1 },
1732                            *task,
1733                        ),
1734                    })
1735                    .collect();
1736                let styles: Vec<Style> = block
1737                    .chars
1738                    .iter()
1739                    .zip(&block.styles)
1740                    .map(|(c, s)| {
1741                        if is_break(*c) {
1742                            Style::default()
1743                        } else {
1744                            s.clone()
1745                        }
1746                    })
1747                    .collect();
1748                (
1749                    containers,
1750                    format!("{:?}", block.leaf),
1751                    block.text(),
1752                    styles,
1753                    block.info.as_deref().map(str::to_string),
1754                )
1755            })
1756            .collect()
1757    }
1758
1759    #[test]
1760    fn random_documents_round_trip() {
1761        let mut rng = Rng(0x9E37_79B9_7F4A_7C15);
1762        let mut failures = Vec::new();
1763        for _ in 0..3000 {
1764            let mut blocks = Vec::new();
1765            random_blocks(&mut rng, Vec::new(), 0, &mut blocks);
1766            let doc = Document { blocks };
1767            let written = serialize(&doc);
1768            let back = parse(&written);
1769            if canon(&back.blocks) != canon(&expected(&doc.blocks)) {
1770                failures.push(format!("not preserved:\n{written}"));
1771                continue;
1772            }
1773            let again = serialize(&back);
1774            if serialize(&parse(&again)) != again {
1775                failures.push(format!("not stable:\n{again}"));
1776            }
1777        }
1778        if !failures.is_empty() {
1779            panic!("{} of 3000 failed, first:\n{}", failures.len(), failures[0]);
1780        }
1781    }
1782}