diff --git a/README.md b/README.md index 1ac66a42..354cdbbd 100644 --- a/README.md +++ b/README.md @@ -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 Specify output format diff --git a/examples/s-expr.rs b/examples/s-expr.rs index eb924f25..4bba55d2 100644 --- a/examples/s-expr.rs +++ b/examples/s-expr.rs @@ -83,6 +83,7 @@ fn dump(source: &str) -> io::Result<()> { .superscript(true) .footnotes(true) .description_lists(true) + .multiline_block_quotes(true) .build() .unwrap(); diff --git a/fuzz/fuzz_targets/all_options.rs b/fuzz/fuzz_targets/all_options.rs index 2da18c7f..1d34e394 100644 --- a/fuzz/fuzz_targets/all_options.rs +++ b/fuzz/fuzz_targets/all_options.rs @@ -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; diff --git a/fuzz/fuzz_targets/quadratic.rs b/fuzz/fuzz_targets/quadratic.rs index 5e22b662..f49e22b1 100644 --- a/fuzz/fuzz_targets/quadratic.rs +++ b/fuzz/fuzz_targets/quadratic.rs @@ -192,6 +192,7 @@ struct FuzzExtensionOptions { superscript: bool, footnotes: bool, description_lists: bool, + multiline_block_quotes: bool, shortcodes: bool, } @@ -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; diff --git a/script/cibuild b/script/cibuild index 60a592ba..1e9c17e5 100755 --- a/script/cibuild +++ b/script/cibuild @@ -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 diff --git a/src/cm.rs b/src/cm.rs index 81edc764..c692089d 100644 --- a/src/cm.rs +++ b/src/cm.rs @@ -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 } diff --git a/src/html.rs b/src/html.rs index 4a33251f..f8092ab7 100644 --- a/src/html.rs +++ b/src/html.rs @@ -993,6 +993,17 @@ impl<'o> HtmlFormatter<'o> { self.output.write_all(b"\n")?; } } + NodeValue::MultilineBlockQuote(_) => { + if entering { + self.cr()?; + self.output.write_all(b"\n")?; + } else { + self.cr()?; + self.output.write_all(b"\n")?; + } + } } Ok(false) } diff --git a/src/main.rs b/src/main.rs index 097958db..d9ac7863 100644 --- a/src/main.rs +++ b/src/main.rs @@ -146,6 +146,7 @@ enum Extension { Superscript, Footnotes, DescriptionLists, + MultilineBlockQuotes, } #[derive(Clone, Copy, Debug, ValueEnum)] @@ -210,6 +211,7 @@ fn main() -> Result<(), Box> { .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")] diff --git a/src/nodes.rs b/src/nodes.rs index ae158203..f3993c46 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -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 { @@ -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. @@ -391,6 +406,7 @@ impl NodeValue { | NodeValue::TableRow(..) | NodeValue::TableCell | NodeValue::TaskItem(..) + | NodeValue::MultilineBlockQuote(_) ) } @@ -464,6 +480,7 @@ impl NodeValue { NodeValue::FootnoteReference(..) => "footnote_reference", #[cfg(feature = "shortcodes")] NodeValue::ShortCode(_) => "shortcode", + NodeValue::MultilineBlockQuote(_) => "multiline_block_quote", } } } @@ -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, } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3416f730..50751590 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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}; @@ -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; @@ -337,6 +340,31 @@ pub struct ExtensionOptions { /// ``` pub front_matter_delimiter: Option, + /// 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), + /// "
\n

paragraph

