use crate::model::archive::Archive;
use crate::parse::LineEnding;
use crate::parse::task_parser::{parse_tasks, task_indent};
pub fn parse_archive(source: &str) -> Archive {
let lines: Vec<String> = source.lines().map(|l| l.to_string()).collect();
let start = lines
.iter()
.position(|l| task_indent(l).is_some())
.unwrap_or(lines.len());
let (tasks, next) = parse_tasks(&lines, start, 0, 0);
Archive {
header: lines[..start].to_vec(),
tasks,
trailing: lines[next.min(lines.len())..].to_vec(),
eol: LineEnding::detect(source),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::serialize_archive;
const CLEAN_WROTE: &str = "\
# Archive — main
- [x] `MAI-002` second task
- added: 2026-08-05
- resolved: 2026-08-05
- [x] `MAI-001` first task
- resolved: 2026-08-05
";
#[test]
fn reads_the_shape_clean_writes() {
let archive = parse_archive(CLEAN_WROTE);
assert_eq!(
archive.header,
vec!["# Archive — main".to_string(), "".into()]
);
assert_eq!(archive.tasks.len(), 2);
assert_eq!(archive.tasks[0].title, "second task");
assert!(archive.trailing.is_empty());
assert_eq!(serialize_archive(&archive), CLEAN_WROTE);
}
#[test]
fn an_empty_or_headerless_file_is_not_a_panic() {
assert!(parse_archive("").tasks.is_empty());
assert!(parse_archive("# Archive — main\n").tasks.is_empty());
assert_eq!(parse_archive("- [x] `M-1` bare\n").tasks.len(), 1);
}
#[test]
fn a_link_bullet_in_the_header_stays_in_the_header() {
let source = "\
# Archive — main
- [context](notes.md)
- [x] `MAI-001` real task
";
let archive = parse_archive(source);
assert_eq!(archive.tasks.len(), 1, "the task must be visible");
assert!(
archive.header.iter().any(|l| l.contains("notes.md")),
"and the link stays where it was: {:?}",
archive.header
);
assert_eq!(serialize_archive(&archive), source);
}
#[test]
fn trailing_content_is_kept() {
let source = "\
# Archive — main
- [x] `MAI-001` task
<!-- archived 2025 sprint notes -->
";
let archive = parse_archive(source);
assert_eq!(archive.tasks.len(), 1);
assert!(
archive.trailing.iter().any(|l| l.contains("sprint notes")),
"trailing content: {:?}",
archive.trailing
);
assert_eq!(serialize_archive(&archive), source);
}
#[test]
fn an_indented_first_task_is_still_found() {
let archive = parse_archive("# Archive — main\n\n - [x] `MAI-001` indented\n");
assert_eq!(archive.tasks.len(), 1);
assert_eq!(archive.tasks[0].title, "indented");
}
#[test]
fn the_line_ending_is_detected() {
assert_eq!(
parse_archive("# Archive — main\r\n\r\n- [x] `M-1` t\r\n").eol,
LineEnding::Crlf
);
assert_eq!(parse_archive(CLEAN_WROTE).eol, LineEnding::Lf);
}
}