todo-txt 0.4.0

Parser for the todo.txt format
Documentation
use ::std::collections::BTreeMap;
use ::nom::rest_s;

fn is_space(chr: char) -> bool
{
    chr == ' ' || chr == '\t' || is_line_ending(chr)
}

fn is_line_ending(chr: char) -> bool
{
    chr == '\n' || chr == '\r'
}

named!(date<&str, ::Date>,
    do_parse!(
        year:
            take!(4) >>
            tag_s!("-") >>
        month:
            take!(2) >>
            tag_s!("-") >>
        day:
            take!(2) >>
            tag_s!(" ") >>
        ({
            let year = match year.parse() {
                Ok(year) => year,
                Err(_) => return ::nom::IResult::Error(::nom::ErrorKind::Custom(1)),
            };

            let month = match month.parse() {
                Ok(month) => month,
                Err(_) => return ::nom::IResult::Error(::nom::ErrorKind::Custom(2)),
            };

            let day = match day.parse() {
                Ok(day) => day,
                Err(_) => return ::nom::IResult::Error(::nom::ErrorKind::Custom(3)),
            };

            match ::Date::from_ymd_opt(year, month, day) {
                Some(date) => date,
                None => return ::nom::IResult::Error(::nom::ErrorKind::Custom(4)),
            }
        })
    )
);

named!(priority<&str, u8>,
    do_parse!(
            tag_s!("(") >>
        priority:
            take!(1) >>
            tag_s!(") ") >>
        ({
            let p = priority.as_bytes()[0];

            if p >= b'A' && p <= b'Z' {
                p - b'A'
            }
            else {
                26
            }
        })
    )
);

named!(context<&str, String>,
    do_parse!(
            take_until_and_consume_s!(" @") >>
        context:
            take_till!(is_space) >>
        (
            context.to_owned()
        )
    )
);

named!(contexts<&str, Vec<String>>, many0!(context));

fn get_contexts(subject: &str) -> Vec<String>
{
    match contexts(subject) {
        ::nom::IResult::Done(_, mut contexts) => {
            contexts.sort();
            contexts.dedup();

            contexts
        },
        _ => Vec::new(),
    }
}

named!(project<&str, String>,
    do_parse!(
            take_until_and_consume_s!(" +") >>
        project:
            take_till!(is_space) >>
        (
            project.to_owned()
        )
    )
);

named!(projects<&str, Vec<String>>, many0!(project));

fn get_projects(subject: &str) -> Vec<String>
{
    match projects(subject) {
        ::nom::IResult::Done(_, mut projects) => {
            projects.sort();
            projects.dedup();

            projects
        },
        _ => Vec::new(),
    }
}

named!(hashtag<&str, String>,
    do_parse!(
            take_until_and_consume_s!(" #") >>
        hashtag:
            take_till!(is_space) >>
        (
            hashtag.to_owned()
        )
    )
);

named!(hashtags<&str, Vec<String>>, many0!(hashtag));

fn get_hashtags(subject: &str) -> Vec<String>
{
    match hashtags(subject) {
        ::nom::IResult::Done(_, mut hashtags) => {
            hashtags.sort();
            hashtags.dedup();

            hashtags
        },
        _ => Vec::new(),
    }
}

fn get_tags(subject: &str) -> (String, BTreeMap<String, String>)
{
    let mut tags = BTreeMap::new();
    let regex = ::regex::Regex::new(r" (?P<key>[^\s]+):(?P<value>[^\s]+)").unwrap();

    let new_subject = regex.replace_all(subject, |caps: &::regex::Captures| {
        let key = caps.name("key").unwrap().as_str();
        let value = caps.name("value").unwrap().as_str();

        tags.insert(key.to_owned(), value.to_owned());

        String::new()
    });

    (new_subject.into_owned(), tags)
}

named!(parse<&str, ::Task>,
    do_parse!(
        finished:
            opt!(complete!(tag_s!("x "))) >>
        priority:
            opt!(complete!(priority)) >>
        finish_date:
            opt!(complete!(date)) >>
        create_date:
            opt!(complete!(date)) >>
        rest:
            rest_s >>
        ({
            let mut task = ::Task {
                priority: match priority {
                    Some(priority) => priority,
                    None => 26,
                },
                create_date: if create_date.is_none() {
                    finish_date
                } else {
                    create_date
                },
                finish_date: if create_date.is_none() {
                    None
                } else {
                    finish_date
                },
                finished: finished.is_some(),
                contexts: get_contexts(rest),
                projects: get_projects(rest),
                hashtags: get_hashtags(rest),

                .. Default::default()
            };

            let (subject, mut tags) = get_tags(rest);
            task.subject = subject;

            if let Some(due) = tags.remove("due") {
                task.due_date = match ::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 ::Date::parse_from_str(t.as_str(), "%Y-%m-%d") {
                    Ok(t) => Some(t),
                    Err(_) => None,
                };
            }

            task.tags = tags;

            task
        })
    )
);

pub fn task(line: &String) -> Result<::Task, ()>
{
    match parse(line) {
        ::nom::IResult::Done(_, task) => Ok(task),
        _ => Err(()),
    }
}