use chrono::NaiveDate;
use nom::branch::alt;
use nom::bytes::complete::{is_not, tag, take};
use nom::character::complete::{line_ending, one_of, space0, space1};
use nom::combinator::{iterator, map, map_opt, map_res, opt, verify};
use nom::sequence::{delimited, preceded, terminated};
use nom::{IResult, Parser};
use nom_locate::position;
use std::borrow::Cow;
use std::str::{CharIndices, FromStr};
use crate::todotxt::{Description, Located, Priority, Span, Tag, Todo};
pub type Input<'a> = nom_locate::LocatedSpan<&'a str>;
type Error<'a> = nom::error::Error<Input<'a>>;
type Headers = (
Option<Span>,
Option<Priority>,
Option<(Located<NaiveDate>, Option<Located<NaiveDate>>)>,
);
struct SplitWhitespace<'a> {
iter: CharIndices<'a>,
len: usize,
}
pub fn from_str(value: &str) -> impl Iterator<Item = Todo> {
iterator(value.into(), delimited(eol, todo(), eol)).filter(|todo| {
!todo.description.text().trim().is_empty()
|| !matches!(
todo,
Todo {
x: None,
priority: None,
date_completed: None,
date_started: None,
..
}
)
})
}
pub fn tags<'a>(description: &'a Description) -> impl Iterator<Item = Tag<'a>> {
let offset = description.span().start();
let text = description.text();
SplitWhitespace::new(text).filter_map(move |(start, end)| {
let token = &text[start..end];
let span = Span::new(start + offset, end + offset);
if token.starts_with('@') && token.len() > 1 {
return Some(Tag::Context(Located {
data: &token[1..],
span,
}));
}
if token.starts_with('+') && token.len() > 1 {
return Some(Tag::Project(Located {
data: &token[1..],
span,
}));
}
token.split_once(':').and_then(|pair| {
if !pair.0.is_empty() && !pair.1.is_empty() {
Some(Tag::Named(Located { data: pair, span }))
} else {
None
}
})
})
}
pub fn todo<'a>() -> impl Parser<Input<'a>, Output = Todo<'a>, Error = Error<'a>> {
alt((
map(
(position, preceded(space0, headers()), description()),
|(pos, headers, description)| {
let (x, priority, dates) = headers;
let (date_completed, date_started) = match dates {
Some((first, Some(second))) => (Some(first), Some(second)),
Some((first, None)) => (None, Some(first)),
None => (None, None),
};
Todo {
line: pos.location_line(),
x,
priority,
date_completed,
date_started,
description,
}
},
),
map((position, description()), |(pos, description)| Todo {
line: pos.location_line(),
x: None,
priority: None,
date_completed: None,
date_started: None,
description,
}),
))
}
fn description<'a>() -> impl Parser<Input<'a>, Output = Description<'a>, Error = Error<'a>> {
map(is_not("\r\n"), |output: Input<'a>| {
let text = output.fragment().trim_end();
let offset = text.len() - text.trim_start().len();
let span_start = output.get_utf8_column() - 1 + offset;
Description(Located {
data: Cow::Borrowed(&text[offset..]),
span: Span::new(span_start, span_start + text.len()),
})
})
}
fn headers<'a>() -> impl Parser<Input<'a>, Output = Headers, Error = Error<'a>> {
verify(
(
opt(map(terminated(tag("x"), space1), |pos| {
Span::locate(&pos, 1)
})),
opt(map(
(
position,
terminated(
delimited(tag("("), one_of("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), tag(")")),
space1,
),
),
|(pos, data)| {
let span = Span::locate(&pos, 3);
Priority(Located { data, span })
},
)),
opt((ymd(), opt(ymd()))),
),
|output| !matches!(output, (None, None, None)),
)
}
fn eol(input: Input) -> IResult<Input, ()> {
let mut rest = input;
while let Ok((next, _)) = line_ending::<_, Error>(rest) {
rest = next;
}
Ok((rest, ()))
}
fn parse_to<'a, R, P>(parser: P) -> impl Parser<Input<'a>, Output = R, Error = Error<'a>>
where
R: FromStr,
P: Parser<Input<'a>, Output = Input<'a>, Error = Error<'a>>,
{
map_res(parser, |output| output.fragment().parse())
}
fn ymd<'a>() -> impl Parser<Input<'a>, Output = Located<NaiveDate>, Error = Error<'a>> {
let triple = (
terminated(parse_to(take(4usize)), tag("-")),
terminated(parse_to(take(2usize)), tag("-")),
parse_to(take(2usize)),
);
map_opt((position, terminated(triple, space1)), |(at, (y, m, d))| {
Some(Located {
data: NaiveDate::from_ymd_opt(y, m, d)?,
span: Span::locate(&at, 10),
})
})
}
impl<'a> SplitWhitespace<'a> {
fn new(value: &'a str) -> Self {
Self {
iter: value.char_indices(),
len: value.len(),
}
}
}
impl<'a> Iterator for SplitWhitespace<'a> {
type Item = (usize, usize);
fn next(&mut self) -> Option<Self::Item> {
let iter = &mut self.iter;
let start = iter.find_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) })?;
let end = iter
.find_map(|(i, c)| if c.is_whitespace() { Some(i) } else { None })
.unwrap_or(self.len);
Some((start, end))
}
}