specfold_core/markdown/
tasks.rs1use serde::Serialize;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7pub struct Task {
8 pub line: usize,
10 pub done: bool,
11 pub text: String,
12}
13
14pub fn list(body: &str) -> Vec<Task> {
16 let mut fence = crate::markdown::Fence::default();
17 let mut out = Vec::new();
18 for (i, l) in body.lines().enumerate() {
19 let t = l.trim_start();
20 if fence.step(t) {
21 continue;
22 }
23 if fence.inside() {
24 continue;
25 }
26 let Some(rest) = ["- ", "* ", "+ "].iter().find_map(|m| t.strip_prefix(m)) else { continue };
27 let done = match rest.get(..3) {
28 Some("[ ]") => false,
29 Some("[x]") | Some("[X]") => true,
30 _ => continue,
31 };
32 let text = &rest[3..];
33 if !(text.is_empty() || text.starts_with(' ')) {
34 continue;
35 }
36 out.push(Task { line: i + 1, done, text: text.trim().to_string() });
37 }
38 out
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 #[test]
46 fn finds_open_and_done_items_outside_code() {
47 let b = "## Steps\n- [ ] write parser\n * [x] read spec\n- [link](x)\n```\n- [ ] not a task\n```\n+ [X] ship\n- [ ]x\n";
48 let t = list(b);
49 assert_eq!(t.len(), 3);
50 assert_eq!(t[0], Task { line: 2, done: false, text: "write parser".into() });
51 assert!(t[1].done && t[2].done);
52 assert_eq!(t[2].line, 8);
53 }
54}