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

View file

@ -2,22 +2,24 @@ use memchr::memchr;
#[inline]
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())
.filter(|&i| {
i != 4
&& text.as_bytes()["[fn:".len()..i]
.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
})
.map(|i| (&text["[fn:".len()..i], i + 1))?;
let (label, off) = memchr(b']', text.as_bytes())
.filter(|&i| {
i != 4
&& text.as_bytes()["[fn:".len()..i]
.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
})
.map(|i| (&text["[fn:".len()..i], i + 1))?;
let (content, off) = memchr(b'\n', text.as_bytes())
.map(|i| (&text[off..i], i))
.unwrap_or_else(|| (&text[off..], text.len()));
let (content, off) = memchr(b'\n', text.as_bytes())
.map(|i| (&text[off..i], i))
.unwrap_or_else(|| (&text[off..], text.len()));
Some((label, content, off))
Some((label, content, off))
} else {
None
}
}
#[cfg(test)]

View file

@ -1,16 +1,63 @@
use memchr::memchr_iter;
use std::iter::once;
// (indentation, ordered, limit, end)
#[inline]
pub fn is_item(text: &str) -> Option<(bool, &str)> {
if text.is_empty() {
return None;
pub fn parse(text: &str) -> Option<(usize, bool, usize, usize)> {
let (indent, tail) = text
.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();
match bytes[0] {
match bytes.get(0)? {
b'*' | b'-' | b'+' => {
if text.len() > 1 && (bytes[1] == b' ' || bytes[1] == b'\n') {
Some((false, &text[0..2]))
Some(false)
} else {
None
}
@ -21,10 +68,10 @@ pub fn is_item(text: &str) -> Option<(bool, &str)> {
.position(|&c| !c.is_ascii_digit())
.unwrap_or_else(|| text.len() - 1);
if (bytes[i] == b'.' || bytes[i] == b')')
&& i + 1 < text.len()
&& text.len() > i + 1
&& (bytes[i + 1] == b' ' || bytes[i + 1] == b'\n')
{
Some((true, &text[0..i + 2]))
Some(true)
} else {
None
}
@ -33,141 +80,79 @@ pub fn is_item(text: &str) -> Option<(bool, &str)> {
}
}
// check if list item ends at this line
#[inline]
fn is_item_ends(line: &str, ident: usize) -> Option<&str> {
debug_assert!(!line.is_empty());
let line_ident = line
.as_bytes()
.iter()
.position(|&c| c != b' ' && c != b'\t')
.unwrap_or(0);
debug_assert!(line_ident >= ident, "{} >= {}", line_ident, ident);
if line_ident == ident {
is_item(&line[ident..]).map(|(_, bullet)| bullet)
} else {
None
}
#[test]
fn test_is_item() {
assert_eq!(is_item("+ item"), Some(false));
assert_eq!(is_item("- item"), Some(false));
assert_eq!(is_item("10. item"), Some(true));
assert_eq!(is_item("10) item"), Some(true));
assert_eq!(is_item("1. item"), Some(true));
assert_eq!(is_item("1) item"), Some(true));
assert_eq!(is_item("10. "), Some(true));
assert_eq!(is_item("10.\n"), Some(true));
assert_eq!(is_item("10."), None);
assert_eq!(is_item("+"), None);
assert_eq!(is_item("-item"), None);
assert_eq!(is_item("+item"), 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("+"), None);
assert_eq!(is_item("-item"), None);
assert_eq!(is_item("+item"), None);
}
#[test]
fn parse() {
use super::parse;
assert_eq!(
parse("item1\n+ item2", 0),
("item1".len(), "item1\n".len(), Some("+ "))
);
assert_eq!(
parse("item1\n \n* item2", 0),
("item1".len(), "item1\n \n".len(), Some("* "))
);
assert_eq!(
parse("item1\n \n \n* item2", 0),
("item1".len(), "item1\n \n \n".len(), None)
);
assert_eq!(
parse("item1\n \n ", 0),
("item1".len(), "item1\n \n ".len(), None)
);
assert_eq!(
parse("item1\n + item2\n ", 0),
(
"item1\n + item2".len(),
"item1\n + item2\n ".len(),
None
)
);
assert_eq!(
parse("item1\n \n + item2\n \n+ item 3", 0),
(
"item1\n \n + item2".len(),
"item1\n \n + item2\n \n".len(),
Some("+ ")
)
);
assert_eq!(
parse("item1\n \n + item2", 2),
("item1".len(), "item1\n \n".len(), Some("+ "))
);
assert_eq!(
parse("1\n\n - 2\n\n - 3\n\n+ 4", 0),
(
"1\n\n - 2\n\n - 3".len(),
"1\n\n - 2\n\n - 3\n\n".len(),
Some("+ ")
)
);
}
#[test]
fn test_parse() {
assert_eq!(
parse("+ item1\n+ item2"),
Some((0, false, "+ item1\n+ item2".len(), "+ item1\n+ item2".len()))
);
assert_eq!(
parse("* item1\n \n* item2"),
Some((
0,
false,
"* item1\n \n* item2".len(),
"* item1\n \n* item2".len()
))
);
assert_eq!(
parse("* item1\n \n \n* item2"),
Some((0, false, "* item1\n".len(), "* item1\n \n \n".len()))
);
assert_eq!(
parse("* item1\n \n "),
Some((0, false, "+ item1\n".len(), "* item1\n \n ".len()))
);
assert_eq!(
parse("+ item1\n + item2\n "),
Some((
0,
false,
"+ item1\n + item2\n".len(),
"+ item1\n + item2\n ".len()
))
);
assert_eq!(
parse("+ item1\n \n + item2\n \n+ item 3"),
Some((
0,
false,
"+ item1\n \n + item2\n \n+ item 3".len(),
"+ item1\n \n + item2\n \n+ item 3".len()
))
);
assert_eq!(
parse(" + item1\n \n + item2"),
Some((
2,
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()
))
);
}