pulldown_cmark_to_cmark/
lib.rs

1#![deny(rust_2018_idioms)]
2
3use std::{
4    borrow::{Borrow, Cow},
5    collections::HashSet,
6    fmt,
7    ops::Range,
8};
9
10use pulldown_cmark::{Alignment as TableAlignment, BlockQuoteKind, Event, LinkType, MetadataBlockKind, Tag, TagEnd};
11
12mod source_range;
13mod text_modifications;
14
15pub use source_range::{
16    cmark_resume_with_source_range, cmark_resume_with_source_range_and_options, cmark_with_source_range,
17    cmark_with_source_range_and_options,
18};
19use text_modifications::*;
20
21/// Similar to [Pulldown-Cmark-Alignment][Alignment], but with required
22/// traits for comparison to allow testing.
23#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub enum Alignment {
25    None,
26    Left,
27    Center,
28    Right,
29}
30
31impl<'a> From<&'a TableAlignment> for Alignment {
32    fn from(s: &'a TableAlignment) -> Self {
33        match *s {
34            TableAlignment::None => Self::None,
35            TableAlignment::Left => Self::Left,
36            TableAlignment::Center => Self::Center,
37            TableAlignment::Right => Self::Right,
38        }
39    }
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub enum CodeBlockKind {
44    Indented,
45    Fenced,
46}
47
48/// The state of the [`cmark_resume()`] and [`cmark_resume_with_options()`] functions.
49/// This does not only allow introspection, but enables the user
50/// to halt the serialization at any time, and resume it later.
51#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
52#[non_exhaustive]
53pub struct State<'a> {
54    /// The amount of newlines to insert after `Event::Start(...)`
55    pub newlines_before_start: usize,
56    /// The lists and their types for which we have seen a `Event::Start(List(...))` tag
57    pub list_stack: Vec<Option<u64>>,
58    /// The computed padding and prefix to print after each newline.
59    /// This changes with the level of `BlockQuote` and `List` events.
60    pub padding: Vec<Cow<'a, str>>,
61    /// Keeps the current table alignments, if we are currently serializing a table.
62    pub table_alignments: Vec<Alignment>,
63    /// Keeps the current table headers, if we are currently serializing a table.
64    pub table_headers: Vec<String>,
65    /// The last seen text when serializing a header
66    pub text_for_header: Option<String>,
67    /// Is set while we are handling text in a code block
68    pub code_block: Option<CodeBlockKind>,
69    /// True if the last event was text and the text does not have trailing newline. Used to inject additional newlines before code block end fence.
70    pub last_was_text_without_trailing_newline: bool,
71    /// True if the last event was a paragraph start. Used to escape spaces at start of line (prevent spurrious indented code).
72    pub last_was_paragraph_start: bool,
73    /// True if the next event is a link, image, or footnote.
74    pub next_is_link_like: bool,
75    /// Currently open links
76    pub link_stack: Vec<LinkCategory<'a>>,
77    /// Currently open images
78    pub image_stack: Vec<ImageLink<'a>>,
79    /// Keeps track of the last seen heading's id, classes, and attributes
80    pub current_heading: Option<Heading<'a>>,
81    /// True whenever between `Start(TableCell)` and `End(TableCell)`
82    pub in_table_cell: bool,
83
84    /// Keeps track of the last seen shortcut/link
85    pub current_shortcut_text: Option<String>,
86    /// A list of shortcuts seen so far for later emission
87    pub shortcuts: Vec<(String, String, String)>,
88    /// Index into the `source` bytes of the end of the range corresponding to the last event.
89    ///
90    /// It's used to see if the current event didn't capture some bytes because of a
91    /// skipped-over backslash.
92    pub last_event_end_index: usize,
93}
94
95#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
96pub enum LinkCategory<'a> {
97    AngleBracketed,
98    Reference {
99        uri: Cow<'a, str>,
100        title: Cow<'a, str>,
101        id: Cow<'a, str>,
102    },
103    Collapsed {
104        uri: Cow<'a, str>,
105        title: Cow<'a, str>,
106    },
107    Shortcut {
108        uri: Cow<'a, str>,
109        title: Cow<'a, str>,
110    },
111    Other {
112        uri: Cow<'a, str>,
113        title: Cow<'a, str>,
114    },
115}
116
117#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub enum ImageLink<'a> {
119    Reference {
120        uri: Cow<'a, str>,
121        title: Cow<'a, str>,
122        id: Cow<'a, str>,
123    },
124    Collapsed {
125        uri: Cow<'a, str>,
126        title: Cow<'a, str>,
127    },
128    Shortcut {
129        uri: Cow<'a, str>,
130        title: Cow<'a, str>,
131    },
132    Other {
133        uri: Cow<'a, str>,
134        title: Cow<'a, str>,
135    },
136}
137
138#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
139pub struct Heading<'a> {
140    id: Option<Cow<'a, str>>,
141    classes: Vec<Cow<'a, str>>,
142    attributes: Vec<(Cow<'a, str>, Option<Cow<'a, str>>)>,
143}
144
145/// Thea mount of code-block tokens one needs to produce a valid fenced code-block.
146pub const DEFAULT_CODE_BLOCK_TOKEN_COUNT: usize = 3;
147
148/// Configuration for the [`cmark_with_options()`] and [`cmark_resume_with_options()`] functions.
149/// The defaults should provide decent spacing and most importantly, will
150/// provide a faithful rendering of your markdown document particularly when
151/// rendering it to HTML.
152///
153/// It's best used with its `Options::default()` implementation.
154#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
155pub struct Options<'a> {
156    pub newlines_after_headline: usize,
157    pub newlines_after_paragraph: usize,
158    pub newlines_after_codeblock: usize,
159    pub newlines_after_htmlblock: usize,
160    pub newlines_after_table: usize,
161    pub newlines_after_rule: usize,
162    pub newlines_after_list: usize,
163    pub newlines_after_blockquote: usize,
164    pub newlines_after_rest: usize,
165    /// The amount of newlines placed after TOML or YAML metadata blocks at the beginning of a document.
166    pub newlines_after_metadata: usize,
167    /// Token count for fenced code block. An appropriate value of this field can be decided by
168    /// [`calculate_code_block_token_count()`].
169    /// Note that the default value is `4` which allows for one level of nested code-blocks,
170    /// which is typically a safe value for common kinds of markdown documents.
171    pub code_block_token_count: usize,
172    pub code_block_token: char,
173    pub list_token: char,
174    pub ordered_list_token: char,
175    pub increment_ordered_list_bullets: bool,
176    pub emphasis_token: char,
177    pub strong_token: &'a str,
178}
179
180const DEFAULT_OPTIONS: Options<'_> = Options {
181    newlines_after_headline: 2,
182    newlines_after_paragraph: 2,
183    newlines_after_codeblock: 2,
184    newlines_after_htmlblock: 1,
185    newlines_after_table: 2,
186    newlines_after_rule: 2,
187    newlines_after_list: 2,
188    newlines_after_blockquote: 2,
189    newlines_after_rest: 1,
190    newlines_after_metadata: 1,
191    code_block_token_count: 4,
192    code_block_token: '`',
193    list_token: '*',
194    ordered_list_token: '.',
195    increment_ordered_list_bullets: false,
196    emphasis_token: '*',
197    strong_token: "**",
198};
199
200impl Default for Options<'_> {
201    fn default() -> Self {
202        DEFAULT_OPTIONS
203    }
204}
205
206impl Options<'_> {
207    pub fn special_characters(&self) -> Cow<'static, str> {
208        // These always need to be escaped, even if reconfigured.
209        const BASE: &str = "#\\_*<>`|[]";
210        if DEFAULT_OPTIONS.code_block_token == self.code_block_token
211            && DEFAULT_OPTIONS.list_token == self.list_token
212            && DEFAULT_OPTIONS.emphasis_token == self.emphasis_token
213            && DEFAULT_OPTIONS.strong_token == self.strong_token
214        {
215            BASE.into()
216        } else {
217            let mut s = String::from(BASE);
218            s.push(self.code_block_token);
219            s.push(self.list_token);
220            s.push(self.emphasis_token);
221            s.push_str(self.strong_token);
222            s.into()
223        }
224    }
225}
226
227/// The error returned by [`cmark_resume_one_event`] and
228/// [`cmark_resume_with_source_range_and_options`].
229#[derive(Debug)]
230pub enum Error {
231    FormatFailed(fmt::Error),
232    UnexpectedEvent,
233}
234
235impl fmt::Display for Error {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        match self {
238            Self::FormatFailed(e) => e.fmt(f),
239            Self::UnexpectedEvent => f.write_str("Unexpected event while reconstructing Markdown"),
240        }
241    }
242}
243
244impl std::error::Error for Error {}
245
246impl From<fmt::Error> for Error {
247    fn from(e: fmt::Error) -> Self {
248        Self::FormatFailed(e)
249    }
250}
251
252/// As [`cmark_with_options()`], but with default [`Options`].
253pub fn cmark<'a, I, E, F>(events: I, mut formatter: F) -> Result<State<'a>, Error>
254where
255    I: Iterator<Item = E>,
256    E: Borrow<Event<'a>>,
257    F: fmt::Write,
258{
259    cmark_with_options(events, &mut formatter, Default::default())
260}
261
262/// As [`cmark_resume_with_options()`], but with default [`Options`].
263pub fn cmark_resume<'a, I, E, F>(events: I, formatter: F, state: Option<State<'a>>) -> Result<State<'a>, Error>
264where
265    I: Iterator<Item = E>,
266    E: Borrow<Event<'a>>,
267    F: fmt::Write,
268{
269    cmark_resume_with_options(events, formatter, state, Options::default())
270}
271
272/// As [`cmark_resume_with_options()`], but with the [`State`] finalized.
273pub fn cmark_with_options<'a, I, E, F>(events: I, mut formatter: F, options: Options<'_>) -> Result<State<'a>, Error>
274where
275    I: Iterator<Item = E>,
276    E: Borrow<Event<'a>>,
277    F: fmt::Write,
278{
279    let state = cmark_resume_with_options(events, &mut formatter, Default::default(), options)?;
280    state.finalize(formatter)
281}
282
283/// Serialize a stream of [pulldown-cmark-Events][Event] into a string-backed buffer.
284///
285/// 1. **events**
286///    * An iterator over [`Events`][Event], for example as returned by the [`Parser`][pulldown_cmark::Parser]
287/// 1. **formatter**
288///    * A format writer, can be a `String`.
289/// 1. **state**
290///    * The optional initial state of the serialization.
291/// 1. **options**
292///    * Customize the appearance of the serialization. All otherwise magic values are contained
293///      here.
294///
295/// *Returns* the [`State`] of the serialization on success. You can use it as initial state in the
296/// next call if you are halting event serialization.
297///
298/// *Errors* if the underlying buffer fails (which is unlikely) or if the [`Event`] stream
299/// cannot ever be produced by deserializing valid Markdown. Each failure mode corresponds to one
300/// of [`Error`]'s variants.
301pub fn cmark_resume_with_options<'a, I, E, F>(
302    events: I,
303    mut formatter: F,
304    state: Option<State<'a>>,
305    options: Options<'_>,
306) -> Result<State<'a>, Error>
307where
308    I: Iterator<Item = E>,
309    E: Borrow<Event<'a>>,
310    F: fmt::Write,
311{
312    let mut state = state.unwrap_or_default();
313    let mut events = events.peekable();
314    while let Some(event) = events.next() {
315        state.next_is_link_like = matches!(
316            events.peek().map(Borrow::borrow),
317            Some(
318                Event::Start(Tag::Link { .. } | Tag::Image { .. } | Tag::FootnoteDefinition(..))
319                    | Event::FootnoteReference(..)
320            )
321        );
322        cmark_resume_one_event(event, &mut formatter, &mut state, &options)?;
323    }
324    Ok(state)
325}
326
327fn cmark_resume_one_event<'a, E, F>(
328    event: E,
329    formatter: &mut F,
330    state: &mut State<'a>,
331    options: &Options<'_>,
332) -> Result<(), Error>
333where
334    E: Borrow<Event<'a>>,
335    F: fmt::Write,
336{
337    use pulldown_cmark::{Event::*, Tag::*};
338
339    let last_was_text_without_trailing_newline = state.last_was_text_without_trailing_newline;
340    state.last_was_text_without_trailing_newline = false;
341    let last_was_paragraph_start = state.last_was_paragraph_start;
342    state.last_was_paragraph_start = false;
343
344    let res = match event.borrow() {
345        Rule => {
346            consume_newlines(formatter, state)?;
347            state.set_minimum_newlines_before_start(options.newlines_after_rule);
348            formatter.write_str("---")
349        }
350        Code(text) => {
351            if let Some(shortcut_text) = state.current_shortcut_text.as_mut() {
352                shortcut_text.push('`');
353                shortcut_text.push_str(text);
354                shortcut_text.push('`');
355            }
356            if let Some(text_for_header) = state.text_for_header.as_mut() {
357                text_for_header.push('`');
358                text_for_header.push_str(text);
359                text_for_header.push('`');
360            }
361
362            // (re)-escape `|` when it appears as part of inline code in the
363            // body of a table.
364            //
365            // NOTE: This does not do *general* escaped-character handling
366            // because the only character which *requires* this handling in this
367            // spot in earlier versions of `pulldown-cmark` is a pipe character
368            // in inline code in a table. Other escaping is handled when `Text`
369            // events are emitted.
370            let text = if state.in_table_cell {
371                Cow::Owned(text.replace('|', "\\|"))
372            } else {
373                Cow::Borrowed(text.as_ref())
374            };
375
376            // When inline code has leading and trailing ' ' characters, additional space is needed
377            // to escape it, unless all characters are space.
378            if text.chars().all(|ch| ch == ' ') {
379                write!(formatter, "`{text}`")
380            } else {
381                // More backticks are needed to delimit the inline code than the maximum number of
382                // backticks in a consecutive run.
383                let backticks = Repeated('`', max_consecutive_chars(&text, '`') + 1);
384                let space = match text.as_bytes() {
385                    &[b'`', ..] | &[.., b'`'] => " ", // Space needed to separate backtick.
386                    &[b' ', .., b' '] => " ",         // Space needed to escape inner space.
387                    _ => "",                          // No space needed.
388                };
389                write!(formatter, "{backticks}{space}{text}{space}{backticks}")
390            }
391        }
392        Start(tag) => {
393            if let List(list_type) = tag {
394                state.list_stack.push(*list_type);
395                if state.list_stack.len() > 1 {
396                    state.set_minimum_newlines_before_start(options.newlines_after_rest);
397                }
398            }
399            let consumed_newlines = state.newlines_before_start != 0;
400            consume_newlines(formatter, state)?;
401            match tag {
402                Item => {
403                    // lazy lists act like paragraphs with no event
404                    state.last_was_paragraph_start = true;
405                    match state.list_stack.last_mut() {
406                        Some(inner) => {
407                            state.padding.push(list_item_padding_of(*inner));
408                            match inner {
409                                Some(n) => {
410                                    let bullet_number = *n;
411                                    if options.increment_ordered_list_bullets {
412                                        *n += 1;
413                                    }
414                                    write!(formatter, "{}{} ", bullet_number, options.ordered_list_token)
415                                }
416                                None => write!(formatter, "{} ", options.list_token),
417                            }
418                        }
419                        None => Ok(()),
420                    }
421                }
422                Table(alignments) => {
423                    state.table_alignments = alignments.iter().map(From::from).collect();
424                    Ok(())
425                }
426                TableHead => Ok(()),
427                TableRow => Ok(()),
428                TableCell => {
429                    state.text_for_header = Some(String::new());
430                    state.in_table_cell = true;
431                    formatter.write_char('|')
432                }
433                Link {
434                    link_type,
435                    dest_url,
436                    title,
437                    id,
438                } => {
439                    state.link_stack.push(match link_type {
440                        LinkType::Autolink | LinkType::Email => {
441                            formatter.write_char('<')?;
442                            LinkCategory::AngleBracketed
443                        }
444                        LinkType::Reference => {
445                            formatter.write_char('[')?;
446                            LinkCategory::Reference {
447                                uri: dest_url.clone().into(),
448                                title: title.clone().into(),
449                                id: id.clone().into(),
450                            }
451                        }
452                        LinkType::Collapsed => {
453                            state.current_shortcut_text = Some(String::new());
454                            formatter.write_char('[')?;
455                            LinkCategory::Collapsed {
456                                uri: dest_url.clone().into(),
457                                title: title.clone().into(),
458                            }
459                        }
460                        LinkType::Shortcut => {
461                            state.current_shortcut_text = Some(String::new());
462                            formatter.write_char('[')?;
463                            LinkCategory::Shortcut {
464                                uri: dest_url.clone().into(),
465                                title: title.clone().into(),
466                            }
467                        }
468                        _ => {
469                            formatter.write_char('[')?;
470                            LinkCategory::Other {
471                                uri: dest_url.clone().into(),
472                                title: title.clone().into(),
473                            }
474                        }
475                    });
476                    Ok(())
477                }
478                Image {
479                    link_type,
480                    dest_url,
481                    title,
482                    id,
483                } => {
484                    state.image_stack.push(match link_type {
485                        LinkType::Reference => ImageLink::Reference {
486                            uri: dest_url.clone().into(),
487                            title: title.clone().into(),
488                            id: id.clone().into(),
489                        },
490                        LinkType::Collapsed => {
491                            state.current_shortcut_text = Some(String::new());
492                            ImageLink::Collapsed {
493                                uri: dest_url.clone().into(),
494                                title: title.clone().into(),
495                            }
496                        }
497                        LinkType::Shortcut => {
498                            state.current_shortcut_text = Some(String::new());
499                            ImageLink::Shortcut {
500                                uri: dest_url.clone().into(),
501                                title: title.clone().into(),
502                            }
503                        }
504                        _ => ImageLink::Other {
505                            uri: dest_url.clone().into(),
506                            title: title.clone().into(),
507                        },
508                    });
509                    formatter.write_str("![")
510                }
511                Emphasis => formatter.write_char(options.emphasis_token),
512                Strong => formatter.write_str(options.strong_token),
513                FootnoteDefinition(name) => {
514                    state.padding.push("    ".into());
515                    write!(formatter, "[^{name}]: ")
516                }
517                Paragraph => {
518                    state.last_was_paragraph_start = true;
519                    Ok(())
520                }
521                Heading {
522                    level,
523                    id,
524                    classes,
525                    attrs,
526                } => {
527                    if state.current_heading.is_some() {
528                        return Err(Error::UnexpectedEvent);
529                    }
530                    state.current_heading = Some(self::Heading {
531                        id: id.as_ref().map(|id| id.clone().into()),
532                        classes: classes.iter().map(|class| class.clone().into()).collect(),
533                        attributes: attrs
534                            .iter()
535                            .map(|(k, v)| (k.clone().into(), v.as_ref().map(|val| val.clone().into())))
536                            .collect(),
537                    });
538                    // Write '#', '##', '###', etc. based on the heading level.
539                    write!(formatter, "{} ", Repeated('#', *level as usize))
540                }
541                BlockQuote(kind) => {
542                    let every_line_padding = " > ";
543                    let first_line_padding = kind
544                        .map(|kind| match kind {
545                            BlockQuoteKind::Note => " > [!NOTE]",
546                            BlockQuoteKind::Tip => " > [!TIP]",
547                            BlockQuoteKind::Important => " > [!IMPORTANT]",
548                            BlockQuoteKind::Warning => " > [!WARNING]",
549                            BlockQuoteKind::Caution => " > [!CAUTION]",
550                        })
551                        .unwrap_or(every_line_padding);
552                    state.newlines_before_start = 1;
553
554                    // if we consumed some newlines, we know that we can just write out the next
555                    // level in our blockquote. This should work regardless if we have other
556                    // padding or if we're in a list
557                    if !consumed_newlines {
558                        write_padded_newline(formatter, state)?;
559                    }
560                    formatter.write_str(first_line_padding)?;
561                    state.padding.push(every_line_padding.into());
562                    Ok(())
563                }
564                CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => {
565                    state.code_block = Some(CodeBlockKind::Indented);
566                    state.padding.push("    ".into());
567                    if consumed_newlines {
568                        formatter.write_str("    ")
569                    } else {
570                        write_padded_newline(formatter, &state)
571                    }
572                }
573                CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(info)) => {
574                    state.code_block = Some(CodeBlockKind::Fenced);
575                    if !consumed_newlines {
576                        write_padded_newline(formatter, &state)?;
577                    }
578
579                    let fence = Repeated(options.code_block_token, options.code_block_token_count);
580                    write!(formatter, "{fence}{info}")?;
581                    write_padded_newline(formatter, &state)
582                }
583                HtmlBlock => Ok(()),
584                MetadataBlock(MetadataBlockKind::YamlStyle) => formatter.write_str("---\n"),
585                MetadataBlock(MetadataBlockKind::PlusesStyle) => formatter.write_str("+++\n"),
586                List(_) => Ok(()),
587                Strikethrough => formatter.write_str("~~"),
588                DefinitionList => Ok(()),
589                DefinitionListTitle => {
590                    state.set_minimum_newlines_before_start(options.newlines_after_rest);
591                    Ok(())
592                }
593                DefinitionListDefinition => {
594                    let every_line_padding = "  ";
595                    let first_line_padding = ": ";
596
597                    padding(formatter, &state.padding).and(formatter.write_str(first_line_padding))?;
598                    state.padding.push(every_line_padding.into());
599                    Ok(())
600                }
601                Superscript => formatter.write_str("<sup>"),
602                Subscript => formatter.write_str("<sub>"),
603            }
604        }
605        End(tag) => match tag {
606            TagEnd::Link => match if let Some(link_cat) = state.link_stack.pop() {
607                link_cat
608            } else {
609                return Err(Error::UnexpectedEvent);
610            } {
611                LinkCategory::AngleBracketed => formatter.write_char('>'),
612                LinkCategory::Reference { uri, title, id } => {
613                    state
614                        .shortcuts
615                        .push((id.to_string(), uri.to_string(), title.to_string()));
616                    formatter.write_str("][")?;
617                    formatter.write_str(&id)?;
618                    formatter.write_char(']')
619                }
620                LinkCategory::Collapsed { uri, title } => {
621                    if let Some(shortcut_text) = state.current_shortcut_text.take() {
622                        state
623                            .shortcuts
624                            .push((shortcut_text, uri.to_string(), title.to_string()));
625                    }
626                    formatter.write_str("][]")
627                }
628                LinkCategory::Shortcut { uri, title } => {
629                    if let Some(shortcut_text) = state.current_shortcut_text.take() {
630                        state
631                            .shortcuts
632                            .push((shortcut_text, uri.to_string(), title.to_string()));
633                    }
634                    formatter.write_char(']')
635                }
636                LinkCategory::Other { uri, title } => close_link(&uri, &title, formatter, LinkType::Inline),
637            },
638            TagEnd::Image => match if let Some(img_link) = state.image_stack.pop() {
639                img_link
640            } else {
641                return Err(Error::UnexpectedEvent);
642            } {
643                ImageLink::Reference { uri, title, id } => {
644                    state
645                        .shortcuts
646                        .push((id.to_string(), uri.to_string(), title.to_string()));
647                    formatter.write_str("][")?;
648                    formatter.write_str(&id)?;
649                    formatter.write_char(']')
650                }
651                ImageLink::Collapsed { uri, title } => {
652                    if let Some(shortcut_text) = state.current_shortcut_text.take() {
653                        state
654                            .shortcuts
655                            .push((shortcut_text, uri.to_string(), title.to_string()));
656                    }
657                    formatter.write_str("][]")
658                }
659                ImageLink::Shortcut { uri, title } => {
660                    if let Some(shortcut_text) = state.current_shortcut_text.take() {
661                        state
662                            .shortcuts
663                            .push((shortcut_text, uri.to_string(), title.to_string()));
664                    }
665                    formatter.write_char(']')
666                }
667                ImageLink::Other { uri, title } => {
668                    close_link(uri.as_ref(), title.as_ref(), formatter, LinkType::Inline)
669                }
670            },
671            TagEnd::Emphasis => formatter.write_char(options.emphasis_token),
672            TagEnd::Strong => formatter.write_str(options.strong_token),
673            TagEnd::Heading(_) => {
674                let Some(self::Heading {
675                    id,
676                    classes,
677                    attributes,
678                }) = state.current_heading.take()
679                else {
680                    return Err(Error::UnexpectedEvent);
681                };
682                let emit_braces = id.is_some() || !classes.is_empty() || !attributes.is_empty();
683                if emit_braces {
684                    formatter.write_str(" {")?;
685                }
686                if let Some(id_str) = id {
687                    formatter.write_char(' ')?;
688                    formatter.write_char('#')?;
689                    formatter.write_str(&id_str)?;
690                }
691                for class in &classes {
692                    formatter.write_char(' ')?;
693                    formatter.write_char('.')?;
694                    formatter.write_str(class)?;
695                }
696                for (key, val) in &attributes {
697                    formatter.write_char(' ')?;
698                    formatter.write_str(key)?;
699                    if let Some(val) = val {
700                        formatter.write_char('=')?;
701                        formatter.write_str(val)?;
702                    }
703                }
704                if emit_braces {
705                    formatter.write_char(' ')?;
706                    formatter.write_char('}')?;
707                }
708                state.set_minimum_newlines_before_start(options.newlines_after_headline);
709                Ok(())
710            }
711            TagEnd::Paragraph => {
712                state.set_minimum_newlines_before_start(options.newlines_after_paragraph);
713                Ok(())
714            }
715            TagEnd::CodeBlock => {
716                state.set_minimum_newlines_before_start(options.newlines_after_codeblock);
717                if last_was_text_without_trailing_newline {
718                    write_padded_newline(formatter, &state)?;
719                }
720                match state.code_block {
721                    Some(CodeBlockKind::Fenced) => {
722                        let fence = Repeated(options.code_block_token, options.code_block_token_count);
723                        write!(formatter, "{fence}")?;
724                    }
725                    Some(CodeBlockKind::Indented) => {
726                        state.padding.pop();
727                    }
728                    None => {}
729                }
730                state.code_block = None;
731                Ok(())
732            }
733            TagEnd::HtmlBlock => {
734                state.set_minimum_newlines_before_start(options.newlines_after_htmlblock);
735                Ok(())
736            }
737            TagEnd::MetadataBlock(MetadataBlockKind::PlusesStyle) => {
738                state.set_minimum_newlines_before_start(options.newlines_after_metadata);
739                formatter.write_str("+++\n")
740            }
741            TagEnd::MetadataBlock(MetadataBlockKind::YamlStyle) => {
742                state.set_minimum_newlines_before_start(options.newlines_after_metadata);
743                formatter.write_str("---\n")
744            }
745            TagEnd::Table => {
746                state.set_minimum_newlines_before_start(options.newlines_after_table);
747                state.table_alignments.clear();
748                state.table_headers.clear();
749                Ok(())
750            }
751            TagEnd::TableCell => {
752                state
753                    .table_headers
754                    .push(state.text_for_header.take().unwrap_or_default());
755                state.in_table_cell = false;
756                Ok(())
757            }
758            t @ (TagEnd::TableRow | TagEnd::TableHead) => {
759                state.set_minimum_newlines_before_start(options.newlines_after_rest);
760                formatter.write_char('|')?;
761
762                if let TagEnd::TableHead = t {
763                    write_padded_newline(formatter, &state)?;
764                    for (alignment, name) in state.table_alignments.iter().zip(state.table_headers.iter()) {
765                        formatter.write_char('|')?;
766                        // NOTE: For perfect counting, count grapheme clusters.
767                        // The reason this is not done is to avoid the dependency.
768
769                        // The minimum width of the column so that we can represent its alignment.
770                        let min_width = match alignment {
771                            // Must at least represent `-`.
772                            Alignment::None => 1,
773                            // Must at least represent `:-` or `-:`
774                            Alignment::Left | Alignment::Right => 2,
775                            // Must at least represent `:-:`
776                            Alignment::Center => 3,
777                        };
778                        let length = name.chars().count().max(min_width);
779                        let last_minus_one = length.saturating_sub(1);
780                        for c in 0..length {
781                            formatter.write_char(
782                                if (c == 0 && (alignment == &Alignment::Center || alignment == &Alignment::Left))
783                                    || (c == last_minus_one
784                                        && (alignment == &Alignment::Center || alignment == &Alignment::Right))
785                                {
786                                    ':'
787                                } else {
788                                    '-'
789                                },
790                            )?;
791                        }
792                    }
793                    formatter.write_char('|')?;
794                }
795                Ok(())
796            }
797            TagEnd::Item => {
798                state.padding.pop();
799                state.set_minimum_newlines_before_start(options.newlines_after_rest);
800                Ok(())
801            }
802            TagEnd::List(_) => {
803                state.list_stack.pop();
804                if state.list_stack.is_empty() {
805                    state.set_minimum_newlines_before_start(options.newlines_after_list);
806                }
807                Ok(())
808            }
809            TagEnd::BlockQuote(_) => {
810                state.padding.pop();
811
812                state.set_minimum_newlines_before_start(options.newlines_after_blockquote);
813
814                Ok(())
815            }
816            TagEnd::FootnoteDefinition => {
817                state.padding.pop();
818                Ok(())
819            }
820            TagEnd::Strikethrough => formatter.write_str("~~"),
821            TagEnd::DefinitionList => {
822                state.set_minimum_newlines_before_start(options.newlines_after_list);
823                Ok(())
824            }
825            TagEnd::DefinitionListTitle => formatter.write_char('\n'),
826            TagEnd::DefinitionListDefinition => {
827                state.padding.pop();
828                write_padded_newline(formatter, &state)
829            }
830            TagEnd::Superscript => formatter.write_str("</sup>"),
831            TagEnd::Subscript => formatter.write_str("</sub>"),
832        },
833        HardBreak => formatter.write_str("  ").and(write_padded_newline(formatter, &state)),
834        SoftBreak => write_padded_newline(formatter, &state),
835        Text(text) => {
836            let mut text = &text[..];
837            if let Some(shortcut_text) = state.current_shortcut_text.as_mut() {
838                shortcut_text.push_str(text);
839            }
840            if let Some(text_for_header) = state.text_for_header.as_mut() {
841                text_for_header.push_str(text);
842            }
843            consume_newlines(formatter, state)?;
844            if last_was_paragraph_start {
845                if text.starts_with('\t') {
846                    formatter.write_str("&#9;")?;
847                    text = &text[1..];
848                } else if text.starts_with(' ') {
849                    formatter.write_str("&#32;")?;
850                    text = &text[1..];
851                }
852            }
853            state.last_was_text_without_trailing_newline = !text.ends_with('\n');
854            let escaped_text = escape_special_characters(text, state, options);
855            print_text_without_trailing_newline(&escaped_text, formatter, &state)
856        }
857        InlineHtml(text) => {
858            consume_newlines(formatter, state)?;
859            print_text_without_trailing_newline(text, formatter, &state)
860        }
861        Html(text) => {
862            let mut lines = text.split('\n');
863            if let Some(line) = lines.next() {
864                formatter.write_str(line)?;
865            }
866            for line in lines {
867                write_padded_newline(formatter, &state)?;
868                formatter.write_str(line)?;
869            }
870            Ok(())
871        }
872        FootnoteReference(name) => write!(formatter, "[^{name}]"),
873        TaskListMarker(checked) => {
874            let check = if *checked { "x" } else { " " };
875            write!(formatter, "[{check}] ")
876        }
877        InlineMath(text) => write!(formatter, "${text}$"),
878        DisplayMath(text) => write!(formatter, "$${text}$$"),
879    };
880
881    Ok(res?)
882}
883
884impl State<'_> {
885    pub fn finalize<F>(mut self, mut formatter: F) -> Result<Self, Error>
886    where
887        F: fmt::Write,
888    {
889        if self.shortcuts.is_empty() {
890            return Ok(self);
891        }
892
893        formatter.write_str("\n")?;
894        let mut written_shortcuts = HashSet::new();
895        for shortcut in self.shortcuts.drain(..) {
896            if written_shortcuts.contains(&shortcut) {
897                continue;
898            }
899            write!(formatter, "\n[{}", shortcut.0)?;
900            close_link(&shortcut.1, &shortcut.2, &mut formatter, LinkType::Shortcut)?;
901            written_shortcuts.insert(shortcut);
902        }
903        Ok(self)
904    }
905
906    pub fn is_in_code_block(&self) -> bool {
907        self.code_block.is_some()
908    }
909
910    /// Ensure that [`State::newlines_before_start`] is at least as large as
911    /// the provided option value.
912    fn set_minimum_newlines_before_start(&mut self, option_value: usize) {
913        if self.newlines_before_start < option_value {
914            self.newlines_before_start = option_value
915        }
916    }
917}
918
919/// Return the `<seen amount of consecutive fenced code-block tokens> + 1` that occur *within* a
920/// fenced code-block `events`.
921///
922/// Use this function to obtain the correct value for `code_block_token_count` field of [`Options`]
923/// to assure that the enclosing code-blocks remain functional as such.
924///
925/// Returns `None` if `events` didn't include any code-block, or the code-block didn't contain
926/// a nested block. In that case, the correct amount of fenced code-block tokens is
927/// [`DEFAULT_CODE_BLOCK_TOKEN_COUNT`].
928///
929/// ```rust
930/// use pulldown_cmark::Event;
931/// use pulldown_cmark_to_cmark::*;
932///
933/// let events = &[Event::Text("text".into())];
934/// let code_block_token_count = calculate_code_block_token_count(events).unwrap_or(DEFAULT_CODE_BLOCK_TOKEN_COUNT);
935/// let options = Options {
936///     code_block_token_count,
937///     ..Default::default()
938/// };
939/// let mut buf = String::new();
940/// cmark_with_options(events.iter(), &mut buf, options);
941/// ```
942pub fn calculate_code_block_token_count<'a, I, E>(events: I) -> Option<usize>
943where
944    I: IntoIterator<Item = E>,
945    E: Borrow<Event<'a>>,
946{
947    let mut in_codeblock = false;
948    let mut max_token_count = 0;
949
950    // token_count should be taken over Text events
951    // because a continuous text may be splitted to some Text events.
952    let mut token_count = 0;
953    let mut prev_token_char = None;
954    for event in events {
955        match event.borrow() {
956            Event::Start(Tag::CodeBlock(_)) => {
957                in_codeblock = true;
958            }
959            Event::End(TagEnd::CodeBlock) => {
960                in_codeblock = false;
961                prev_token_char = None;
962            }
963            Event::Text(x) if in_codeblock => {
964                for c in x.chars() {
965                    let prev_token = prev_token_char.take();
966                    if c == '`' || c == '~' {
967                        prev_token_char = Some(c);
968                        if Some(c) == prev_token {
969                            token_count += 1;
970                        } else {
971                            max_token_count = max_token_count.max(token_count);
972                            token_count = 1;
973                        }
974                    }
975                }
976            }
977            _ => prev_token_char = None,
978        }
979    }
980
981    max_token_count = max_token_count.max(token_count);
982    (max_token_count >= 3).then_some(max_token_count + 1)
983}