Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blockquote tags markup gets parsed in a confusingly strict way that renders it incompatible with autoformatting #890

Open
ssokolow opened this issue May 16, 2024 · 1 comment

Comments

@ssokolow
Copy link

ssokolow commented May 16, 2024

Prettier is a popular code formatter (more or less the Markdown formatter) which takes a "they're wrong, not us" attitude toward being an opinionated (i.e. minimally configurable) formatter.

(eg. They refuse to accept Doxygen applying semantic distinctions to different valid Markdown syntax for the same concept so long as Doxygen continues to read it from files with the standard .md extension.)

Because it doesn't recognize GFM-style admonition syntax, it's currently impossible to produce admonition markup that pulldown-cmark will recognize if using something like the gVim/Neovim ALE plugin to run on-save formatting of Markdown using Prettier.

Example 1

What the user types (which, might I add, runs counter to my intution about when to add blank lines):

> [!NOTE]
> Note

What Prettier (or really any admonition-unaware Markdown formatter) converts it to:

> [!NOTE] Note

What pulldown-cmark interprets it as:

TRACE Start(BlockQuote(None))
WARN Broken link of type Shortcut: !NOTE at offset 13688
TRACE Start(Paragraph)
TRACE Start(Link { link_type: ShortcutUnknown, dest_url: Borrowed(":BROKEN_LINK:"), title: Borrowed(""), id: Borrowed("!NOTE") })
TRACE Text(Borrowed("!NOTE"))
TRACE End(Link)
TRACE Text(Borrowed(" Note"))
TRACE End(Paragraph)
TRACE End(BlockQuote)

(Please excuse the transformations from my frontend. The :BROKEN_LINK: is an unfinished bit of code that will become a workaround for #635)

Example 2

What the user types:

> [!NOTE]
>
> Note

What Prettier allows through:

> [!NOTE]
>
> Note

What pulldown-cmark interprets it as:

TRACE Start(BlockQuote(Some(Note)))
TRACE End(BlockQuote)
TRACE Start(BlockQuote(None))
TRACE Start(Paragraph)
TRACE Text(Borrowed("Note"))
TRACE End(Paragraph)
TRACE End(BlockQuote)

Parsing this as two BlockQuote elements rather than one, despite the > on the whitespace between them is surprising, a footgun, and, as demonstrated, incompatible with admonition-unaware formatters.

At the moment, I'm deciding whether to leave admonition support off and wait for a bugfix from pulldown-cmark or just burn the time to implement an iterator adapter which collapses Start(BlockQuote(Some(x))), End(BlockQuote), Start(BlockQuote(None)) into Start(BlockQuote(Some(x))) in the token stream.

@ssokolow ssokolow changed the title Blockquote tags markup parsed in a confusingly strict way that renders it incompatible with autoformatting Blockquote tags markup gets parsed in a confusingly strict way that renders it incompatible with autoformatting May 17, 2024
@ssokolow
Copy link
Author

ssokolow commented May 18, 2024

I decided to write the iterator adapter:

/// An iterator adapter to collapse together the incorrectly split BlockQuote elements produced by
/// pulldown-cmark when using blockquote type tags/admonitions.
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),
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant