Skip to content

More compatible admonition parsing #3066

@ssokolow

Description

@ssokolow

Problem

Markdown prettifiers which don't support mdBook's implementation of GFM-style admonitions will mangle them by removing the newline after the admonition header unless you use two newlines... which mdBook doesn't recognize as an admonition.

This markup...

> [!NOTE]
> Lorem ipsum

Note

Lorem ipsum

...gets reformatted into:

> [!NOTE] Lorem ipsum

[!NOTE] Lorem ipsum

Even if it's not formatted automatically, this is a huge footgun, since any contributor might trigger that change without noticing.

Proposed Solution

Support this relaxation of the syntax which passes through the prettifier unaltered and is consistent with what prettifiers generally do to ATX headings without a blank line after them:

> [!NOTE]
>
> Lorem ipsum

Note

Lorem ipsum

(Note that GitHub itself accepts this syntax.)

pulldown-cmark already WONTFIX'd my request for this change, so I'd like to offer this filter I wrote for my own pulldown-cmark-based Markdown processor:

Rust Source Code: Iterator Adapter
/// An iterator adapter to collapse together the incorrectly split BlockQuote elements produced by
/// pulldown-cmark when using blockquote type tags/admonitions with an extra blank line to keep
/// them from being mangled by Markdown formatters which aren't aware of that extension.
pub struct FixPulldownCmarkIssue890<'a, T: Iterator> {
    inner: std::iter::Peekable<T>,
    buffer: Vec<Option<Event<'a>>>,
}
impl<'a, T> FixPulldownCmarkIssue890<'a, T>
where
    T: Iterator<Item = Event<'a>>,
{
    pub fn new(iterator: T) -> Self {
        Self { inner: iterator.peekable(), buffer: Vec::new() }
    }
}

impl<'a, T> Iterator for FixPulldownCmarkIssue890<'a, T>
where
    T: Iterator<Item = Event<'a>>,
{
    type Item = Event<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            match (self.inner.peek(), self.buffer.as_slice()) {
                // If we see 3 after accumulating 1&2, drop 2&3 and return 1.
                (
                    Some(Event::Start(Tag::BlockQuote(None))),
                    [Some(Event::Start(Tag::BlockQuote(Some(_)))), Some(Event::End(TagEnd::BlockQuote(_)))],
                ) => {
                    let _ = self.inner.next();
                    let e = self.buffer.swap_remove(0);
                    self.buffer.clear();
                    return e;
                },
                // If we see 2 and we've accumulated 1, buffer it and go around again
                (
                    Some(Event::End(TagEnd::BlockQuote(_))),
                    [Some(Event::Start(Tag::BlockQuote(Some(_))))],
                ) => {
                    self.buffer.push(self.inner.next());
                },
                // If we see 1 and the buffer is empty, buffer it and go around again
                (Some(Event::Start(Tag::BlockQuote(Some(_)))), []) => {
                    self.buffer.push(self.inner.next());
                },
                // Otherwise, if the buffer is empty, just pass it through
                (_, []) => return self.inner.next(),
                // Otherwise, drain the buffer
                (_, [_, ..]) => return self.buffer.remove(0),
            }
        }
    }
}

Notes

To repeat my earlier observation, this would bring mdBook more into line with how GitHub parses GFM admonitions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-enhancementCategory: Enhancement or feature request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions