Skip to content

Commit

Permalink
Merge pull request #359 from digitalmoksha/bw-multiline-blockquote
Browse files Browse the repository at this point in the history
Add a multiline blockquote extension
  • Loading branch information
kivikakk authored Jan 24, 2024
2 parents bb104e6 + 2027e48 commit ba5b1e1
Show file tree
Hide file tree
Showing 20 changed files with 886 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Options:
Multiple extensions can be delimited with ",", e.g. --extension strikethrough,table
[possible values: strikethrough, tagfilter, table, autolink, tasklist, superscript,
footnotes, description-lists]
footnotes, description-lists, multiline-block-quotes]

-t, --to <FORMAT>
Specify output format
Expand Down
1 change: 1 addition & 0 deletions examples/s-expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ fn dump(source: &str) -> io::Result<()> {
.superscript(true)
.footnotes(true)
.description_lists(true)
.multiline_block_quotes(true)
.build()
.unwrap();

Expand Down
1 change: 1 addition & 0 deletions fuzz/fuzz_targets/all_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ fuzz_target!(|s: &str| {
extension.header_ids = Some("user-content-".to_string());
extension.footnotes = true;
extension.description_lists = true;
extension.multiline_block_quotes = true;
extension.front_matter_delimiter = Some("---".to_string());
extension.shortcodes = true;

Expand Down
2 changes: 2 additions & 0 deletions fuzz/fuzz_targets/quadratic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ struct FuzzExtensionOptions {
superscript: bool,
footnotes: bool,
description_lists: bool,
multiline_block_quotes: bool,
shortcodes: bool,
}

Expand All @@ -206,6 +207,7 @@ impl FuzzExtensionOptions {
extension.superscript = self.superscript;
extension.footnotes = self.footnotes;
extension.description_lists = self.description_lists;
extension.multiline_block_quotes = self.multiline_block_quotes;
extension.shortcodes = self.shortcodes;
extension.front_matter_delimiter = None;
extension.header_ids = None;
Expand Down
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 -e multiline-block-quotes" \
|| 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
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ enum Extension {
Superscript,
Footnotes,
DescriptionLists,
MultilineBlockQuotes,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
Expand Down Expand Up @@ -210,6 +211,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.header_ids(cli.header_ids)
.footnotes(exts.contains(&Extension::Footnotes))
.description_lists(exts.contains(&Extension::DescriptionLists))
.multiline_block_quotes(exts.contains(&Extension::MultilineBlockQuotes))
.front_matter_delimiter(cli.front_matter_delimiter);

#[cfg(feature = "shortcodes")]
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(_) => "multiline_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
106 changes: 105 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 @@ -337,6 +340,31 @@ pub struct ExtensionOptions {
/// ```
pub front_matter_delimiter: Option<String>,

/// Enables the multiline block quote extension.
///
/// Place `>>>` before and after text to make it into
/// a block quote.
///
/// ``` md
/// Paragraph one
///
/// >>>
/// Paragraph two
///
/// - one
/// - two
/// >>>
/// ```
///
/// ```
/// # use comrak::{markdown_to_html, Options};
/// let mut options = Options::default();
/// options.extension.multiline_block_quotes = true;
/// assert_eq!(markdown_to_html(">>>\nparagraph\n>>>", &options),
/// "<blockquote>\n<p>paragraph</p>\n</blockquote>\n");
/// ```
pub multiline_block_quotes: bool,

#[cfg(feature = "shortcodes")]
#[cfg_attr(docsrs, doc(cfg(feature = "shortcodes")))]
/// Phrases wrapped inside of ':' blocks will be replaced with emojis.
Expand Down Expand Up @@ -963,6 +991,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 +1023,26 @@ 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
&& self.options.extension.multiline_block_quotes
&& 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 +1501,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 +1586,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 +1767,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 fence marker.
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 ba5b1e1

Please sign in to comment.