use crate::{
HasSpan, Parser, Span,
attributes::Attrlist,
blocks::{
AdmonitionBlock, Break, CompoundDelimitedBlock, ContentModel, IsBlock, ListBlock, ListItem,
ListItemMarker, MediaBlock, Preamble, QuoteBlock, RawDelimitedBlock, SectionBlock,
SimpleBlock, TableBlock, TocBlock, is_built_in_context, media::TargetResolution,
metadata::BlockMetadata, starts_with_admonition_label,
},
content::{Content, SubstitutionGroup, substitute_attributes_in_reftext},
document::{Attribute, InterpretedValue, RefType},
parser::{InlineSubstitutionRenderer, ReferenceResolver, ReferenceWarnings, XrefSignifier},
span::MatchedItem,
strings::CowStr,
warnings::{MatchAndWarnings, Warning, WarningType},
};
#[derive(Clone, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Block<'src> {
Simple(SimpleBlock<'src>),
Media(MediaBlock<'src>),
Section(SectionBlock<'src>),
List(ListBlock<'src>),
ListItem(ListItem<'src>),
RawDelimited(RawDelimitedBlock<'src>),
CompoundDelimited(CompoundDelimitedBlock<'src>),
Admonition(AdmonitionBlock<'src>),
Quote(QuoteBlock<'src>),
Table(TableBlock<'src>),
Preamble(Preamble<'src>),
Break(Break<'src>),
Toc(TocBlock<'src>),
DocumentAttribute(Attribute<'src>),
}
impl<'src> std::fmt::Debug for Block<'src> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Block::Simple(block) => f.debug_tuple("Block::Simple").field(block).finish(),
Block::Media(block) => f.debug_tuple("Block::Media").field(block).finish(),
Block::Section(block) => f.debug_tuple("Block::Section").field(block).finish(),
Block::List(block) => f.debug_tuple("Block::List").field(block).finish(),
Block::ListItem(block) => f.debug_tuple("Block::ListItem").field(block).finish(),
Block::RawDelimited(block) => {
f.debug_tuple("Block::RawDelimited").field(block).finish()
}
Block::CompoundDelimited(block) => f
.debug_tuple("Block::CompoundDelimited")
.field(block)
.finish(),
Block::Admonition(block) => f.debug_tuple("Block::Admonition").field(block).finish(),
Block::Quote(block) => f.debug_tuple("Block::Quote").field(block).finish(),
Block::Table(block) => f.debug_tuple("Block::Table").field(block).finish(),
Block::Preamble(block) => f.debug_tuple("Block::Preamble").field(block).finish(),
Block::Break(break_) => f.debug_tuple("Block::Break").field(break_).finish(),
Block::Toc(block) => f.debug_tuple("Block::Toc").field(block).finish(),
Block::DocumentAttribute(block) => f
.debug_tuple("Block::DocumentAttribute")
.field(block)
.finish(),
}
}
}
#[allow(clippy::large_enum_variant)]
pub(crate) enum BlockParseOutcome<'src> {
Parsed(MatchedItem<'src, Block<'src>>),
Dropped(Span<'src>),
NoMatch,
}
impl<'src> Block<'src> {
#[cfg(test)]
pub(crate) fn parse(
source: Span<'src>,
parser: &mut Parser,
) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
let MatchAndWarnings { item, warnings } = Self::parse_internal(source, parser, None, false);
MatchAndWarnings {
item: match item {
BlockParseOutcome::Parsed(mi) => Some(mi),
BlockParseOutcome::Dropped(_) | BlockParseOutcome::NoMatch => None,
},
warnings,
}
}
pub(crate) fn parse_with_outcome(
source: Span<'src>,
parser: &mut Parser,
) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
Self::parse_internal(source, parser, None, false)
}
pub(crate) fn parse_for_list_item(
source: Span<'src>,
parser: &mut Parser,
parent_list_markers: &[ListItemMarker<'src>],
is_continuation: bool,
) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
Self::parse_internal(source, parser, Some(parent_list_markers), is_continuation)
}
fn parse_internal(
source: Span<'src>,
parser: &mut Parser,
parent_list_markers: Option<&[ListItemMarker<'src>]>,
is_continuation: bool,
) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
let mut content_start: Option<Span<'src>> = None;
let mut result = Self::parse_internal_inner(
source,
parser,
parent_list_markers,
is_continuation,
&mut content_start,
);
if let BlockParseOutcome::Parsed(matched_item) = &result.item
&& let Some(warning) = unknown_block_style_warning(&matched_item.item)
{
let span = content_start
.unwrap_or_else(|| matched_item.item.span())
.take_normalized_line()
.item;
result.warnings.push(Warning::new(span, warning));
}
result
}
fn parse_internal_inner(
source: Span<'src>,
parser: &mut Parser,
parent_list_markers: Option<&[ListItemMarker<'src>]>,
is_continuation: bool,
content_start: &mut Option<Span<'src>>,
) -> MatchAndWarnings<'src, BlockParseOutcome<'src>> {
let first_line = source.take_line().item.discard_whitespace();
if let Some(first_char) = first_line.chars().next()
&& !matches!(
first_char,
'.' | '#'
| '='
| '/'
| '-'
| '+'
| '*'
| '_'
| '`'
| '['
| ':'
| '\''
| '<'
| '>'
| '"'
| '•'
)
&& !first_line.contains("::")
&& !first_line.contains(";;")
&& !TableBlock::is_table_delimiter(&first_line)
&& !ListItemMarker::starts_with_marker(first_line)
&& !starts_with_admonition_label(first_line)
&& parent_list_markers.is_none()
&& parser.pending_block_title.is_none()
&& let Some(MatchedItem {
item: simple_block,
after,
}) = SimpleBlock::parse_fast(source, parser)
{
let mut warnings = vec![];
let block = Self::Simple(simple_block);
Self::register_block_id(
block.id(),
Self::block_reftext(&block, None).as_deref(),
Self::block_signifier(&block, parser),
block.span(),
parser,
&mut warnings,
);
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem { item: block, after }),
warnings,
};
}
if first_line.starts_with(':')
&& (first_line.ends_with(':') || first_line.contains(": "))
&& let Some(attr) = Attribute::parse(source, parser)
{
let mut warnings: Vec<Warning<'src>> = vec![];
parser.set_attribute_from_body(&attr.item, &mut warnings);
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: Self::DocumentAttribute(attr.item),
after: attr.after,
}),
warnings,
};
}
let MatchAndWarnings {
item: mut metadata,
mut warnings,
} = BlockMetadata::parse(source, parser);
if let Some(pending_title) = parser.pending_block_title.take()
&& metadata.title.is_none()
{
metadata.title = Some(crate::content::Content::from_owned_title(
metadata.block_start,
pending_title,
));
}
if parent_list_markers.is_none() && !metadata.is_empty() {
let after_blanks = metadata.block_start.discard_empty_lines();
if after_blanks != metadata.block_start && !after_blanks.is_empty() {
metadata.block_start = after_blanks;
}
}
*content_start = Some(metadata.block_start);
let anchor_reftext = metadata
.anchor_reftext
.as_ref()
.map(|span| substitute_attributes_in_reftext(*span, parser));
let is_literal =
metadata.attrlist.as_ref().and_then(|a| a.block_style()) == Some("literal") && {
let first_line = metadata.block_start.take_normalized_line().item;
!RawDelimitedBlock::is_valid_delimiter(&first_line)
&& !CompoundDelimitedBlock::is_valid_delimiter(&first_line)
&& !TableBlock::is_table_delimiter(&first_line)
};
let mut simple_block_mi = None;
if !is_literal {
if let Some(mut adm_maw) = AdmonitionBlock::parse(&metadata, parser)
&& let Some(adm) = adm_maw.item
{
if !adm_maw.warnings.is_empty() {
warnings.append(&mut adm_maw.warnings);
}
let block = Self::Admonition(adm.item);
Self::register_block_id(
block.id(),
Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
Self::block_signifier(&block, parser),
block.span(),
parser,
&mut warnings,
);
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: block,
after: adm.after,
}),
warnings,
};
}
if let Some(mut quote_maw) = QuoteBlock::parse(&metadata, parser)
&& let Some(quote) = quote_maw.item
{
if !quote_maw.warnings.is_empty() {
warnings.append(&mut quote_maw.warnings);
}
let block = Self::Quote(quote.item);
Self::register_block_id(
block.id(),
Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
Self::block_signifier(&block, parser),
block.span(),
parser,
&mut warnings,
);
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: block,
after: quote.after,
}),
warnings,
};
}
if let Some(mut rdb_maw) = RawDelimitedBlock::parse(&metadata, parser)
&& let Some(rdb) = rdb_maw.item
{
if !rdb_maw.warnings.is_empty() {
warnings.append(&mut rdb_maw.warnings);
}
let block = Self::RawDelimited(rdb.item);
Self::register_block_id(
block.id(),
Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
Self::block_signifier(&block, parser),
block.span(),
parser,
&mut warnings,
);
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: block,
after: rdb.after,
}),
warnings,
};
}
if let Some(mut cdb_maw) = CompoundDelimitedBlock::parse(&metadata, parser)
&& let Some(cdb) = cdb_maw.item
{
if !cdb_maw.warnings.is_empty() {
warnings.append(&mut cdb_maw.warnings);
}
let block = Self::CompoundDelimited(cdb.item);
Self::register_block_id(
block.id(),
Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
Self::block_signifier(&block, parser),
block.span(),
parser,
&mut warnings,
);
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: block,
after: cdb.after,
}),
warnings,
};
}
if let Some(mut table_maw) = TableBlock::parse(&metadata, parser)
&& let Some(table) = table_maw.item
{
if !table_maw.warnings.is_empty() {
warnings.append(&mut table_maw.warnings);
}
let block = Self::Table(table.item);
Self::register_block_id(
block.id(),
Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
Self::block_signifier(&block, parser),
block.span(),
parser,
&mut warnings,
);
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: block,
after: table.after,
}),
warnings,
};
}
let line = metadata.block_start.take_normalized_line();
if line.item.starts_with("image::")
|| line.item.starts_with("video::")
|| line.item.starts_with("audio::")
{
let mut media_block_maw = MediaBlock::parse(&metadata, parser);
if let Some(mut media_block) = media_block_maw.item {
if !media_block_maw.warnings.is_empty() {
warnings.append(&mut media_block_maw.warnings);
}
if media_block.item.resolve_target(parser) == TargetResolution::Drop {
return MatchAndWarnings {
item: BlockParseOutcome::Dropped(media_block.after),
warnings,
};
}
media_block.item.assign_caption(parser);
let block = Self::Media(media_block.item);
Self::register_block_id(
block.id(),
Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
Self::block_signifier(&block, parser),
block.span(),
parser,
&mut warnings,
);
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: block,
after: media_block.after,
}),
warnings,
};
}
}
if line.item.starts_with("toc::") {
let mut toc_block_maw = TocBlock::parse(&metadata, parser);
if let Some(toc_block) = toc_block_maw.item {
if !toc_block_maw.warnings.is_empty() {
warnings.append(&mut toc_block_maw.warnings);
}
let block = Self::Toc(toc_block.item);
Self::register_block_id(
block.id(),
Self::block_reftext(&block, anchor_reftext.as_deref()).as_deref(),
Self::block_signifier(&block, parser),
block.span(),
parser,
&mut warnings,
);
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: block,
after: toc_block.after,
}),
warnings,
};
}
}
if (line.item.starts_with('=') || line.item.starts_with('#'))
&& let Some(mi_section_block) =
SectionBlock::parse(&metadata, parser, &mut warnings)
{
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: Self::Section(mi_section_block.item),
after: mi_section_block.after,
}),
warnings,
};
}
if (line.item.starts_with('\'')
|| line.item.starts_with('-')
|| line.item.starts_with('*')
|| line.item.starts_with('_')
|| line.item.starts_with('<'))
&& let Some(mi_break) = Break::parse(&metadata, parser)
{
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: Self::Break(mi_break.item),
after: mi_break.after,
}),
warnings,
};
}
if parent_list_markers.is_none()
&& let Some(mi_list) = ListBlock::parse(&metadata, parser, &mut warnings)
{
return MatchAndWarnings {
item: BlockParseOutcome::Parsed(MatchedItem {
item: Self::List(mi_list.item),
after: mi_list.after,
}),
warnings,
};
}
simple_block_mi = if let Some(plm) = parent_list_markers {
SimpleBlock::parse_for_list_item(&metadata, parser, is_continuation, plm)
} else {
SimpleBlock::parse(&metadata, parser)
};
if simple_block_mi.is_none() {
if !metadata.is_empty() {
warnings.push(Warning::new(
metadata.source,
WarningType::MissingBlockAfterTitleOrAttributeList,
));
metadata.title_source = None;
metadata.title = None;
metadata.anchor = None;
metadata.attrlist = None;
metadata.block_start = metadata.source;
} else if !metadata.source.data().is_empty() {
return MatchAndWarnings {
item: BlockParseOutcome::Dropped(metadata.block_start),
warnings,
};
}
}
}
let simple_block_mi = match simple_block_mi {
Some(mi) => Some(mi),
None => {
if let Some(plm) = parent_list_markers {
SimpleBlock::parse_for_list_item(&metadata, parser, is_continuation, plm)
} else {
SimpleBlock::parse(&metadata, parser)
}
}
};
let mut result = MatchAndWarnings {
item: match simple_block_mi {
Some(mi) => BlockParseOutcome::Parsed(MatchedItem {
item: Self::Simple(mi.item),
after: mi.after,
}),
None => BlockParseOutcome::NoMatch,
},
warnings,
};
if let BlockParseOutcome::Parsed(ref matched_item) = result.item {
Self::register_block_id(
matched_item.item.id(),
Self::block_reftext(&matched_item.item, anchor_reftext.as_deref()).as_deref(),
Self::block_signifier(&matched_item.item, parser),
matched_item.item.span(),
parser,
&mut result.warnings,
);
}
result
}
fn block_signifier<'a>(block: &'a Block<'a>, parser: &Parser) -> Option<XrefSignifier> {
let caption = block.caption()?;
let has_explicit_reftext = block
.attrlist()
.and_then(|attrlist| attrlist.named_attribute("reftext"))
.is_some()
|| block.anchor_reftext().is_some();
if has_explicit_reftext {
return None;
}
if Self::has_caption_override(block, parser) {
return None;
}
let label = caption.strip_suffix(". ").unwrap_or(caption).to_string();
Some(XrefSignifier {
label,
emphasize: false,
})
}
fn has_caption_override<'a>(block: &'a Block<'a>, parser: &Parser) -> bool {
let attribute_override = block
.attrlist()
.and_then(|attrlist| attrlist.named_attribute("caption"))
.is_some()
|| matches!(block, Block::Media(media)
if media.macro_attrlist().named_attribute("caption").is_some());
attribute_override
|| matches!(
parser.attribute_value("caption"),
InterpretedValue::Value(value) if !value.is_empty(),
)
}
fn block_reftext<'a>(block: &'a Block<'a>, anchor_reftext: Option<&str>) -> Option<CowStr<'a>> {
if let Some(attr) = block
.attrlist()
.and_then(|attrlist| attrlist.named_attribute("reftext"))
{
return Some(CowStr::from(attr.value()));
}
if let Some(anchor_reftext) = anchor_reftext {
return Some(CowStr::from(anchor_reftext.to_string()));
}
block.title().map(CowStr::from)
}
fn register_block_id(
id: Option<&str>,
reftext: Option<&str>,
signifier: Option<XrefSignifier>,
span: Span<'src>,
parser: &mut Parser,
warnings: &mut Vec<Warning<'src>>,
) {
if let Some(id) = id {
match parser.register_ref(id, reftext, RefType::Anchor) {
Ok(()) => {
if let Some(signifier) = signifier {
parser.set_ref_signifier(id, signifier);
}
}
Err(_duplicate_error) => {
warnings.push(Warning::new(span, WarningType::DuplicateId(id.to_string())));
}
}
}
}
pub(crate) fn as_list_item(&self) -> Option<&ListItem<'src>> {
match self {
Self::ListItem(li) => Some(li),
_ => None,
}
}
pub(crate) fn resolve_references(
&mut self,
resolver: &dyn ReferenceResolver,
renderer: &dyn InlineSubstitutionRenderer,
warnings: &mut ReferenceWarnings<'src>,
) {
if let Some(content) = self.content_mut() {
content.resolve_references(resolver, renderer, warnings);
}
if let Self::Table(table) = self {
table.resolve_references(resolver, renderer, warnings);
}
if let Self::Quote(quote) = self {
quote.resolve_references(resolver, renderer, warnings);
}
for child in self.child_blocks_mut() {
child.resolve_references(resolver, renderer, warnings);
}
}
pub(crate) fn block_title_content_mut(&mut self) -> Option<&mut Content<'src>> {
match self {
Self::Simple(b) => b.title_content_mut(),
Self::Media(b) => b.title_content_mut(),
Self::List(b) => b.title_content_mut(),
Self::RawDelimited(b) => b.title_content_mut(),
Self::CompoundDelimited(b) => b.title_content_mut(),
Self::Admonition(b) => b.title_content_mut(),
Self::Quote(b) => b.title_content_mut(),
Self::Table(b) => b.title_content_mut(),
Self::Break(b) => b.title_content_mut(),
Self::Toc(b) => b.title_content_mut(),
_ => None,
}
}
}
impl<'src> IsBlock<'src> for Block<'src> {
fn content_model(&self) -> ContentModel {
match self {
Self::Simple(_) => ContentModel::Simple,
Self::Media(b) => b.content_model(),
Self::Section(_) => ContentModel::Compound,
Self::List(b) => b.content_model(),
Self::ListItem(b) => b.content_model(),
Self::RawDelimited(b) => b.content_model(),
Self::CompoundDelimited(b) => b.content_model(),
Self::Admonition(b) => b.content_model(),
Self::Quote(b) => b.content_model(),
Self::Table(b) => b.content_model(),
Self::Preamble(b) => b.content_model(),
Self::Break(b) => b.content_model(),
Self::Toc(b) => b.content_model(),
Self::DocumentAttribute(b) => b.content_model(),
}
}
fn declared_style(&'src self) -> Option<&'src str> {
match self {
Self::Simple(b) => b.declared_style(),
Self::Media(b) => b.declared_style(),
Self::Section(b) => b.declared_style(),
Self::List(b) => b.declared_style(),
Self::ListItem(b) => b.declared_style(),
Self::RawDelimited(b) => b.declared_style(),
Self::CompoundDelimited(b) => b.declared_style(),
Self::Admonition(b) => b.declared_style(),
Self::Quote(b) => b.declared_style(),
Self::Table(b) => b.declared_style(),
Self::Preamble(b) => b.declared_style(),
Self::Break(b) => b.declared_style(),
Self::Toc(b) => b.declared_style(),
Self::DocumentAttribute(b) => b.declared_style(),
}
}
fn rendered_content(&'src self) -> Option<&'src str> {
match self {
Self::Simple(b) => b.rendered_content(),
Self::Media(b) => b.rendered_content(),
Self::Section(b) => b.rendered_content(),
Self::List(b) => b.rendered_content(),
Self::ListItem(b) => b.rendered_content(),
Self::RawDelimited(b) => b.rendered_content(),
Self::CompoundDelimited(b) => b.rendered_content(),
Self::Admonition(b) => b.rendered_content(),
Self::Quote(b) => b.rendered_content(),
Self::Table(b) => b.rendered_content(),
Self::Preamble(b) => b.rendered_content(),
Self::Break(b) => b.rendered_content(),
Self::Toc(b) => b.rendered_content(),
Self::DocumentAttribute(b) => b.rendered_content(),
}
}
fn raw_context(&self) -> CowStr<'src> {
match self {
Self::Simple(b) => b.raw_context(),
Self::Media(b) => b.raw_context(),
Self::Section(b) => b.raw_context(),
Self::List(b) => b.raw_context(),
Self::ListItem(b) => b.raw_context(),
Self::RawDelimited(b) => b.raw_context(),
Self::CompoundDelimited(b) => b.raw_context(),
Self::Admonition(b) => b.raw_context(),
Self::Quote(b) => b.raw_context(),
Self::Table(b) => b.raw_context(),
Self::Preamble(b) => b.raw_context(),
Self::Break(b) => b.raw_context(),
Self::Toc(b) => b.raw_context(),
Self::DocumentAttribute(b) => b.raw_context(),
}
}
fn child_blocks_mut(&mut self) -> &mut [Block<'src>] {
match self {
Self::Simple(b) => b.child_blocks_mut(),
Self::Media(b) => b.child_blocks_mut(),
Self::Section(b) => b.child_blocks_mut(),
Self::List(b) => b.child_blocks_mut(),
Self::ListItem(b) => b.child_blocks_mut(),
Self::RawDelimited(b) => b.child_blocks_mut(),
Self::CompoundDelimited(b) => b.child_blocks_mut(),
Self::Admonition(b) => b.child_blocks_mut(),
Self::Quote(b) => b.child_blocks_mut(),
Self::Table(b) => b.child_blocks_mut(),
Self::Preamble(b) => b.child_blocks_mut(),
Self::Break(b) => b.child_blocks_mut(),
Self::Toc(b) => b.child_blocks_mut(),
Self::DocumentAttribute(b) => b.child_blocks_mut(),
}
}
fn content_mut(&mut self) -> Option<&mut Content<'src>> {
match self {
Self::Simple(b) => b.content_mut(),
Self::Media(b) => b.content_mut(),
Self::Section(b) => b.content_mut(),
Self::List(b) => b.content_mut(),
Self::ListItem(b) => b.content_mut(),
Self::RawDelimited(b) => b.content_mut(),
Self::CompoundDelimited(b) => b.content_mut(),
Self::Admonition(b) => b.content_mut(),
Self::Quote(b) => b.content_mut(),
Self::Table(b) => b.content_mut(),
Self::Preamble(b) => b.content_mut(),
Self::Break(b) => b.content_mut(),
Self::Toc(b) => b.content_mut(),
Self::DocumentAttribute(b) => b.content_mut(),
}
}
fn title_source(&'src self) -> Option<Span<'src>> {
match self {
Self::Simple(b) => b.title_source(),
Self::Media(b) => b.title_source(),
Self::Section(b) => b.title_source(),
Self::List(b) => b.title_source(),
Self::ListItem(b) => b.title_source(),
Self::RawDelimited(b) => b.title_source(),
Self::CompoundDelimited(b) => b.title_source(),
Self::Admonition(b) => b.title_source(),
Self::Quote(b) => b.title_source(),
Self::Table(b) => b.title_source(),
Self::Preamble(b) => b.title_source(),
Self::Break(b) => b.title_source(),
Self::Toc(b) => b.title_source(),
Self::DocumentAttribute(b) => b.title_source(),
}
}
fn title(&self) -> Option<&str> {
match self {
Self::Simple(b) => b.title(),
Self::Media(b) => b.title(),
Self::Section(b) => b.title(),
Self::List(b) => b.title(),
Self::ListItem(b) => b.title(),
Self::RawDelimited(b) => b.title(),
Self::CompoundDelimited(b) => b.title(),
Self::Admonition(b) => b.title(),
Self::Quote(b) => b.title(),
Self::Table(b) => b.title(),
Self::Preamble(b) => b.title(),
Self::Break(b) => b.title(),
Self::Toc(b) => b.title(),
Self::DocumentAttribute(b) => b.title(),
}
}
fn caption(&self) -> Option<&str> {
match self {
Self::Simple(b) => b.caption(),
Self::Media(b) => b.caption(),
Self::Section(b) => b.caption(),
Self::List(b) => b.caption(),
Self::ListItem(b) => b.caption(),
Self::RawDelimited(b) => b.caption(),
Self::CompoundDelimited(b) => b.caption(),
Self::Admonition(b) => b.caption(),
Self::Quote(b) => b.caption(),
Self::Table(b) => b.caption(),
Self::Preamble(b) => b.caption(),
Self::Break(b) => b.caption(),
Self::Toc(b) => b.caption(),
Self::DocumentAttribute(b) => b.caption(),
}
}
fn number(&self) -> Option<usize> {
match self {
Self::Simple(b) => b.number(),
Self::Media(b) => b.number(),
Self::Section(b) => b.number(),
Self::List(b) => b.number(),
Self::ListItem(b) => b.number(),
Self::RawDelimited(b) => b.number(),
Self::CompoundDelimited(b) => b.number(),
Self::Admonition(b) => b.number(),
Self::Quote(b) => b.number(),
Self::Table(b) => b.number(),
Self::Preamble(b) => b.number(),
Self::Break(b) => b.number(),
Self::Toc(b) => b.number(),
Self::DocumentAttribute(b) => b.number(),
}
}
fn id(&'src self) -> Option<&'src str> {
match self {
Self::Media(b) => b.id(),
Self::Section(b) => b.id(),
Self::Toc(b) => b.id(),
_ => self
.anchor()
.map(|a| a.data())
.or_else(|| self.attrlist().and_then(|attrlist| attrlist.id())),
}
}
fn anchor(&'src self) -> Option<Span<'src>> {
match self {
Self::Simple(b) => b.anchor(),
Self::Media(b) => b.anchor(),
Self::Section(b) => b.anchor(),
Self::List(b) => b.anchor(),
Self::ListItem(b) => b.anchor(),
Self::RawDelimited(b) => b.anchor(),
Self::CompoundDelimited(b) => b.anchor(),
Self::Admonition(b) => b.anchor(),
Self::Quote(b) => b.anchor(),
Self::Table(b) => b.anchor(),
Self::Preamble(b) => b.anchor(),
Self::Break(b) => b.anchor(),
Self::Toc(b) => b.anchor(),
Self::DocumentAttribute(b) => b.anchor(),
}
}
fn anchor_reftext(&'src self) -> Option<Span<'src>> {
match self {
Self::Simple(b) => b.anchor_reftext(),
Self::Media(b) => b.anchor_reftext(),
Self::Section(b) => b.anchor_reftext(),
Self::List(b) => b.anchor_reftext(),
Self::ListItem(b) => b.anchor_reftext(),
Self::RawDelimited(b) => b.anchor_reftext(),
Self::CompoundDelimited(b) => b.anchor_reftext(),
Self::Admonition(b) => b.anchor_reftext(),
Self::Quote(b) => b.anchor_reftext(),
Self::Table(b) => b.anchor_reftext(),
Self::Preamble(b) => b.anchor_reftext(),
Self::Break(b) => b.anchor_reftext(),
Self::Toc(b) => b.anchor_reftext(),
Self::DocumentAttribute(b) => b.anchor_reftext(),
}
}
fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
match self {
Self::Simple(b) => b.attrlist(),
Self::Media(b) => b.attrlist(),
Self::Section(b) => b.attrlist(),
Self::List(b) => b.attrlist(),
Self::ListItem(b) => b.attrlist(),
Self::RawDelimited(b) => b.attrlist(),
Self::CompoundDelimited(b) => b.attrlist(),
Self::Admonition(b) => b.attrlist(),
Self::Quote(b) => b.attrlist(),
Self::Table(b) => b.attrlist(),
Self::Preamble(b) => b.attrlist(),
Self::Break(b) => b.attrlist(),
Self::Toc(b) => b.attrlist(),
Self::DocumentAttribute(b) => b.attrlist(),
}
}
fn substitution_group(&self) -> SubstitutionGroup {
match self {
Self::Simple(b) => b.substitution_group(),
Self::Media(b) => b.substitution_group(),
Self::Section(b) => b.substitution_group(),
Self::List(b) => b.substitution_group(),
Self::ListItem(b) => b.substitution_group(),
Self::RawDelimited(b) => b.substitution_group(),
Self::CompoundDelimited(b) => b.substitution_group(),
Self::Admonition(b) => b.substitution_group(),
Self::Quote(b) => b.substitution_group(),
Self::Table(b) => b.substitution_group(),
Self::Preamble(b) => b.substitution_group(),
Self::Break(b) => b.substitution_group(),
Self::Toc(b) => b.substitution_group(),
Self::DocumentAttribute(b) => b.substitution_group(),
}
}
}
impl<'src> HasSpan<'src> for Block<'src> {
fn span(&self) -> Span<'src> {
match self {
Self::Simple(b) => b.span(),
Self::Media(b) => b.span(),
Self::Section(b) => b.span(),
Self::List(b) => b.span(),
Self::ListItem(b) => b.span(),
Self::RawDelimited(b) => b.span(),
Self::CompoundDelimited(b) => b.span(),
Self::Admonition(b) => b.span(),
Self::Quote(b) => b.span(),
Self::Table(b) => b.span(),
Self::Preamble(b) => b.span(),
Self::Break(b) => b.span(),
Self::Toc(b) => b.span(),
Self::DocumentAttribute(b) => b.span(),
}
}
}
const ADMONITION_STYLES: &[&str] = &["NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"];
const KNOWN_STYLE_KEYWORDS: &[&str] = &[
"abstract",
"asciimath",
"comment",
"latexmath",
"normal",
"partintro",
"source",
];
const STYLED_BLOCK_CONTEXTS: &[&str] = &[
"comment",
"example",
"listing",
"literal",
"open",
"paragraph",
"pass",
"quote",
"sidebar",
"stem",
"table",
"verse",
];
fn unknown_block_style_warning(block: &Block<'_>) -> Option<WarningType> {
let style = block.declared_style()?;
let context = block.raw_context();
let context = context.as_ref();
if !is_plausible_style_name(style) {
return None;
}
if !STYLED_BLOCK_CONTEXTS.contains(&context) {
return None;
}
if is_built_in_context(style)
|| KNOWN_STYLE_KEYWORDS.contains(&style)
|| ADMONITION_STYLES.contains(&style)
{
return None;
}
Some(WarningType::UnknownBlockStyle(
context.to_string(),
style.to_string(),
))
}
fn is_plausible_style_name(style: &str) -> bool {
!style.is_empty()
&& style
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
mod unknown_block_style {
use crate::{
Parser,
warnings::{WarningSeverity, WarningType},
};
fn only_warning(input: &str) -> Option<(WarningSeverity, WarningType)> {
let doc = Parser::default().parse(input);
let mut warnings = doc.warnings();
let warning = warnings.next()?;
assert!(
warnings.next().is_none(),
"expected at most one warning for {input:?}"
);
Some((warning.severity, warning.warning.clone()))
}
#[test]
fn unknown_style_on_open_block_is_debug() {
assert_eq!(
only_warning("[foo]\n--\nbar\n--\n"),
Some((
WarningSeverity::Debug,
WarningType::UnknownBlockStyle("open".to_string(), "foo".to_string())
))
);
}
#[test]
fn unknown_style_on_paragraph_is_debug() {
assert_eq!(
only_warning("[foo]\nbar\n"),
Some((
WarningSeverity::Debug,
WarningType::UnknownBlockStyle("paragraph".to_string(), "foo".to_string())
))
);
}
#[test]
fn nested_unknown_style_is_reported() {
assert_eq!(
only_warning("====\n[bar]\n--\nx\n--\n====\n"),
Some((
WarningSeverity::Debug,
WarningType::UnknownBlockStyle("open".to_string(), "bar".to_string())
))
);
}
#[test]
fn recognized_context_style_does_not_warn() {
assert_eq!(only_warning("[example]\n--\nx\n--\n"), None);
assert_eq!(only_warning("[sidebar]\n--\nx\n--\n"), None);
}
#[test]
fn recognized_keyword_style_does_not_warn() {
assert_eq!(only_warning("[source]\n----\nx\n----\n"), None);
assert_eq!(only_warning("[verse]\n____\nx\n____\n"), None);
assert_eq!(only_warning("[abstract]\n--\nx\n--\n"), None);
assert_eq!(only_warning("[asciimath]\n++++\nx\n++++\n"), None);
}
#[test]
fn any_built_in_context_style_does_not_warn() {
assert_eq!(only_warning("[image]\nbar\n"), None);
assert_eq!(only_warning("[audio]\n--\nx\n--\n"), None);
assert_eq!(only_warning("[video]\nbar\n"), None);
assert_eq!(only_warning("[table]\nbar\n"), None);
}
#[test]
fn admonition_style_does_not_warn() {
assert_eq!(only_warning("[NOTE]\n--\nx\n--\n"), None);
}
#[test]
fn style_naming_its_own_context_does_not_warn() {
assert_eq!(only_warning("[table]\n|===\n| x\n|===\n"), None);
}
#[test]
fn style_on_list_or_section_does_not_warn() {
assert_eq!(only_warning("[square]\n* a\n* b\n"), None);
assert_eq!(only_warning("[discrete]\n== Heading\n"), None);
}
#[test]
fn malformed_attrlist_debris_does_not_warn() {
assert_eq!(only_warning("[[notice]\nThis is a paragraph.\n"), None);
}
}
mod is_plausible_style_name {
use crate::blocks::block::is_plausible_style_name;
#[test]
fn accepts_style_tokens() {
assert!(is_plausible_style_name("foo"));
assert!(is_plausible_style_name("NOTE"));
assert!(is_plausible_style_name("foo-bar"));
assert!(is_plausible_style_name("foo_bar"));
assert!(is_plausible_style_name("style2"));
}
#[test]
fn rejects_debris_and_empty() {
assert!(!is_plausible_style_name(""));
assert!(!is_plausible_style_name("[notice"));
assert!(!is_plausible_style_name("-foo = bar"));
assert!(!is_plausible_style_name("a,b"));
assert!(!is_plausible_style_name("has space"));
}
}
}