Skip to content

Commit

Permalink
feat: Add support for compound delimited blocks (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
scouten authored Nov 10, 2024
1 parent a8746cf commit 2dbb146
Show file tree
Hide file tree
Showing 10 changed files with 2,227 additions and 5 deletions.
37 changes: 33 additions & 4 deletions src/blocks/block.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::slice::Iter;

use crate::{
blocks::{ContentModel, IsBlock, MacroBlock, RawDelimitedBlock, SectionBlock, SimpleBlock},
blocks::{
CompoundDelimitedBlock, ContentModel, IsBlock, MacroBlock, RawDelimitedBlock, SectionBlock,
SimpleBlock,
},
span::MatchedItem,
strings::CowStr,
warnings::{MatchAndWarnings, Warning},
Expand Down Expand Up @@ -40,6 +43,9 @@ pub enum Block<'src> {
/// content between the matching delimiters is not parsed for block
/// syntax.
RawDelimited(RawDelimitedBlock<'src>),

/// A delimited block that can contain other blocks.
CompoundDelimited(CompoundDelimitedBlock<'src>),
}

impl<'src> Block<'src> {
Expand All @@ -52,9 +58,6 @@ impl<'src> Block<'src> {
let mut warnings: Vec<Warning<'src>> = vec![];
let source = source.discard_empty_lines();

// Try to discern the block type by scanning the first line.
let line = source.take_normalized_line();

if let Some(mut rdb_maw) = RawDelimitedBlock::parse(source) {
// If we found an initial delimiter without its matching
// closing delimiter, we will issue an unmatched delimiter warning
Expand All @@ -74,6 +77,28 @@ impl<'src> Block<'src> {
}
}

if let Some(mut cdb_maw) = CompoundDelimitedBlock::parse(source) {
// If we found an initial delimiter without its matching
// closing delimiter, we will issue an unmatched delimiter warning
// and attempt to parse this as some other kind of block.
if !cdb_maw.warnings.is_empty() {
warnings.append(&mut cdb_maw.warnings);
}

if let Some(cdb) = cdb_maw.item {
return MatchAndWarnings {
item: Some(MatchedItem {
item: Self::CompoundDelimited(cdb.item),
after: cdb.after,
}),
warnings,
};
}
}

// Try to discern the block type by scanning the first line.
let line = source.take_normalized_line();

if line.item.contains("::") {
let mut macro_block_maw = MacroBlock::parse(source);

Expand Down Expand Up @@ -135,6 +160,7 @@ impl<'src> IsBlock<'src> for Block<'src> {
Self::Macro(b) => b.content_model(),
Self::Section(_) => ContentModel::Compound,
Self::RawDelimited(b) => b.content_model(),
Self::CompoundDelimited(b) => b.content_model(),
}
}

Expand All @@ -144,6 +170,7 @@ impl<'src> IsBlock<'src> for Block<'src> {
Self::Macro(b) => b.context(),
Self::Section(b) => b.context(),
Self::RawDelimited(b) => b.context(),
Self::CompoundDelimited(b) => b.context(),
}
}

Expand All @@ -153,6 +180,7 @@ impl<'src> IsBlock<'src> for Block<'src> {
Self::Macro(b) => b.nested_blocks(),
Self::Section(b) => b.nested_blocks(),
Self::RawDelimited(b) => b.nested_blocks(),
Self::CompoundDelimited(b) => b.nested_blocks(),
}
}
}
Expand All @@ -164,6 +192,7 @@ impl<'src> HasSpan<'src> for Block<'src> {
Self::Macro(b) => b.span(),
Self::Section(b) => b.span(),
Self::RawDelimited(b) => b.span(),
Self::CompoundDelimited(b) => b.span(),
}
}
}
140 changes: 140 additions & 0 deletions src/blocks/compound_delimited.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use std::slice::Iter;

