Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions crates/mdbook-markdown/src/admonition_blockquote_merge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Merges blockquote events split by pulldown-cmark when an admonition header is
//! followed by a blank `>` line.
//!
//! See <https://github.com/pulldown-cmark/pulldown-cmark/issues/890>.

use pulldown_cmark::{Event, Tag, TagEnd};

/// An iterator adapter to collapse together the incorrectly split [`Event::Start`]
/// / [`Event::End`] blockquote pairs produced by pulldown-cmark when using GFM
/// admonitions with an extra blank line after the header.
pub(crate) struct AdmonitionBlockquoteMerge<'a, I>
where
I: Iterator<Item = Event<'a>>,
{
inner: std::iter::Peekable<I>,
buffer: Vec<Option<Event<'a>>>,
}

impl<'a, I> AdmonitionBlockquoteMerge<'a, I>
where
I: Iterator<Item = Event<'a>>,
{
pub(crate) fn new(iterator: I) -> Self {
Self {
inner: iterator.peekable(),
buffer: Vec::new(),
}
}
}

impl<'a, I> Iterator for AdmonitionBlockquoteMerge<'a, I>
where
I: 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),
}
}
}
}
39 changes: 36 additions & 3 deletions crates/mdbook-markdown/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,35 @@
//! crate is used as the underlying parser. This crate re-exports
//! [`pulldown_cmark`] so that you can access its types.

use pulldown_cmark::{Options, Parser};
use pulldown_cmark::{Event, Options, Parser};

mod admonition_blockquote_merge;
use admonition_blockquote_merge::AdmonitionBlockquoteMerge;

#[doc(inline)]
pub use pulldown_cmark;

/// Markdown event iterator returned by [`new_cmark_parser`].
pub struct MarkdownParser<'text> {
inner: MarkdownParserInner<'text>,
}

enum MarkdownParserInner<'text> {
Plain(Parser<'text>),
WithAdmonitionMerge(AdmonitionBlockquoteMerge<'text, Parser<'text>>),
}

impl<'text> Iterator for MarkdownParser<'text> {
type Item = Event<'text>;

fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
MarkdownParserInner::Plain(parser) => parser.next(),
MarkdownParserInner::WithAdmonitionMerge(parser) => parser.next(),
}
}
}

/// Options for parsing markdown.
#[non_exhaustive]
pub struct MarkdownOptions {
Expand Down Expand Up @@ -41,7 +65,10 @@ impl Default for MarkdownOptions {
}

/// Creates a new pulldown-cmark parser of the given text.
pub fn new_cmark_parser<'text>(text: &'text str, options: &MarkdownOptions) -> Parser<'text> {
pub fn new_cmark_parser<'text>(
text: &'text str,
options: &MarkdownOptions,
) -> MarkdownParser<'text> {
let mut opts = Options::empty();
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_FOOTNOTES);
Expand All @@ -57,5 +84,11 @@ pub fn new_cmark_parser<'text>(text: &'text str, options: &MarkdownOptions) -> P
if options.admonitions {
opts.insert(Options::ENABLE_GFM);
}
Parser::new_ext(text, opts)
let parser = Parser::new_ext(text, opts);
let inner = if options.admonitions {
MarkdownParserInner::WithAdmonitionMerge(AdmonitionBlockquoteMerge::new(parser))
} else {
MarkdownParserInner::Plain(parser)
};
MarkdownParser { inner }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ <h1 id="admonitions"><a class="header" href="#admonitions">Admonitions</a></h1>
<p>This is a note.</p>
<p>There are multiple paragraphs.</p>
</blockquote>
<blockquote class="blockquote-tag blockquote-tag-note">
<p class="blockquote-tag-title"><svg viewbox="0 0 16 16" width="18" height="18"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>Note</p>
<p>Blank line after the admonition header (prettifier-safe).</p>
</blockquote>
<blockquote class="blockquote-tag blockquote-tag-tip">
<p class="blockquote-tag-title"><svg viewbox="0 0 16 16" width="18" height="18"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg>Tip</p>
<p>This is a tip.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ <h1 id="admonitions"><a class="header" href="#admonitions">Admonitions</a></h1>
<p>There are multiple paragraphs.</p>
</blockquote>
<blockquote>
<p>[!NOTE]</p>
<p>Blank line after the admonition header (prettifier-safe).</p>
</blockquote>
<blockquote>
<p>[!TIP]
This is a tip.</p>
</blockquote>
Expand Down
4 changes: 4 additions & 0 deletions tests/testsuite/markdown/admonitions/src/admonitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
>
> There are multiple paragraphs.

> [!NOTE]
>
> Blank line after the admonition header (prettifier-safe).

> [!TIP]
> This is a tip.

Expand Down
Loading