Skip to content

Commit

Permalink
Support block quote (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep authored Feb 25, 2025
1 parent 3ed3547 commit 7e1d6e0
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 2 deletions.
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ build:

# Runs clippy on the sources
check:
cargo hack --feature-powerset clippy --locked -- -D warnings
cargo hack --feature-powerset clippy --locked --all-targets -- -D warnings

# Runs unit tests
test:
Expand Down
29 changes: 29 additions & 0 deletions parser/src/conversion/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ fn convert_body_elem(pair: Pair<Rule>) -> Result<c::BodyElement, Error> {
Rule::paragraph => convert_paragraph(pair)?.into(),
Rule::target => convert_target(pair)?.into(),
Rule::substitution_def => convert_substitution_def(pair)?.into(),
Rule::block_quote_directive => convert_block_quote_directive(pair)?.into(),
Rule::admonition_gen => convert_admonition_gen(pair)?,
Rule::image => convert_image::<e::Image>(pair)?.into(),
Rule::bullet_list => convert_bullet_list(pair)?.into(),
Rule::block_quote => convert_block_quote(pair)?.into(),
Rule::literal_block => convert_literal_block(pair).into(),
Rule::code_directive => convert_code_directive(pair).into(),
Rule::raw_directive => convert_raw_directive(pair).into(),
Expand Down Expand Up @@ -215,6 +217,33 @@ fn convert_bullet_item(pair: Pair<Rule>) -> Result<e::ListItem, Error> {
Ok(e::ListItem::with_children(children))
}

fn convert_block_quote(pair: Pair<Rule>) -> Result<e::BlockQuote, Error> {
Ok(e::BlockQuote::with_children(
pair.into_inner()
.map(convert_block_quote_inner)
.collect::<Result<_, _>>()?,
))
}

fn convert_block_quote_directive(pair: Pair<Rule>) -> Result<e::BlockQuote, Error> {
let mut iter = pair.into_inner();
let typ = iter.next().unwrap().as_str();
let children: Vec<c::SubBlockQuote> = iter
.map(convert_block_quote_inner)
.collect::<Result<_, _>>()?;
let mut bq = e::BlockQuote::with_children(children);
bq.classes_mut().push(typ.to_owned());
Ok(bq)
}

fn convert_block_quote_inner(pair: Pair<Rule>) -> Result<c::SubBlockQuote, Error> {
Ok(if pair.as_rule() == Rule::attribution {
e::Attribution::with_children(convert_inlines(pair)?).into()
} else {
convert_body_elem(pair)?.into()
})
}

fn convert_literal_block(pair: Pair<Rule>) -> e::LiteralBlock {
convert_literal_lines(pair.into_inner().next().unwrap())
}
Expand Down
17 changes: 16 additions & 1 deletion parser/src/rst.pest
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ hanging_block = _{
| raw_directive
| admonition
| admonition_gen
| block_quote_directive
| target
| literal_block
// Comments should be below the directives to try to match them first, but
// above the title that will interpret ".." as a title marker.
| block_comment
| title
| bullet_list
| block_quote
| paragraph
// TODO: implement all those things:
// | block_quote
// | verbatim
// | doctest_block
// | horizontal_rule
Expand Down Expand Up @@ -59,6 +60,12 @@ bullet_list = { bullet_item ~ (PEEK[..] ~ bullet_item)* }
bullet_item = { bullet_marker ~ PUSH(" "+) ~ line ~ blank_line* ~ blist_body? ~ DROP }
blist_body = _{ PEEK[..-1] ~ PUSH(" " ~ POP) ~ hanging_block ~ block* }

// Block quote. A block type.
block_quote = { PUSH(" " ~ " "+) ~ block_quote_content ~ DROP }
block_quote_content = _{ hanging_block ~ (blank_line* ~ !attribution ~ block)* ~ blank_line* ~ attribution? }
// https://github.com/docutils/docutils/blob/f704fb58904d62bf5c6d7db82a9628f29e4ed246/docutils/docutils/parsers/rst/states.py#L1201
attribution = { PEEK[..] ~ (("--" ~ "-"? ~ !"-") | "\u{2014}") ~ " "* ~ line+ }

// paragraph. A block type.
paragraph = { inlines }

Expand Down Expand Up @@ -118,6 +125,14 @@ admonition_gen = { ".." ~ PUSH(" "+) ~ admonition_type ~ "::" ~ (blank_lin
admonition_type = { ^"attention" | ^"caution" | ^"danger" | ^"error" | ^"hint" | ^"important" | ^"note" | ^"tip" | ^"warning" }
admonition_content = _{ PEEK[..-1] ~ PUSH(" " ~ POP) ~ hanging_block ~ block* } //TODO: merge with other directives?

// Block quote directives. The generic one is defined by simply indenting a block

block_quote_directive = {
".." ~ PUSH(" "+) ~ block_quote_type ~ "::" ~ (blank_line | line) ~ blank_line* ~
PEEK[..-1] ~ PUSH(" " ~ POP) ~ block_quote_content ~ DROP
}
block_quote_type = { ^"epigraph" | ^"highlights" | ^"pull-quote" }

// Comments.

block_comment = {
Expand Down
185 changes: 185 additions & 0 deletions parser/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,191 @@ fn admonitions() {
};
}

#[test]
fn blockquote_no_attribution() {
parses_to! {
parser: RstParser,
input: "\
Paragraph.
Block quote.
Paragraph.
",
rule: Rule::document,
tokens: [
paragraph(0, 10, [ str(0, 10) ]),
block_quote(12, 29, [
paragraph(15, 27, [ str(15, 27) ]),
]),
paragraph(29, 39, [ str(29, 39) ]),
]
};
}

#[test]
fn blockquote_fake_attribution() {
parses_to! {
parser: RstParser,
input: "\
Paragraph.
-- Not an attribution
Paragraph.
",
rule: Rule::document,
tokens: [
paragraph(0, 10, [ str(0, 10) ]),
block_quote(12, 38, [
paragraph(15, 36, [ str(15, 36) ]),
]),
paragraph(38, 48, [ str(38, 48) ]),
]
};
}

#[test]
fn blockquote_simple() {
parses_to! {
parser: RstParser,
input: "\
Paragraph.
Block quote.
-- Attribution
Paragraph.
",
rule: Rule::document,
tokens: [
paragraph(0, 10, [ str(0, 10) ]),
block_quote(12, 47, [
paragraph(15, 27, [ str(15, 27) ]),
attribution(29, 47, [ line(35, 47, [ str(35, 46) ]) ]),
]),
paragraph(48, 58, [ str(48, 58) ]),
]
};
}

#[test]
fn blockquote_multiline_attribution() {
parses_to! {
parser: RstParser,
input: "\
Paragraph.
Block quote.
--Attribution line one
and line two
Paragraph.
",
rule: Rule::document,
tokens: [
paragraph(0, 10, [ str(0, 10) ]),
block_quote(12, 71, [
paragraph(15, 27, [ str(15, 27) ]),
attribution(29, 71, [
line(34, 55, [ str(34, 54) ]),
line(55, 71, [ str(55, 70) ])
]),
]),
paragraph(72, 82, [ str(72, 82) ]),
]
};
}

#[test]
fn blockquote_multiline_attribution_2() {
parses_to! {
parser: RstParser,
input: "\
Paragraph.
-- Invalid attribution
Block quote.
-- Attribution line one
and line two
Paragraph.
",
rule: Rule::document,
tokens: [
paragraph(0, 10, [ str(0, 10) ]),
block_quote(12, 102, [
paragraph(15, 37, [ str(15, 37) ]),
paragraph(42, 54, [ str(42, 54) ]),
attribution(56, 102, [
line(62, 83, [ str(62, 82) ]),
line(83, 102, [ str(83, 101) ])
])
]),
paragraph(103, 113, [ str(103, 113) ]),
]
};
}

#[test]
fn blockquote_back_to_back() {
parses_to! {
parser: RstParser,
input: "\
Paragraph.
Block quote 1.
-- Attribution 1
Block quote 2.
--Attribution 2
Paragraph.
",
rule: Rule::document,
tokens: [
paragraph(0, 10, [ str(0, 10) ]),
block_quote(12, 51, [
paragraph(15, 29, [ str(15, 29) ]),
attribution(31, 51, [ line(37, 51, [ str(37, 50) ]) ]),
]),
block_quote(52, 90, [
paragraph(55, 69, [ str(55, 69) ]),
attribution(71, 90, [ line(76, 90, [ str(76, 89) ]) ]),
]),
paragraph(91, 101, [ str(91, 101) ]),
]
};
}
#[test]

fn block_quote_directive() {
parses_to! {
parser: RstParser,
input: "\
.. epigraph::
Single line
The end
",
rule: Rule::document,
tokens: [
block_quote_directive(0, 31, [
block_quote_type(3, 11),
paragraph(18, 29, [ str(18, 29) ]),
]),
paragraph(31, 38, [ str(31, 38) ]),
]
};
}

#[allow(clippy::cognitive_complexity)]
#[test]
fn literal_block() {
Expand Down

0 comments on commit 7e1d6e0

Please sign in to comment.