use pest_consume::{match_nodes, Parser};
use crate::todo::body_token::TodoBodyToken;
use crate::todo::builder::TodoBuilder;
use crate::todo::Todo;
use crate::TODO_DATE_FORMAT;
#[derive(Parser)]
#[grammar = "todo/todotxt.pest"]
pub struct TodoParser;
pub type PestResult<T> = std::result::Result<T, pest_consume::Error<Rule>>;
type Node<'i> = pest_consume::Node<'i, Rule, ()>;
fn parse_date(n: Node, input: &str) -> PestResult<time::Date> {
time::Date::parse(input, TODO_DATE_FORMAT).map_err(|e| n.error(e))
}
#[pest_consume::parser]
impl TodoParser {
fn completed(_n: Node) -> PestResult<bool> {
Ok(true)
}
fn opt_completed(n: Node) -> PestResult<Option<bool>> {
Ok(match_nodes!(n.into_children();
[completed(b)] => Some(b),
[] => None
))
}
fn priority_char(n: Node) -> PestResult<char> {
Ok(n.as_str()
.chars()
.next()
.unwrap()
.to_uppercase()
.next()
.unwrap())
}
fn priority(n: Node) -> PestResult<char> {
Ok(match_nodes!(n.into_children();
[priority_char(c)] => c,
))
}
fn opt_priority(n: Node) -> PestResult<Option<char>> {
Ok(match_nodes!(n.into_children();
[priority(c)] => Some(c),
[] => None
))
}
fn date(n: Node) -> PestResult<time::Date> {
let s = n.as_str();
Ok(parse_date(n, s)?)
}
fn opt_date(n: Node) -> PestResult<Option<time::Date>> {
Ok(match_nodes!(n.into_children();
[date(d)] => Some(d),
[] => None
))
}
fn creation_date(n: Node) -> PestResult<time::Date> {
TodoParser::date(n.children().single()?)
}
fn opt_creation_date(n: Node) -> PestResult<Option<time::Date>> {
Ok(match_nodes!(n.into_children();
[creation_date(d)] => Some(d),
[] => None
))
}
fn completion_date(n: Node) -> PestResult<time::Date> {
TodoParser::date(n.children().single()?)
}
fn context(n: Node) -> PestResult<TodoBodyToken> {
Ok(TodoBodyToken::Context(n.as_str().to_string()))
}
fn project(n: Node) -> PestResult<TodoBodyToken> {
Ok(TodoBodyToken::Project(n.as_str().to_string()))
}
fn threshold_date(n: Node) -> PestResult<TodoBodyToken> {
Ok(match_nodes!(n.into_children();
[date(d)] => TodoBodyToken::ThresholdDate(d)
))
}
fn due_date(n: Node) -> PestResult<TodoBodyToken> {
Ok(match_nodes!(n.into_children();
[date(d)] => TodoBodyToken::DueDate(d)
))
}
fn bool_01(n: Node) -> PestResult<bool> {
Ok(match n.as_str() {
"0" => false,
"1" => true,
_ => unreachable!(n.as_str()),
})
}
fn hidden(n: Node) -> PestResult<TodoBodyToken> {
Ok(match_nodes!(n.into_children();
[bool_01(b)] => TodoBodyToken::Hidden(b),
))
}
fn word(n: Node) -> PestResult<TodoBodyToken> {
Ok(TodoBodyToken::Word(n.as_str().to_string()))
}
fn token(n: Node) -> PestResult<TodoBodyToken> {
Ok(match_nodes!(n.into_children();
[context(t)] => t,
[project(t)] => t,
[threshold_date(t)] => t,
[due_date(t)] => t,
[hidden(t)] => t,
[word(t)] => t,
))
}
fn todo_completed_simpletask(n: Node) -> PestResult<Todo> {
let mut tb = TodoBuilder::default();
match_nodes!(n.into_children();
[completed(_c), completion_date(cod), opt_priority(opt_pri), opt_creation_date(opt_crd), token(tokens)..] => {
tb.completed(true)
.completion_date(cod);
if let Some(pri) = opt_pri {
tb.priority(pri);
}
tb.tokens(tokens.collect());
match opt_crd {
Some(crd) => {
if crd > cod {
log::debug!(
"{:#?} swapping dates : completion_date={:?}, creation_date={:?}",
tb,
crd,
cod
);
tb.completion_date(crd);
tb.creation_date(cod);
} else {
tb.creation_date(crd);
}
},
None => {
log::debug!(
"{:#?} Completed Simpletask-style todo with completion_date only; completion_date={:?}",
tb,
cod
);
}
}
}
);
Ok(tb.build())
}
fn todo_txt(n: Node) -> PestResult<Todo> {
let mut tb = TodoBuilder::default();
match_nodes!(n.into_children();
[opt_completed(opt_comp), opt_priority(opt_pri), opt_date(opt_date1), opt_date(opt_date2), token(tokens)..] => {
if let Some(c) = opt_comp {
tb.completed(true);
}
if let Some(pri) = opt_pri {
tb.priority(pri);
}
tb.tokens(tokens.collect());
match (opt_comp, opt_date1, opt_date2) {
(Some(_comp), Some(date1), Some(date2)) => {
if date1 > date2 {
tb.completion_date(date1);
tb.creation_date(date2);
} else {
tb.completion_date(date2);
tb.creation_date(date1);
}
},
(None, Some(date1), Some(date2)) => {
let crd = std::cmp::min(date1, date2);
log::debug!(
"{:#?} Incomplete todo had 2 dates ({:?},{:?}) keeping earliest as creation date ({:?}).",
tb,
date1,
date2,
crd
);
tb.creation_date(crd);
},
(None, Some(date1), None) => {
tb.creation_date(date1);
},
(Some(_comp), Some(date1), None) => {
tb.creation_date(date1);
},
(_, None, Some(date2)) => {
unreachable!("{:#?}; opt_date1 should be Some, got date2={:?}", tb, date2)
},
(Some(comp), None, None) => {
}
(None, None, None) => {},
}
}
);
Ok(tb.build())
}
pub fn todo(node: Node) -> PestResult<Todo> {
Ok(match_nodes!(node.into_children();
[todo_completed_simpletask(t)] => t,
[todo_txt(t)] => t,
))
}
}
#[derive(Debug)]
pub enum TodoParserError {
EmptyInput,
ParseError(pest_consume::Error<Rule>),
}
impl std::fmt::Display for TodoParserError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for TodoParserError {}
impl std::convert::From<pest_consume::Error<Rule>> for TodoParserError {
fn from(e: pest_consume::Error<Rule>) -> Self {
TodoParserError::ParseError(e)
}
}
#[cfg(test)]
mod test {
use crate::todo::builder::TodoBodyTokensBuilder;
use super::*;
fn parse_node<T>(
root_rule: Rule,
visitor: fn(Node) -> PestResult<T>,
input: &str,
) -> PestResult<T> {
let nodes = TodoParser::parse(root_rule, input)?;
let node = nodes.single()?;
visitor(node)
}
fn parse_token(input: &str) -> PestResult<TodoBodyToken> {
parse_node(Rule::token, TodoParser::token, input)
}
#[test]
fn parse_intermediate_parsers_work_ok() {
assert_eq!(
Ok(true),
parse_node(Rule::completed, TodoParser::completed, "x")
);
assert_eq!(
Ok('D'),
parse_node(Rule::priority, TodoParser::priority, "(D)")
);
assert_eq!(
Ok(time::date!(2020 - 12 - 13)),
parse_node(Rule::date, TodoParser::date, "2020-12-13")
);
assert_eq!(
Ok(time::date!(2020 - 12 - 13)),
parse_node(Rule::creation_date, TodoParser::creation_date, "2020-12-13")
);
assert_eq!(
Ok(time::date!(2020 - 02 - 29)),
parse_node(
Rule::completion_date,
TodoParser::completion_date,
"2020-02-29",
)
);
assert_eq!(
Ok(TodoBodyToken::Context("@foo".to_string())),
parse_node(Rule::context, TodoParser::context, "@foo")
);
assert_eq!(
Ok(TodoBodyToken::Project("+foo".to_string())),
parse_node(Rule::project, TodoParser::project, "+foo")
);
assert_eq!(
Ok(TodoBodyToken::ThresholdDate(time::date!(2020 - 11 - 14))),
parse_node(
Rule::threshold_date,
TodoParser::threshold_date,
"t:2020-11-14",
)
);
assert_eq!(
Ok(TodoBodyToken::DueDate(time::date!(2020 - 04 - 04))),
parse_node(Rule::due_date, TodoParser::due_date, "due:2020-04-04")
);
assert_eq!(
Ok(TodoBodyToken::Hidden(true)),
parse_node(Rule::hidden, TodoParser::hidden, "h:1")
);
assert_eq!(
Ok(TodoBodyToken::Hidden(false)),
parse_node(Rule::hidden, TodoParser::hidden, "h:0")
);
assert_eq!(
Ok(TodoBodyToken::Word("hi".to_string())),
parse_node(Rule::word, TodoParser::word, "hi")
);
assert_eq!(
Ok(TodoBodyToken::Context("@foo".to_string())),
parse_token("@foo")
);
assert_eq!(
Ok(TodoBodyToken::Project("+foo".to_string())),
parse_token("+foo")
);
assert_eq!(
Ok(TodoBodyToken::ThresholdDate(time::date!(2020 - 11 - 14))),
parse_token("t:2020-11-14")
);
assert_eq!(
Ok(TodoBodyToken::DueDate(time::date!(2020 - 04 - 04))),
parse_token("due:2020-04-04")
);
assert_eq!(Ok(TodoBodyToken::Hidden(true)), parse_token("h:1"));
assert_eq!(Ok(TodoBodyToken::Hidden(false)), parse_token("h:0"));
assert_eq!(Ok(TodoBodyToken::Word("hi".to_string())), parse_token("hi"));
}
#[test]
fn parsing_a_todo() {
let got = Todo::parse(
"x (A) 2020-10-10 2020-12-29 write +tudor Todo parser @pc t:2020-12-25 due:2020-12-30",
)
.unwrap();
let exp = TodoBuilder::default()
.completed(true)
.priority('A')
.creation_date(time::date!(2020 - 10 - 10))
.completion_date(time::date!(2020 - 12 - 29))
.tokens(
TodoBodyTokensBuilder::default()
.word("write")
.project("+tudor")
.word("Todo")
.word("parser")
.context("@pc")
.threshold_date(time::date!(2020 - 12 - 25))
.due_date(time::date!(2020 - 12 - 30))
.build(),
)
.build();
assert_eq!(got, exp);
}
#[test]
fn parsing_a_todo_with_ws() {
let got = Todo::parse(
" x (B) 2020-10-10 2020-12-29 parse todos with whitespace +tudor @pc t:2020-12-25 due:2020-12-31 ",
).unwrap();
let exp = TodoBuilder::default()
.completed(true)
.priority('B')
.creation_date(time::date!(2020 - 10 - 10))
.completion_date(time::date!(2020 - 12 - 29))
.tokens(
TodoBodyTokensBuilder::default()
.word("parse")
.word("todos")
.word("with")
.word("whitespace")
.project("+tudor")
.context("@pc")
.threshold_date(time::date!(2020 - 12 - 25))
.due_date(time::date!(2020 - 12 - 31))
.build(),
)
.build();
assert_eq!(got, exp);
}
#[test]
fn parsing_a_todo_with_body_only() {
let got = Todo::parse("try out pest @pc").unwrap();
let exp = TodoBuilder::default()
.tokens(
TodoBodyTokensBuilder::default()
.word("try")
.word("out")
.word("pest")
.context("@pc")
.build(),
)
.build();
assert_eq!(got, exp);
}
#[test]
fn parsing_a_todo_with_emoji() {
let got = Todo::parse("try out emojis 🎊 @pc +tudor").unwrap();
let exp = TodoBuilder::default()
.tokens(
TodoBodyTokensBuilder::default()
.word("try")
.word("out")
.word("emojis")
.word("🎊")
.context("@pc")
.project("+tudor")
.build(),
)
.build();
assert_eq!(got, exp);
}
#[test]
fn parsing_some_other_todos() {
assert!(Todo::parse("(A)").is_ok());
assert!(Todo::parse("(S) 1029-12-19 1029-12-20 try out pest @pc").is_ok());
assert!(Todo::parse("(S) 1029-12-19 try out pest @pc").is_ok());
assert!(Todo::parse("(S) try out pest @pc").is_ok());
assert!(Todo::parse("1029-12-19 token token").is_ok());
assert!(Todo::parse("1029-12-19").is_ok());
assert!(Todo::parse("1039-12-12 1999-12-12 try out pest @pc").is_ok());
assert!(Todo::parse("1039-12-12 try out pest @pc").is_ok());
assert!(Todo::parse("x (A) 1029-12-20 token token").is_ok());
assert!(Todo::parse("x (S) 1029-12-19 1029-12-19 try out pest @pc").is_ok());
assert!(Todo::parse("x (S) 1029-12-19 try out pest @pc").is_ok());
assert!(Todo::parse("x (S) try out pest @pc").is_ok());
assert!(Todo::parse("x (S)").is_ok());
assert!(Todo::parse("x 1029-12-19 1029-12-20 token token").is_ok());
assert!(Todo::parse("x 1039-12-12 try out pest @pc").is_ok());
assert!(Todo::parse("x try out pest @pc").is_ok());
assert!(Todo::parse("x").is_ok());
}
#[test]
fn parsing_a_simpletask_style_todo() {
let t = Todo::parse("x 2021-01-05 (A) 2021-01-03 Pick up TPS reports @office").unwrap();
assert_eq!(t.creation_date, Some(time::date!(2021 - 01 - 03)));
assert_eq!(t.completion_date, Some(time::date!(2021 - 01 - 05)));
}
#[test]
fn parsing_a_simpletask_style_todo_with_no_creation_date() {
let t = Todo::parse("x 2015-04-05 (A) @phone peter : can you pick me up at the station and drive me to the hotel?
").unwrap();
let cod = Some(time::date!(2015 - 04 - 05));
assert_eq!(t.priority, Some('A'));
assert_eq!(t.creation_date, None);
assert_eq!(t.completion_date, cod);
let t = Todo::parse("x 2016-07-31 (A) @ita pack linen shirts?").unwrap();
let cod = Some(time::date!(2016 - 07 - 31));
assert_eq!(t.priority, Some('A'));
assert_eq!(t.creation_date, None);
assert_eq!(t.completion_date, cod);
}
#[test]
fn parsing_a_simpletask_style_todo_with_swapped_dates() {
let t = Todo::parse("x 2021-01-03 (A) 2021-01-05 Pick up TPS reports @office").unwrap();
assert_eq!(t.priority, Some('A'));
assert_eq!(t.creation_date, Some(time::date!(2021 - 01 - 03)));
assert_eq!(t.completion_date, Some(time::date!(2021 - 01 - 05)));
}
#[test]
fn parsing_a_todotxt_style_todo_with_swapped_dates() {
let t = Todo::parse("x (A) 2021-01-03 2021-01-05 Pick up TPS reports @office").unwrap();
assert_eq!(t.creation_date, Some(time::date!(2021 - 01 - 03)));
assert_eq!(t.completion_date, Some(time::date!(2021 - 01 - 05)));
}
#[test]
fn parsing_a_complete_todos_with_one_date() {
let t = Todo::parse("x (A) 2021-01-05 Pick up TPS reports @office").unwrap();
assert_eq!(t.priority, Some('A'));
assert_eq!(t.creation_date, Some(time::date!(2021 - 01 - 05)));
assert_eq!(t.completion_date, None);
let t = Todo::parse("x 2021-01-05 Pick up TPS reports @office").unwrap();
assert_eq!(t.priority, None);
assert_eq!(t.creation_date, None);
assert_eq!(t.completion_date, Some(time::date!(2021 - 01 - 05)));
}
#[test]
fn parsing_an_incomplete_todo_with_two_dates() {
let t = Todo::parse("(A) 2021-01-03 2021-01-05 Pick up TPS reports @office").unwrap();
assert_eq!(t.creation_date, Some(time::date!(2021 - 01 - 03)));
assert_eq!(t.completion_date, None);
let t = Todo::parse("(A) 2021-01-05 2021-01-03 Pick up TPS reports @office").unwrap();
assert_eq!(t.creation_date, Some(time::date!(2021 - 01 - 03)));
assert_eq!(t.completion_date, None);
}
}