Skip to main content

specfold_core/markdown/
tasks.rs

1//! Task list items (`- [ ]` and `- [x]`), so a plan's progress can be read without parsing prose.
2
3use serde::Serialize;
4
5/// One checkbox item of a body.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7pub struct Task {
8    /// 1-based line within the body.
9    pub line: usize,
10    pub done: bool,
11    pub text: String,
12}
13
14/// Every task item outside fenced code, in order.
15pub 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}