todotxt_rs 0.1.1

A fast, lightweight parser for the todo.txt file format.
Documentation
use pest::Parser;
use pest_derive::Parser;
use std::boxed::Box;
use std::convert::From;
use thiserror::Error;

/// Структура парсера, яка вказує на файл граматики `todotxt.pest`.
#[derive(Parser)]
#[grammar = "todotxt.pest"]
pub struct TodotxtParser;

/// Перелік помилок, які можуть виникнути під час парсингу.
#[derive(Error, Debug)]
pub enum ParseError {
    /// Помилка виникає, коли парсер `pest` не може розібрати рядок.
    #[error("Pest parsing error: {0}")]
    PestError(Box<pest::error::Error<Rule>>),
}

/// Реалізація трейту From для автоматичного перетворення помилок `pest` у `ParseError`.
/// Це дозволяє використовувати оператор `?` у функції `parse_line`.
impl From<pest::error::Error<Rule>> for ParseError {
    fn from(e: pest::error::Error<Rule>) -> Self {
        ParseError::PestError(Box::new(e))
    }
}

/// Представляє одне завдання (один рядок) з файлу todo.txt.
#[derive(Debug, PartialEq, Default)]
pub struct Task {
    /// Чи позначено завдання як виконане (напр., "x ...").
    pub is_completed: bool,
    /// Опціональний пріоритет (напр., 'A', 'B') з тега "(A)".
    pub priority: Option<char>,
    /// Опціональна дата створення (напр., "2025-11-12"), якщо вона вказана на початку рядка.
    pub creation_date: Option<String>,
    /// Опціональна дата виконання (напр., "due:2025-11-13"), отримана з тега `due:`.
    pub due_date: Option<String>,
    /// Список проєктних тегів (напр., "+project").
    pub projects: Vec<String>,
    /// Список контекстних тегів (напр., "@context").
    pub contexts: Vec<String>,
    /// Усі інші текстові частини опису завдання.
    pub description_parts: Vec<String>,
}

/// Парсить один рядок з файлу todo.txt у структуру [Task].
///
/// Ця функція використовує граматику, визначену в `todotxt.pest`,
/// для розбору рядка на компоненти, дотримуючись пріоритету елементів.
///
/// # Аргументи
///
/// * `line` - Рядковий зріз (&str), що представляє один рядок файлу.
///
/// # Помилки
///
/// Повертає `ParseError::PestError`, якщо рядок не відповідає граматиці.
pub fn parse_line(line: &str) -> Result<Task, ParseError> {
    let mut pairs = TodotxtParser::parse(Rule::task_line, line)?;

    let task_line_pairs = pairs.next().unwrap().into_inner();

    let mut task = Task::default();

    for pair in task_line_pairs {
        match pair.as_rule() {
            // MARKER: Правила початку рядка (впорядковані)
            Rule::completion => {
                task.is_completed = true;
            }
            Rule::priority => {
                let tag = pair.into_inner().next().unwrap().as_str();
                task.priority = tag.chars().nth(1);
            }
            Rule::creation_date => {
                let date_str = pair.into_inner().next().unwrap().as_str();
                task.creation_date = Some(date_str.to_string());
            }

            // MARKER: Правила опису (невпорядковані)
            Rule::project => {
                task.projects.push(pair.as_str().to_string());
            }
            Rule::context => {
                task.contexts.push(pair.as_str().to_string());
            }
            Rule::due_date => {
                let full_tag = pair.as_str();
                let date_part = full_tag.strip_prefix("due:").unwrap_or(full_tag);
                task.due_date = Some(date_part.to_string());
            }
            Rule::text_part => {
                task.description_parts.push(pair.as_str().to_string());
            }

            // MARKER: Інші правила
            Rule::EOI => {}
            _ => {}
        }
    }

    Ok(task)
}