\n
\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. @@ -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); + } + } _ => {} } } @@ -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; @@ -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>, @@ -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, }; @@ -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(); diff --git a/src/parser/multiline_block_quote.rs b/src/parser/multiline_block_quote.rs new file mode 100644 index 00000000..2a8b5710 --- /dev/null +++ b/src/parser/multiline_block_quote.rs @@ -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, +} diff --git a/src/scanners.re b/src/scanners.re index 691a2880..0b4fb1d9 100644 --- a/src/scanners.re +++ b/src/scanners.re @@ -376,6 +376,28 @@ pub fn shortcode(s: &[u8]) -> Option { */ } +pub fn open_multiline_block_quote_fence(s: &[u8]) -> Option { + 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 { + 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; diff --git a/src/scanners.rs b/src/scanners.rs index 77d0aca6..4d48848e 100644 --- a/src/scanners.rs +++ b/src/scanners.rs @@ -22548,6 +22548,318 @@ pub fn shortcode(s: &[u8]) -> Option { } } +pub fn open_multiline_block_quote_fence(s: &[u8]) -> Option { + let mut cursor = 0; + let mut marker = 0; + let mut ctxmarker = 0; + let len = s.len(); + + { + #[allow(unused_assignments)] + let mut yych: u8 = 0; + let mut yystate: usize = 0; + 'yyl: loop { + match yystate { + 0 => { + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + cursor += 1; + match yych { + 0x3E => { + yystate = 3; + continue 'yyl; + } + _ => { + yystate = 1; + continue 'yyl; + } + } + } + 1 => { + yystate = 2; + continue 'yyl; + } + 2 => { + return None; + } + 3 => { + marker = cursor; + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + match yych { + 0x3E => { + cursor += 1; + yystate = 4; + continue 'yyl; + } + _ => { + yystate = 2; + continue 'yyl; + } + } + } + 4 => { + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + match yych { + 0x3E => { + cursor += 1; + yystate = 6; + continue 'yyl; + } + _ => { + yystate = 5; + continue 'yyl; + } + } + } + 5 => { + cursor = marker; + yystate = 2; + continue 'yyl; + } + 6 => { + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + match yych { + 0x09 | 0x20 => { + ctxmarker = cursor; + cursor += 1; + yystate = 7; + continue 'yyl; + } + 0x0A | 0x0D => { + ctxmarker = cursor; + cursor += 1; + yystate = 8; + continue 'yyl; + } + 0x3E => { + cursor += 1; + yystate = 6; + continue 'yyl; + } + _ => { + yystate = 5; + continue 'yyl; + } + } + } + 7 => { + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + match yych { + 0x09 | 0x20 => { + cursor += 1; + yystate = 7; + continue 'yyl; + } + 0x0A | 0x0D => { + cursor += 1; + yystate = 8; + continue 'yyl; + } + _ => { + yystate = 5; + continue 'yyl; + } + } + } + 8 => { + cursor = ctxmarker; + { + return Some(cursor); + } + } + _ => { + panic!("internal lexer error") + } + } + } + } +} + +pub fn close_multiline_block_quote_fence(s: &[u8]) -> Option { + let mut cursor = 0; + let mut marker = 0; + let mut ctxmarker = 0; + let len = s.len(); + + { + #[allow(unused_assignments)] + let mut yych: u8 = 0; + let mut yystate: usize = 0; + 'yyl: loop { + match yystate { + 0 => { + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + cursor += 1; + match yych { + 0x3E => { + yystate = 3; + continue 'yyl; + } + _ => { + yystate = 1; + continue 'yyl; + } + } + } + 1 => { + yystate = 2; + continue 'yyl; + } + 2 => { + return None; + } + 3 => { + marker = cursor; + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + match yych { + 0x3E => { + cursor += 1; + yystate = 4; + continue 'yyl; + } + _ => { + yystate = 2; + continue 'yyl; + } + } + } + 4 => { + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + match yych { + 0x3E => { + cursor += 1; + yystate = 6; + continue 'yyl; + } + _ => { + yystate = 5; + continue 'yyl; + } + } + } + 5 => { + cursor = marker; + yystate = 2; + continue 'yyl; + } + 6 => { + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + match yych { + 0x09 | 0x20 => { + ctxmarker = cursor; + cursor += 1; + yystate = 7; + continue 'yyl; + } + 0x0A | 0x0D => { + ctxmarker = cursor; + cursor += 1; + yystate = 8; + continue 'yyl; + } + 0x3E => { + cursor += 1; + yystate = 6; + continue 'yyl; + } + _ => { + yystate = 5; + continue 'yyl; + } + } + } + 7 => { + yych = unsafe { + if cursor < len { + *s.get_unchecked(cursor) + } else { + 0 + } + }; + match yych { + 0x09 | 0x20 => { + cursor += 1; + yystate = 7; + continue 'yyl; + } + 0x0A | 0x0D => { + cursor += 1; + yystate = 8; + continue 'yyl; + } + _ => { + yystate = 5; + continue 'yyl; + } + } + } + 8 => { + cursor = ctxmarker; + { + return Some(cursor); + } + } + _ => { + panic!("internal lexer error") + } + } + } + } +} + // Returns both the length of the match, and the tasklist character. pub fn tasklist(s: &[u8]) -> Option<(usize, u8)> { let mut cursor = 0; diff --git a/src/tests.rs b/src/tests.rs index e75d37d1..38e04395 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -12,6 +12,7 @@ mod description_lists; mod footnotes; mod fuzz; mod header_ids; +mod multiline_block_quotes; mod options; mod pathological; mod plugins; @@ -117,6 +118,7 @@ macro_rules! html_opts { header_ids: Some("user-content-".to_string()), footnotes: true, description_lists: true, + multiline_block_quotes: true, front_matter_delimiter: Some("---".to_string()), shortcodes: true, }, diff --git a/src/tests/api.rs b/src/tests/api.rs index eaa375ca..fff54aa8 100644 --- a/src/tests/api.rs +++ b/src/tests/api.rs @@ -44,6 +44,7 @@ fn exercise_full_api() { extension.header_ids(Some("abc".to_string())); extension.footnotes(false); extension.description_lists(false); + extension.multiline_block_quotes(false); extension.front_matter_delimiter(None); #[cfg(feature = "shortcodes")] extension.shortcodes(true); @@ -212,5 +213,9 @@ fn exercise_full_api() { let _: String = nfr.name; let _: u32 = nfr.ix; } + nodes::NodeValue::MultilineBlockQuote(mbc) => { + let _: usize = mbc.fence_length; + let _: usize = mbc.fence_offset; + } } } diff --git a/src/tests/fixtures/multiline_blockquote.txt b/src/tests/fixtures/multiline_blockquote.txt new file mode 100644 index 00000000..3e29f29b --- /dev/null +++ b/src/tests/fixtures/multiline_blockquote.txt @@ -0,0 +1,311 @@ +--- +title: GitLab Flavored Markdown Spec +version: 0.1 +date: '2023-12-18' +license: '[CC-BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/)' +... + +## Multi-line Blockquotes + +Simple container + +```````````````````````````````` example +>>> +*content* +>>> +. +
+

content

+
+```````````````````````````````` + + +Can contain block elements + +```````````````````````````````` example +>>> +### heading + +----------- +>>> +. +
+

heading

+
+
+```````````````````````````````` + + +Ending marker can be longer + +```````````````````````````````` example +>>>>>> + hello world +>>>>>>>>>>> +normal +. +
+

hello world

+
+

normal

+```````````````````````````````` + + +Nested blockquotes + +```````````````````````````````` example +>>>>> +>>>> +foo +>>>> +>>>>> +. +
+
+

foo

+
+
+```````````````````````````````` + +Incorrectly nested blockquotes + +```````````````````````````````` example +>>>> +this block is closed with 5 markers below + +>>>>> + +auto-closed blocks +>>>>> +>>>> +. +
+

this block is closed with 5 markers below

+
+

auto-closed blocks

+
+
+
+
+```````````````````````````````` + + +Marker can be indented up to 3 spaces + +```````````````````````````````` example + >>>> + first-level blockquote + >>> + second-level blockquote + >>> + >>>> + regular paragraph +. +
+

first-level blockquote

+
+

second-level blockquote

+
+
+

regular paragraph

+```````````````````````````````` + + +Fours spaces makes it a code block + +```````````````````````````````` example + >>> + content + >>> +. +
>>>
+content
+>>>
+
+```````````````````````````````` + + +Detection of embedded 4 spaces code block starts in the +column the blockquote starts, not from the beginning of +the line. + +```````````````````````````````` example + >>> + code block + >>> +. +
+
code block
+
+
+```````````````````````````````` + +```````````````````````````````` example + >>>> + content + >>> + code block + >>> + >>>> +. +
+

content

+
+
code block
+
+
+
+```````````````````````````````` + +Closing marker can't have text on the same line + +```````````````````````````````` example +>>> +foo +>>> arg=123 +. +
+

foo

+
+
+
+

arg=123

+
+
+
+
+```````````````````````````````` + + +Blockquotes self-close at the end of the document + +```````````````````````````````` example +>>> +foo +. +
+

foo

+
+```````````````````````````````` + + +They should terminate paragraphs + +```````````````````````````````` example +blah blah +>>> +content +>>> +. +

blah blah

+
+

content

+
+```````````````````````````````` + + +They can be nested in lists + +```````````````````````````````` example + - >>> + - foo + >>> +. +
    +
  • +
    +
      +
    • foo
    • +
    +
    +
  • +
+```````````````````````````````` + + +Or in blockquotes + +```````````````````````````````` example +> >>> +> foo +>> bar +> baz +> >>> +. +
+
+

foo

+
+

bar +baz

+
+
+
+```````````````````````````````` + + +List indentation + +```````````````````````````````` example + - >>> + foo + bar + >>> + + - >>> + foo + bar + >>> +. +
    +
  • +
    +

    foo +bar

    +
    +
  • +
  • +
    +

    foo +bar

    +
    +
  • +
+```````````````````````````````` + + +Ignored inside code blocks: + +```````````````````````````````` example +```txt +# Code +>>> +# Code +>>> +# Code +``` +. +
# Code
+>>>
+# Code
+>>>
+# Code
+
+```````````````````````````````` + + +Does not require a leading or trailing blank line + +```````````````````````````````` example +Some text +>>> +A quote +>>> +Some other text +. +

Some text

+
+

A quote

+
+

Some other text

+```````````````````````````````` diff --git a/src/tests/multiline_block_quotes.rs b/src/tests/multiline_block_quotes.rs new file mode 100644 index 00000000..27460b70 --- /dev/null +++ b/src/tests/multiline_block_quotes.rs @@ -0,0 +1,75 @@ +use super::*; + +#[test] +fn multiline_block_quotes() { + html_opts!( + [extension.multiline_block_quotes], + concat!(">>>\n", "Paragraph 1\n", "\n", "Paragraph 2\n", ">>>\n",), + concat!( + "
\n", + "

Paragraph 1

\n", + "

Paragraph 2

\n", + "
\n", + ), + ); + + html_opts!( + [extension.multiline_block_quotes], + concat!( + "- item one\n", + "\n", + " >>>\n", + " Paragraph 1\n", + "\n", + " Paragraph 2\n", + " >>>\n", + "- item two\n" + ), + concat!( + "
    \n", + "
  • \n", + "

    item one

    \n", + "
    \n", + "

    Paragraph 1

    \n", + "

    Paragraph 2

    \n", + "
    \n", + "
  • \n", + "
  • \n", + "

    item two

    \n", + "
  • \n", + "
\n", + ), + ); +} + +#[test] +fn sourcepos() { + assert_ast_match!( + [extension.multiline_block_quotes], + "- item one\n" + "\n" + " >>>\n" + " Paragraph 1\n" + " >>>\n" + "- item two\n", + (document (1:1-6:10) [ + (list (1:1-6:10) [ + (item (1:1-5:5) [ // (description_item (1:1-3:4) [ + (paragraph (1:3-1:10) [ + (text (1:3-1:10) "item one") + ]) + (multiline_block_quote (3:3-5:5) [ + (paragraph (4:3-4:13) [ + (text (4:3-4:13) "Paragraph 1") + ]) + ]) + ]) + (item (6:1-6:10) [ // (description_item (5:1-7:6) [ + (paragraph (6:3-6:10) [ + (text (6:3-6:10) "item two") + ]) + ]) + ]) + ]) + ); +} diff --git a/src/tests/propfuzz.rs b/src/tests/propfuzz.rs index 645cb5ea..aaeb5b0f 100644 --- a/src/tests/propfuzz.rs +++ b/src/tests/propfuzz.rs @@ -17,6 +17,7 @@ fn propfuzz_doesnt_crash(md: String) { header_ids: Some("user-content-".to_string()), footnotes: true, description_lists: true, + multiline_block_quotes: true, front_matter_delimiter: None, #[cfg(feature = "shortcodes")] shortcodes: true, diff --git a/src/xml.rs b/src/xml.rs index 6ad68bc1..036f84ae 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -172,6 +172,7 @@ impl<'o> XmlFormatter<'o> { } NodeValue::FrontMatter(_) => (), NodeValue::BlockQuote => {} + NodeValue::MultilineBlockQuote(..) => {} NodeValue::Item(..) => {} NodeValue::DescriptionList => {} NodeValue::DescriptionItem(..) => (), diff --git a/vendor/cmark-gfm b/vendor/cmark-gfm index 587a12bb..2f13eeed 160000 --- a/vendor/cmark-gfm +++ b/vendor/cmark-gfm @@ -1 +1 @@ -Subproject commit 587a12bb54d95ac37241377e6ddc93ea0e45439b +Subproject commit 2f13eeedfe9906c72a1843b03552550af7bee29a