orgize/src/headline.rs

269 lines
7.2 KiB
Rust
Raw Normal View History

2019-01-10 12:58:13 +00:00
#[cfg_attr(test, derive(PartialEq, Debug))]
2019-01-04 08:53:20 +00:00
pub struct Headline<'a> {
pub level: usize,
pub priority: Option<char>,
pub tags: Option<&'a str>,
pub title: &'a str,
pub keyword: Option<&'a str>,
}
impl<'a> Headline<'a> {
#[inline]
fn parse_priority(src: &str) -> Option<char> {
let bytes = src.as_bytes();
if bytes.len() > 4
&& bytes[0] == b'['
&& bytes[1] == b'#'
&& bytes[2].is_ascii_uppercase()
&& bytes[3] == b']'
&& bytes[4] == b' '
{
Some(bytes[2] as char)
} else {
None
}
}
#[inline]
fn parse_keyword(src: &'a str) -> Option<(&'a str, usize)> {
let mut pos = 0;
while pos < src.len() {
if src.as_bytes()[pos] == b' ' {
break;
} else if src.as_bytes()[pos].is_ascii_uppercase() {
pos += 1;
} else {
return None;
}
}
if pos == src.len() || src[0..pos] == *"COMMENT" {
None
} else {
Some((&src[0..pos], pos))
}
}
#[inline]
fn parse_tags(src: &'a str) -> (Option<&'a str>, usize) {
if let Some(last) = src.split_whitespace().last() {
if last.starts_with(':') && last.ends_with(':') {
(Some(last), src.rfind(':').unwrap() - last.len())
} else {
(None, src.len())
}
} else {
(None, src.len())
}
}
pub fn parse(src: &'a str) -> (Headline<'a>, usize, usize) {
let mut level = 0;
loop {
if src.as_bytes()[level] == b'*' {
level += 1;
} else {
break;
}
}
let eol = eol!(src);
let end = Headline::find_level(&src[eol..], level) + eol;
2019-01-10 12:58:13 +00:00
let mut title_start = skip_space!(src, level);
2019-01-04 08:53:20 +00:00
let keyword = match Headline::parse_keyword(&src[title_start..eol]) {
Some((k, l)) => {
title_start += l;
Some(k)
}
None => None,
};
2019-01-10 12:58:13 +00:00
title_start = skip_space!(src, title_start);
2019-01-04 08:53:20 +00:00
let priority = match Headline::parse_priority(&src[title_start..eol]) {
Some(p) => {
title_start += 4;
Some(p)
}
None => None,
};
2019-01-10 12:58:13 +00:00
title_start = skip_space!(src, title_start);
2019-01-04 08:53:20 +00:00
let (tags, title_off) = Headline::parse_tags(&src[title_start..eol]);
// println!("{:?} {:?} {:?}", keyword, priority, tags);
// println!("{:?} {}", title_start, title_off);
(
Headline::new(
level,
keyword,
priority,
&src[title_start..title_start + title_off],
tags,
),
eol,
end,
)
}
// TODO: optimize
pub fn find_level(src: &str, level: usize) -> usize {
let mut pos = 0;
2019-01-11 14:35:06 +00:00
loop {
2019-01-04 08:53:20 +00:00
if pos >= src.len() {
2019-01-10 12:58:13 +00:00
return src.len();
2019-01-04 08:53:20 +00:00
}
if src.as_bytes()[pos] == b'*' && (pos == 0 || src.as_bytes()[pos - 1] == b'\n') {
let pos_ = pos;
2019-01-11 14:35:06 +00:00
loop {
2019-01-04 08:53:20 +00:00
if pos >= src.len() {
2019-01-10 12:58:13 +00:00
return src.len();
2019-01-04 08:53:20 +00:00
}
if src.as_bytes()[pos] == b'*' {
pos += 1;
} else if src.as_bytes()[pos] == b' ' && pos - pos_ <= level {
2019-01-10 12:58:13 +00:00
return pos_;
2019-01-04 08:53:20 +00:00
} else {
2019-01-11 14:35:06 +00:00
break;
2019-01-04 08:53:20 +00:00
}
}
}
2019-01-10 12:58:13 +00:00
pos += 1
2019-01-04 08:53:20 +00:00
}
}
pub fn is_commented(&self) -> bool {
self.title.starts_with("COMMENT ")
}
pub fn is_archived(&self) -> bool {
self.tags
.map(|tags| tags[1..].split_terminator(':').any(|t| t == "ARCHIVE"))
.unwrap_or(false)
}
pub fn new(
level: usize,
keyword: Option<&'a str>,
priority: Option<char>,
title: &'a str,
tags: Option<&'a str>,
) -> Headline<'a> {
Headline {
level,
keyword,
priority,
title,
tags,
}
}
}
#[test]
fn parse() {
assert_eq!(
Headline::parse("**** TODO [#A] COMMENT Title :tag:a2%:").0,
Headline::new(
4,
Some("TODO"),
Some('A'),
"COMMENT Title",
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]
fn is_commented() {
assert!(Headline::parse("* COMMENT Title").0.is_commented());
assert!(!Headline::parse("* Title").0.is_commented());
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());
}