use crate::{
HasSpan, Parser, Span,
attributes::Attrlist,
blocks::{
CompoundDelimitedBlock, ContentModel, IsBlock, ListItemMarker, RawDelimitedBlock,
caption::assign_block_caption, metadata::BlockMetadata,
},
content::{Content, SubstitutionGroup},
span::MatchedItem,
strings::CowStr,
};
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum SimpleBlockStyle {
Paragraph,
Literal,
Listing,
Source,
}
impl std::fmt::Debug for SimpleBlockStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SimpleBlockStyle::Paragraph => write!(f, "SimpleBlockStyle::Paragraph"),
SimpleBlockStyle::Literal => write!(f, "SimpleBlockStyle::Literal"),
SimpleBlockStyle::Listing => write!(f, "SimpleBlockStyle::Listing"),
SimpleBlockStyle::Source => write!(f, "SimpleBlockStyle::Source"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SimpleBlock<'src> {
content: Content<'src>,
source: Span<'src>,
style: SimpleBlockStyle,
title_source: Option<Span<'src>>,
title: Option<Content<'src>>,
caption: Option<String>,
number: Option<usize>,
anchor: Option<Span<'src>>,
anchor_reftext: Option<Span<'src>>,
attrlist: Option<Attrlist<'src>>,
}
impl<'src> SimpleBlock<'src> {
pub(crate) fn title_content_mut(&mut self) -> Option<&mut Content<'src>> {
self.title.as_mut()
}
pub(crate) fn parse(
metadata: &BlockMetadata<'src>,
parser: &mut Parser,
) -> Option<MatchedItem<'src, Self>> {
let MatchedItem {
item: (content, style),
after,
} = parse_lines(
metadata.block_start,
&metadata.attrlist,
false,
false,
false,
parser,
&[],
)?;
let caption = assign_block_caption(
parser,
"paragraph",
metadata.attrlist.as_ref(),
metadata.title.is_some(),
);
let number = caption.as_ref().and_then(|caption| caption.number);
let caption = caption.map(|caption| caption.prefix);
Some(MatchedItem {
item: Self {
content,
source: metadata
.source
.trim_remainder(after)
.trim_trailing_whitespace(),
style,
title_source: metadata.title_source,
title: metadata.title.clone(),
caption,
number,
anchor: metadata.anchor,
anchor_reftext: metadata.anchor_reftext,
attrlist: metadata.attrlist.clone(),
},
after: after.discard_empty_lines(),
})
}
pub(crate) fn parse_for_list_item(
metadata: &BlockMetadata<'src>,
parser: &mut Parser,
is_continuation: bool,
parent_list_markers: &[ListItemMarker<'src>],
) -> Option<MatchedItem<'src, Self>> {
let MatchedItem {
item: (content, style),
after,
} = parse_lines(
metadata.block_start,
&metadata.attrlist,
true,
false,
is_continuation,
parser,
parent_list_markers,
)?;
let caption = assign_block_caption(
parser,
"paragraph",
metadata.attrlist.as_ref(),
metadata.title.is_some(),
);
let number = caption.as_ref().and_then(|caption| caption.number);
let caption = caption.map(|caption| caption.prefix);
Some(MatchedItem {
item: Self {
content,
source: metadata
.source
.trim_remainder(after)
.trim_trailing_whitespace(),
style,
title_source: metadata.title_source,
title: metadata.title.clone(),
caption,
number,
anchor: metadata.anchor,
anchor_reftext: metadata.anchor_reftext,
attrlist: metadata.attrlist.clone(),
},
after,
})
}
pub(crate) fn parse_for_definition_list(
metadata: &BlockMetadata<'src>,
parser: &mut Parser,
) -> Option<MatchedItem<'src, Self>> {
let MatchedItem {
item: (content, style),
after,
} = parse_lines(
metadata.block_start,
&metadata.attrlist,
true,
true,
false,
parser,
&[],
)?;
let caption = assign_block_caption(
parser,
"paragraph",
metadata.attrlist.as_ref(),
metadata.title.is_some(),
);
let number = caption.as_ref().and_then(|caption| caption.number);
let caption = caption.map(|caption| caption.prefix);
Some(MatchedItem {
item: Self {
content,
source: metadata
.source
.trim_remainder(after)
.trim_trailing_whitespace(),
style,
title_source: metadata.title_source,
title: metadata.title.clone(),
caption,
number,
anchor: metadata.anchor,
anchor_reftext: metadata.anchor_reftext,
attrlist: metadata.attrlist.clone(),
},
after,
})
}
pub(crate) fn parse_fast(
source: Span<'src>,
parser: &Parser,
) -> Option<MatchedItem<'src, Self>> {
let MatchedItem {
item: (content, style),
after,
} = parse_lines(source, &None, false, false, false, parser, &[])?;
let source = content.original();
Some(MatchedItem {
item: Self {
content,
source,
style,
title_source: None,
title: None,
caption: None,
number: None,
anchor: None,
anchor_reftext: None,
attrlist: None,
},
after: after.discard_empty_lines(),
})
}
pub fn content(&self) -> &Content<'src> {
&self.content
}
pub fn style(&self) -> SimpleBlockStyle {
self.style
}
}
fn parse_lines<'src>(
source: Span<'src>,
attrlist: &Option<Attrlist<'src>>,
mut stop_for_list_items: bool,
force_paragraph_style: bool,
preserve_literal_indent: bool,
parser: &Parser,
parent_list_markers: &[ListItemMarker<'src>],
) -> Option<MatchedItem<'src, (Content<'src>, SimpleBlockStyle)>> {
let source_after_whitespace = source.discard_whitespace();
let first_line_indent = source_after_whitespace.col() - 1;
let mut indented_literal_mode = false;
let mut style = if source_after_whitespace.col() == source.col() || force_paragraph_style {
if source_after_whitespace.col() != source.col() {
indented_literal_mode = true;
}
SimpleBlockStyle::Paragraph
} else {
stop_for_list_items = false;
SimpleBlockStyle::Literal
};
if let Some(attrlist) = attrlist {
match attrlist.block_style() {
Some("normal") => {
style = SimpleBlockStyle::Paragraph;
}
Some("literal") => {
stop_for_list_items = false;
indented_literal_mode = false;
style = SimpleBlockStyle::Literal;
}
Some("listing") => {
stop_for_list_items = false;
indented_literal_mode = false;
style = SimpleBlockStyle::Listing;
}
Some("source") => {
stop_for_list_items = false;
indented_literal_mode = false;
style = SimpleBlockStyle::Source;
}
_ => {}
}
}
let comment_style = is_comment_style(attrlist.as_ref());
let mut next = source;
let mut filtered_lines: Vec<&'src str> = vec![];
let mut filtered_line_spans: Vec<Span<'src>> = vec![];
let mut skipped_comment_line = false;
let in_definition_list = parent_list_markers
.iter()
.any(|m| matches!(m, ListItemMarker::DefinedTerm { .. }));
let strip_indent =
if preserve_literal_indent && style == SimpleBlockStyle::Literal && in_definition_list {
let mut scan = source;
let mut min_indent = first_line_indent;
let mut line_count = 0;
while let Some(line_mi) = scan.take_non_empty_line() {
let line = line_mi.item;
if line_count > 0 && line.data() == "+" {
break;
}
if let Some(n) = line.position(|c| c != ' ' && c != '\t') {
min_indent = min_indent.min(n);
}
line_count += 1;
scan = line_mi.after;
}
min_indent
} else {
first_line_indent
};
while let Some(line_mi) = next.take_non_empty_line() {
let mut line = line_mi.item;
if !stop_for_list_items
&& skipped_comment_line
&& style == SimpleBlockStyle::Paragraph
&& is_section_header(line.data(), parser.level_offset())
{
break;
}
if !filtered_lines.is_empty() {
let should_check_for_list_marker =
stop_for_list_items && (!indented_literal_mode || line.col() == 1);
if should_check_for_list_marker
&& let Some(marker_mi) = ListItemMarker::parse(line, parser)
{
let is_ancestor_list = parent_list_markers
.iter()
.any(|p| p.is_match_for(&marker_mi.item));
if is_ancestor_list || !preserve_literal_indent {
break;
}
}
if line.data() == "+" {
break;
}
if line.starts_with('[') && line.ends_with(']') {
break;
}
if (line.starts_with('/')
|| line.starts_with('-')
|| line.starts_with('.')
|| line.starts_with('+')
|| line.starts_with('=')
|| line.starts_with('*')
|| line.starts_with('_')
|| line.starts_with('`'))
&& (RawDelimitedBlock::is_valid_delimiter(&line)
|| CompoundDelimitedBlock::is_valid_delimiter(&line))
{
break;
}
}
next = line_mi.after;
if !comment_style
&& style == SimpleBlockStyle::Paragraph
&& line.starts_with("//")
&& !line.starts_with("///")
{
skipped_comment_line = true;
continue;
}
let should_strip_indent = strip_indent > 0;
if should_strip_indent && let Some(n) = line.position(|c| c != ' ' && c != '\t') {
line = line.into_parse_result(n.min(strip_indent)).after;
};
let line = line.trim_trailing_whitespace();
filtered_line_spans.push(line);
filtered_lines.push(line.data());
}
let source = source.trim_remainder(next).trim_trailing_whitespace();
if source.is_empty() {
return None;
}
let mut content: Content<'src> =
Content::from_filtered_lines(source, &filtered_lines, filtered_line_spans);
let sub_group = if comment_style {
SubstitutionGroup::None
} else {
base_substitution_group(style).override_via_attrlist(attrlist.as_ref(), Some(parser))
};
sub_group.apply(&mut content, parser, attrlist.as_ref());
Some(MatchedItem {
item: (content, style),
after: next,
})
}
fn base_substitution_group(style: SimpleBlockStyle) -> SubstitutionGroup {
match style {
SimpleBlockStyle::Literal => SubstitutionGroup::Verbatim,
SimpleBlockStyle::Listing | SimpleBlockStyle::Source | SimpleBlockStyle::Paragraph => {
SubstitutionGroup::Normal
}
}
}
fn is_comment_style(attrlist: Option<&Attrlist<'_>>) -> bool {
attrlist.and_then(|attrlist| attrlist.block_style()) == Some("comment")
}
impl<'src> IsBlock<'src> for SimpleBlock<'src> {
fn content_model(&self) -> ContentModel {
ContentModel::Simple
}
fn content_mut(&mut self) -> Option<&mut Content<'src>> {
Some(&mut self.content)
}
fn rendered_content(&self) -> Option<&str> {
Some(self.content.rendered())
}
fn raw_context(&self) -> CowStr<'src> {
"paragraph".into()
}
fn title_source(&'src self) -> Option<Span<'src>> {
self.title_source
}
fn title(&self) -> Option<&str> {
self.title.as_ref().map(Content::rendered_str)
}
fn caption(&self) -> Option<&str> {
self.caption.as_deref()
}
fn number(&self) -> Option<usize> {
self.number
}
fn anchor(&'src self) -> Option<Span<'src>> {
self.anchor
}
fn anchor_reftext(&'src self) -> Option<Span<'src>> {
self.anchor_reftext
}
fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
self.attrlist.as_ref()
}
fn substitution_group(&'src self) -> SubstitutionGroup {
if is_comment_style(self.attrlist.as_ref()) {
SubstitutionGroup::None
} else {
base_substitution_group(self.style).override_via_attrlist(self.attrlist.as_ref(), None)
}
}
}
impl<'src> HasSpan<'src> for SimpleBlock<'src> {
fn span(&self) -> Span<'src> {
self.source
}
}
pub(crate) fn is_section_header(line: &str, level_offset: i32) -> bool {
let rest = if line.starts_with('=') {
line.trim_start_matches('=')
} else if line.starts_with('#') {
line.trim_start_matches('#')
} else {
return false;
};
let count = line.len() - rest.len();
if count == 0 || count > 6 || !rest.starts_with([' ', '\t']) {
return false;
}
let syntactic_level = (count - 1) as i32;
syntactic_level > 0 || syntactic_level.saturating_add(level_offset) >= 1
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use std::ops::Deref;
use crate::{
blocks::{ContentModel, SimpleBlockStyle, metadata::BlockMetadata},
tests::prelude::*,
};
#[test]
fn impl_clone() {
let mut parser = Parser::default();
let b1 =
crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
let b2 = b1.item.clone();
assert_eq!(b1.item, b2);
}
#[test]
fn style_enum_impl_debug() {
assert_eq!(
format!("{:?}", SimpleBlockStyle::Paragraph),
"SimpleBlockStyle::Paragraph"
);
assert_eq!(
format!("{:?}", SimpleBlockStyle::Literal),
"SimpleBlockStyle::Literal"
);
assert_eq!(
format!("{:?}", SimpleBlockStyle::Listing),
"SimpleBlockStyle::Listing"
);
assert_eq!(
format!("{:?}", SimpleBlockStyle::Source),
"SimpleBlockStyle::Source"
);
}
#[test]
fn empty_source() {
let mut parser = Parser::default();
assert!(crate::blocks::SimpleBlock::parse(&BlockMetadata::new(""), &mut parser).is_none());
}
#[test]
fn only_spaces() {
let mut parser = Parser::default();
assert!(
crate::blocks::SimpleBlock::parse(&BlockMetadata::new(" "), &mut parser).is_none()
);
}
#[test]
fn single_line() {
let mut parser = Parser::default();
let mi =
crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc"), &mut parser).unwrap();
assert_eq!(
mi.item,
SimpleBlock {
content: Content {
original: Span {
data: "abc",
line: 1,
col: 1,
offset: 0,
},
rendered: "abc",
},
source: Span {
data: "abc",
line: 1,
col: 1,
offset: 0,
},
style: SimpleBlockStyle::Paragraph,
title_source: None,
title: None,
caption: None,
number: None,
anchor: None,
anchor_reftext: None,
attrlist: None,
},
);
assert_eq!(mi.item.content_model(), ContentModel::Simple);
assert_eq!(mi.item.rendered_content().unwrap(), "abc");
assert_eq!(mi.item.raw_context().deref(), "paragraph");
assert_eq!(mi.item.resolved_context().deref(), "paragraph");
assert!(mi.item.declared_style().is_none());
assert!(mi.item.id().is_none());
assert!(mi.item.roles().is_empty());
assert!(mi.item.options().is_empty());
assert!(mi.item.title_source().is_none());
assert!(mi.item.title().is_none());
assert!(mi.item.anchor().is_none());
assert!(mi.item.anchor_reftext().is_none());
assert!(mi.item.attrlist().is_none());
assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
assert_eq!(
mi.after,
Span {
data: "",
line: 1,
col: 4,
offset: 3
}
);
}
#[test]
fn multiple_lines() {
let mut parser = Parser::default();
let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\ndef"), &mut parser)
.unwrap();
assert_eq!(
mi.item,
SimpleBlock {
content: Content {
original: Span {
data: "abc\ndef",
line: 1,
col: 1,
offset: 0,
},
rendered: "abc\ndef",
},
source: Span {
data: "abc\ndef",
line: 1,
col: 1,
offset: 0,
},
style: SimpleBlockStyle::Paragraph,
title_source: None,
title: None,
caption: None,
number: None,
anchor: None,
anchor_reftext: None,
attrlist: None,
}
);
assert_eq!(
mi.after,
Span {
data: "",
line: 2,
col: 4,
offset: 7
}
);
assert_eq!(mi.item.rendered_content().unwrap(), "abc\ndef");
}
#[test]
fn consumes_blank_lines_after() {
let mut parser = Parser::default();
let mi = crate::blocks::SimpleBlock::parse(&BlockMetadata::new("abc\n\ndef"), &mut parser)
.unwrap();
assert_eq!(
mi.item,
SimpleBlock {
content: Content {
original: Span {
data: "abc",
line: 1,
col: 1,
offset: 0,
},
rendered: "abc",
},
source: Span {
data: "abc",
line: 1,
col: 1,
offset: 0,
},
style: SimpleBlockStyle::Paragraph,
title_source: None,
title: None,
caption: None,
number: None,
anchor: None,
anchor_reftext: None,
attrlist: None,
}
);
assert_eq!(
mi.after,
Span {
data: "def",
line: 3,
col: 1,
offset: 5
}
);
}
#[test]
fn overrides_sub_group_via_subs_attribute() {
let mut parser = Parser::default();
let mi = crate::blocks::SimpleBlock::parse(
&BlockMetadata::new("[subs=quotes]\na<b>c *bold*\n\ndef"),
&mut parser,
)
.unwrap();
assert_eq!(
mi.item,
SimpleBlock {
content: Content {
original: Span {
data: "a<b>c *bold*",
line: 2,
col: 1,
offset: 14,
},
rendered: "a<b>c <strong>bold</strong>",
},
source: Span {
data: "[subs=quotes]\na<b>c *bold*",
line: 1,
col: 1,
offset: 0,
},
style: SimpleBlockStyle::Paragraph,
title_source: None,
title: None,
caption: None,
number: None,
anchor: None,
anchor_reftext: None,
attrlist: Some(Attrlist {
attributes: &[ElementAttribute {
name: Some("subs"),
value: "quotes",
shorthand_items: &[],
},],
anchor: None,
source: Span {
data: "subs=quotes",
line: 1,
col: 2,
offset: 1,
},
},),
}
);
assert_eq!(
mi.after,
Span {
data: "def",
line: 4,
col: 1,
offset: 28
}
);
assert_eq!(
mi.item.rendered_content().unwrap(),
"a<b>c <strong>bold</strong>"
);
}
mod is_section_header {
use super::super::is_section_header;
#[test]
fn multi_marker_is_always_a_section_regardless_of_offset() {
assert!(is_section_header("== Section", 0));
assert!(is_section_header("=== Section", 0));
assert!(is_section_header("## Section", 0));
assert!(is_section_header("== Section", -1));
}
#[test]
fn single_marker_is_a_section_only_under_positive_offset() {
assert!(!is_section_header("= Title", 0));
assert!(!is_section_header("# Title", 0));
assert!(is_section_header("= Title", 1));
assert!(is_section_header("# Title", 1));
assert!(is_section_header("= Title", 2));
}
#[test]
fn requires_a_blank_after_the_marker() {
assert!(!is_section_header("==nospace", 0));
assert!(!is_section_header("=nospace", 1));
assert!(!is_section_header("##nospace", 0));
}
#[test]
fn accepts_a_tab_after_the_marker() {
assert!(is_section_header("==\tSection", 0));
assert!(is_section_header("=\tSection", 1));
assert!(!is_section_header("=\tSection", 0));
}
#[test]
fn non_marker_and_over_long_marker_are_not_sections() {
assert!(!is_section_header("paragraph", 1));
assert!(!is_section_header("======= Too deep", 0));
}
}
}