feat(headline): support custom keywords

This commit is contained in:
PoiScript 2019-02-15 11:32:52 +08:00
parent 88e1f8d62d
commit c7de340479
2 changed files with 159 additions and 113 deletions

View file

@ -2,6 +2,9 @@
use memchr::memchr2; use memchr::memchr2;
const HEADLINE_DEFAULT_KEYWORDS: &'static [&'static str] =
&["TODO", "DONE", "NEXT", "WAITING", "LATER", "CANCELLED"];
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq))]
#[derive(Debug)] #[derive(Debug)]
pub struct Headline<'a> { pub struct Headline<'a> {
@ -35,10 +38,10 @@ impl<'a> Headline<'a> {
} }
#[inline] #[inline]
fn parse_keyword(src: &'a str) -> Option<(&'a str, usize)> { fn parse_keyword(src: &'a str, keywords: &'a [&'a str]) -> Option<(&'a str, usize)> {
let pos = memchr2(b' ', b'\n', src.as_bytes()).unwrap_or_else(|| src.len()); let pos = memchr2(b' ', b'\n', src.as_bytes()).unwrap_or_else(|| src.len());
let word = &src[0..pos]; let word = &src[0..pos];
if word.as_bytes().iter().all(|&c| c.is_ascii_uppercase()) && word != "COMMENT" { if keywords.contains(&word) {
Some((word, pos)) Some((word, pos))
} else { } else {
None None
@ -74,6 +77,13 @@ impl<'a> Headline<'a> {
/// assert_eq!(hdl.keyword, Some("DONE")); /// assert_eq!(hdl.keyword, Some("DONE"));
/// ``` /// ```
pub fn parse(src: &'a str) -> (Headline<'a>, usize, usize) { pub fn parse(src: &'a str) -> (Headline<'a>, usize, usize) {
Self::parse_with_keywords(src, HEADLINE_DEFAULT_KEYWORDS)
}
pub fn parse_with_keywords(
src: &'a str,
keywords: &'a [&'a str],
) -> (Headline<'a>, usize, usize) {
let level = memchr2(b'\n', b' ', src.as_bytes()).unwrap_or_else(|| src.len()); let level = memchr2(b'\n', b' ', src.as_bytes()).unwrap_or_else(|| src.len());
debug_assert!(level > 0); debug_assert!(level > 0);
@ -85,7 +95,7 @@ impl<'a> Headline<'a> {
let mut title_start = skip_space!(src, level); let mut title_start = skip_space!(src, level);
let keyword = Headline::parse_keyword(&src[title_start..eol]).map(|(k, l)| { let keyword = Headline::parse_keyword(&src[title_start..eol], keywords).map(|(k, l)| {
title_start += l; title_start += l;
k k
}); });
@ -153,119 +163,145 @@ impl<'a> Headline<'a> {
} }
} }
#[test] #[cfg(test)]
fn parse() { mod tests {
assert_eq!( use super::Headline;
Headline::parse("**** TODO [#A] COMMENT Title :tag:a2%:").0,
Headline {
level: 4,
priority: Some('A'),
keyword: Some("TODO"),
title: "COMMENT Title",
tags: Some(":tag:a2%:"),
},
);
assert_eq!(
Headline::parse("**** ToDO [#A] COMMENT Title :tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: Some(":tag:a2%:"),
title: "ToDO [#A] COMMENT Title",
keyword: None,
},
);
assert_eq!(
Headline::parse("**** T0DO [#A] COMMENT Title :tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: Some(":tag:a2%:"),
title: "T0DO [#A] COMMENT Title",
keyword: None,
},
);
assert_eq!(
Headline::parse("**** TODO [#1] COMMENT Title :tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: Some(":tag:a2%:"),
title: "[#1] COMMENT Title",
keyword: Some("TODO")
},
);
assert_eq!(
Headline::parse("**** TODO [#a] COMMENT Title :tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: Some(":tag:a2%:"),
title: "[#a] COMMENT Title",
keyword: Some("TODO")
},
);
assert_eq!(
Headline::parse("**** TODO [#A] COMMENT Title :tag:a2%").0,
Headline {
level: 4,
priority: Some('A'),
tags: None,
title: "COMMENT Title :tag:a2%",
keyword: Some("TODO"),
},
);
assert_eq!(
Headline::parse("**** TODO [#A] COMMENT Title tag:a2%:").0,
Headline {
level: 4,
priority: Some('A'),
tags: None,
title: "COMMENT Title tag:a2%:",
keyword: Some("TODO"),
},
);
assert_eq!(
Headline::parse("**** COMMENT Title tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: None,
title: "COMMENT Title tag:a2%:",
keyword: None,
},
);
}
#[test] #[test]
fn is_commented() { fn parse() {
assert!(Headline::parse("* COMMENT Title").0.is_commented()); assert_eq!(
assert!(!Headline::parse("* Title").0.is_commented()); Headline::parse("**** TODO [#A] COMMENT Title :tag:a2%:").0,
assert!(!Headline::parse("* C0MMENT Title").0.is_commented()); Headline {
assert!(!Headline::parse("* comment Title").0.is_commented()); level: 4,
} priority: Some('A'),
keyword: Some("TODO"),
title: "COMMENT Title",
tags: Some(":tag:a2%:"),
},
);
assert_eq!(
Headline::parse("**** ToDO [#A] COMMENT Title :tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: Some(":tag:a2%:"),
title: "ToDO [#A] COMMENT Title",
keyword: None,
},
);
assert_eq!(
Headline::parse("**** T0DO [#A] COMMENT Title :tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: Some(":tag:a2%:"),
title: "T0DO [#A] COMMENT Title",
keyword: None,
},
);
assert_eq!(
Headline::parse("**** TODO [#1] COMMENT Title :tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: Some(":tag:a2%:"),
title: "[#1] COMMENT Title",
keyword: Some("TODO")
},
);
assert_eq!(
Headline::parse("**** TODO [#a] COMMENT Title :tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: Some(":tag:a2%:"),
title: "[#a] COMMENT Title",
keyword: Some("TODO")
},
);
assert_eq!(
Headline::parse("**** TODO [#A] COMMENT Title :tag:a2%").0,
Headline {
level: 4,
priority: Some('A'),
tags: None,
title: "COMMENT Title :tag:a2%",
keyword: Some("TODO"),
},
);
assert_eq!(
Headline::parse("**** TODO [#A] COMMENT Title tag:a2%:").0,
Headline {
level: 4,
priority: Some('A'),
tags: None,
title: "COMMENT Title tag:a2%:",
keyword: Some("TODO"),
},
);
assert_eq!(
Headline::parse("**** COMMENT Title tag:a2%:").0,
Headline {
level: 4,
priority: None,
tags: None,
title: "COMMENT Title tag:a2%:",
keyword: None,
},
);
#[test] assert_eq!(
fn is_archived() { Headline::parse_with_keywords("**** TODO [#A] COMMENT Title :tag:a2%:", &[]).0,
assert!(Headline::parse("* Title :ARCHIVE:").0.is_archived()); Headline {
assert!(Headline::parse("* Title :tag:ARCHIVE:").0.is_archived()); level: 4,
assert!(Headline::parse("* Title :ARCHIVE:tag:").0.is_archived()); priority: None,
assert!(!Headline::parse("* Title").0.is_commented()); keyword: None,
assert!(!Headline::parse("* Title :ARCHIVED:").0.is_archived()); title: "TODO [#A] COMMENT Title",
assert!(!Headline::parse("* Title :ARCHIVES:").0.is_archived()); tags: Some(":tag:a2%:"),
assert!(!Headline::parse("* Title :archive:").0.is_archived()); },
} );
assert_eq!(
Headline::parse_with_keywords("**** TASK [#A] COMMENT Title :tag:a2%:", &["TASK"]).0,
Headline {
level: 4,
priority: Some('A'),
keyword: Some("TASK"),
title: "COMMENT Title",
tags: Some(":tag:a2%:"),
},
);
}
#[test] #[test]
fn find_level() { fn is_commented() {
assert_eq!( assert!(Headline::parse("* COMMENT Title").0.is_commented());
Headline::find_level( assert!(!Headline::parse("* Title").0.is_commented());
r#" assert!(!Headline::parse("* C0MMENT Title").0.is_commented());
assert!(!Headline::parse("* comment Title").0.is_commented());
}
#[test]
fn is_archived() {
assert!(Headline::parse("* Title :ARCHIVE:").0.is_archived());
assert!(Headline::parse("* Title :tag:ARCHIVE:").0.is_archived());
assert!(Headline::parse("* Title :ARCHIVE:tag:").0.is_archived());
assert!(!Headline::parse("* Title").0.is_commented());
assert!(!Headline::parse("* Title :ARCHIVED:").0.is_archived());
assert!(!Headline::parse("* Title :ARCHIVES:").0.is_archived());
assert!(!Headline::parse("* Title :archive:").0.is_archived());
}
#[test]
fn find_level() {
assert_eq!(
Headline::find_level(
r#"
** Title ** Title
* Title * Title
** Title"#, ** Title"#,
1 1
), ),
10 10
); );
}
} }

View file

@ -200,6 +200,7 @@ pub struct Parser<'a> {
ele_buf: Option<(Element<'a>, usize)>, ele_buf: Option<(Element<'a>, usize)>,
obj_buf: Option<(Object<'a>, usize)>, obj_buf: Option<(Object<'a>, usize)>,
has_more_item: bool, has_more_item: bool,
keywords: Option<&'a [&'a str]>,
} }
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
@ -212,6 +213,7 @@ impl<'a> Parser<'a> {
ele_buf: None, ele_buf: None,
obj_buf: None, obj_buf: None,
has_more_item: false, has_more_item: false,
keywords: None,
} }
} }
@ -225,6 +227,10 @@ impl<'a> Parser<'a> {
self.stack.len() self.stack.len()
} }
pub fn set_keywords(&mut self, keywords: &'a [&'a str]) {
self.keywords = Some(keywords)
}
fn next_sec_or_hdl(&mut self) -> Event<'a> { fn next_sec_or_hdl(&mut self) -> Event<'a> {
let end = Headline::find_level(&self.text[self.off..], std::usize::MAX); let end = Headline::find_level(&self.text[self.off..], std::usize::MAX);
debug_assert!(end <= self.text[self.off..].len()); debug_assert!(end <= self.text[self.off..].len());
@ -239,7 +245,11 @@ impl<'a> Parser<'a> {
} }
fn next_hdl(&mut self) -> Event<'a> { fn next_hdl(&mut self) -> Event<'a> {
let (hdl, off, end) = Headline::parse(&self.text[self.off..]); let (hdl, off, end) = if let Some(keywords) = self.keywords {
Headline::parse_with_keywords(&self.text[self.off..], keywords)
} else {
Headline::parse(&self.text[self.off..])
};
debug_assert!(end <= self.text[self.off..].len()); debug_assert!(end <= self.text[self.off..].len());
self.stack.push(Container::Headline { self.stack.push(Container::Headline {
beg: self.off + off, beg: self.off + off,