Skip to content

Commit

Permalink
Add multline blockquote extension
Browse files Browse the repository at this point in the history
Adds the ability to enclose multiple lines in a blockquote.
Such as:

>>>
Paragraph one

Paragraph two
>>>
  • Loading branch information
digitalmoksha committed Jan 23, 2024
1 parent bb104e6 commit c8c518d
Show file tree
Hide file tree
Showing 12 changed files with 771 additions and 2 deletions.
2 changes: 2 additions & 0 deletions script/cibuild
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ if [ x"$SPEC" = "xtrue" ]; then
# python3 roundtrip_tests.py --spec extensions-table-prefer-style-attributes.txt "$PROGRAM_ARG --table-prefer-style-attributes" --extensions "table strikethrough autolink tagfilter footnotes tasklist" || failed=1
python3 roundtrip_tests.py --spec extensions-full-info-string.txt "$PROGRAM_ARG --full-info-string" \
|| failed=1
python3 spec_tests.py --no-normalize --spec ../../../src/tests/fixtures/multiline_blockquote.txt "$PROGRAM_ARG" \
|| failed=1

python3 spec_tests.py --no-normalize --spec regression.txt "$PROGRAM_ARG" \
|| failed=1
Expand Down
1 change: 1 addition & 0 deletions src/cm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ impl<'a, 'o> CommonMarkFormatter<'a, 'o> {
NodeValue::FootnoteReference(ref nfr) => {
self.format_footnote_reference(nfr.name.as_bytes(), entering)
}
NodeValue::MultilineBlockQuote(..) => self.format_block_quote(entering),
};
true
}
Expand Down
11 changes: 11 additions & 0 deletions src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,17 @@ impl<'o> HtmlFormatter<'o> {
self.output.write_all(b"</li>\n")?;
}
}
NodeValue::MultilineBlockQuote(_) => {
if entering {
self.cr()?;
self.output.write_all(b"<blockquote")?;
self.render_sourcepos(node)?;
self.output.write_all(b">\n")?;
} else {
self.cr()?;
self.output.write_all(b"</blockquote>\n")?;
}
}
}
Ok(false)
}
Expand Down
21 changes: 21 additions & 0 deletions src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use std::convert::TryFrom;
#[cfg(feature = "shortcodes")]
use crate::parser::shortcodes::NodeShortCode;

use crate::parser::multiline_block_quote::NodeMultilineBlockQuote;

/// The core AST node enum.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeValue {
Expand Down Expand Up @@ -151,6 +153,19 @@ pub enum NodeValue {
#[cfg(feature = "shortcodes")]
/// **Inline**. An Emoji character generated from a shortcode. Enable with feature "shortcodes".
ShortCode(NodeShortCode),

/// **Block**. A [multiline block quote](https://github.github.com/gfm/#block-quotes). Spans multiple
/// lines and contains other **blocks**.
///
/// ``` md
/// >>>
/// A paragraph.
///
/// - item one
/// - item two
/// >>>
/// ```
MultilineBlockQuote(NodeMultilineBlockQuote),
}

/// Alignment of a single table cell.
Expand Down Expand Up @@ -391,6 +406,7 @@ impl NodeValue {
| NodeValue::TableRow(..)
| NodeValue::TableCell
| NodeValue::TaskItem(..)
| NodeValue::MultilineBlockQuote(_)
)
}

Expand Down Expand Up @@ -464,6 +480,7 @@ impl NodeValue {
NodeValue::FootnoteReference(..) => "footnote_reference",
#[cfg(feature = "shortcodes")]
NodeValue::ShortCode(_) => "shortcode",
NodeValue::MultilineBlockQuote(_) => "block_quote",
}
}
}
Expand Down Expand Up @@ -647,6 +664,10 @@ pub fn can_contain_type<'a>(node: &'a AstNode<'a>, child: &NodeValue) -> bool {
| NodeValue::HtmlInline(..)
),

NodeValue::MultilineBlockQuote(_) => {
child.block() && !matches!(*child, NodeValue::Item(..) | NodeValue::TaskItem(..))
}

_ => false,
}
}
Expand Down
80 changes: 79 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod inlines;
pub mod shortcodes;
mod table;

pub mod multiline_block_quote;

use crate::adapters::SyntaxHighlighterAdapter;
use crate::arena_tree::Node;
use crate::ctype::{isdigit, isspace};
Expand All @@ -25,6 +27,7 @@ use std::str;
use typed_arena::Arena;

use crate::adapters::HeadingAdapter;
use crate::parser::multiline_block_quote::NodeMultilineBlockQuote;

use self::inlines::RefMap;

