block: add MeteredBlock as intermediate struct

This commit is contained in:
Noah Hellman 2023-01-29 09:24:46 +01:00
parent 1c77b035b2
commit dcb3b787a2
3 changed files with 460 additions and 291 deletions

View file

@ -85,9 +85,6 @@ pub enum Container {
/// Span is class specifier, possibly empty. /// Span is class specifier, possibly empty.
Div, Div,
/// Span is `:`.
DescriptionList,
/// Span is the list marker of the first list item in the list. /// Span is the list marker of the first list item in the list.
List { ty: ListType, tight: bool }, List { ty: ListType, tight: bool },
@ -112,6 +109,7 @@ pub enum ListType {
Unordered(u8), Unordered(u8),
Ordered(crate::OrderedListNumbering, crate::OrderedListStyle), Ordered(crate::OrderedListNumbering, crate::OrderedListStyle),
Task, Task,
Description,
} }
#[derive(Debug)] #[derive(Debug)]
@ -136,7 +134,7 @@ struct TreeParser<'s> {
/// Stack of currently open lists. /// Stack of currently open lists.
open_lists: Vec<OpenList>, open_lists: Vec<OpenList>,
/// Stack of currently open sections. /// Stack of currently open sections.
open_sections: Vec<u32>, open_sections: Vec<usize>,
/// Alignments for each column in for the current table. /// Alignments for each column in for the current table.
alignments: Vec<Alignment>, alignments: Vec<Alignment>,
} }
@ -176,29 +174,30 @@ impl<'s> TreeParser<'s> {
/// Recursively parse a block and all of its children. Return number of lines the block uses. /// Recursively parse a block and all of its children. Return number of lines the block uses.
fn parse_block(&mut self, lines: &mut [Span], top_level: bool) -> usize { fn parse_block(&mut self, lines: &mut [Span], top_level: bool) -> usize {
BlockParser::parse(lines.iter().map(|sp| sp.of(self.src))).map_or( if let Some(MeteredBlock {
0, kind,
|(indent, kind, span, line_count)| { span,
let truncated = line_count > lines.len(); line_count,
let l = line_count.min(lines.len()); }) = MeteredBlock::new(lines.iter().map(|sp| sp.of(self.src)))
let lines = &mut lines[..l]; {
let lines = &mut lines[..line_count];
let span = span.translate(lines[0].start()); let span = span.translate(lines[0].start());
// skip part of first inline that is shared with the block span // skip part of first inline that is shared with the block span
lines[0] = lines[0].with_start(span.end()); lines[0] = lines[0].with_start(span.end());
// remove junk from footnotes / link defs // remove "]:" from footnote / link def
if matches!( if matches!(kind, Kind::Definition { .. }) {
kind,
Block::Leaf(LinkDefinition) | Block::Container(Footnote { .. })
) {
assert_eq!(&lines[0].of(self.src).chars().as_str()[0..2], "]:"); assert_eq!(&lines[0].of(self.src).chars().as_str()[0..2], "]:");
lines[0] = lines[0].skip(2); lines[0] = lines[0].skip(2);
} }
// skip fences of code blocks / divs // skip opening and closing fence of code block / div
let lines = if matches!(kind, Block::Leaf(CodeBlock) | Block::Container(Div)) { let lines = if let Kind::Fenced {
let l = lines.len() - usize::from(!truncated); has_closing_fence, ..
} = kind
{
let l = lines.len() - usize::from(has_closing_fence);
&mut lines[1..l] &mut lines[1..l]
} else { } else {
lines lines
@ -208,10 +207,7 @@ impl<'s> TreeParser<'s> {
if let Some(OpenList { ty, depth, .. }) = self.open_lists.last() { if let Some(OpenList { ty, depth, .. }) = self.open_lists.last() {
assert!(usize::from(*depth) <= self.tree.depth()); assert!(usize::from(*depth) <= self.tree.depth());
if self.tree.depth() == (*depth).into() if self.tree.depth() == (*depth).into()
&& !matches!( && !matches!(kind, Kind::ListItem{ ty: ty_new, .. } if *ty == ty_new)
kind,
Block::Container(Container::ListItem(ty_new)) if *ty == ty_new,
)
{ {
self.tree.exit(); // list self.tree.exit(); // list
self.open_lists.pop(); self.open_lists.pop();
@ -219,13 +215,13 @@ impl<'s> TreeParser<'s> {
} }
// set list to loose if blankline discovered // set list to loose if blankline discovered
if matches!(kind, Block::Atom(Atom::Blankline)) { if matches!(kind, Kind::Atom(Atom::Blankline)) {
self.prev_blankline = true; self.prev_blankline = true;
} else { } else {
if self.prev_blankline { if self.prev_blankline {
for OpenList { node, depth, .. } in &self.open_lists { for OpenList { node, depth, .. } in &self.open_lists {
if usize::from(*depth) < self.tree.depth() if usize::from(*depth) < self.tree.depth()
&& matches!(kind, Block::Container(Container::ListItem { .. })) && matches!(kind, Kind::ListItem { .. })
{ {
continue; continue;
} }
@ -243,30 +239,31 @@ impl<'s> TreeParser<'s> {
self.prev_blankline = false; self.prev_blankline = false;
} }
match kind { match Block::from(&kind) {
Block::Atom(a) => self.tree.atom(a, span), Block::Atom(a) => self.tree.atom(a, span),
Block::Leaf(l) => self.parse_leaf(l, lines, span, indent, top_level), Block::Leaf(l) => self.parse_leaf(l, &kind, span, lines, top_level),
Block::Container(Table) => self.parse_table(lines, span), Block::Container(Table) => self.parse_table(lines, span),
Block::Container(c) => self.parse_container(c, lines, span, indent), Block::Container(c) => self.parse_container(c, &kind, span, lines),
} }
line_count line_count
}, } else {
) 0
}
} }
fn parse_leaf( fn parse_leaf(
&mut self, &mut self,
l: Leaf, leaf: Leaf,
lines: &mut [Span], k: &Kind,
span: Span, span: Span,
indent: usize, lines: &mut [Span],
top_level: bool, top_level: bool,
) { ) {
if matches!(l, Leaf::CodeBlock) { if let Kind::Fenced { indent, .. } = k {
for line in lines.iter_mut() { for line in lines.iter_mut() {
let indent_line = line.len() - line.trim_start(self.src).len(); let indent_line = line.len() - line.trim_start(self.src).len();
*line = line.skip(indent.min(indent_line)); *line = line.skip((*indent).min(indent_line));
} }
} else { } else {
// trim starting whitespace of each inline // trim starting whitespace of each inline
@ -282,33 +279,35 @@ impl<'s> TreeParser<'s> {
} }
} }
if matches!(l, Leaf::Heading) && top_level { if let Kind::Heading { level, .. } = k {
let level = u32::try_from(span.len()).unwrap(); // open and close sections
if top_level {
let first_close = self let first_close = self
.open_sections .open_sections
.iter() .iter()
.rposition(|l| *l < level) .rposition(|l| l < level)
.map_or(0, |i| i + 1); .map_or(0, |i| i + 1);
self.open_sections.drain(first_close..).for_each(|_| { self.open_sections.drain(first_close..).for_each(|_| {
self.tree.exit(); // section self.tree.exit(); // section
}); });
self.open_sections.push(level); self.open_sections.push(*level);
self.tree.enter(Node::Container(Section), span); self.tree.enter(Node::Container(Section), span);
} }
}
self.tree.enter(Node::Leaf(l), span); self.tree.enter(Node::Leaf(leaf), span);
lines.iter().for_each(|line| self.tree.inline(*line)); lines.iter().for_each(|line| self.tree.inline(*line));
self.tree.exit(); self.tree.exit();
} }
fn parse_container(&mut self, c: Container, lines: &mut [Span], span: Span, indent: usize) { fn parse_container(&mut self, c: Container, k: &Kind, span: Span, lines: &mut [Span]) {
// update spans, remove indentation / container prefix // update spans, remove indentation / container prefix
lines.iter_mut().skip(1).for_each(|sp| { lines.iter_mut().skip(1).for_each(|sp| {
let src = sp.of(self.src); let src = sp.of(self.src);
let src_t = src.trim(); let src_t = src.trim();
let spaces = src.len() - src.trim_start().len(); let spaces = src.len() - src.trim_start().len();
let skip = match c { let skip = match k {
Blockquote => { Kind::Blockquote => {
if src_t == ">" { if src_t == ">" {
spaces + 1 spaces + 1
} else if src_t.starts_with("> ") { } else if src_t.starts_with("> ") {
@ -317,16 +316,16 @@ impl<'s> TreeParser<'s> {
0 0
} }
} }
ListItem(..) | Footnote | Div => spaces.min(indent), Kind::ListItem { indent, .. }
List { .. } | DescriptionList | Table | TableRow { .. } | Section => { | Kind::Definition { indent, .. }
panic!() | Kind::Fenced { indent, .. } => spaces.min(*indent),
} _ => panic!("non-container {:?}", k),
}; };
let len = sp.len() - usize::from(sp.of(self.src).ends_with('\n')); let len = sp.len() - usize::from(sp.of(self.src).ends_with('\n'));
*sp = sp.skip(skip.min(len)); *sp = sp.skip(skip.min(len));
}); });
if let Container::ListItem(ty) = c { if let ListItem(ty) = c {
if self if self
.open_lists .open_lists
.last() .last()
@ -500,141 +499,201 @@ impl<'s> TreeParser<'s> {
} }
/// Parser for a single block. /// Parser for a single block.
struct BlockParser { struct MeteredBlock {
indent: usize, kind: Kind,
kind: Block,
span: Span, span: Span,
fence: Option<(char, usize)>, line_count: usize,
caption: bool,
} }
impl BlockParser { impl MeteredBlock {
/// Parse a single block. Return number of lines the block uses. /// Identify and measure the line length of a single block.
fn parse<'s, I: Iterator<Item = &'s str>>(mut lines: I) -> Option<(usize, Block, Span, usize)> { fn new<'s, I: Iterator<Item = &'s str>>(mut lines: I) -> Option<Self> {
lines.next().map(|l| { lines.next().map(|l| {
let mut p = BlockParser::new(l); let IdentifiedBlock { mut kind, span } = IdentifiedBlock::new(l);
let has_end_delimiter = let line_count = 1 + lines.take_while(|l| kind.continues(l)).count();
matches!(p.kind, Block::Leaf(CodeBlock) | Block::Container(Div)); Self {
let line_count_match = lines.take_while(|l| p.continues(l)).count(); kind,
let line_count = 1 + line_count_match + usize::from(has_end_delimiter); span,
(p.indent, p.kind, p.span, line_count) line_count,
}
}) })
} }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FenceKind {
Div,
CodeBlock(u8),
}
#[cfg_attr(test, derive(PartialEq, Eq))]
#[derive(Debug)]
enum Kind {
Atom(Atom),
Paragraph,
Heading {
level: usize,
},
Fenced {
indent: usize,
fence_length: usize,
kind: FenceKind,
has_spec: bool,
has_closing_fence: bool,
},
Definition {
indent: usize,
footnote: bool,
},
Blockquote,
ListItem {
indent: usize,
ty: ListType,
},
Table {
caption: bool,
},
}
struct IdentifiedBlock {
kind: Kind,
span: Span,
}
impl From<&Kind> for Block {
fn from(kind: &Kind) -> Self {
match kind {
Kind::Atom(a) => Self::Atom(*a),
Kind::Paragraph => Self::Leaf(Paragraph),
Kind::Heading { .. } => Self::Leaf(Heading),
Kind::Fenced {
kind: FenceKind::CodeBlock(..),
..
} => Self::Leaf(CodeBlock),
Kind::Fenced {
kind: FenceKind::Div,
..
} => Self::Container(Div),
Kind::Definition {
footnote: false, ..
} => Self::Leaf(LinkDefinition),
Kind::Definition { footnote: true, .. } => Self::Container(Footnote),
Kind::Blockquote => Self::Container(Blockquote),
Kind::ListItem { ty, .. } => Self::Container(ListItem(*ty)),
Kind::Table { .. } => Self::Container(Table),
}
}
}
impl IdentifiedBlock {
fn new(line: &str) -> Self { fn new(line: &str) -> Self {
let start = line let indent = line
.chars() .chars()
.take_while(|c| *c != '\n' && c.is_whitespace()) .take_while(|c| *c != '\n' && c.is_whitespace())
.map(char::len_utf8) .map(char::len_utf8)
.sum(); .sum();
let line_t = &line[start..]; let line = &line[indent..];
let mut chars = line_t.chars(); let line_t = line.trim_end();
let l = line.len();
let lt = line_t.len();
let mut fence = None; let mut chars = line.chars();
let (kind, span) = match chars.next().unwrap_or(EOF) { match chars.next().unwrap_or(EOF) {
EOF => Some((Block::Atom(Blankline), Span::empty_at(start))), EOF => Some((Kind::Atom(Blankline), Span::empty_at(indent))),
'\n' => Some((Block::Atom(Blankline), Span::by_len(start, 1))), '\n' => Some((Kind::Atom(Blankline), Span::by_len(indent, 1))),
'#' => chars '#' => chars
.find(|c| *c != '#') .find(|c| *c != '#')
.map_or(true, char::is_whitespace) .map_or(true, char::is_whitespace)
.then(|| { .then(|| {
( let level = l - chars.as_str().len() - 1;
Block::Leaf(Heading), (Kind::Heading { level }, Span::by_len(indent, level))
Span::by_len(start, line_t.len() - chars.as_str().len() - 1),
)
}), }),
'>' => { '>' => chars
if let Some(c) = chars.next() { .next()
c.is_whitespace().then(|| { .map_or(Some(false), |c| c.is_whitespace().then(|| true))
( .map(|space_after| {
Block::Container(Blockquote), let len = l - chars.as_str().len() - usize::from(space_after);
Span::by_len(start, line_t.len() - chars.as_str().len() - 1), (Kind::Blockquote, Span::by_len(indent, len))
) }),
}) '{' => (attr::valid(line.chars()).0 == lt)
} else { .then(|| (Kind::Atom(Attributes), Span::by_len(indent, l))),
Some((
Block::Container(Blockquote),
Span::by_len(start, line_t.len() - chars.as_str().len()),
))
}
}
'{' => (attr::valid(line_t.chars()).0 == line_t.trim_end().len())
.then(|| (Block::Atom(Attributes), Span::by_len(start, line_t.len()))),
'|' => { '|' => {
let l = line_t.trim_end().len();
// FIXME: last byte may be pipe but end of prefixed unicode char // FIXME: last byte may be pipe but end of prefixed unicode char
(line_t.as_bytes()[l - 1] == b'|' && line_t.as_bytes()[l - 2] != b'\\') (line.as_bytes()[lt - 1] == b'|' && line.as_bytes()[lt - 2] != b'\\')
.then(|| (Block::Container(Table), Span::empty_at(start))) .then(|| (Kind::Table { caption: false }, Span::empty_at(indent)))
} }
'[' => chars.as_str().find("]:").map(|l| { '[' => chars.as_str().find("]:").map(|l| {
let tag = &chars.as_str()[0..l]; let tag = &chars.as_str()[0..l];
let (tag, is_footnote) = if let Some(tag) = tag.strip_prefix('^') { let footnote = tag.starts_with('^');
(tag, true)
} else {
(tag, false)
};
( (
if is_footnote { Kind::Definition { indent, footnote },
Block::Container(Footnote) Span::by_len(indent + 1, l).skip(usize::from(footnote)),
} else {
Block::Leaf(LinkDefinition)
},
Span::from_slice(line, tag),
) )
}), }),
'-' | '*' if Self::is_thematic_break(chars.clone()) => Some(( '-' | '*' if Self::is_thematic_break(chars.clone()) => {
Block::Atom(ThematicBreak), Some((Kind::Atom(ThematicBreak), Span::by_len(indent, lt)))
Span::from_slice(line, line_t.trim()), }
)),
b @ ('-' | '*' | '+') => chars.next().map_or(true, char::is_whitespace).then(|| { b @ ('-' | '*' | '+') => chars.next().map_or(true, char::is_whitespace).then(|| {
let task_list = chars.next() == Some('[') let task_list = chars.next() == Some('[')
&& matches!(chars.next(), Some('x' | 'X' | ' ')) && matches!(chars.next(), Some('x' | 'X' | ' '))
&& chars.next() == Some(']') && chars.next() == Some(']')
&& chars.next().map_or(true, char::is_whitespace); && chars.next().map_or(true, char::is_whitespace);
if task_list { if task_list {
(Block::Container(ListItem(Task)), Span::by_len(start, 5)) (Kind::ListItem { indent, ty: Task }, Span::by_len(indent, 5))
} else { } else {
( (
Block::Container(ListItem(Unordered(b as u8))), Kind::ListItem {
Span::by_len(start, 1), indent,
ty: Unordered(b as u8),
},
Span::by_len(indent, 1),
) )
} }
}), }),
':' if chars.clone().next().map_or(true, char::is_whitespace) => { ':' if chars.clone().next().map_or(true, char::is_whitespace) => Some((
Some((Block::Container(DescriptionList), Span::by_len(start, 1))) Kind::ListItem {
} indent,
ty: Description,
},
Span::by_len(indent, 1),
)),
f @ ('`' | ':' | '~') => { f @ ('`' | ':' | '~') => {
let fence_length = (&mut chars).take_while(|c| *c == f).count() + 1; let fence_length = 1 + (&mut chars).take_while(|c| *c == f).count();
fence = Some((f, fence_length)); let spec = &line_t[fence_length..].trim_start();
let lang = line_t[fence_length..].trim();
let valid_spec = let valid_spec =
!lang.chars().any(char::is_whitespace) && !lang.chars().any(|c| c == '`'); !spec.chars().any(char::is_whitespace) && !spec.chars().any(|c| c == '`');
let skip = line_t.len() - spec.len();
(valid_spec && fence_length >= 3).then(|| { (valid_spec && fence_length >= 3).then(|| {
( (
match f { Kind::Fenced {
':' => Block::Container(Div), indent,
_ => Block::Leaf(CodeBlock), fence_length,
kind: match f {
':' => FenceKind::Div,
_ => FenceKind::CodeBlock(f as u8),
}, },
Span::from_slice(line, lang), has_spec: !spec.is_empty(),
has_closing_fence: false,
},
Span::by_len(indent + skip, spec.len()),
) )
}) })
} }
c => Self::maybe_ordered_list_item(c, &mut chars).map(|(num, style, len)| { c => Self::maybe_ordered_list_item(c, chars).map(|(num, style, len)| {
( (
Block::Container(ListItem(Ordered(num, style))), Kind::ListItem {
Span::by_len(start, len), indent,
ty: Ordered(num, style),
},
Span::by_len(indent, len),
) )
}), }),
} }
.unwrap_or((Block::Leaf(Paragraph), Span::new(0, 0))); .map(|(kind, span)| Self { kind, span })
.unwrap_or(Self {
Self { kind: Kind::Paragraph,
indent: start, span: Span::empty_at(indent),
kind, })
span,
fence,
caption: false,
}
} }
fn is_thematic_break(chars: std::str::Chars) -> bool { fn is_thematic_break(chars: std::str::Chars) -> bool {
@ -649,60 +708,9 @@ impl BlockParser {
n >= 3 n >= 3
} }
/// Determine if this line continues the block.
fn continues(&mut self, line: &str) -> bool {
let line_t = line.trim();
let empty = line_t.is_empty();
match self.kind {
Block::Atom(..) => false,
Block::Leaf(Paragraph | Heading) | Block::Container(Blockquote) => !empty,
Block::Leaf(LinkDefinition) => line.starts_with(' ') && !empty,
Block::Container(ListItem(..)) => {
let spaces = line.chars().take_while(|c| c.is_whitespace()).count();
empty
|| spaces > self.indent
|| matches!(
Self::parse(std::iter::once(line)),
Some((_, Block::Leaf(Leaf::Paragraph), _, _)),
)
}
Block::Container(Footnote) => {
let spaces = line.chars().take_while(|c| c.is_whitespace()).count();
empty || spaces > self.indent
}
Block::Container(Div) | Block::Leaf(CodeBlock) => {
let (fence_char, l0) = self.fence.unwrap();
let l1 = line_t.len();
let is_end_fence = line_t.chars().all(|c| c == fence_char)
&& (l0 == l1 || (l0 < l1 && matches!(self.kind, Block::Container(..))));
!is_end_fence
}
Block::Container(Table) if self.caption => !empty,
Block::Container(Table) => {
let l = line_t.len();
match l {
0 => true,
1..=2 => false,
_ => {
if line_t.starts_with("^ ") {
self.caption = true;
true
} else {
line_t.as_bytes()[l - 1] == b'|' && line_t.as_bytes()[l - 2] != b'\\'
}
}
}
}
Block::Container(List { .. } | DescriptionList | TableRow { .. } | Section)
| Block::Leaf(TableCell(..) | Caption) => {
panic!()
}
}
}
fn maybe_ordered_list_item( fn maybe_ordered_list_item(
mut first: char, mut first: char,
chars: &mut std::str::Chars, mut chars: std::str::Chars,
) -> Option<(crate::OrderedListNumbering, crate::OrderedListStyle, usize)> { ) -> Option<(crate::OrderedListNumbering, crate::OrderedListStyle, usize)> {
fn is_roman_lower_digit(c: char) -> bool { fn is_roman_lower_digit(c: char) -> bool {
matches!(c, 'i' | 'v' | 'x' | 'l' | 'c' | 'd' | 'm') matches!(c, 'i' | 'v' | 'x' | 'l' | 'c' | 'd' | 'm')
@ -778,6 +786,68 @@ impl BlockParser {
} }
} }
impl Kind {
/// Determine if a line continues the block.
fn continues(&mut self, line: &str) -> bool {
let IdentifiedBlock { kind: next, .. } = IdentifiedBlock::new(line);
match self {
Self::Atom(..)
| Self::Fenced {
has_closing_fence: true,
..
} => false,
Self::Blockquote => matches!(next, Self::Blockquote | Self::Paragraph),
Self::Heading { .. } => matches!(next, Self::Paragraph),
Self::Paragraph | Self::Table { caption: true } => {
!matches!(next, Self::Atom(Blankline))
}
Self::ListItem { indent, .. } => {
let spaces = line.chars().take_while(|c| c.is_whitespace()).count();
matches!(next, Self::Atom(Blankline))
|| spaces > *indent
|| matches!(next, Self::Paragraph)
}
Self::Definition { indent, footnote } => {
if *footnote {
let spaces = line.chars().take_while(|c| c.is_whitespace()).count();
matches!(next, Self::Atom(Blankline)) || spaces > *indent
} else {
line.starts_with(' ') && !matches!(next, Self::Atom(Blankline))
}
}
Self::Fenced {
fence_length,
kind,
has_closing_fence,
..
} => {
if let Kind::Fenced {
kind: k,
fence_length: l,
has_spec: false,
..
} = next
{
*has_closing_fence = k == *kind
&& (l == *fence_length
|| (matches!(k, FenceKind::Div) && l > *fence_length));
}
true
}
Self::Table { caption } => {
matches!(next, Self::Table { .. } | Self::Atom(Blankline)) || {
if line.trim().starts_with("^ ") {
*caption = true;
true
} else {
false
}
}
}
}
}
}
impl std::fmt::Display for Block { impl std::fmt::Display for Block {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -822,8 +892,9 @@ mod test {
use crate::OrderedListStyle::*; use crate::OrderedListStyle::*;
use super::Atom::*; use super::Atom::*;
use super::Block;
use super::Container::*; use super::Container::*;
use super::FenceKind;
use super::Kind;
use super::Leaf::*; use super::Leaf::*;
use super::ListType::*; use super::ListType::*;
use super::Node::*; use super::Node::*;
@ -1677,9 +1748,9 @@ mod test {
macro_rules! test_block { macro_rules! test_block {
($src:expr, $kind:expr, $str:expr, $len:expr $(,)?) => { ($src:expr, $kind:expr, $str:expr, $len:expr $(,)?) => {
let lines = super::lines($src).map(|sp| sp.of($src)); let lines = super::lines($src).map(|sp| sp.of($src));
let (_indent, kind, sp, len) = super::BlockParser::parse(lines).unwrap(); let mb = super::MeteredBlock::new(lines).unwrap();
assert_eq!( assert_eq!(
(kind, sp.of($src), len), (mb.kind, mb.span.of($src), mb.line_count),
($kind, $str, $len), ($kind, $str, $len),
"\n\n{}\n\n", "\n\n{}\n\n",
$src $src
@ -1689,15 +1760,15 @@ mod test {
#[test] #[test]
fn block_blankline() { fn block_blankline() {
test_block!("\n", Block::Atom(Blankline), "\n", 1); test_block!("\n", Kind::Atom(Blankline), "\n", 1);
test_block!(" \n", Block::Atom(Blankline), "\n", 1); test_block!(" \n", Kind::Atom(Blankline), "\n", 1);
} }
#[test] #[test]
fn block_multiline() { fn block_multiline() {
test_block!( test_block!(
"# heading\n spanning two lines\n", "# heading\n spanning two lines\n",
Block::Leaf(Heading), Kind::Heading { level: 1 },
"#", "#",
2 2
); );
@ -1713,7 +1784,7 @@ mod test {
">\n", // ">\n", //
"> c\n", // "> c\n", //
), ),
Block::Container(Blockquote), Kind::Blockquote,
">", ">",
5, 5,
); );
@ -1721,14 +1792,14 @@ mod test {
#[test] #[test]
fn block_thematic_break() { fn block_thematic_break() {
test_block!("---\n", Block::Atom(ThematicBreak), "---", 1); test_block!("---\n", Kind::Atom(ThematicBreak), "---", 1);
test_block!( test_block!(
concat!( concat!(
" -*- -*-\n", " -*- -*-\n",
"\n", "\n",
"para", // "para", //
), ),
Block::Atom(ThematicBreak), Kind::Atom(ThematicBreak),
"-*- -*-", "-*- -*-",
1 1
); );
@ -1744,7 +1815,13 @@ mod test {
" l1\n", " l1\n",
"````", // "````", //
), ),
Block::Leaf(CodeBlock), Kind::Fenced {
indent: 0,
kind: FenceKind::CodeBlock(b'`'),
fence_length: 4,
has_spec: true,
has_closing_fence: true,
},
"lang", "lang",
5, 5,
); );
@ -1757,7 +1834,13 @@ mod test {
"bbb\n", // "bbb\n", //
"```\n", // "```\n", //
), ),
Block::Leaf(CodeBlock), Kind::Fenced {
indent: 0,
kind: FenceKind::CodeBlock(b'`'),
fence_length: 3,
has_spec: false,
has_closing_fence: true,
},
"", "",
3, 3,
); );
@ -1767,7 +1850,7 @@ mod test {
"l0\n", "l0\n",
"```\n", // "```\n", //
), ),
Block::Leaf(Paragraph), Kind::Paragraph,
"", "",
3, 3,
); );
@ -1775,7 +1858,15 @@ mod test {
#[test] #[test]
fn block_link_definition() { fn block_link_definition() {
test_block!("[tag]: url\n", Block::Leaf(LinkDefinition), "tag", 1); test_block!(
"[tag]: url\n",
Kind::Definition {
indent: 0,
footnote: false
},
"tag",
1
);
} }
#[test] #[test]
@ -1785,7 +1876,10 @@ mod test {
"[tag]: uuu\n", "[tag]: uuu\n",
" rl\n", // " rl\n", //
), ),
Block::Leaf(LinkDefinition), Kind::Definition {
indent: 0,
footnote: false
},
"tag", "tag",
2, 2,
); );
@ -1794,7 +1888,10 @@ mod test {
"[tag]: url\n", "[tag]: url\n",
"para\n", // "para\n", //
), ),
Block::Leaf(LinkDefinition), Kind::Definition {
indent: 0,
footnote: false
},
"tag", "tag",
1, 1,
); );
@ -1802,12 +1899,28 @@ mod test {
#[test] #[test]
fn block_footnote_empty() { fn block_footnote_empty() {
test_block!("[^tag]:\n", Block::Container(Footnote), "tag", 1); test_block!(
"[^tag]:\n",
Kind::Definition {
indent: 0,
footnote: true
},
"tag",
1
);
} }
#[test] #[test]
fn block_footnote_single() { fn block_footnote_single() {
test_block!("[^tag]: a\n", Block::Container(Footnote), "tag", 1); test_block!(
"[^tag]: a\n",
Kind::Definition {
indent: 0,
footnote: true
},
"tag",
1
);
} }
#[test] #[test]
@ -1817,7 +1930,10 @@ mod test {
"[^tag]: a\n", "[^tag]: a\n",
" b\n", // " b\n", //
), ),
Block::Container(Footnote), Kind::Definition {
indent: 0,
footnote: true
},
"tag", "tag",
2, 2,
); );
@ -1832,7 +1948,10 @@ mod test {
"\n", "\n",
"para\n", // "para\n", //
), ),
Block::Container(Footnote), Kind::Definition {
indent: 0,
footnote: true
},
"tag", "tag",
3, 3,
); );
@ -1842,71 +1961,117 @@ mod test {
fn block_list_bullet() { fn block_list_bullet() {
test_block!( test_block!(
"- abc\n", "- abc\n",
Block::Container(ListItem(Unordered(b'-'))), Kind::ListItem {
indent: 0,
ty: Unordered(b'-')
},
"-", "-",
1 1
); );
test_block!( test_block!(
"+ abc\n", "+ abc\n",
Block::Container(ListItem(Unordered(b'+'))), Kind::ListItem {
indent: 0,
ty: Unordered(b'+')
},
"+", "+",
1 1
); );
test_block!( test_block!(
"* abc\n", "* abc\n",
Block::Container(ListItem(Unordered(b'*'))), Kind::ListItem {
indent: 0,
ty: Unordered(b'*')
},
"*", "*",
1 1
); );
} }
#[test]
fn block_list_description() {
test_block!(": abc\n", Block::Container(DescriptionList), ":", 1);
}
#[test] #[test]
fn block_list_task() { fn block_list_task() {
test_block!("- [ ] abc\n", Block::Container(ListItem(Task)), "- [ ]", 1); test_block!(
test_block!("+ [x] abc\n", Block::Container(ListItem(Task)), "+ [x]", 1); "- [ ] abc\n",
test_block!("* [X] abc\n", Block::Container(ListItem(Task)), "* [X]", 1); Kind::ListItem {
indent: 0,
ty: Task
},
"- [ ]",
1
);
test_block!(
"+ [x] abc\n",
Kind::ListItem {
indent: 0,
ty: Task
},
"+ [x]",
1
);
test_block!(
"* [X] abc\n",
Kind::ListItem {
indent: 0,
ty: Task
},
"* [X]",
1
);
} }
#[test] #[test]
fn block_list_ordered() { fn block_list_ordered() {
test_block!( test_block!(
"123. abc\n", "123. abc\n",
Block::Container(ListItem(Ordered(Decimal, Period))), Kind::ListItem {
indent: 0,
ty: Ordered(Decimal, Period)
},
"123.", "123.",
1 1
); );
test_block!( test_block!(
"i. abc\n", "i. abc\n",
Block::Container(ListItem(Ordered(RomanLower, Period))), Kind::ListItem {
indent: 0,
ty: Ordered(RomanLower, Period)
},
"i.", "i.",
1 1
); );
test_block!( test_block!(
"I. abc\n", "I. abc\n",
Block::Container(ListItem(Ordered(RomanUpper, Period))), Kind::ListItem {
indent: 0,
ty: Ordered(RomanUpper, Period)
},
"I.", "I.",
1 1
); );
test_block!( test_block!(
"IJ. abc\n", "IJ. abc\n",
Block::Container(ListItem(Ordered(AlphaUpper, Period))), Kind::ListItem {
indent: 0,
ty: Ordered(AlphaUpper, Period)
},
"IJ.", "IJ.",
1 1
); );
test_block!( test_block!(
"(a) abc\n", "(a) abc\n",
Block::Container(ListItem(Ordered(AlphaLower, ParenParen))), Kind::ListItem {
indent: 0,
ty: Ordered(AlphaLower, ParenParen)
},
"(a)", "(a)",
1 1
); );
test_block!( test_block!(
"a) abc\n", "a) abc\n",
Block::Container(ListItem(Ordered(AlphaLower, Paren))), Kind::ListItem {
indent: 0,
ty: Ordered(AlphaLower, Paren)
},
"a)", "a)",
1 1
); );

View file

@ -589,24 +589,28 @@ impl<'s> Parser<'s> {
attributes = Attributes::new(); attributes = Attributes::new();
continue; continue;
} }
block::Container::DescriptionList => Container::DescriptionList,
block::Container::List { ty, tight } => { block::Container::List { ty, tight } => {
if matches!(ty, block::ListType::Description) {
Container::DescriptionList
} else {
let kind = match ty { let kind = match ty {
block::ListType::Unordered(..) => ListKind::Unordered, block::ListType::Unordered(..) => ListKind::Unordered,
block::ListType::Task => ListKind::Task,
block::ListType::Ordered(numbering, style) => { block::ListType::Ordered(numbering, style) => {
let marker = ev.span.of(self.src); let start = numbering
let start = .parse_number(style.number(content))
numbering.parse_number(style.number(marker)).max(1); .max(1);
ListKind::Ordered { ListKind::Ordered {
numbering, numbering,
style, style,
start, start,
} }
} }
block::ListType::Task => ListKind::Task, block::ListType::Description => unreachable!(),
}; };
Container::List { kind, tight } Container::List { kind, tight }
} }
}
block::Container::ListItem(ty) => { block::Container::ListItem(ty) => {
if matches!(ty, block::ListType::Task) { if matches!(ty, block::ListType::Task) {
let marker = ev.span.of(self.src); let marker = ev.span.of(self.src);

View file

@ -89,10 +89,6 @@ impl Span {
&s[self.start()..self.end()] &s[self.start()..self.end()]
} }
pub fn from_slice(s: &str, slice: &str) -> Self {
Self::by_len(slice.as_ptr() as usize - s.as_ptr() as usize, slice.len())
}
pub fn trim_start(self, s: &str) -> Self { pub fn trim_start(self, s: &str) -> Self {
Self::from_slice(s, self.of(s).trim_start()) Self::from_slice(s, self.of(s).trim_start())
} }
@ -104,6 +100,10 @@ impl Span {
pub fn trim(self, s: &str) -> Self { pub fn trim(self, s: &str) -> Self {
Self::from_slice(s, self.of(s).trim_start().trim_end()) Self::from_slice(s, self.of(s).trim_start().trim_end())
} }
fn from_slice(s: &str, slice: &str) -> Self {
Self::by_len(slice.as_ptr() as usize - s.as_ptr() as usize, slice.len())
}
} }
pub trait DiscontinuousString<'s> { pub trait DiscontinuousString<'s> {