feat(parser): improve list parsing

This commit is contained in:
PoiScript 2019-05-17 21:27:01 +08:00
parent ecf0d7e67d
commit c4041aefb6
6 changed files with 208 additions and 210 deletions

View file

@ -25,20 +25,23 @@ pub enum Clock<'a> {
impl<'a> Clock<'a> { impl<'a> Clock<'a> {
pub(crate) fn parse(text: &'a str) -> Option<(Clock<'a>, usize)> { pub(crate) fn parse(text: &'a str) -> Option<(Clock<'a>, usize)> {
let (text, off) = memchr(b'\n', text.as_bytes()) let (text, eol) = memchr(b'\n', text.as_bytes())
.map(|i| (text[..i].trim(), i + 1)) .map(|i| (text[..i].trim(), i + 1))
.unwrap_or_else(|| (text.trim(), text.len())); .unwrap_or_else(|| (text.trim(), text.len()));
let tail = memchr(b' ', text.as_bytes()) if !text.starts_with("CLOCK:") {
.filter(|&i| &text[0..i] == "CLOCK:") return None;
.map(|i| text[i..].trim_start())?; }
let tail = &text["CLOCK:".len()..].trim_start();
if !tail.starts_with('[') { if !tail.starts_with('[') {
return None; return None;
} }
let (timestamp, tail) = let (timestamp, off) = Timestamp::parse_inactive(tail)?;
Timestamp::parse_inactive(tail).map(|(t, off)| (t, tail[off..].trim_start()))?;
let tail = tail[off..].trim();
match timestamp { match timestamp {
Timestamp::InactiveRange { Timestamp::InactiveRange {
@ -62,7 +65,7 @@ impl<'a> Clock<'a> {
delay, delay,
duration, duration,
}, },
off, eol,
)) ))
} else { } else {
None None
@ -72,20 +75,14 @@ impl<'a> Clock<'a> {
start, start,
repeater, repeater,
delay, delay,
} => { } if tail.is_empty() => Some((
if tail.as_bytes().iter().all(u8::is_ascii_whitespace) {
Some((
Clock::Running { Clock::Running {
start, start,
repeater, repeater,
delay, delay,
}, },
off, eol,
)) )),
} else {
None
}
}
_ => None, _ => None,
} }
} }

View file

