markdown_fmt/
formatter.rs

1use pulldown_cmark::MetadataBlockKind;
2
3use super::*;
4
5/// Used to format Markdown inputs.
6///
7/// To get a [MarkdownFormatter] use [FormatterBuilder::build]
8///
9/// [FormatterBuilder::build]: crate::FormatterBuilder::build
10pub struct MarkdownFormatter {
11    code_block_formatter: CodeBlockFormatter,
12    config: Config,
13}
14
15impl MarkdownFormatter {
16    /// Format Markdown input
17    ///
18    /// ```rust
19    /// # use markdown_fmt::FormatterBuilder;
20    /// let builder = FormatterBuilder::default();
21    /// let formatter = builder.build();
22    /// let input = "   #  Header! ";
23    /// let rewrite = formatter.format(input).unwrap();
24    /// assert_eq!(rewrite, String::from("# Header!"));
25    /// ```
26    pub fn format(self, input: &str) -> Result<String, std::fmt::Error> {
27        self.format_with_paragraph_and_html_block_formatter::<Paragraph, PreservingHtmlBlock>(input)
28    }
29
30    /// Format Markdown input with the given [`ParagraphFormatter`] `P` to
31    /// format paragraphs, and `H` to format HTML blocks.
32    ///
33    /// ```rust
34    /// # use markdown_fmt::{FormatterBuilder, Paragraph, PreservingHtmlBlock};
35    /// let builder = FormatterBuilder::default();
36    /// let formatter = builder.build();
37    /// let input = "   #  Header! ";
38    /// let rewrite = formatter
39    ///     .format_with_paragraph_and_html_block_formatter::<Paragraph, PreservingHtmlBlock>(input)
40    ///     .unwrap();
41    /// assert_eq!(rewrite, String::from("# Header!"));
42    /// ```
43    pub fn format_with_paragraph_and_html_block_formatter<P, H>(
44        self,
45        input: &str,
46    ) -> Result<String, std::fmt::Error>
47    where
48        P: ParagraphFormatter,
49        H: ParagraphFormatter,
50    {
51        // callback that will always revcover broken links
52        let mut callback = |broken_link| {
53            tracing::trace!("found boken link: {broken_link:?}");
54            Some(("".into(), "".into()))
55        };
56
57        let mut options = Options::all();
58        options.remove(Options::ENABLE_SMART_PUNCTUATION);
59
60        let parser = Parser::new_with_broken_link_callback(input, options, Some(&mut callback));
61
62        // There can't be any characters besides spaces, tabs, or newlines after the title
63        // See https://spec.commonmark.org/0.30/#link-reference-definition for the
64        // definition and https://spec.commonmark.org/0.30/#example-209 as an example.
65        //
66        // It seems that `pulldown_cmark` sometimes parses titles when it shouldn't.
67        // To work around edge cases where a paragraph starting with a quoted string might be
68        // interpreted as a link title we check that only whitespace follows the title
69        let is_false_title = |input: &str, span: Range<usize>| {
70            input[span.end..]
71                .chars()
72                .take_while(|c| *c != '\n')
73                .any(|c| !c.is_whitespace())
74        };
75
76        let reference_links = parser
77            .reference_definitions()
78            .iter()
79            .sorted_by(|(_, link_a), (_, link_b)| {
80                // We want to sort these in descending order based on the ranges
81                // This creates a stack of reference links that we can pop off of.
82                link_b.span.start.cmp(&link_a.span.start)
83            })
84            // TODO: Fix typo.
85            .map(|(link_lable, link_def)| {
86                let (dest, title, span) = (&link_def.dest, &link_def.title, &link_def.span);
87                let full_link = &input[span.clone()];
88                if title.is_some() && is_false_title(input, span.clone()) {
89                    let end = input[span.clone()]
90                        .find(dest.as_ref())
91                        .map(|idx| idx + dest.len())
92                        .unwrap_or(span.end);
93                    return (
94                        link_lable.to_string(),
95                        dest.to_string(),
96                        None,
97                        span.start..end,
98                    );
99                }
100
101                if let Some((url, title)) = links::recover_escaped_link_destination_and_title(
102                    full_link,
103                    link_lable,
104                    title.is_some(),
105                ) {
106                    (link_lable.to_string(), url, title, span.clone())
107                } else {
108                    // Couldn't recover URL from source, just use what we've been given
109                    (
110                        link_lable.to_string(),
111                        dest.to_string(),
112                        title.clone().map(|s| (s.to_string(), '"')),
113                        span.clone(),
114                    )
115                }
116            })
117            .collect::<Vec<_>>();
118
119        let iter = parser.into_offset_iter().all_loose_lists();
120
121        let fmt_state = <FormatState<_, _, P, H>>::new_with_paragraph_and_html_block_formatter(
122            input,
123            self.config,
124            self.code_block_formatter,
125            iter,
126            reference_links,
127        );
128        fmt_state.format()
129    }
130
131    /// Helper method to easily initiazlie the [MarkdownFormatter].
132    ///
133    /// This is marked as `pub(crate)` because users are expected to use the [FormatterBuilder]
134    /// When creating a [MarkdownFormatter].
135    ///
136    /// [FormatterBuilder]: crate::FormatterBuilder
137    pub(crate) fn new(code_block_formatter: CodeBlockFormatter, config: Config) -> Self {
138        Self {
139            code_block_formatter,
140            config,
141        }
142    }
143}
144
145type ReferenceLinkDefinition = (String, String, Option<(String, char)>, Range<usize>);
146
147pub(crate) struct FormatState<'i, F, I, P, H>
148where
149    I: Iterator,
150{
151    /// Raw markdown input
152    input: &'i str,
153    pub(crate) last_was_softbreak: bool,
154    /// Iterator Supplying Markdown Events
155    events: Peekable<I>,
156    rewrite_buffer: String,
157    /// Stores code that we might try to format
158    code_block_buffer: String,
159    /// Stack that keeps track of nested list markers.
160    /// Unordered list markers are one of `*`, `+`, or `-`,
161    /// while ordered lists markers start with 0-9 digits followed by a `.` or `)`.
162    // TODO(ytmimi) Add a configuration to allow incrementing ordered lists
163    // list_markers: Vec<ListMarker>,
164    /// Stack that keeps track of indentation.
165    indentation: Vec<Cow<'static, str>>,
166    /// Stack that keeps track of whether we're formatting inside of another element.
167    nested_context: Vec<Tag<'i>>,
168    /// A set of reference link definitions that will be output after formatting.
169    /// Reference style links contain 3 parts:
170    /// 1. Text to display
171    /// 2. URL
172    /// 3. (Optional) Title
173    /// ```markdown
174    /// [title]: link "optional title"
175    /// ```
176    reference_links: Vec<ReferenceLinkDefinition>,
177    /// keep track of the current setext header.
178    /// ```markdown
179    /// Header
180    /// ======
181    /// ```
182    setext_header: Option<&'i str>,
183    /// Store the fragment identifier and classes from the header start tag.
184    header_id_and_classes: Option<(Option<CowStr<'i>>, Vec<CowStr<'i>>)>,
185    /// next Start event should push indentation
186    needs_indent: bool,
187    table_state: Option<TableState<'i>>,
188    last_position: usize,
189    code_block_formatter: F,
190    trim_link_or_image_start: bool,
191    /// Handles paragraph formatting.
192    paragraph: Option<P>,
193    /// Handles HTML block formatting.
194    html_block: Option<H>,
195    /// Force write into rewrite buffer.
196    force_rewrite_buffer: bool,
197    /// Format configurations
198    config: Config,
199}
200
201/// Depnding on the formatting context there are a few different buffers where we might want to
202/// write formatted markdown events. The Write impl helps us centralize this logic.
203impl<'i, F, I, P, H> Write for FormatState<'i, F, I, P, H>
204where
205    I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
206    P: ParagraphFormatter,
207    H: ParagraphFormatter,
208{
209    fn write_str(&mut self, text: &str) -> std::fmt::Result {
210        if let Some(writer) = self.current_buffer() {
211            tracing::trace!(text, "write_str");
212            writer.write_str(text)?
213        }
214        Ok(())
215    }
216
217    fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::fmt::Result {
218        if let Some(writer) = self.current_buffer() {
219            writer.write_fmt(args)?
220        }
221        Ok(())
222    }
223}
224
225impl<'i, F, I, P, H> FormatState<'i, F, I, P, H>
226where
227    I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
228    P: ParagraphFormatter,
229    H: ParagraphFormatter,
230{
231    /// Peek at the next Markdown Event
232    fn peek(&mut self) -> Option<&Event<'i>> {
233        self.events.peek().map(|(e, _)| e)
234    }
235
236    /// Peek at the next Markdown Event and it's original position in the input
237    fn peek_with_range(&mut self) -> Option<(&Event, &Range<usize>)> {
238        self.events.peek().map(|(e, r)| (e, r))
239    }
240
241    /// Check if the next Event is an `Event::End`
242    fn is_next_end_event(&mut self) -> bool {
243        matches!(self.peek(), Some(Event::End(_)))
244    }
245
246    /// Check if we should write newlines and indentation before the next Start Event
247    fn check_needs_indent(&mut self, event: &Event<'i>) {
248        self.needs_indent = match self.peek() {
249            Some(Event::Start(_) | Event::Rule | Event::Html(_) | Event::End(TagEnd::Item)) => true,
250            Some(Event::End(TagEnd::BlockQuote)) => matches!(event, Event::End(_)),
251            Some(Event::Text(_)) => matches!(event, Event::End(_) | Event::Start(Tag::Item)),
252            _ => matches!(event, Event::Rule),
253        };
254    }
255
256    /// Check if we're formatting a fenced code block
257    fn in_fenced_code_block(&self) -> bool {
258        matches!(
259            self.nested_context.last(),
260            Some(Tag::CodeBlock(CodeBlockKind::Fenced(_)))
261        )
262    }
263
264    /// Check if we're formatting an indented code block
265    fn in_indented_code_block(&self) -> bool {
266        matches!(
267            self.nested_context.last(),
268            Some(Tag::CodeBlock(CodeBlockKind::Indented))
269        )
270    }
271
272    /// Check if we're in an HTML block.
273    fn in_html_block(&self) -> bool {
274        self.html_block.is_some()
275    }
276
277    // check if we're formatting a table header
278    fn in_table_header(&self) -> bool {
279        self.nested_context
280            .iter()
281            .rfind(|tag| **tag == Tag::TableHead)
282            .is_some()
283    }
284
285    // check if we're formatting a table row
286    fn in_table_row(&self) -> bool {
287        self.nested_context
288            .iter()
289            .rfind(|tag| **tag == Tag::TableRow)
290            .is_some()
291    }
292
293    /// Check if we're formatting a link
294    fn in_link_or_image(&self) -> bool {
295        matches!(
296            self.nested_context.last(),
297            Some(Tag::Link { .. } | Tag::Image { .. })
298        )
299    }
300
301    /// Check if we're in a "paragraph". A `Paragraph` might not necessarily be on the
302    /// nested_context stack.
303    fn in_paragraph(&self) -> bool {
304        self.paragraph.is_some()
305    }
306
307    /// Check if we're formatting in a nested context
308    fn is_nested(&self) -> bool {
309        !self.nested_context.is_empty()
310    }
311
312    /// Get the length of the indentation
313    fn indentation_len(&self) -> usize {
314        self.indentation.iter().map(|i| i.len()).sum()
315    }
316
317    /// Get an exclusive reference to the current buffer we're writing to. That could be the main
318    /// rewrite buffer, the code block buffer, the internal table state, or anything else we're
319    /// writing to while reformatting
320    fn current_buffer(&mut self) -> Option<&mut dyn std::fmt::Write> {
321        if self.force_rewrite_buffer {
322            tracing::trace!("rewrite_buffer");
323            Some(&mut self.rewrite_buffer)
324        } else if self.in_fenced_code_block() || self.in_indented_code_block() {
325            tracing::trace!("code_block_buffer");
326            Some(&mut self.code_block_buffer)
327        } else if self.in_html_block() {
328            tracing::trace!("html_block");
329            self.html_block
330                .as_mut()
331                .map(|h| h as &mut dyn std::fmt::Write)
332        } else if self.in_table_header() || self.in_table_row() {
333            tracing::trace!("table_state");
334            self.table_state
335                .as_mut()
336                .map(|s| s as &mut dyn std::fmt::Write)
337        } else if self.in_paragraph() {
338            tracing::trace!("paragraph");
339            self.paragraph
340                .as_mut()
341                .map(|p| p as &mut dyn std::fmt::Write)
342        } else {
343            tracing::trace!("rewrite_buffer");
344            Some(&mut self.rewrite_buffer)
345        }
346    }
347
348    /// Check if the current buffer we're writting to is empty
349    fn is_current_buffer_empty(&self) -> bool {
350        if self.in_fenced_code_block() || self.in_indented_code_block() {
351            self.code_block_buffer.is_empty()
352        } else if self.in_html_block() {
353            self.html_block.as_ref().is_some_and(|h| h.is_empty())
354        } else if self.in_table_header() || self.in_table_row() {
355            self.table_state.as_ref().is_some_and(|s| s.is_empty())
356        } else if self.in_paragraph() {
357            self.paragraph.as_ref().is_some_and(|p| p.is_empty())
358        } else {
359            self.rewrite_buffer.is_empty()
360        }
361    }
362
363    fn count_newlines(&self, range: &Range<usize>) -> usize {
364        if self.last_position == range.start {
365            return 0;
366        }
367
368        let snippet = if self.last_position < range.start {
369            // between two markdown evernts
370            &self.input[self.last_position..range.start]
371        } else {
372            // likely in some nested context
373            self.input[self.last_position..range.end].trim_end_matches('\n')
374        };
375
376        snippet.bytes().filter(|b| *b == b'\n').count()
377    }
378
379    fn write_indentation(&mut self, trim_trailing_whiltespace: bool) -> std::fmt::Result {
380        let last_non_complete_whitespace_indent = self
381            .indentation
382            .iter()
383            .rposition(|indent| !indent.chars().all(char::is_whitespace));
384
385        let position = if trim_trailing_whiltespace {
386            let Some(position) = last_non_complete_whitespace_indent else {
387                // All indents are just whitespace. We don't want to push blank lines
388                return Ok(());
389            };
390            position
391        } else {
392            self.indentation.len()
393        };
394
395        // Temporarily take indentation to work around the borrow checker
396        let indentation = std::mem::take(&mut self.indentation);
397
398        for (i, indent) in indentation.iter().take(position + 1).enumerate() {
399            let is_last = i == position;
400
401            if is_last && trim_trailing_whiltespace {
402                self.write_str(indent.trim())?;
403            } else {
404                self.write_str(indent)?;
405            }
406        }
407        // Put the indentation back!
408        self.indentation = indentation;
409        Ok(())
410    }
411
412    fn write_newlines(&mut self, max_newlines: usize) -> std::fmt::Result {
413        self.write_newlines_inner(max_newlines, false)
414    }
415
416    fn write_newlines_no_trailing_whitespace(&mut self, max_newlines: usize) -> std::fmt::Result {
417        self.write_newlines_inner(max_newlines, true)
418    }
419
420    fn write_newlines_inner(
421        &mut self,
422        max_newlines: usize,
423        always_trim_trailing_whitespace: bool,
424    ) -> std::fmt::Result {
425        if self.is_current_buffer_empty() {
426            return Ok(());
427        }
428        let newlines = self
429            .rewrite_buffer
430            .chars()
431            .rev()
432            .take_while(|c| *c == '\n')
433            .count();
434
435        let nested = self.is_nested();
436        let newlines_to_write = max_newlines.saturating_sub(newlines);
437        let next_is_end_event = self.is_next_end_event();
438        tracing::trace!(newlines, nested, newlines_to_write, next_is_end_event);
439
440        for i in 0..newlines_to_write {
441            let is_last = i == newlines_to_write - 1;
442
443            writeln!(self)?;
444
445            if nested {
446                self.write_indentation(!is_last || always_trim_trailing_whitespace)?;
447            }
448        }
449        if !nested {
450            self.write_indentation(next_is_end_event || always_trim_trailing_whitespace)?;
451        }
452        Ok(())
453    }
454
455    fn write_reference_link_definition_inner(
456        &mut self,
457        label: &str,
458        dest: &str,
459        title: Option<&(String, char)>,
460    ) -> std::fmt::Result {
461        // empty links can be specified with <>
462        let dest = links::format_link_url(dest, true);
463        self.write_newlines(1)?;
464        if let Some((title, quote)) = title {
465            write!(self, r#"[{}]: {dest} {quote}{title}{quote}"#, label.trim())?;
466        } else {
467            write!(self, "[{}]: {dest}", label.trim())?;
468        }
469        Ok(())
470    }
471
472    fn rewrite_reference_link_definitions(&mut self, range: &Range<usize>) -> std::fmt::Result {
473        if self.reference_links.is_empty() {
474            return Ok(());
475        }
476        // use std::mem::take to work around the borrow checker
477        let mut reference_links = std::mem::take(&mut self.reference_links);
478
479        loop {
480            match reference_links.last() {
481                Some((_, _, _, link_range)) if link_range.start > range.start => {
482                    // The reference link on the top of the stack comes further along in the file
483                    break;
484                }
485                None => break,
486                _ => {}
487            }
488
489            let (label, dest, title, link_range) = reference_links.pop().expect("we have a value");
490            let newlines = self.count_newlines(&link_range);
491            self.write_newlines(newlines)?;
492            self.write_reference_link_definition_inner(&label, &dest, title.as_ref())?;
493            self.last_position = link_range.end;
494            self.needs_indent = true;
495        }
496
497        // put the reference_links back
498        self.reference_links = reference_links;
499        Ok(())
500    }
501
502    /// Write out reference links at the end of the file
503    fn rewrite_final_reference_links(mut self) -> Result<String, std::fmt::Error> {
504        // use std::mem::take to work around the borrow checker
505        let reference_links = std::mem::take(&mut self.reference_links);
506
507        // need to iterate in reverse because reference_links is a stack
508        for (label, dest, title, range) in reference_links.into_iter().rev() {
509            let newlines = self.count_newlines(&range);
510            self.write_newlines(newlines)?;
511
512            // empty links can be specified with <>
513            self.write_reference_link_definition_inner(&label, &dest, title.as_ref())?;
514            self.last_position = range.end
515        }
516        Ok(self.rewrite_buffer)
517    }
518
519    fn join_with_indentation(
520        &mut self,
521        buffer: &str,
522        start_with_indentation: bool,
523    ) -> std::fmt::Result {
524        self.force_rewrite_buffer = true;
525        let mut lines = buffer.trim_end().lines().peekable();
526        while let Some(line) = lines.next() {
527            let is_last = lines.peek().is_none();
528            let is_next_empty = lines
529                .peek()
530                .map(|l| l.trim().is_empty())
531                .unwrap_or_default();
532
533            if start_with_indentation {
534                self.write_indentation(line.trim().is_empty())?;
535            }
536
537            if !line.trim().is_empty() {
538                self.write_str(line)?;
539            }
540
541            if !is_last {
542                writeln!(self)?;
543            }
544
545            if !is_last && !start_with_indentation {
546                self.write_indentation(is_next_empty)?;
547            }
548        }
549        self.force_rewrite_buffer = false;
550        Ok(())
551    }
552}
553
554impl<'i, F, I, P, H> FormatState<'i, F, I, P, H>
555where
556    I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
557    P: ParagraphFormatter,
558{
559}
560
561#[allow(dead_code)] // For testing.
562impl<'i, F, I> FormatState<'i, F, I, Paragraph, PreservingHtmlBlock>
563where
564    F: Fn(&str, String) -> String,
565    I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
566{
567    pub(crate) fn new(
568        input: &'i str,
569        config: Config,
570        code_block_formatter: F,
571        iter: I,
572        reference_links: Vec<ReferenceLinkDefinition>,
573    ) -> Self {
574        Self::new_with_paragraph_and_html_block_formatter(
575            input,
576            config,
577            code_block_formatter,
578            iter,
579            reference_links,
580        )
581    }
582}
583
584impl<'i, F, I, P, H> FormatState<'i, F, I, P, H>
585where
586    F: Fn(&str, String) -> String,
587    I: Iterator<Item = (Event<'i>, std::ops::Range<usize>)>,
588    P: ParagraphFormatter,
589    H: ParagraphFormatter,
590{
591    pub(crate) fn new_with_paragraph_and_html_block_formatter(
592        input: &'i str,
593        config: Config,
594        code_block_formatter: F,
595        iter: I,
596        reference_links: Vec<ReferenceLinkDefinition>,
597    ) -> Self {
598        Self {
599            input,
600            last_was_softbreak: false,
601            events: iter.peekable(),
602            rewrite_buffer: String::with_capacity(input.len() * 2),
603            code_block_buffer: String::with_capacity(256),
604            // TODO(ytmimi) Add a configuration to allow incrementing ordered lists
605            // list_markers: vec![],
606            indentation: vec![],
607            nested_context: vec![],
608            reference_links,
609            setext_header: None,
610            header_id_and_classes: None,
611            needs_indent: false,
612            table_state: None,
613            last_position: 0,
614            code_block_formatter,
615            trim_link_or_image_start: false,
616            paragraph: None,
617            html_block: None,
618            force_rewrite_buffer: false,
619            config,
620        }
621    }
622
623    fn format_code_buffer(&mut self, info_string: Option<&str>) -> String {
624        // use std::mem::take to work around the borrow checker
625        let code_block_buffer = std::mem::take(&mut self.code_block_buffer);
626
627        let Some(info_string) = info_string else {
628            // An indented code block won't have an info_string
629            return code_block_buffer;
630        };
631
632        // Call the code_block_formatter fn
633        (self.code_block_formatter)(info_string, code_block_buffer)
634    }
635
636    fn write_code_block_buffer(&mut self, info_string: Option<&str>) -> std::fmt::Result {
637        let code = self.format_code_buffer(info_string);
638
639        if code.trim().is_empty() && info_string.is_some() {
640            // The code fence is empty, and a newline should already ahve been added
641            // when pushing the opening code fence, so just return.
642            return Ok(());
643        }
644
645        self.join_with_indentation(&code, info_string.is_some())?;
646
647        if info_string.is_some() {
648            // In preparation for the closing code fence write a newline.
649            writeln!(self)?
650        }
651
652        Ok(())
653    }
654
655    fn formatter_width(&self) -> Option<usize> {
656        self.config
657            .max_width
658            .map(|w| w.saturating_sub(self.indentation_len()))
659    }
660
661    /// The main entry point for markdown formatting.
662    pub fn format(mut self) -> Result<String, std::fmt::Error> {
663        while let Some((event, range)) = self.events.next() {
664            tracing::debug!(?event, ?range);
665            let mut last_position = self.input[..range.end]
666                .bytes()
667                .rposition(|b| !b.is_ascii_whitespace())
668                .unwrap_or(0);
669
670            match event {
671                Event::Start(tag) => {
672                    self.rewrite_reference_link_definitions(&range)?;
673                    last_position = range.start;
674                    self.start_tag(tag.clone(), range)?;
675                }
676                Event::End(ref tag) => {
677                    self.end_tag(*tag, range)?;
678                    self.check_needs_indent(&event);
679                }
680                // TODO: Distinguish math.
681                Event::Text(ref parsed_text)
682                | Event::InlineMath(ref parsed_text)
683                | Event::DisplayMath(ref parsed_text) => {
684                    last_position = range.end;
685                    let starts_with_escape = self.input[..range.start].ends_with('\\');
686                    let newlines = self.count_newlines(&range);
687                    let text_from_source = &self.input[range];
688                    let mut text = if text_from_source.is_empty() {
689                        // This seems to happen when the parsed text is whitespace only.
690                        // To preserve leading whitespace use the parsed text instead.
691                        parsed_text.as_ref()
692                    } else {
693                        text_from_source
694                    };
695
696                    if self.in_html_block() {
697                        text = text.trim_start_matches(' ');
698                    }
699
700                    if self.in_link_or_image() && self.trim_link_or_image_start {
701                        // Trim leading whitespace from reference links or images
702                        text = text.trim_start();
703                        // Make sure we only trim leading whitespace once
704                        self.trim_link_or_image_start = false
705                    }
706
707                    if matches!(
708                        self.peek(),
709                        Some(Event::End(TagEnd::Link { .. } | TagEnd::Image { .. }))
710                    ) {
711                        text = text.trim_end();
712                    }
713
714                    if self.needs_indent {
715                        self.write_newlines(newlines)?;
716                        self.needs_indent = false;
717                    }
718
719                    if starts_with_escape || self.needs_escape(text) {
720                        // recover escape characters
721                        write!(self, "\\{text}")?;
722                    } else {
723                        write!(self, "{text}")?;
724                    }
725                    self.check_needs_indent(&event);
726                }
727                Event::Code(_) => {
728                    write!(self, "{}", &self.input[range])?;
729                }
730                Event::SoftBreak => {
731                    last_position = range.end;
732                    if self.in_link_or_image() {
733                        let next_is_end = matches!(
734                            self.peek(),
735                            Some(Event::End(TagEnd::Link { .. } | TagEnd::Image { .. }))
736                        );
737                        if self.trim_link_or_image_start || next_is_end {
738                            self.trim_link_or_image_start = false
739                        } else {
740                            write!(self, " ")?;
741                        }
742                    } else {
743                        write!(self, "{}", &self.input[range])?;
744
745                        // paraphraphs write their indentation after reformatting the text
746                        if !self.in_paragraph() {
747                            self.write_indentation(false)?;
748                        }
749
750                        self.last_was_softbreak = true;
751                    }
752                }
753                Event::HardBreak => {
754                    write!(self, "{}", &self.input[range])?;
755                }
756                Event::Html(_) => {
757                    // NOTE: This limitation is because Pulldown-CMark
758                    // incorrectly include spaces before HTML.
759                    let html = &self.input[range].trim_start_matches(' ');
760                    write!(self, "{}", html)?; // Write HTML as is.
761                    self.check_needs_indent(&event);
762                }
763                Event::InlineHtml(_) => {
764                    let newlines = self.count_newlines(&range);
765                    if self.needs_indent {
766                        self.write_newlines(newlines)?;
767                    }
768                    write!(self, "{}", &self.input[range].trim_end_matches('\n'))?;
769                    self.check_needs_indent(&event);
770                }
771                Event::Rule => {
772                    let newlines = self.count_newlines(&range);
773                    self.write_newlines(newlines)?;
774                    write!(self, "{}", &self.input[range])?;
775                    self.check_needs_indent(&event)
776                }
777                Event::FootnoteReference(text) => {
778                    write!(self, "[^{text}]")?;
779                }
780                Event::TaskListMarker(done) => {
781                    if done {
782                        write!(self, "[x] ")?;
783                    } else {
784                        write!(self, "[ ] ")?;
785                    }
786                }
787            }
788            self.last_position = last_position
789        }
790        debug_assert!(self.nested_context.is_empty());
791        let trailing_newline = self.input.ends_with('\n');
792        self.rewrite_final_reference_links().map(|mut output| {
793            if trailing_newline {
794                output.push('\n');
795            }
796            output
797        })
798    }
799
800    fn start_tag(&mut self, tag: Tag<'i>, range: Range<usize>) -> std::fmt::Result {
801        match tag {
802            Tag::Paragraph => {
803                if self.needs_indent {
804                    let newlines = self.count_newlines(&range);
805                    self.write_newlines(newlines)?;
806                    self.needs_indent = false;
807                }
808                self.nested_context.push(tag);
809                let capacity = (range.end - range.start) * 2;
810                let width = self.formatter_width();
811                self.paragraph = Some(P::new(width, capacity));
812            }
813            Tag::Heading {
814                level, id, classes, ..
815            } => {
816                self.header_id_and_classes = Some((id, classes));
817                if self.needs_indent {
818                    let newlines = self.count_newlines(&range);
819                    self.write_newlines(newlines)?;
820                    self.needs_indent = false;
821                }
822                let full_header = self.input[range].trim();
823
824                if full_header.contains('\n') && full_header.ends_with(['=', '-']) {
825                    // support for alternative syntax for H1 and H2
826                    // <https://www.markdownguide.org/basic-syntax/#alternate-syntax>
827                    let header_marker = full_header.split('\n').last().unwrap().trim();
828                    self.setext_header.replace(header_marker);
829                    // setext header are handled in `end_tag`
830                    return Ok(());
831                }
832
833                let header = match level {
834                    HeadingLevel::H1 => "# ",
835                    HeadingLevel::H2 => "## ",
836                    HeadingLevel::H3 => "### ",
837                    HeadingLevel::H4 => "#### ",
838                    HeadingLevel::H5 => "##### ",
839                    HeadingLevel::H6 => "###### ",
840                };
841
842                let empty_header = full_header
843                    .trim_start_matches(header)
844                    .trim_end_matches(|c: char| c.is_whitespace() || matches!(c, '#' | '\\'))
845                    .is_empty();
846
847                if empty_header {
848                    write!(self, "{}", header.trim())?;
849                } else {
850                    write!(self, "{header}")?;
851                }
852            }
853            Tag::BlockQuote(_) => {
854                // Just in case we're starting a new block quote in a nested context where
855                // We alternate indentation levels we want to remove trailing whitespace
856                // from the blockquote that we're about to push on top of
857                if let Some(indent) = self.indentation.last_mut() {
858                    if indent == "> " {
859                        *indent = ">".into()
860                    }
861                }
862
863                let newlines = self.count_newlines(&range);
864                if self.needs_indent {
865                    self.write_newlines(newlines)?;
866                    self.needs_indent = false;
867                }
868
869                self.nested_context.push(tag);
870
871                match self.peek_with_range().map(|(e, r)| (e.clone(), r.clone())) {
872                    Some((Event::End(TagEnd::BlockQuote), _)) => {
873                        // The next event is `End(BlockQuote)` so the current blockquote is empty!
874                        write!(self, ">")?;
875                        self.indentation.push(">".into());
876
877                        let snippet = &self.input[range].trim_end();
878                        let newlines = snippet.bytes().filter(|b| matches!(b, b'\n')).count();
879                        self.write_newlines(newlines)?;
880                    }
881                    Some((Event::Start(Tag::BlockQuote(_)), next_range)) => {
882                        // The next event is `Start(BlockQuote) so we're adding another level
883                        // of indentation.
884                        write!(self, ">")?;
885                        self.indentation.push(">".into());
886
887                        // Now add any missing newlines for empty block quotes between
888                        // the current start and the next start
889                        let snippet = &self.input[range.start..next_range.start];
890                        let newlines = snippet.bytes().filter(|b| matches!(b, b'\n')).count();
891                        self.write_newlines(newlines)?;
892                    }
893                    Some((_, next_range)) => {
894                        // Now add any missing newlines for empty block quotes between
895                        // the current start and the next start
896                        let snippet = &self.input[range.start..next_range.start];
897                        let newlines = snippet.bytes().filter(|b| matches!(b, b'\n')).count();
898
899                        self.indentation.push("> ".into());
900                        if newlines > 0 {
901                            write!(self, ">")?;
902                            self.write_newlines(newlines)?;
903                        } else {
904                            write!(self, "> ")?;
905                        }
906                    }
907                    None => {
908                        // Peeking at the next event should always return `Some()` for start events
909                        unreachable!("At the very least we'd expect an `End(BlockQuote)` event.");
910                    }
911                }
912            }
913            Tag::CodeBlock(ref kind) => {
914                let newlines = self.count_newlines(&range);
915                if self.needs_indent && newlines > 0 {
916                    self.write_newlines(newlines)?;
917                    self.needs_indent = false;
918                }
919                match kind {
920                    CodeBlockKind::Fenced(info_string) => {
921                        rewrite_marker(self.input, &range, self)?;
922
923                        if info_string.is_empty() {
924                            writeln!(self)?;
925                            self.nested_context.push(tag);
926                            return Ok(());
927                        }
928
929                        let starts_with_space = self.input[range.clone()]
930                            .trim_start_matches(['`', '~'])
931                            .starts_with(char::is_whitespace);
932
933                        let info_string = self.input[range]
934                            .lines()
935                            .next()
936                            .unwrap_or_else(|| info_string.as_ref())
937                            .trim_start_matches(['`', '~'])
938                            .trim();
939
940                        if starts_with_space {
941                            writeln!(self, " {info_string}")?;
942                        } else {
943                            writeln!(self, "{info_string}")?;
944                        }
945                    }
946                    CodeBlockKind::Indented => {
947                        // TODO(ytmimi) support tab as an indent
948                        let indentation = "    ";
949
950                        if !matches!(self.peek(), Some(Event::End(TagEnd::CodeBlock))) {
951                            // Only write indentation if this isn't an empty indented code block
952                            self.write_str(indentation)?;
953                        }
954
955                        self.indentation.push(indentation.into());
956                    }
957                }
958                self.nested_context.push(tag);
959            }
960            Tag::List(_) => {
961                if self.needs_indent {
962                    let newlines = self.count_newlines(&range);
963                    self.write_newlines(newlines)?;
964                    self.needs_indent = false;
965                }
966
967                // TODO(ytmimi) Add a configuration to allow incrementing ordered lists
968                // let list_marker = ListMarker::from_str(&self.input[range])
969                //    .expect("Should be able to parse a list marker");
970                // self.list_markers.push(list_marker);
971                self.nested_context.push(tag);
972            }
973            Tag::Item => {
974                let newlines = self.count_newlines(&range);
975                if self.needs_indent && newlines > 0 {
976                    self.write_newlines(newlines)?;
977                }
978
979                let empty_list_item = match self.events.peek() {
980                    Some((Event::End(TagEnd::Item), _)) => true,
981                    Some((_, next_range)) => {
982                        let snippet = &self.input[range.start..next_range.start];
983                        // It's an empty list if there are newlines between the list marker
984                        // and the next event. For example,
985                        //
986                        // ```markdown
987                        // -
988                        //   foo
989                        // ```
990                        snippet.bytes().filter(|b| matches!(b, b'\n')).count() > 0
991                    }
992                    None => false,
993                };
994
995                // We need to push a newline and indentation before the next event if
996                // this is an empty list item
997                self.needs_indent = empty_list_item;
998
999                let list_marker = self
1000                    .config
1001                    .list_marker(&self.input[range.clone()])
1002                    .expect("Should be able to parse a list marker");
1003                tracing::debug!(?list_marker, source = &self.input[range]);
1004                // TODO(ytmimi) Add a configuration to allow incrementing ordered lists
1005                // Take list_marker so we can use `write!(self, ...)`
1006                // let mut list_marker = self
1007                //     .list_markers
1008                //     .pop()
1009                //     .expect("can't have list item without marker");
1010                let marker_char = list_marker.marker_char();
1011                match &list_marker {
1012                    ListMarker::Ordered { number, .. } if empty_list_item => {
1013                        let zero_padding = list_marker.zero_padding();
1014                        write!(self, "{zero_padding}{number}{marker_char}")?;
1015                    }
1016                    ListMarker::Ordered { number, .. } => {
1017                        let zero_padding = list_marker.zero_padding();
1018                        write!(self, "{zero_padding}{number}{marker_char} ")?;
1019                    }
1020                    ListMarker::Unordered(_) if empty_list_item => {
1021                        write!(self, "{marker_char}")?;
1022                    }
1023                    ListMarker::Unordered(_) => {
1024                        write!(self, "{marker_char} ")?;
1025                    }
1026                }
1027
1028                self.nested_context.push(tag);
1029                // Increment the list marker in case this is a ordered list and
1030                // swap the list marker we took earlier
1031                self.indentation.push(
1032                    self.config
1033                        .fixed_indentation
1034                        .clone()
1035                        .unwrap_or_else(|| list_marker.indentation()),
1036                );
1037                // TODO(ytmimi) Add a configuration to allow incrementing ordered lists
1038                // list_marker.increment_count();
1039                // self.list_markers.push(list_marker)
1040            }
1041            Tag::FootnoteDefinition(label) => {
1042                write!(self, "[^{label}]: ")?;
1043            }
1044            Tag::Emphasis => {
1045                rewrite_marker_with_limit(self.input, &range, self, Some(1))?;
1046            }
1047            Tag::Strong => {
1048                rewrite_marker_with_limit(self.input, &range, self, Some(2))?;
1049            }
1050            Tag::Strikethrough => {
1051                rewrite_marker(self.input, &range, self)?;
1052            }
1053            Tag::Link { link_type, .. } => {
1054                let newlines = self.count_newlines(&range);
1055                if self.needs_indent && newlines > 0 {
1056                    self.write_newlines(newlines)?;
1057                    self.needs_indent = false;
1058                }
1059
1060                let email_or_auto = matches!(link_type, LinkType::Email | LinkType::Autolink);
1061                let opener = if email_or_auto { "<" } else { "[" };
1062                self.write_str(opener)?;
1063                self.nested_context.push(tag);
1064
1065                if matches!(self.peek(), Some(Event::Text(_) | Event::SoftBreak)) {
1066                    self.trim_link_or_image_start = true
1067                }
1068            }
1069            Tag::Image { .. } => {
1070                let newlines = self.count_newlines(&range);
1071                if self.needs_indent && newlines > 0 {
1072                    self.write_newlines(newlines)?;
1073                    self.needs_indent = false;
1074                }
1075
1076                write!(self, "![")?;
1077                self.nested_context.push(tag);
1078
1079                if matches!(self.peek(), Some(Event::Text(_) | Event::SoftBreak)) {
1080                    self.trim_link_or_image_start = true
1081                }
1082            }
1083            Tag::Table(ref alignment) => {
1084                if self.needs_indent {
1085                    let newlines = self.count_newlines(&range);
1086                    self.write_newlines(newlines)?;
1087                    self.needs_indent = false;
1088                }
1089                self.table_state.replace(TableState::new(alignment.clone()));
1090                write!(self, "|")?;
1091                self.indentation.push("|".into());
1092                self.nested_context.push(tag);
1093            }
1094            Tag::TableHead => {
1095                self.nested_context.push(tag);
1096            }
1097            Tag::TableRow => {
1098                self.nested_context.push(tag);
1099                if let Some(state) = self.table_state.as_mut() {
1100                    state.push_row()
1101                }
1102            }
1103            Tag::TableCell => {
1104                if !matches!(self.peek(), Some(Event::End(TagEnd::TableCell))) {
1105                    return Ok(());
1106                }
1107
1108                if let Some(state) = self.table_state.as_mut() {
1109                    state.write(String::new().into());
1110                }
1111            }
1112            Tag::HtmlBlock => {
1113                if matches!(self.events.peek(), Some((Event::End(TagEnd::Paragraph), _))) {
1114                    tracing::debug!("HTML block start before paragraph end.");
1115                    // NOTE: Pulldown-CMark has a bug where it starts an HTML
1116                    // block before ending a paragraph.
1117                    // In this case, we consume the paragraph first.
1118                    let (_, range) = self.events.next().expect("We peeked.");
1119                    self.end_tag(TagEnd::Paragraph, range)?;
1120                }
1121
1122                let capacity = (range.end - range.start) * 2;
1123                let width = self.formatter_width();
1124                let mut html_block = H::new(width, capacity);
1125                let newlines = self.count_newlines(&range);
1126                tracing::trace!(newlines);
1127                for _ in 0..newlines {
1128                    html_block.write_char('\n')?;
1129                }
1130                self.html_block = Some(html_block);
1131            }
1132            Tag::MetadataBlock(kind) => {
1133                self.write_metadata_block_separator(&kind, range)?;
1134            }
1135        }
1136        Ok(())
1137    }
1138
1139    fn end_tag(&mut self, tag: TagEnd, range: Range<usize>) -> std::fmt::Result {
1140        match tag {
1141            TagEnd::Paragraph => {
1142                let popped_tag = self.nested_context.pop();
1143                debug_assert_eq!(popped_tag, Some(Tag::Paragraph));
1144
1145                if let Some(p) = self.paragraph.take() {
1146                    self.join_with_indentation(&p.into_buffer(), false)?;
1147                }
1148            }
1149            TagEnd::Heading(_) => {
1150                let (fragment_identifier, classes) = self
1151                    .header_id_and_classes
1152                    .take()
1153                    .expect("Should have pushed a header tag");
1154                match (fragment_identifier, classes.is_empty()) {
1155                    (Some(id), false) => {
1156                        let classes = rewirte_header_classes(classes)?;
1157                        write!(self, " {{#{id}{classes}}}")?;
1158                    }
1159                    (Some(id), true) => {
1160                        write!(self, " {{#{id}}}")?;
1161                    }
1162                    (None, false) => {
1163                        let classes = rewirte_header_classes(classes)?;
1164                        write!(self, " {{{}}}", classes.trim())?;
1165                    }
1166                    (None, true) => {}
1167                }
1168
1169                if let Some(marker) = self.setext_header.take() {
1170                    self.write_newlines(1)?;
1171                    write!(self, "{marker}")?;
1172                }
1173            }
1174            TagEnd::BlockQuote => {
1175                let newlines = self.count_newlines(&range);
1176                if self.needs_indent && newlines > 0 {
1177                    // Recover empty block quote lines
1178                    if let Some(last) = self.indentation.last_mut() {
1179                        // Avoid trailing whitespace by replacing the last indentation with '>'
1180                        *last = ">".into()
1181                    }
1182                    self.write_newlines(newlines)?;
1183                }
1184                let popped_tag = self.nested_context.pop();
1185                debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1186
1187                let popped_indentation = self
1188                    .indentation
1189                    .pop()
1190                    .expect("we pushed a blockquote marker in start_tag");
1191                if let Some(indentation) = self.indentation.last_mut() {
1192                    if indentation == ">" {
1193                        *indentation = popped_indentation
1194                    }
1195                }
1196            }
1197            TagEnd::CodeBlock => {
1198                let popped_tag = self.nested_context.pop();
1199                let Some(Tag::CodeBlock(kind)) = &popped_tag else {
1200                    unreachable!("Should have pushed a code block start tag");
1201                };
1202
1203                match kind {
1204                    CodeBlockKind::Fenced(info_string) => {
1205                        self.write_code_block_buffer(Some(info_string))?;
1206                        // write closing code fence
1207                        self.write_indentation(false)?;
1208                        rewrite_marker(self.input, &range, self)?;
1209                    }
1210                    CodeBlockKind::Indented => {
1211                        // Maybe we'll consider formatting indented code blocks??
1212                        self.write_code_block_buffer(None)?;
1213
1214                        let popped_indentation = self
1215                            .indentation
1216                            .pop()
1217                            .expect("we added 4 spaces in start_tag");
1218                        debug_assert_eq!(popped_indentation, "    ");
1219                    }
1220                }
1221            }
1222            TagEnd::List(_) => {
1223                let popped_tag = self.nested_context.pop();
1224                debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1225                // TODO(ytmimi) Add a configuration to allow incrementing ordered lists
1226                // self.list_markers.pop();
1227
1228                // To prevent the next code block from being interpreted as a list we'll add an
1229                // HTML comment See https://spec.commonmark.org/0.30/#example-308, which states:
1230                //
1231                //     To separate consecutive lists of the same type, or to separate a list from an
1232                //     indented code block that would otherwise be parsed as a subparagraph of the
1233                //     final list item, you can insert a blank HTML comment
1234                if let Some(Event::Start(Tag::CodeBlock(CodeBlockKind::Indented))) = self.peek() {
1235                    self.write_newlines(1)?;
1236                    writeln!(self, "<!-- Don't absorb code block into list -->")?;
1237                    write!(self, "<!-- Consider a fenced code block instead -->")?;
1238                };
1239            }
1240            TagEnd::Item => {
1241                let newlines = self.count_newlines(&range);
1242                if self.needs_indent && newlines > 0 {
1243                    self.write_newlines_no_trailing_whitespace(newlines)?;
1244                }
1245                let popped_tag = self.nested_context.pop();
1246                debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1247                let popped_indentation = self.indentation.pop();
1248                debug_assert!(popped_indentation.is_some());
1249
1250                // if the next event is a Start(Item), then we need to set needs_indent
1251                self.needs_indent = matches!(self.peek(), Some(Event::Start(Tag::Item)));
1252            }
1253            TagEnd::FootnoteDefinition => {}
1254            TagEnd::Emphasis => {
1255                rewrite_marker_with_limit(self.input, &range, self, Some(1))?;
1256            }
1257            TagEnd::Strong => {
1258                rewrite_marker_with_limit(self.input, &range, self, Some(2))?;
1259            }
1260            TagEnd::Strikethrough => {
1261                rewrite_marker(self.input, &range, self)?;
1262            }
1263            TagEnd::Link | TagEnd::Image => {
1264                let popped_tag = self
1265                    .nested_context
1266                    .pop()
1267                    .expect("Should have pushed a start tag.");
1268                debug_assert_eq!(popped_tag.to_end(), tag);
1269                let (link_type, url, title) = match popped_tag {
1270                    Tag::Link {
1271                        link_type,
1272                        dest_url,
1273                        title,
1274                        ..
1275                    }
1276                    | Tag::Image {
1277                        link_type,
1278                        dest_url,
1279                        title,
1280                        ..
1281                    } => (link_type, dest_url, title),
1282                    _ => unreachable!("Should reach the end of a corresponding tag."),
1283                };
1284
1285                let text = &self.input[range.clone()];
1286
1287                match link_type {
1288                    LinkType::Inline => {
1289                        if let Some((source_url, title_and_quote)) =
1290                            crate::links::find_inline_url_and_title(text)
1291                        {
1292                            self.write_inline_link(&source_url, title_and_quote)?;
1293                        } else {
1294                            let title = if title.is_empty() {
1295                                None
1296                            } else {
1297                                Some((title, '"'))
1298                            };
1299                            self.write_inline_link(&url, title)?;
1300                        }
1301                    }
1302                    LinkType::Reference | LinkType::ReferenceUnknown => {
1303                        let label = crate::links::find_reference_link_label(text);
1304                        write!(self, "][{label}]")?;
1305                    }
1306                    LinkType::Collapsed | LinkType::CollapsedUnknown => write!(self, "][]")?,
1307                    LinkType::Shortcut | LinkType::ShortcutUnknown => write!(self, "]")?,
1308                    LinkType::Autolink | LinkType::Email => write!(self, ">")?,
1309                }
1310            }
1311            TagEnd::Table => {
1312                let popped_tag = self.nested_context.pop();
1313                debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1314                if let Some(state) = self.table_state.take() {
1315                    self.join_with_indentation(&state.format()?, false)?;
1316                }
1317                let popped_indentation = self.indentation.pop().expect("we added `|` in start_tag");
1318                debug_assert_eq!(popped_indentation, "|");
1319            }
1320            TagEnd::TableRow | TagEnd::TableHead => {
1321                let popped_tag = self.nested_context.pop();
1322                debug_assert_eq!(popped_tag.unwrap().to_end(), tag);
1323            }
1324            TagEnd::TableCell => {
1325                if let Some(state) = self.table_state.as_mut() {
1326                    // We finished formatting this cell. Setup the state to format the next cell
1327                    state.increment_col_index()
1328                }
1329            }
1330            TagEnd::HtmlBlock => {
1331                let newlines = self.count_newlines(&range);
1332                self.write_newlines(newlines)?;
1333                if let Some(h) = self.html_block.take() {
1334                    self.join_with_indentation(&h.into_buffer(), false)?;
1335                }
1336            }
1337            TagEnd::MetadataBlock(kind) => {
1338                self.write_metadata_block_separator(&kind, range)?;
1339            }
1340        }
1341        Ok(())
1342    }
1343
1344    fn write_metadata_block_separator(
1345        &mut self,
1346        kind: &MetadataBlockKind,
1347        range: Range<usize>,
1348    ) -> std::fmt::Result {
1349        let newlines = self.count_newlines(&range);
1350        self.write_newlines(newlines)?;
1351        let marker = match kind {
1352            MetadataBlockKind::YamlStyle => "---",
1353            MetadataBlockKind::PlusesStyle => "+++",
1354        };
1355        writeln!(self, "{marker}")
1356    }
1357}
1358
1359/// Find some marker that denotes the start of a markdown construct.
1360/// for example, `**` for bold or `_` for italics.
1361fn find_marker<'i, P>(input: &'i str, range: &Range<usize>, predicate: P) -> &'i str
1362where
1363    P: FnMut(char) -> bool,
1364{
1365    let end = if let Some(position) = input[range.start..].chars().position(predicate) {
1366        range.start + position
1367    } else {
1368        range.end
1369    };
1370    &input[range.start..end]
1371}
1372
1373/// Find some marker, but limit the size
1374fn rewrite_marker_with_limit<W: std::fmt::Write>(
1375    input: &str,
1376    range: &Range<usize>,
1377    writer: &mut W,
1378    size_limit: Option<usize>,
1379) -> std::fmt::Result {
1380    let marker_char = input[range.start..].chars().next().unwrap();
1381    let marker = find_marker(input, range, |c| c != marker_char);
1382    if let Some(mark_max_width) = size_limit {
1383        write!(writer, "{}", &marker[..mark_max_width])
1384    } else {
1385        write!(writer, "{marker}")
1386    }
1387}
1388
1389/// Finds a marker in the source text and writes it to the buffer
1390fn rewrite_marker<W: std::fmt::Write>(
1391    input: &str,
1392    range: &Range<usize>,
1393    writer: &mut W,
1394) -> std::fmt::Result {
1395    rewrite_marker_with_limit(input, range, writer, None)
1396}
1397
1398/// Rewrite a list of h1, h2, h3, h4, h5, h6 classes
1399fn rewirte_header_classes(classes: Vec<CowStr>) -> Result<String, std::fmt::Error> {
1400    let item_len = classes.iter().map(|i| i.len()).sum::<usize>();
1401    let capacity = item_len + classes.len() * 2;
1402    let mut result = String::with_capacity(capacity);
1403    for class in classes {
1404        write!(result, " .{class}")?;
1405    }
1406    Ok(result)
1407}