use std::{borrow::Cow, slice::Iter};
use crate::{
Document,
blocks::{Block, IsBlock, TableCellContent},
};
pub trait FindBlocks<'a>: sealed::Sealed<'a> {
fn child_blocks(&'a self) -> ChildBlocks<'a> {
ChildBlocks(self.seed_children(false))
}
fn descendant_blocks(&'a self) -> Descendants<'a> {
Descendants {
stack: vec![self.seed_children(false)],
traverse_documents: false,
}
}
fn find_blocks(&'a self, selector: &BlockSelector<'a>) -> FindBlocksIter<'a> {
FindBlocksIter {
inner: Descendants {
stack: vec![self.seed_children(selector.traverse_documents)],
traverse_documents: selector.traverse_documents,
},
selector: selector.clone(),
}
}
fn find_block_by_id(&'a self, id: &str) -> Option<&'a Block<'a>> {
self.descendant_blocks()
.find(|block| block.id() == Some(id))
}
fn traverse_blocks<F>(&'a self, control: F) -> TraverseBlocks<'a, F>
where
F: FnMut(&Block<'a>) -> Descend,
{
TraverseBlocks {
stack: vec![self.seed_children(false)],
control,
}
}
}
impl<'a> FindBlocks<'a> for Document<'a> {}
impl<'a> FindBlocks<'a> for Block<'a> {}
mod sealed {
use super::{ChildBlocksInner, children_of};
use crate::Document;
pub trait Sealed<'a> {
fn seed_children(&'a self, traverse_documents: bool) -> ChildBlocksInner<'a>;
}
impl<'a> Sealed<'a> for Document<'a> {
fn seed_children(&'a self, _traverse_documents: bool) -> ChildBlocksInner<'a> {
ChildBlocksInner::Slice(self.top_level_blocks().iter())
}
}
impl<'a> Sealed<'a> for super::Block<'a> {
fn seed_children(&'a self, traverse_documents: bool) -> ChildBlocksInner<'a> {
children_of(self, traverse_documents)
}
}
}
#[derive(Clone, Debug, Default)]
pub struct BlockSelector<'a> {
context: Option<Cow<'a, str>>,
style: Option<Cow<'a, str>>,
id: Option<Cow<'a, str>>,
role: Option<Cow<'a, str>>,
traverse_documents: bool,
}
impl<'a> BlockSelector<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn context(mut self, context: impl Into<Cow<'a, str>>) -> Self {
self.context = Some(context.into());
self
}
pub fn style(mut self, style: impl Into<Cow<'a, str>>) -> Self {
self.style = Some(style.into());
self
}
pub fn id(mut self, id: impl Into<Cow<'a, str>>) -> Self {
self.id = Some(id.into());
self
}
pub fn role(mut self, role: impl Into<Cow<'a, str>>) -> Self {
self.role = Some(role.into());
self
}
pub fn traverse_documents(mut self, traverse_documents: bool) -> Self {
self.traverse_documents = traverse_documents;
self
}
pub fn matches(&self, block: &Block<'_>) -> bool {
if let Some(context) = &self.context
&& block.resolved_context().as_ref() != context.as_ref()
{
return false;
}
if let Some(style) = &self.style
&& block.declared_style() != Some(style.as_ref())
{
return false;
}
if let Some(id) = &self.id
&& block.id() != Some(id.as_ref())
{
return false;
}
if let Some(role) = &self.role
&& !block.roles().iter().any(|r| *r == role.as_ref())
{
return false;
}
true
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Descend {
Accept,
Skip,
Reject,
Prune,
}
pub struct Descendants<'a> {
stack: Vec<ChildBlocksInner<'a>>,
traverse_documents: bool,
}
impl<'a> Iterator for Descendants<'a> {
type Item = &'a Block<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.stack.last_mut()?.next() {
None => {
self.stack.pop();
}
Some(block) => {
self.stack.push(children_of(block, self.traverse_documents));
return Some(block);
}
}
}
}
}
pub struct FindBlocksIter<'a> {
inner: Descendants<'a>,
selector: BlockSelector<'a>,
}
impl<'a> Iterator for FindBlocksIter<'a> {
type Item = &'a Block<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.inner
.by_ref()
.find(|block| self.selector.matches(block))
}
}
pub struct TraverseBlocks<'a, F> {
stack: Vec<ChildBlocksInner<'a>>,
control: F,
}
impl<'a, F> Iterator for TraverseBlocks<'a, F>
where
F: FnMut(&Block<'a>) -> Descend,
{
type Item = &'a Block<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let block = match self.stack.last_mut()?.next() {
None => {
self.stack.pop();
continue;
}
Some(block) => block,
};
match (self.control)(block) {
Descend::Accept => {
self.stack.push(children_of(block, false));
return Some(block);
}
Descend::Skip => {
self.stack.push(children_of(block, false));
}
Descend::Prune => return Some(block),
Descend::Reject => {}
}
}
}
}
pub struct ChildBlocks<'a>(ChildBlocksInner<'a>);
impl<'a> ChildBlocks<'a> {
pub(crate) fn from_slice(blocks: &'a [Block<'a>]) -> Self {
ChildBlocks(ChildBlocksInner::Slice(blocks.iter()))
}
pub(crate) fn empty() -> Self {
ChildBlocks(ChildBlocksInner::Empty)
}
}
impl<'a> Iterator for ChildBlocks<'a> {
type Item = &'a Block<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
#[doc(hidden)]
pub enum ChildBlocksInner<'a> {
Slice(Iter<'a, Block<'a>>),
Boxed(Box<dyn Iterator<Item = &'a Block<'a>> + 'a>),
Empty,
}
impl<'a> Iterator for ChildBlocksInner<'a> {
type Item = &'a Block<'a>;
fn next(&mut self) -> Option<Self::Item> {
match self {
ChildBlocksInner::Slice(iter) => iter.next(),
ChildBlocksInner::Boxed(iter) => iter.next(),
ChildBlocksInner::Empty => None,
}
}
}
fn children_of<'a>(block: &'a Block<'a>, traverse_documents: bool) -> ChildBlocksInner<'a> {
match block {
Block::Table(table) => {
if traverse_documents {
let blocks = table
.header_row()
.into_iter()
.chain(table.body_rows().iter())
.chain(table.footer_row())
.flat_map(|row| row.cells().iter())
.filter_map(|cell| match cell.content() {
TableCellContent::AsciiDoc(cell) => Some(cell.blocks().iter()),
TableCellContent::Simple(_) => None,
})
.flatten();
ChildBlocksInner::Boxed(Box::new(blocks))
} else {
ChildBlocksInner::Empty
}
}
Block::Quote(quote) => quote.child_blocks().0,
Block::Admonition(admonition) => admonition.child_blocks().0,
Block::Section(section) => section.child_blocks().0,
Block::List(list) => list.child_blocks().0,
Block::ListItem(list_item) => list_item.child_blocks().0,
Block::Preamble(preamble) => preamble.child_blocks().0,
Block::CompoundDelimited(compound) => compound.child_blocks().0,
Block::Simple(_)
| Block::Media(_)
| Block::RawDelimited(_)
| Block::Break(_)
| Block::Toc(_)
| Block::DocumentAttribute(_) => ChildBlocksInner::Empty,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use crate::{
blocks::{Block, BlockSelector, Descend, FindBlocks, IsBlock},
tests::prelude::*,
};
fn contexts<'a, T: FindBlocks<'a>>(node: &'a T) -> Vec<String> {
node.descendant_blocks()
.map(|b| b.resolved_context().as_ref().to_string())
.collect()
}
#[test]
fn empty_document_has_no_descendants() {
let doc = Parser::default().parse("");
assert_eq!(doc.descendant_blocks().count(), 0);
assert!(doc.find_block_by_id("anything").is_none());
}
#[test]
fn document_order_and_nesting() {
let doc = Parser::default()
.parse("== Section\n\n* one\n* two\n\n[source,rust]\n----\nfn main() {}\n----\n");
let contexts = contexts(&doc);
assert_eq!(
contexts,
vec![
"section",
"list",
"list_item",
"paragraph",
"list_item",
"paragraph",
"listing"
]
);
}
#[test]
fn descendant_blocks_compose_with_std_combinators() {
let doc = Parser::default().parse("== A\n\ntext\n\n== B\n\ntext\n");
let sections = doc
.descendant_blocks()
.filter(|b| matches!(b, Block::Section(_)))
.count();
assert_eq!(sections, 2);
}
#[test]
fn find_blocks_by_context_and_style() {
let doc = Parser::default()
.parse("[source,rust]\n----\nfn main() {}\n----\n\n----\nplain listing\n----\n");
assert_eq!(
doc.find_blocks(&BlockSelector::new().context("listing"))
.count(),
2
);
let sources: Vec<_> = doc
.find_blocks(&BlockSelector::new().context("listing").style("source"))
.collect();
assert_eq!(sources.len(), 1);
assert_eq!(sources.first().unwrap().declared_style(), Some("source"));
}
#[test]
fn find_blocks_by_context_excludes_non_matching() {
let doc = Parser::default().parse("A paragraph.\n\n----\na listing\n----\n");
let listings: Vec<_> = doc
.find_blocks(&BlockSelector::new().context("listing"))
.collect();
assert_eq!(listings.len(), 1);
assert_eq!(
listings.first().unwrap().resolved_context().as_ref(),
"listing"
);
}
#[test]
fn find_blocks_by_id_selector() {
let doc = Parser::default().parse("[#intro]\nHello.\n\nWorld.\n");
let matched: Vec<_> = doc.find_blocks(&BlockSelector::new().id("intro")).collect();
assert_eq!(matched.len(), 1);
assert_eq!(matched.first().unwrap().id(), Some("intro"));
}
#[test]
fn find_blocks_by_role() {
let doc = Parser::default().parse("[.important]\nAttention.\n\nOrdinary.\n");
let matched: Vec<_> = doc
.find_blocks(&BlockSelector::new().role("important"))
.collect();
assert_eq!(matched.len(), 1);
assert!(matched.first().unwrap().roles().contains(&"important"));
}
#[test]
fn find_block_by_id_hit_and_miss() {
let doc = Parser::default().parse("[#intro]\nHello.\n\nWorld.\n");
let intro = doc.find_block_by_id("intro").unwrap();
assert_eq!(intro.id(), Some("intro"));
assert!(doc.find_block_by_id("missing").is_none());
}
#[test]
fn traverse_blocks_accept_yields_everything() {
let doc = Parser::default().parse("== S\n\n* one\n* two\n");
let all: Vec<_> = doc.traverse_blocks(|_| Descend::Accept).collect();
let plain: Vec<_> = doc.descendant_blocks().collect();
assert_eq!(all, plain);
}
#[test]
fn traverse_blocks_prune_stops_at_matched_subtree() {
let doc = Parser::default()
.parse("****\nOuter.\n\n[.inner]\n*****\nInner.\n*****\n****\n\nAfter.\n");
let sidebars: Vec<_> = doc
.traverse_blocks(|b| {
if b.resolved_context().as_ref() == "sidebar" {
Descend::Prune
} else {
Descend::Reject
}
})
.collect();
assert_eq!(sidebars.len(), 1);
}
#[test]
fn traverse_blocks_reject_excludes_block_and_children() {
let doc = Parser::default().parse("****\nInside.\n****\n\nOutside.\n");
let contexts: Vec<_> = doc
.traverse_blocks(|b| {
if b.resolved_context().as_ref() == "sidebar" {
Descend::Reject
} else {
Descend::Accept
}
})
.map(|b| b.resolved_context().as_ref().to_string())
.collect();
assert_eq!(contexts, vec!["paragraph"]);
}
#[test]
fn traverse_blocks_skip_excludes_block_but_visits_children() {
let doc = Parser::default().parse("****\nInside sidebar.\n****\n");
let contexts: Vec<_> = doc
.traverse_blocks(|b| {
if b.resolved_context().as_ref() == "sidebar" {
Descend::Skip
} else {
Descend::Accept
}
})
.map(|b| b.resolved_context().as_ref().to_string())
.collect();
assert_eq!(contexts, vec!["paragraph"]);
}
#[test]
fn markdown_quote_children_are_reached() {
let doc = Parser::default().parse("> A quoted paragraph.\n");
let contexts = contexts(&doc);
assert_eq!(contexts, vec!["quote", "paragraph"]);
}
#[test]
fn table_cells_require_traverse_documents() {
let doc = Parser::default().parse("|===\na| Cell _text_.\n|===\n");
assert_eq!(doc.find_blocks(&BlockSelector::new()).count(), 1);
assert_eq!(
doc.find_blocks(&BlockSelector::new().context("table"))
.count(),
1
);
assert_eq!(
doc.find_blocks(&BlockSelector::new().traverse_documents(true))
.count(),
2
);
}
#[test]
fn traverse_documents_skips_non_asciidoc_cells() {
let doc = Parser::default().parse("|===\n| plain\na| AsciiDoc _text_.\n|===\n");
let deep = doc
.find_blocks(&BlockSelector::new().traverse_documents(true))
.count();
assert_eq!(deep, 2);
}
#[test]
fn find_blocks_on_a_block_searches_its_subtree() {
let doc = Parser::default().parse("== Section\n\n* one\n* two\n");
let section = doc
.descendant_blocks()
.find(|b| matches!(b, Block::Section(_)))
.unwrap();
assert_eq!(
contexts(section),
vec!["list", "list_item", "paragraph", "list_item", "paragraph"]
);
}
#[test]
fn child_blocks_yields_direct_children_only() {
let doc = Parser::default().parse("== Section\n\n* one\n* two\n");
let child_contexts: Vec<_> = doc
.child_blocks()
.map(|b| b.resolved_context().as_ref().to_string())
.collect();
assert_eq!(child_contexts, vec!["section"]);
let section = doc.child_blocks().next().unwrap();
let section_child_contexts: Vec<_> = section
.child_blocks()
.map(|b| b.resolved_context().as_ref().to_string())
.collect();
assert_eq!(section_child_contexts, vec!["list"]);
}
#[test]
fn child_blocks_reaches_markdown_quote_children() {
let doc = Parser::default().parse("> A quoted paragraph.\n");
let quote = doc.child_blocks().next().unwrap();
assert_eq!(quote.resolved_context().as_ref(), "quote");
let quote_child_contexts: Vec<_> = quote
.child_blocks()
.map(|b| b.resolved_context().as_ref().to_string())
.collect();
assert_eq!(quote_child_contexts, vec!["paragraph"]);
}
#[test]
fn leaf_block_types_report_no_child_blocks() {
let doc = Parser::default().parse(
"A paragraph.\n\nimage::sunset.jpg[]\n\n----\nlisting\n----\n\n|===\n|cell\n|===\n\n'''\n\n== Section\n\nInside.\n",
);
let mut saw_simple = false;
let mut saw_media = false;
let mut saw_raw = false;
let mut saw_table = false;
let mut saw_break = false;
for block in doc.descendant_blocks() {
match block {
Block::Simple(b) => {
assert_eq!(b.child_blocks().count(), 0);
saw_simple = true;
}
Block::Media(b) => {
assert_eq!(b.child_blocks().count(), 0);
saw_media = true;
}
Block::RawDelimited(b) => {
assert_eq!(b.child_blocks().count(), 0);
saw_raw = true;
}
Block::Table(b) => {
assert_eq!(b.child_blocks().count(), 0);
saw_table = true;
}
Block::Break(b) => {
assert_eq!(b.child_blocks().count(), 0);
saw_break = true;
}
_ => {}
}
}
assert!(saw_simple && saw_media && saw_raw && saw_table && saw_break);
}
#[test]
fn child_blocks_does_not_enter_table_cells() {
let doc = Parser::default().parse("|===\na| Cell _text_.\n|===\n");
let table = doc.child_blocks().next().unwrap();
assert_eq!(table.resolved_context().as_ref(), "table");
assert_eq!(table.child_blocks().count(), 0);
}
}