Expand Down Expand Up @@ -963,6 +966,16 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
return (false, container, should_continue);
}
}
NodeValue::MultilineBlockQuote(..) => {
if !self.parse_multiline_block_quote_prefix(
line,
container,
ast,
&mut should_continue,
) {
return (false, container, should_continue);
}
}
_ => {}
}
}
Expand All @@ -985,7 +998,25 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
self.find_first_nonspace(line);
let indented = self.indent >= CODE_INDENT;

if !indented && line[self.first_nonspace] == b'>' {
if !indented
&& unwrap_into(
scanners::open_multiline_block_quote_fence(&line[self.first_nonspace..]),
&mut matched,
)
{
let first_nonspace = self.first_nonspace;
let offset = self.offset;
let nmbc = NodeMultilineBlockQuote {
fence_length: matched,
fence_offset: first_nonspace - offset,
};
*container = self.add_child(
container,
NodeValue::MultilineBlockQuote(nmbc),
self.first_nonspace + 1,
);
self.advance_offset(line, first_nonspace + matched - offset, false);
} else if !indented && line[self.first_nonspace] == b'>' {
let blockquote_startpos = self.first_nonspace;

let offset = self.first_nonspace + 1 - self.offset;
Expand Down Expand Up @@ -1444,6 +1475,51 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
}
}

fn parse_multiline_block_quote_prefix(
&mut self,
line: &[u8],
container: &'a AstNode<'a>,
ast: &mut Ast,
should_continue: &mut bool,
) -> bool {
let (fence_length, fence_offset) = match ast.value {
NodeValue::MultilineBlockQuote(ref node_value) => {
(node_value.fence_length, node_value.fence_offset)
}
_ => unreachable!(),
};

let matched = if self.indent <= 3 && line[self.first_nonspace] == b'>' {
scanners::close_multiline_block_quote_fence(&line[self.first_nonspace..]).unwrap_or(0)
} else {
0
};

if matched >= fence_length {
*should_continue = false;
self.advance_offset(line, matched, false);

// The last child, like an indented codeblock, could be left open.
// Make sure it's finalized.
if nodes::last_child_is_open(container) {
let child = container.last_child().unwrap();
let child_ast = &mut *child.data.borrow_mut();

self.finalize_borrowed(child, child_ast).unwrap();
}

self.current = self.finalize_borrowed(container, ast).unwrap();
return false;
}

let mut i = fence_offset;
while i > 0 && strings::is_space_or_tab(line[self.offset]) {
self.advance_offset(line, 1, true);
i -= 1;
}
true
}

fn add_child(
&mut self,
mut parent: &'a AstNode<'a>,
Expand Down Expand Up @@ -1484,6 +1560,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
container.first_child().is_some()
|| container.data.borrow().sourcepos.start.line != self.line_number
}
NodeValue::MultilineBlockQuote(..) => false,
_ => true,
};

Expand Down Expand Up @@ -1664,6 +1741,7 @@ impl<'a, 'o, 'c> Parser<'a, 'o, 'c> {
NodeValue::Document => true,
NodeValue::CodeBlock(ref ncb) => ncb.fenced,
NodeValue::Heading(ref nh) => nh.setext,
NodeValue::MultilineBlockQuote(..) => true,
_ => false,
} {
ast.sourcepos.end = (self.line_number, self.curline_end_col).into();
Expand Down
9 changes: 9 additions & 0 deletions src/parser/multiline_block_quote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// The metadata of a multiline blockquote.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NodeMultilineBlockQuote {
/// The length of the fence.
pub fence_length: usize,

/// ??? The indentation level of the code within the block.
pub fence_offset: usize,
}
22 changes: 22 additions & 0 deletions src/scanners.re
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,28 @@ pub fn shortcode(s: &[u8]) -> Option<usize> {
*/
}

pub fn open_multiline_block_quote_fence(s: &[u8]) -> Option<usize> {
let mut cursor = 0;
let mut marker = 0;
let mut ctxmarker = 0;
let len = s.len();
/*!re2c
[>]{3,} / [ \t]*[\r\n] { return Some(cursor); }
* { return None; }
*/
}

pub fn close_multiline_block_quote_fence(s: &[u8]) -> Option<usize> {
let mut cursor = 0;
let mut marker = 0;
let mut ctxmarker = 0;
let len = s.len();
/*!re2c
[>]{3,} / [ \t]*[\r\n] { return Some(cursor); }
* { return None; }
*/
}

// Returns both the length of the match, and the tasklist character.
pub fn tasklist(s: &[u8]) -> Option<(usize, u8)> {
let mut cursor = 0;
Expand Down
Loading

0 comments on commit c8c518d

Please sign in to comment.