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 = false;
17 let mut out = Vec::new();
18 for (i, l) in body.lines().enumerate() {
19 let t = l.trim_start();
20 if t.starts_with("```") || t.starts_with("~~~") {
21 fence = !fence;
22 continue;
23 }
24 if fence {
25 continue;
26 }
27 let Some(rest) = ["- ", "* ", "+ "].iter().find_map(|m| t.strip_prefix(m)) else { continue };
28 let done = match rest.get(..3) {
29 Some("[ ]") => false,
30 Some("[x]") | Some("[X]") => true,
31 _ => continue,
32 };
33 let text = &rest[3..];
34 if !(text.is_empty() || text.starts_with(' ')) {
35 continue;
36 }
37 out.push(Task { line: i + 1, done, text: text.trim().to_string() });
38 }
39 out
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 #[test]
47 fn finds_open_and_done_items_outside_code() {
48 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";
49 let t = list(b);
50 assert_eq!(t.len(), 3);
51 assert_eq!(t[0], Task { line: 2, done: false, text: "write parser".into() });
52 assert!(t[1].done && t[2].done);
53 assert_eq!(t[2].line, 8);
54 }
55}