Skip to main content

opendev_runtime/todo/
parsing.rs

1use super::TodoStatus;
2
3/// Map status alias strings to `TodoStatus`.
4///
5/// Accepts: `pending`, `todo`, `in_progress`, `doing`, `in-progress`,
6/// `completed`, `done`, `complete`.
7pub fn parse_status(s: &str) -> Option<TodoStatus> {
8    match s.to_lowercase().trim() {
9        "pending" | "todo" => Some(TodoStatus::Pending),
10        "in_progress" | "doing" | "in-progress" | "in progress" => Some(TodoStatus::InProgress),
11        "completed" | "done" | "complete" => Some(TodoStatus::Completed),
12        _ => None,
13    }
14}
15
16/// Strip basic markdown formatting from text (bold, italic, code).
17pub fn strip_markdown(text: &str) -> String {
18    text.replace("**", "")
19        .replace("__", "")
20        .replace('*', "")
21        .replace('_', " ")
22        .replace('`', "")
23        .replace("~~", "")
24}
25
26/// Parse plan markdown content and extract numbered implementation steps.
27///
28/// First looks for a section header like `## Implementation Steps` or `## Steps`,
29/// then extracts numbered list items from that section. If no such section exists,
30/// falls back to extracting all numbered items from the entire document.
31pub fn parse_plan_steps(plan_content: &str) -> Vec<String> {
32    // First try: section-aware extraction
33    let mut steps = Vec::new();
34    let mut in_steps_section = false;
35
36    for line in plan_content.lines() {
37        let trimmed = line.trim();
38
39        // Detect steps section header
40        if trimmed.starts_with("## Implementation Steps")
41            || trimmed.starts_with("## Steps")
42            || trimmed.starts_with("## implementation steps")
43        {
44            in_steps_section = true;
45            continue;
46        }
47
48        // End of section on next header
49        if in_steps_section && trimmed.starts_with("## ") {
50            break;
51        }
52
53        // Extract numbered items
54        if in_steps_section
55            && let Some(text) = extract_numbered_step(trimmed)
56            && !text.is_empty()
57        {
58            steps.push(text);
59        }
60    }
61
62    // Fallback: if no section header found, extract all numbered items
63    if steps.is_empty() {
64        for line in plan_content.lines() {
65            let trimmed = line.trim();
66            // Skip markdown headers themselves
67            if trimmed.starts_with('#') {
68                continue;
69            }
70            if let Some(text) = extract_numbered_step(trimmed)
71                && !text.is_empty()
72            {
73                steps.push(text);
74            }
75        }
76    }
77
78    steps
79}
80
81/// Extract the text from a numbered list item.
82///
83/// Handles formats like:
84/// - `1. Step text`
85/// - `1) Step text`
86/// - `1 - Step text`
87fn extract_numbered_step(line: &str) -> Option<String> {
88    let line = line.trim();
89    if line.is_empty() {
90        return None;
91    }
92
93    // Check if line starts with a digit
94    let mut chars = line.chars();
95    let first = chars.next()?;
96    if !first.is_ascii_digit() {
97        return None;
98    }
99
100    // Skip remaining digits
101    let rest: String = chars.collect();
102    let rest = rest.trim_start_matches(|c: char| c.is_ascii_digit());
103
104    // Check for separator (. or ) or -)
105    let rest = if let Some(s) = rest.strip_prefix(". ") {
106        s
107    } else if let Some(s) = rest.strip_prefix(") ") {
108        s
109    } else if let Some(s) = rest.strip_prefix(" - ") {
110        s
111    } else {
112        return None;
113    };
114
115    let text = rest.trim();
116    if text.is_empty() {
117        None
118    } else {
119        // Strip markdown bold/emphasis markers for cleaner titles
120        let text = text.replace("**", "").replace("__", "");
121        Some(text)
122    }
123}
124
125#[cfg(test)]
126#[path = "parsing_tests.rs"]
127mod tests;