use lazy_static::lazy_static;
use nom::combinator::rest;
use nom::{complete, opt, return_error, tag, take};
use std::collections::BTreeMap;
macro_rules! return_error (
($num:expr) => {
return Err(
nom::Err::Error(
nom::error::make_error(
"",
nom::error::ErrorKind::Tag
)
)
);
}
);
fn date(input: &str) -> nom::IResult<&str, crate::Date> {
nom::do_parse!(
input,
year: take!(4)
>> tag!("-")
>> month: take!(2)
>> tag!("-")
>> day: take!(2)
>> tag!(" ")
>> ({
let year = match year.parse() {
Ok(year) => year,
Err(_) => return_error!(1),
};
let month = match month.parse() {
Ok(month) => month,
Err(_) => return_error!(2),
};
let day = match day.parse() {
Ok(day) => day,
Err(_) => return_error!(3),
};
match crate::Date::from_ymd_opt(year, month, day) {
Some(date) => date,
None => return_error!(4),
}
})
)
}
fn priority(input: &str) -> nom::IResult<&str, u8> {
nom::do_parse!(
input,
tag!("(")
>> priority: take!(1)
>> tag!(") ")
>> ({
let p = priority.as_bytes()[0];
if p >= b'A' && p <= b'Z' {
p - b'A'
} else {
26
}
})
)
}
fn get_tags(regex: &::regex::Regex, subject: &str) -> Vec<String> {
let mut tags = regex
.captures_iter(subject)
.map(|x| x["tag"].to_lowercase())
.filter(|x| !x.is_empty())
.collect::<Vec<_>>();
tags.sort();
tags.dedup();
tags
}
macro_rules! regex_tags_shared {
() => {
"(?P<space>^|[\\s]){}(?P<tag>[\\w\\\\-]+)"
};
}
fn get_contexts(subject: &str) -> Vec<String> {
lazy_static! {
static ref REGEX: regex::Regex =
regex::Regex::new(&format!(regex_tags_shared!(), "@")).unwrap();
}
get_tags(®EX, subject)
}
fn get_projects(subject: &str) -> Vec<String> {
lazy_static! {
static ref REGEX: regex::Regex =
regex::Regex::new(&format!(regex_tags_shared!(), "\\+")).unwrap();
}
get_tags(®EX, subject)
}
fn get_hashtags(subject: &str) -> Vec<String> {
lazy_static! {
static ref REGEX: regex::Regex =
regex::Regex::new(&format!(regex_tags_shared!(), "#")).unwrap();
}
get_tags(®EX, subject)
}
fn get_keywords(subject: &str) -> (String, BTreeMap<String, String>) {
lazy_static! {
static ref REGEX: regex::Regex =
regex::Regex::new(r"(\s+|^)(?P<key>[^\s]+?):(?P<value>[^\s]+)").unwrap();
}
let mut tags = BTreeMap::new();
let new_subject = REGEX.replace_all(subject, |caps: ®ex::Captures<'_>| {
let key = caps.name("key").unwrap().as_str();
let value = caps.name("value").unwrap().as_str();
if value.starts_with('/') {
format!(" {}:{}", key, value)
} else {
tags.insert(key.to_owned(), value.to_owned());
String::new()
}
});
(new_subject.trim().to_owned(), tags)
}
fn parse(input: &str) -> nom::IResult<&str, crate::Task> {
nom::do_parse!(
input,
finished: opt!(complete!(tag!("x ")))
>> priority: opt!(complete!(priority))
>> finish_date: opt!(complete!(date))
>> create_date: opt!(complete!(date))
>> rest: rest
>> ({
let mut task = crate::Task {
priority: priority.unwrap_or(26),
create_date: create_date.or(finish_date),
finish_date: create_date.and(finish_date),
finished: finished.is_some(),
contexts: get_contexts(rest),
projects: get_projects(rest),
hashtags: get_hashtags(rest),
..crate::Task::default()
};
let (subject, mut tags) = get_keywords(rest);
task.subject = subject;
if let Some(due) = tags.remove("due") {
task.due_date = match crate::Date::parse_from_str(due.as_str(), "%Y-%m-%d") {
Ok(due) => Some(due),
Err(_) => None,
};
}
if let Some(t) = tags.remove("t") {
task.threshold_date = match crate::Date::parse_from_str(t.as_str(), "%Y-%m-%d")
{
Ok(t) => Some(t),
Err(_) => None,
};
}
task.tags = tags;
task
})
)
}
pub fn task(line: &str) -> crate::Task {
parse(line).unwrap().1
}