use crate::{
blocks::{parse_utils::parse_blocks_until, Block, ContentModel, IsBlock},
span::MatchedItem,
strings::CowStr,
warnings::{MatchAndWarnings, Warning, WarningType},
HasSpan, Span,
};

/// A delimited block that can contain other blocks.
///
/// The following delimiters are recognized as compound delimited blocks:
///
/// | Delimiter | Content type |
/// |-----------|--------------|
/// | `====` | Example |
/// | `--` | Open |
/// | `****` | Sidebar |
/// | `____` | Quote |
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CompoundDelimitedBlock<'src> {
blocks: Vec<Block<'src>>,
context: CowStr<'src>,
source: Span<'src>,
}

impl<'src> CompoundDelimitedBlock<'src> {
pub(crate) fn is_valid_delimiter(line: &Span<'src>) -> bool {
let data = line.data();

if data == "--" || data == "---" {
// TO DO (https://github.com/scouten/asciidoc-parser/issues/146):
// Seek spec clarity on whether three hyphens can be used to
// delimit an open block. Assuming yes for now.
return true;
}

// TO DO (https://github.com/scouten/asciidoc-parser/issues/145):
// Seek spec clarity: Do the characters after the fourth char
// have to match the first four?

if data.len() >= 4 {
if data.starts_with("====") {
data.split_at(4).1.chars().all(|c| c == '=')
} else if data.starts_with("****") {
data.split_at(4).1.chars().all(|c| c == '*')
} else if data.starts_with("____") {
data.split_at(4).1.chars().all(|c| c == '_')
} else {
false
}
} else {
false
}
}

pub(crate) fn parse(
source: Span<'src>,
) -> Option<MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>>> {
let delimiter = source.take_normalized_line();
let maybe_delimiter_text = delimiter.item.data();

// TO DO (https://github.com/scouten/asciidoc-parser/issues/146):
// Seek spec clarity on whether three hyphens can be used to
// delimit an open block. Assuming yes for now.
let context = match maybe_delimiter_text
.split_at(maybe_delimiter_text.len().min(4))
.0
{
"====" => "example",
"--" | "---" => "open",
"****" => "sidebar",
"____" => "quote",
_ => return None,
};

if !Self::is_valid_delimiter(&delimiter.item) {
return None;
}

let mut next = delimiter.after;
let closing_delimiter = loop {
if next.is_empty() {
return Some(MatchAndWarnings {
item: None,
warnings: vec![Warning {
source: delimiter.item,
warning: WarningType::UnterminatedDelimitedBlock,
}],
});
}

let line = next.take_normalized_line();
if line.item.data() == delimiter.item.data() {
break line;
}
next = line.after;
};

let inside_delimiters = delimiter.after.trim_remainder(closing_delimiter.item);

let maw_blocks = parse_blocks_until(inside_delimiters, |_| false);

let blocks = maw_blocks.item;
let source = source.trim_remainder(closing_delimiter.after);

Some(MatchAndWarnings {
item: Some(MatchedItem {
item: Self {
blocks: blocks.item,
context: context.into(),
source,
},
after: closing_delimiter.after,
}),
warnings: maw_blocks.warnings,
})
}
}

impl<'src> IsBlock<'src> for CompoundDelimitedBlock<'src> {
fn content_model(&self) -> ContentModel {
ContentModel::Compound
}

fn context(&self) -> CowStr<'src> {
self.context.clone()
}

fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
self.blocks.iter()
}
}

impl<'src> HasSpan<'src> for CompoundDelimitedBlock<'src> {
fn span(&'src self) -> &'src Span<'src> {
&self.source
}
}
3 changes: 3 additions & 0 deletions src/blocks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
mod block;
pub use block::Block;

mod compound_delimited;
pub use compound_delimited::CompoundDelimitedBlock;

mod is_block;
pub use is_block::{ContentModel, IsBlock};

Expand Down
Loading

0 comments on commit 2dbb146

Please sign in to comment.