to2txt 0.1.1

A zero-copy parser for the todo.txt format.
Documentation
use std::str::CharIndices;

#[cfg(feature = "serde")]
use serde::{Serialize, Serializer};

#[cfg(feature = "serde")]
pub struct SerializeTags<'a>(pub &'a str);

#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd)]
pub struct Span(usize, usize);

#[cfg_attr(
    feature = "serde",
    derive(Serialize),
    serde(tag = "type", rename_all = "lowercase")
)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd)]
pub enum Tag<'a> {
    Context {
        span: Span,
        value: &'a str,
    },
    Project {
        span: Span,
        value: &'a str,
    },
    Named {
        span: Span,
        name: &'a str,
        value: &'a str,
    },
}

struct Tokens<'a> {
    iter: CharIndices<'a>,
    len: usize,
}

pub fn tags(description: &str) -> impl Iterator<Item = Tag> {
    Tokens::new(description).filter_map(|span| {
        let token = &description[span.0..span.1];

        if token.starts_with('@') && token.len() > 1 {
            return Some(Tag::Context {
                span,
                value: &token[1..],
            });
        }

        if token.starts_with('+') && token.len() > 1 {
            return Some(Tag::Project {
                span,
                value: &token[1..],
            });
        }

        token.split_once(':').and_then(|(name, value)| {
            if !name.is_empty() && !value.is_empty() {
                Some(Tag::Named { span, name, value })
            } else {
                None
            }
        })
    })
}

#[cfg(feature = "serde")]
impl Serialize for SerializeTags<'_> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.collect_seq(tags(self.0))
    }
}

impl<'a> Tokens<'a> {
    fn new(value: &'a str) -> Self {
        Self {
            iter: value.char_indices(),
            len: value.len(),
        }
    }
}

impl<'a> Iterator for Tokens<'a> {
    type Item = Span;

    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(Span(start, end))
    }
}