Skip to content

Commit

Permalink
feat: tagged blockquotes, 2 consecutive ones cannot be tagged currently
Browse files Browse the repository at this point in the history
Fixes: #718
  • Loading branch information
Martin1887 committed Mar 24, 2024
1 parent cf8e316 commit 9f94f7e
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 56 deletions.
2 changes: 1 addition & 1 deletion pulldown-cmark/examples/parser-map-tag-print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ fn main() {
Tag::Emphasis => println!("Emphasis (this is a span tag)"),
Tag::Strong => println!("Strong (this is a span tag)"),
Tag::Strikethrough => println!("Strikethrough (this is a span tag)"),
Tag::BlockQuote => println!("BlockQuote"),
Tag::BlockQuote(kind) => println!("BlockQuote ({:?})", kind),
Tag::CodeBlock(code_block_kind) => {
println!("CodeBlock code_block_kind: {:?}", code_block_kind)
}
Expand Down
105 changes: 105 additions & 0 deletions pulldown-cmark/specs/blockquotes_tags.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
Run this with `cargo test --features gen-tests suite::blockquotes_tags`.

Blockquotes can optionally have one of the following tags:

- [!NOTE]
- [!TIP]
- [!IMPORTANT]
- [!WARNING]
- [!CAUTION]

Using one of these tags adds a class with the same name but in lowercase
(note, tip, etc.).


```````````````````````````````` example
> This is a normal blockquote without tag.
.
<blockquote><p>This is a normal blockquote without tag.</p></blockquote>
````````````````````````````````

```````````````````````````````` example
> [!NOTE]
> Note blockquote
.
<blockquote class="note"><p>Note blockquote</p></blockquote>
````````````````````````````````

```````````````````````````````` example
> [!TIP]
> Tip blockquote
.
<blockquote class="tip"><p>Tip blockquote</p></blockquote>
````````````````````````````````

```````````````````````````````` example
> [!IMPORTANT]
> Important blockquote
.
<blockquote class="important"><p>Important blockquote</p></blockquote>
````````````````````````````````

```````````````````````````````` example
> [!WARNING]
> Warning blockquote
.
<blockquote class="warning"><p>Warning blockquote</p></blockquote>
````````````````````````````````

```````````````````````````````` example
> [!CAUTION]
> Caution blockquote
.
<blockquote class="caution"><p>Caution blockquote</p></blockquote>
````````````````````````````````

A blockquote with tag can be empty:
```````````````````````````````` example
> [!CAUTION]
.
<blockquote class="caution"></blockquote>
````````````````````````````````

An a blockquote can have several lines:
```````````````````````````````` example
> [!CAUTION]
> Line 1.
> Line 2.
.
<blockquote class="caution"><p>Line 1.
Line 2.</p></blockquote>
````````````````````````````````

Tags are ignored in subsequent lines, literally written:
```````````````````````````````` example
> [!CAUTION]
> Line 1.
> [!CAUTION]
> Line 2.
.
<blockquote class="caution"><p>Line 1.
[!CAUTION]
Line 2.</p></blockquote>
````````````````````````````````

But nested blockquotes can have their own tag:
```````````````````````````````` example
> [!CAUTION]
> Line 1.
> > [!TIP]
> Line 2.
.
<blockquote class="caution"><p>Line 1.</p><blockquote class="tip"><p>Line 2.</p></blockquote></blockquote>
````````````````````````````````

And consecutive blockquotes too:
```````````````````````````````` example
> [!CAUTION]
> Line 1.


> [!TIP]
> Line 2.
.
<blockquote class="caution"><p>Line 1.</p></blockquote><blockquote class="tip"><p>Line 2.</p></blockquote>
````````````````````````````````
86 changes: 49 additions & 37 deletions pulldown-cmark/src/firstpass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use std::cmp::max;
use std::ops::Range;

use crate::parse::{
scan_containers, Allocations, FootnoteDef, HeadingAttributes, Item, ItemBody, LinkDef, LINK_MAX_NESTED_PARENS,
scan_containers, Allocations, FootnoteDef, HeadingAttributes, Item, ItemBody, LinkDef,
LINK_MAX_NESTED_PARENS,
};
use crate::strings::CowStr;
use crate::tree::{Tree, TreeIndex};
Expand Down Expand Up @@ -164,12 +165,27 @@ impl<'a, 'b> FirstPass<'a, 'b> {
body: ItemBody::TaskListMarker(is_checked),
});
}
} else if line_start.scan_blockquote_marker() {
} else if let Some(kind) = line_start.scan_blockquote_marker(
// only search tags in the first line of a blockquote
self.tree.cur().is_none()
|| !matches!(
self.tree[self.tree.cur().unwrap()],
crate::tree::Node {
child: _,
next: _,
item: Item {
start: _,
end: _,
body: ItemBody::BlockQuote(_)
}
}
),
) {
self.finish_list(start_ix);
self.tree.append(Item {
start: container_start,
end: 0, // will get set later
body: ItemBody::BlockQuote,
body: ItemBody::BlockQuote(kind),
});
self.tree.push();
} else {
Expand All @@ -182,7 +198,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
if let Some(n) = scan_blank_line(&bytes[ix..]) {
if let Some(node_ix) = self.tree.peek_up() {
match &mut self.tree[node_ix].item.body {
ItemBody::BlockQuote => (),
ItemBody::BlockQuote(..) => (),
ItemBody::ListItem(indent) if self.begin_list_item.is_some() => {
self.last_line_blank = true;
// This is a blank list item.
Expand Down Expand Up @@ -324,20 +340,15 @@ impl<'a, 'b> FirstPass<'a, 'b> {
// https://github.com/raphlinus/pulldown-cmark/issues/832
let mut missing_empty_cells = 0;
// parse header. this shouldn't fail because we made sure the table header is ok
let (_sep_start, thead_ix) = self.parse_table_row_inner(
head_start,
table_cols,
&mut missing_empty_cells,
)?;
let (_sep_start, thead_ix) =
self.parse_table_row_inner(head_start, table_cols, &mut missing_empty_cells)?;
self.tree[thead_ix].item.body = ItemBody::TableHead;

// parse body
let mut ix = body_start;
while let Some((next_ix, _row_ix)) = self.parse_table_row(
ix,
table_cols,
&mut missing_empty_cells,
) {
while let Some((next_ix, _row_ix)) =
self.parse_table_row(ix, table_cols, &mut missing_empty_cells)
{
ix = next_ix;
}

Expand Down Expand Up @@ -794,8 +805,17 @@ impl<'a, 'b> FirstPass<'a, 'b> {
}
b'$' => {
let string_suffix = &self.text[ix..];
let can_open = !string_suffix[1..].as_bytes().first().copied().map_or(true, is_ascii_whitespace);
let can_close = ix > start && !self.text[..ix].as_bytes().last().copied().map_or(true, is_ascii_whitespace);
let can_open = !string_suffix[1..]
.as_bytes()
.first()
.copied()
.map_or(true, is_ascii_whitespace);
let can_close = ix > start
&& !self.text[..ix]
.as_bytes()
.last()
.copied()
.map_or(true, is_ascii_whitespace);

// 0xFFFF_FFFF... represents the root brace context. Using None would require
// storing Option<u8>, which is bigger than u8.
Expand All @@ -805,24 +825,21 @@ impl<'a, 'b> FirstPass<'a, 'b> {
//
// Unbalanced braces will cause the root to be changed, which is why it gets
// stored here.
let brace_context = if self.brace_context_stack.len() > MATH_BRACE_CONTEXT_MAX_NESTING {
self.brace_context_next as u8
} else {
self.brace_context_stack.last().copied().unwrap_or_else(|| {
self.brace_context_stack.push(!0);
!0
})
};
let brace_context =
if self.brace_context_stack.len() > MATH_BRACE_CONTEXT_MAX_NESTING {
self.brace_context_next as u8
} else {
self.brace_context_stack.last().copied().unwrap_or_else(|| {
self.brace_context_stack.push(!0);
!0
})
};

self.tree.append_text(begin_text, ix, backslash_escaped);
self.tree.append(Item {
start: ix,
end: ix + 1,
body: ItemBody::MaybeMath(
can_open,
can_close,
brace_context,
),
body: ItemBody::MaybeMath(can_open, can_close, brace_context),
});
begin_text = ix + 1;
LoopInstruction::ContinueAndSkip(0)
Expand Down Expand Up @@ -1555,13 +1572,7 @@ impl<'a, 'b> FirstPass<'a, 'b> {
// GitHub doesn't allow footnote definition labels to contain line breaks.
// It actually does allow this for link definitions under certain circumstances,
// but for this it's simpler to avoid it.
scan_link_label_rest(
&self.text[start + 2..],
&|_| {
None
},
self.tree.is_in_table(),
)?
scan_link_label_rest(&self.text[start + 2..], &|_| None, self.tree.is_in_table())?
} else {
self.parse_refdef_label(start + 2)?
};
Expand Down Expand Up @@ -2653,7 +2664,8 @@ mod simd {
#[test]
fn exhaustive_search() {
let chars = [
b'\n', b'\r', b'*', b'_', b'~', b'|', b'&', b'\\', b'[', b']', b'<', b'!', b'`', b'$', b'{', b'}'
b'\n', b'\r', b'*', b'_', b'~', b'|', b'&', b'\\', b'[', b']', b'<', b'!', b'`',
b'$', b'{', b'}',
];

for &c in &chars {
Expand Down
18 changes: 14 additions & 4 deletions pulldown-cmark/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use std::io::{self, Write};

use crate::strings::CowStr;
use crate::Event::*;
use crate::{Alignment, CodeBlockKind, Event, LinkType, Tag, TagEnd};
use crate::{Alignment, BlockQuoteKind, CodeBlockKind, Event, LinkType, Tag, TagEnd};
use pulldown_cmark_escape::{
escape_href, escape_html, escape_html_body_text, StrWrite, WriteWrapper,
};
Expand Down Expand Up @@ -236,11 +236,21 @@ where
_ => self.write(">"),
}
}
Tag::BlockQuote => {
Tag::BlockQuote(kind) => {
let class_str = match kind {
None => "",
Some(kind) => match kind {
BlockQuoteKind::Note => " class=\"note\"",
BlockQuoteKind::Tip => " class=\"tip\"",
BlockQuoteKind::Important => " class=\"important\"",
BlockQuoteKind::Warning => " class=\"warning\"",
BlockQuoteKind::Caution => " class=\"caution\"",
},
};
if self.end_newline {
self.write("<blockquote>\n")
self.write(&format!("<blockquote{}>\n", class_str))
} else {
self.write("\n<blockquote>\n")
self.write(&format!("\n<blockquote{}>\n", class_str))
}
}
Tag::CodeBlock(info) => {
Expand Down
16 changes: 14 additions & 2 deletions pulldown-cmark/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ impl<'a> CodeBlockKind<'a> {
}
}

/// BlockQuote kind (Note, Tip, Important, Warning, Caution).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum BlockQuoteKind {
Note,
Tip,
Important,
Warning,
Caution,
}

/// Metadata block kind.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum MetadataBlockKind {
Expand All @@ -148,7 +160,7 @@ pub enum Tag<'a> {
attrs: Vec<(CowStr<'a>, Option<CowStr<'a>>)>,
},

BlockQuote,
BlockQuote(Option<BlockQuoteKind>),
/// A code block.
CodeBlock(CodeBlockKind<'a>),

Expand Down Expand Up @@ -207,7 +219,7 @@ impl<'a> Tag<'a> {
match self {
Tag::Paragraph => TagEnd::Paragraph,
Tag::Heading { level, .. } => TagEnd::Heading(*level),
Tag::BlockQuote => TagEnd::BlockQuote,
Tag::BlockQuote(_) => TagEnd::BlockQuote,
Tag::CodeBlock(_) => TagEnd::CodeBlock,
Tag::HtmlBlock => TagEnd::HtmlBlock,
Tag::List(number) => TagEnd::List(number.is_some()),
Expand Down

0 comments on commit 9f94f7e

Please sign in to comment.