@ -2,8 +2,7 @@ use memchr::memchr;
#[inline] #[inline]
pub fn parse(text: &str) -> Option<(&str, &str, usize)> { pub fn parse(text: &str) -> Option<(&str, &str, usize)> {
debug_assert!(text.starts_with("[fn:")); if text.starts_with("[fn:") {
let (label, off) = memchr(b']', text.as_bytes()) let (label, off) = memchr(b']', text.as_bytes())
.filter(|&i| { .filter(|&i| {
i != 4 i != 4
@ -18,6 +17,9 @@ pub fn parse(text: &str) -> Option<(&str, &str, usize)> {
.unwrap_or_else(|| (&text[off..], text.len())); .unwrap_or_else(|| (&text[off..], text.len()));
Some((label, content, off)) Some((label, content, off))
} else {
None
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,16 +1,63 @@
use memchr::memchr_iter; use memchr::memchr_iter;
use std::iter::once;
// (indentation, ordered, limit, end)
#[inline] #[inline]
pub fn is_item(text: &str) -> Option<(bool, &str)> { pub fn parse(text: &str) -> Option<(usize, bool, usize, usize)> {
if text.is_empty() { let (indent, tail) = text
return None; .find(|c| c != ' ')
.map(|off| (off, &text[off..]))
.unwrap_or((0, text));
let ordered = is_item(tail)?;
let bytes = text.as_bytes();
let mut lines = memchr_iter(b'\n', bytes)
.map(|i| i + 1)
.chain(once(text.len()));
let mut pos = lines.next()?;
while let Some(i) = lines.next() {
let line = &text[pos..i];
return if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) {
// this line is no empty
if line_indent < indent
|| (line_indent == indent && is_item(&line[line_indent..]).is_none())
{
Some((indent, ordered, pos, pos))
} else {
pos = i;
continue;
}
} else if let Some(next_i) = lines.next() {
// this line is empty
let line = &text[i..next_i];
if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) {
if line_indent < indent
|| (line_indent == indent && is_item(&line[line_indent..]).is_none())
{
Some((indent, ordered, pos, pos))
} else {
pos = next_i;
continue;
}
} else {
Some((indent, ordered, pos, next_i))
}
} else {
Some((indent, ordered, pos, i))
};
} }
Some((indent, ordered, pos, pos))
}
#[inline]
pub fn is_item(text: &str) -> Option<bool> {
let bytes = text.as_bytes(); let bytes = text.as_bytes();
match bytes[0] { match bytes.get(0)? {
b'*' | b'-' | b'+' => { b'*' | b'-' | b'+' => {
if text.len() > 1 && (bytes[1] == b' ' || bytes[1] == b'\n') { if text.len() > 1 && (bytes[1] == b' ' || bytes[1] == b'\n') {
Some((false, &text[0..2])) Some(false)
} else { } else {
None None
} }
@ -21,10 +68,10 @@ pub fn is_item(text: &str) -> Option<(bool, &str)> {
.position(|&c| !c.is_ascii_digit()) .position(|&c| !c.is_ascii_digit())
.unwrap_or_else(|| text.len() - 1); .unwrap_or_else(|| text.len() - 1);
if (bytes[i] == b'.' || bytes[i] == b')') if (bytes[i] == b'.' || bytes[i] == b')')
&& i + 1 < text.len() && text.len() > i + 1
&& (bytes[i + 1] == b' ' || bytes[i + 1] == b'\n') && (bytes[i + 1] == b' ' || bytes[i + 1] == b'\n')
{ {
Some((true, &text[0..i + 2])) Some(true)
} else { } else {
None None
} }
@ -33,141 +80,79 @@ pub fn is_item(text: &str) -> Option<(bool, &str)> {
} }
} }
// check if list item ends at this line #[test]
#[inline] fn test_is_item() {
fn is_item_ends(line: &str, ident: usize) -> Option<&str> { assert_eq!(is_item("+ item"), Some(false));
debug_assert!(!line.is_empty()); assert_eq!(is_item("- item"), Some(false));
assert_eq!(is_item("10. item"), Some(true));
let line_ident = line assert_eq!(is_item("10) item"), Some(true));
.as_bytes() assert_eq!(is_item("1. item"), Some(true));
.iter() assert_eq!(is_item("1) item"), Some(true));
.position(|&c| c != b' ' && c != b'\t') assert_eq!(is_item("10. "), Some(true));
.unwrap_or(0); assert_eq!(is_item("10.\n"), Some(true));
debug_assert!(line_ident >= ident, "{} >= {}", line_ident, ident);
if line_ident == ident {
is_item(&line[ident..]).map(|(_, bullet)| bullet)
} else {
None
}
}
// return (limit, end, next item bullet)
#[inline]
pub fn parse(text: &str, ident: usize) -> (usize, usize, Option<&str>) {
let bytes = text.as_bytes();
let mut lines = memchr_iter(b'\n', bytes);
let mut pos = if let Some(i) = lines.next() {
i + 1
} else {
return (text.len(), text.len(), None);
};
while let Some(i) = lines.next() {
return if bytes[pos..i].iter().all(u8::is_ascii_whitespace) {
if let Some(nexti) = lines.next() {
if bytes[i + 1..nexti].iter().all(u8::is_ascii_whitespace) {
// two consecutive empty lines
(pos - 1, nexti + 1, None)
} else if let Some(next) = is_item_ends(&text[i + 1..nexti], ident) {
(pos - 1, i + 1, Some(next))
} else {
pos = nexti + 1;
continue;
}
} else if bytes[i + 1..].iter().all(u8::is_ascii_whitespace) {
// two consecutive empty lines
(pos - 1, text.len(), None)
} else if let Some(next) = is_item_ends(&text[i + 1..], ident) {
(pos - 1, i + 1, Some(next))
} else {
(text.len(), text.len(), None)
}
} else if let Some(next) = is_item_ends(&text[pos..i], ident) {
(pos - 1, pos, Some(next))
} else {
pos = i + 1;
continue;
};
}
if bytes[pos..].iter().all(u8::is_ascii_whitespace) {
(pos - 1, text.len(), None)
} else if let Some(next) = is_item_ends(&text[pos..], ident) {
(pos - 1, pos, Some(next))
} else {
(text.len(), text.len(), None)
}
}
#[cfg(test)]
mod tests {
#[test]
fn is_item() {
use super::is_item;
assert_eq!(is_item("+ item"), Some((false, "+ ")));
assert_eq!(is_item("- item"), Some((false, "- ")));
assert_eq!(is_item("10. item"), Some((true, "10. ")));
assert_eq!(is_item("10) item"), Some((true, "10) ")));
assert_eq!(is_item("1. item"), Some((true, "1. ")));
assert_eq!(is_item("1) item"), Some((true, "1) ")));
assert_eq!(is_item("10. "), Some((true, "10. ")));
assert_eq!(is_item("10.\n"), Some((true, "10.\n")));
assert_eq!(is_item("10."), None); assert_eq!(is_item("10."), None);
assert_eq!(is_item("+"), None); assert_eq!(is_item("+"), None);
assert_eq!(is_item("-item"), None); assert_eq!(is_item("-item"), None);
assert_eq!(is_item("+item"), None); assert_eq!(is_item("+item"), None);
} }
#[test] #[test]
fn parse() { fn test_parse() {
use super::parse; assert_eq!(
parse("+ item1\n+ item2"),
assert_eq!( Some((0, false, "+ item1\n+ item2".len(), "+ item1\n+ item2".len()))
parse("item1\n+ item2", 0), );
("item1".len(), "item1\n".len(), Some("+ ")) assert_eq!(
); parse("* item1\n \n* item2"),
assert_eq!( Some((
parse("item1\n \n* item2", 0), 0,
("item1".len(), "item1\n \n".len(), Some("* ")) false,
); "* item1\n \n* item2".len(),
assert_eq!( "* item1\n \n* item2".len()
parse("item1\n \n \n* item2", 0), ))
("item1".len(), "item1\n \n \n".len(), None) );
); assert_eq!(
assert_eq!( parse("* item1\n \n \n* item2"),
parse("item1\n \n ", 0), Some((0, false, "* item1\n".len(), "* item1\n \n \n".len()))
("item1".len(), "item1\n \n ".len(), None) );
); assert_eq!(
assert_eq!( parse("* item1\n \n "),
parse("item1\n + item2\n ", 0), Some((0, false, "+ item1\n".len(), "* item1\n \n ".len()))
( );
"item1\n + item2".len(), assert_eq!(
"item1\n + item2\n ".len(), parse("+ item1\n + item2\n "),
None Some((
) 0,
); false,
assert_eq!( "+ item1\n + item2\n".len(),
parse("item1\n \n + item2\n \n+ item 3", 0), "+ item1\n + item2\n ".len()
( ))
"item1\n \n + item2".len(), );
"item1\n \n + item2\n \n".len(), assert_eq!(
Some("+ ") parse("+ item1\n \n + item2\n \n+ item 3"),
) Some((
); 0,
assert_eq!( false,
parse("item1\n \n + item2", 2), "+ item1\n \n + item2\n \n+ item 3".len(),
("item1".len(), "item1\n \n".len(), Some("+ ")) "+ item1\n \n + item2\n \n+ item 3".len()
); ))
assert_eq!( );
parse("1\n\n - 2\n\n - 3\n\n+ 4", 0), assert_eq!(
( parse(" + item1\n \n + item2"),
"1\n\n - 2\n\n - 3".len(), Some((
"1\n\n - 2\n\n - 3\n\n".len(), 2,
Some("+ ") false,
) " + item1\n \n + item2".len(),
); " + item1\n \n + item2".len()
} ))
);
assert_eq!(
parse("+ 1\n\n - 2\n\n - 3\n\n+ 4"),
Some((
0,
false,
"+ 1\n\n - 2\n\n - 3\n\n+ 4".len(),
"+ 1\n\n - 2\n\n - 3\n\n+ 4".len()
))
);
} }

View file

@ -106,14 +106,14 @@ pub trait HtmlHandler<W: Write, E: From<Error>> {
fn dyn_block_end(&mut self, w: &mut W) -> Result<(), E> { fn dyn_block_end(&mut self, w: &mut W) -> Result<(), E> {
Ok(()) Ok(())
} }
fn list_beg(&mut self, w: &mut W, ordered: bool) -> Result<(), E> { fn list_beg(&mut self, w: &mut W, _indent: usize, ordered: bool) -> Result<(), E> {
if ordered { if ordered {
Ok(write!(w, "<ol>")?) Ok(write!(w, "<ol>")?)
} else { } else {
Ok(write!(w, "<ul>")?) Ok(write!(w, "<ul>")?)
} }
} }
fn list_end(&mut self, w: &mut W, ordered: bool) -> Result<(), E> { fn list_end(&mut self, w: &mut W, _indent: usize, ordered: bool) -> Result<(), E> {
if ordered { if ordered {
Ok(write!(w, "</ol>")?) Ok(write!(w, "</ol>")?)
} else { } else {

View file

@ -25,8 +25,8 @@ macro_rules! handle_event {
VerseBlock { cont, args } => $handler.verse_block($writer, cont, args)?, VerseBlock { cont, args } => $handler.verse_block($writer, cont, args)?,
DynBlockBeg { name, args } => $handler.dyn_block_beg($writer, name, args)?, DynBlockBeg { name, args } => $handler.dyn_block_beg($writer, name, args)?,
DynBlockEnd => $handler.dyn_block_end($writer)?, DynBlockEnd => $handler.dyn_block_end($writer)?,
ListBeg { ordered } => $handler.list_beg($writer, ordered)?, ListBeg { indent, ordered } => $handler.list_beg($writer, indent, ordered)?,
ListEnd { ordered } => $handler.list_end($writer, ordered)?, ListEnd { indent, ordered } => $handler.list_end($writer, indent, ordered)?,
ListItemBeg { bullet } => $handler.list_beg_item($writer, bullet)?, ListItemBeg { bullet } => $handler.list_beg_item($writer, bullet)?,
ListItemEnd => $handler.list_end_item($writer)?, ListItemEnd => $handler.list_end_item($writer)?,
Call { value } => $handler.call($writer, value)?, Call { value } => $handler.call($writer, value)?,

View file

@ -70,9 +70,11 @@ pub enum Event<'a> {
}, },
ListBeg { ListBeg {
indent: usize,
ordered: bool, ordered: bool,
}, },
ListEnd { ListEnd {
indent: usize,
ordered: bool, ordered: bool,
}, },
ListItemBeg { ListItemBeg {
@ -138,7 +140,6 @@ pub enum Event<'a> {
pub struct Parser<'a> { pub struct Parser<'a> {
text: &'a str, text: &'a str,
stack: Vec<(Container, usize, usize)>, stack: Vec<(Container, usize, usize)>,
next_item: Vec<Option<&'a str>>,
off: usize, off: usize,
ele_buf: Option<(Event<'a>, usize, usize, usize)>, ele_buf: Option<(Event<'a>, usize, usize, usize)>,
obj_buf: Option<(Event<'a>, usize, usize, usize)>, obj_buf: Option<(Event<'a>, usize, usize, usize)>,
@ -151,7 +152,6 @@ impl<'a> Parser<'a> {
Parser { Parser {
text, text,
stack: Vec::new(), stack: Vec::new(),
next_item: Vec::new(),
off: 0, off: 0,
ele_buf: None, ele_buf: None,
obj_buf: None, obj_buf: None,
@ -164,7 +164,6 @@ impl<'a> Parser<'a> {
Parser { Parser {
text, text,
stack: Vec::new(), stack: Vec::new(),
next_item: Vec::new(),
off: 0, off: 0,
ele_buf: None, ele_buf: None,
obj_buf: None, obj_buf: None,
@ -191,7 +190,6 @@ impl<'a> Parser<'a> {
pub fn set_text(&mut self, text: &'a str) { pub fn set_text(&mut self, text: &'a str) {
self.off = 0; self.off = 0;
self.stack.clear(); self.stack.clear();
self.next_item.clear();
self.ele_buf = None; self.ele_buf = None;
self.obj_buf = None; self.obj_buf = None;
self.text = text; self.text = text;
@ -208,7 +206,7 @@ impl<'a> Parser<'a> {
Container::DynBlock => Event::DynBlockEnd, Container::DynBlock => Event::DynBlockEnd,
Container::Headline(_) => Event::HeadlineEnd, Container::Headline(_) => Event::HeadlineEnd,
Container::Italic => Event::ItalicEnd, Container::Italic => Event::ItalicEnd,
Container::List(_, ordered) => Event::ListEnd { ordered }, Container::List(indent, ordered) => Event::ListEnd { indent, ordered },
Container::ListItem => Event::ListItemEnd, Container::ListItem => Event::ListItemEnd,
Container::Paragraph => Event::ParagraphEnd, Container::Paragraph => Event::ParagraphEnd,
Container::QteBlock => Event::QteBlockEnd, Container::QteBlock => Event::QteBlockEnd,
@ -300,8 +298,8 @@ impl<'a> Parser<'a> {
Event::CtrBlockBeg => self.push_stack(Container::CtrBlock, limit, end), Event::CtrBlockBeg => self.push_stack(Container::CtrBlock, limit, end),
Event::SplBlockBeg { .. } => self.push_stack(Container::SplBlock, limit, end), Event::SplBlockBeg { .. } => self.push_stack(Container::SplBlock, limit, end),
Event::DynBlockBeg { .. } => self.push_stack(Container::DynBlock, limit, end), Event::DynBlockBeg { .. } => self.push_stack(Container::DynBlock, limit, end),
Event::ListBeg { ordered, .. } => { Event::ListBeg { ordered, indent } => {
self.push_stack(Container::List(limit, ordered), end, end) self.push_stack(Container::List(indent, ordered), limit, end)
} }
_ => (), _ => (),
} }
@ -315,10 +313,10 @@ impl<'a> Parser<'a> {
fn real_next_ele(&mut self, text: &'a str) -> Option<(Event<'a>, usize, usize, usize)> { fn real_next_ele(&mut self, text: &'a str) -> Option<(Event<'a>, usize, usize, usize)> {
debug_assert!(!text.starts_with('\n')); debug_assert!(!text.starts_with('\n'));
if text.starts_with("[fn:") {
if let Some((label, cont, off)) = fn_def::parse(text) { if let Some((label, cont, off)) = fn_def::parse(text) {
return Some((Event::FnDef { label, cont }, off + 1, 0, 0)); return Some((Event::FnDef { label, cont }, off + 1, 0, 0));
} } else if let Some((indent, ordered, limit, end)) = list::parse(text) {
return Some((Event::ListBeg { indent, ordered }, 0, limit, end));
} }
let (tail, line_begin) = text let (tail, line_begin) = text
@ -326,16 +324,9 @@ impl<'a> Parser<'a> {
.map(|off| (&text[off..], off)) .map(|off| (&text[off..], off))
.unwrap_or((text, 0)); .unwrap_or((text, 0));
if let Some((ordered, bullet)) = list::is_item(tail) {
self.next_item.push(Some(bullet));
return Some((Event::ListBeg { ordered }, 0, line_begin, text.len()));
}
if tail.starts_with("CLOCK:") {
if let Some((clock, off)) = Clock::parse(tail) { if let Some((clock, off)) = Clock::parse(tail) {
return Some((Event::Clock(clock), off + line_begin, 0, 0)); return Some((Event::Clock(clock), off + line_begin, 0, 0));
} }
}
// TODO: LaTeX environment // TODO: LaTeX environment
if tail.starts_with("\\begin{") {} if tail.starts_with("\\begin{") {}
@ -556,6 +547,31 @@ impl<'a> Parser<'a> {
} }
} }
fn next_list_item(&self, text: &'a str, indent: usize) -> (&'a str, usize, usize, usize) {
use std::iter::once;
debug_assert!(&text[0..indent].trim().is_empty());
let off = &text[indent..].find(' ').unwrap() + 1 + indent;
let bytes = text.as_bytes();
let mut lines = memchr_iter(b'\n', bytes)
.map(|i| i + 1)
.chain(once(text.len()));
let mut pos = lines.next().unwrap();
while let Some(i) = lines.next() {
let line = &text[pos..i];
if let Some(line_indent) = line.find(|c: char| !c.is_whitespace()) {
if line_indent == indent {
return (&text[indent..off], off, pos, pos);
}
}
pos = i;
}
(&text[indent..off], off, text.len(), text.len())
}
#[inline] #[inline]
fn push_stack(&mut self, container: Container, limit: usize, end: usize) { fn push_stack(&mut self, container: Container, limit: usize, end: usize) {
self.stack self.stack
@ -572,7 +588,7 @@ impl<'a> Parser<'a> {
Container::DynBlock => Event::DynBlockEnd, Container::DynBlock => Event::DynBlockEnd,
Container::Headline(_) => Event::HeadlineEnd, Container::Headline(_) => Event::HeadlineEnd,
Container::Italic => Event::ItalicEnd, Container::Italic => Event::ItalicEnd,
Container::List(_, ordered) => Event::ListEnd { ordered }, Container::List(indent, ordered) => Event::ListEnd { indent, ordered },
Container::ListItem => Event::ListItemEnd, Container::ListItem => Event::ListItemEnd,
Container::Paragraph => Event::ParagraphEnd, Container::Paragraph => Event::ParagraphEnd,
Container::QteBlock => Event::QteBlockEnd, Container::QteBlock => Event::QteBlockEnd,
@ -602,7 +618,7 @@ impl<'a> Iterator for Parser<'a> {
let tail = &self.text[self.off..limit]; let tail = &self.text[self.off..limit];
// eprintln!("{:?} {:?} {:?}", container, tail, self.next_item); // eprintln!("{:?} {:?}", container, tail);
Some(match container { Some(match container {
Container::Headline(beg) => { Container::Headline(beg) => {
@ -646,18 +662,16 @@ impl<'a> Iterator for Parser<'a> {
self.next_ele(tail) self.next_ele(tail)
} }
} }
Container::List(ident, ordered) => { Container::List(indent, ordered) => {
if let Some(bullet) = self.next_item.pop().unwrap() { if self.off < limit {
let off = bullet.len() + ident; let (bullet, off, limit, end) = self.next_list_item(tail, indent);
self.off += off;
let (limit, end, next) = list::parse(&tail[off..], ident);
self.push_stack(Container::ListItem, limit, end); self.push_stack(Container::ListItem, limit, end);
self.next_item.push(next); self.off += off;
Event::ListItemBeg { bullet } Event::ListItemBeg { bullet }
} else { } else {
self.off = end; self.off = end;
self.stack.pop(); self.stack.pop();
Event::ListEnd { ordered } Event::ListEnd { indent, ordered }
} }
} }
Container::Paragraph Container::Paragraph