marmota 1.0.3

Daily note templating system
use chrono::{DateTime, Datelike, Local};

pub fn process_tokens(template: &str, yesterday: &str, date: DateTime<Local>) -> String {
    let mut result = "".to_string();
    let mut current_section = "";
    for line in template.lines() {
        let is_header = line.starts_with("#");
        if is_header {
            current_section = line;
        }
        result += &process_token(line, yesterday, current_section, date);
        result += "\n";
    }
    result.trim().to_string()
}

fn process_token(
    line: &str,
    yesterday: &str,
    current_section: &str,
    date: DateTime<Local>,
) -> String {
    let binding = get_token(line);
    let token = binding.as_str();
    match token {
        "!copy_tasks" => get_section_text(current_section, yesterday, &vec!["- [x]"]),
        "!copy_last_day" => get_section_text(current_section, yesterday, &vec![]),
        "!by_weekday" => by_weekday(line, date),
        "!on_date" => on_date(line, date),
        _ => line.to_string(),
    }
}

fn get_token(line: &str) -> String {
    let token_end = line.find("(");
    let token = match token_end {
        Some(token_end) => line[..token_end].trim(),
        None => line,
    };
    token.to_string()
}

fn get_section_text(section: &str, yesterday: &str, dont_copy: &Vec<&str>) -> String {
    let mut result = "".to_string();
    let mut copy = false;

    let header_end = section.find(" ").unwrap();
    let header = &section[..header_end + 1];

    let lines = yesterday
        .lines()
        .filter(|line| !should_skip(line, dont_copy));
    for line in lines {
        if line.starts_with(header) {
            copy = false;
        }
        if copy {
            result += line;
            result += "\n";
        }
        if line.starts_with(section) {
            copy = true;
        }
    }
    result.trim().to_string()
}

fn should_skip(line: &str, dont_copy: &Vec<&str>) -> bool {
    for token in dont_copy {
        if line.starts_with(token) {
            return true;
        }
    }
    false
}

fn by_weekday(line: &str, date: DateTime<Local>) -> String {
    let result = get_content_between_parentheses(line);
    let content_by_day: Vec<&str> = result.split(";").collect();
    let index = date.weekday().num_days_from_sunday();
    let index = usize::try_from(index).expect("Cannot convert weekday to usize");
    content_by_day[index].to_string()
}

fn on_date(line: &str, date: DateTime<Local>) -> String {
    let result = get_content_between_parentheses(line);
    let (date_expected, text) = result.split_once(", ").unwrap();

    let month = date.month();
    let day = date.day();
    let date_real = format!("{:0>2}/{:0>2}", month, day);
    if date_real == date_expected {
        return text.to_string();
    }
    "".to_string()
}

fn get_content_between_parentheses(line: &str) -> String {
    let start_bytes = line
        .find("(")
        .expect("no opening brackets found in by weekday template line");
    let end_bytes = line
        .find(")")
        .expect("no closing brackets found in by weekday template line");
    let result = &line[start_bytes + 1..end_bytes];
    result.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::TimeZone;

    fn date(year: i32, month: u32, day: u32) -> DateTime<Local> {
        Local
            .with_ymd_and_hms(year, month, day, 12, 0, 0)
            .single()
            .unwrap()
    }

    #[test]
    fn weekday_token_selects_sunday_through_saturday() {
        let expected = [
            (2024, 1, 7, "Sunday"),
            (2024, 1, 8, "Monday"),
            (2024, 1, 9, "Tuesday"),
            (2024, 1, 10, "Wednesday"),
            (2024, 1, 11, "Thursday"),
            (2024, 1, 12, "Friday"),
            (2024, 1, 13, "Saturday"),
        ];

        for (year, month, day, value) in expected {
            assert_eq!(
                by_weekday(
                    "!by_weekday(Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday)",
                    date(year, month, day)
                ),
                value
            );
        }
    }

    #[test]
    fn process_tokens_replaces_weekday_and_preserves_regular_lines() {
        let output = process_tokens(
            "# Day\n!by_weekday(Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday)\nKeep this",
            "",
            date(2024, 1, 8),
        );
        assert_eq!(output, "# Day\nMonday\nKeep this");
    }

    #[test]
    fn copy_last_day_copies_the_matching_section_until_the_next_heading() {
        let yesterday = "# Work\n- unfinished\n## Subsection\n# Personal\n- unrelated";

        let output = process_tokens(
            "# Work\n!copy_last_day\n# Personal",
            yesterday,
            date(2024, 1, 8),
        );

        assert_eq!(output, "# Work\n- unfinished\n## Subsection\n# Personal");
    }

    #[test]
    fn copy_tasks_skips_completed_items_but_keeps_open_items() {
        let yesterday = "# Tasks\n- [x] done\n- [ ] pending\n# Other\n- unrelated";
        let output = process_tokens("# Tasks\n!copy_tasks", yesterday, date(2024, 1, 8));
        assert_eq!(output, "# Tasks\n- [ ] pending");
    }
    #[test]
    fn on_date_returns_content_on_date() {
        let template = "!on_date(01/01, test text)";
        let output = process_tokens(template, "", date(2024, 1, 1));
        assert_eq!(output, "test text");
    }
    #[test]
    fn on_date_doesnt_return_on_wrong_date() {
        let template = "no text\n!on_date(01/01, test text)";
        let output = process_tokens(template, "", date(2024, 1, 2));
        assert_eq!(output, "no text");
    }
}