use crate::task::{Task, TaskPriority};
use regex::Regex;
use std::sync::LazyLock;
#[derive(Debug, Clone)]
pub struct ParsedStep {
pub number: usize,
pub description: String,
pub indent_level: usize,
pub is_priority: bool,
}
pub fn parse_plan_steps(content: &str) -> Vec<ParsedStep> {
let mut steps = Vec::new();
static NUMBERED_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\s*)(\d+)[.)]\s*(.+)$").expect("valid regex"));
static STEP_COLON_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\s*)(?:Step\s+)?(\d+):\s*(.+)$").expect("valid regex"));
static BULLET_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\s*)[-*]\s+(.+)$").expect("valid regex"));
let numbered_re = &*NUMBERED_RE;
let step_colon_re = &*STEP_COLON_RE;
let bullet_re = &*BULLET_RE;
let mut current_number = 0;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(caps) = numbered_re.captures(line) {
let indent = caps.get(1).map(|m| m.as_str().len()).unwrap_or(0);
let _num: usize = caps
.get(2)
.expect("group 2 always present in match")
.as_str()
.parse()
.unwrap_or(0);
let desc = caps
.get(3)
.expect("group 3 always present in match")
.as_str()
.trim();
current_number += 1;
let indent_level = indent / 2;
steps.push(ParsedStep {
number: current_number,
description: desc.to_string(),
indent_level,
is_priority: desc.to_lowercase().contains("important")
|| desc.to_lowercase().contains("critical")
|| desc.contains("!"),
});
continue;
}
if let Some(caps) = step_colon_re.captures(line) {
let indent = caps.get(1).map(|m| m.as_str().len()).unwrap_or(0);
let _num: usize = caps
.get(2)
.expect("group 2 always present in match")
.as_str()
.parse()
.unwrap_or(0);
let desc = caps
.get(3)
.expect("group 3 always present in match")
.as_str()
.trim();
current_number += 1;
let indent_level = indent / 2;
steps.push(ParsedStep {
number: current_number,
description: desc.to_string(),
indent_level,
is_priority: desc.to_lowercase().contains("important")
|| desc.to_lowercase().contains("critical"),
});
continue;
}
if let Some(caps) = bullet_re.captures(line) {
let indent = caps.get(1).map(|m| m.as_str().len()).unwrap_or(0);
let desc = caps
.get(2)
.expect("group 2 always present in match")
.as_str()
.trim();
if desc.starts_with("Note:") || desc.starts_with("Warning:") {
continue;
}
if desc.len() > 10
&& (desc.to_lowercase().contains("create")
|| desc.to_lowercase().contains("add")
|| desc.to_lowercase().contains("implement")
|| desc.to_lowercase().contains("update")
|| desc.to_lowercase().contains("modify")
|| desc.to_lowercase().contains("configure")
|| desc.to_lowercase().contains("set up")
|| desc.to_lowercase().contains("install")
|| desc.to_lowercase().contains("test")
|| desc.to_lowercase().contains("verify")
|| desc.to_lowercase().contains("check")
|| desc.to_lowercase().contains("review")
|| desc.to_lowercase().contains("fix")
|| desc.to_lowercase().contains("remove")
|| desc.to_lowercase().contains("delete"))
{
current_number += 1;
let indent_level = indent / 2;
steps.push(ParsedStep {
number: current_number,
description: desc.to_string(),
indent_level,
is_priority: false,
});
}
}
}
steps
}
pub fn steps_to_tasks(steps: &[ParsedStep], plan_id: &str) -> Vec<Task> {
let mut tasks = Vec::new();
let mut parent_stack: Vec<String> = Vec::new();
for step in steps {
let task_id = format!("{}-step-{}", &plan_id[..8.min(plan_id.len())], step.number);
let priority = if step.is_priority {
TaskPriority::High
} else {
TaskPriority::Normal
};
let mut task = Task::new_for_plan(
task_id.clone(),
step.description.clone(),
plan_id.to_string(),
);
task.priority = priority;
if step.indent_level == 0 {
parent_stack.clear();
parent_stack.push(task_id.clone());
} else if step.indent_level <= parent_stack.len() {
parent_stack.truncate(step.indent_level);
if let Some(parent_id) = parent_stack.last() {
task.parent_id = Some(parent_id.clone());
}
parent_stack.push(task_id.clone());
} else {
if let Some(parent_id) = parent_stack.last() {
task.parent_id = Some(parent_id.clone());
}
parent_stack.push(task_id.clone());
}
if !tasks.is_empty() && step.indent_level == 0 {
let prev_task: &Task = &tasks[tasks.len() - 1];
if prev_task.parent_id.is_none() {
task.depends_on.push(prev_task.id.clone());
}
}
tasks.push(task);
}
let task_ids: Vec<_> = tasks.iter().map(|t| t.id.clone()).collect();
for i in 0..tasks.len() {
if let Some(ref parent_id) = tasks[i].parent_id.clone() {
for task in tasks.iter_mut() {
if task.id == *parent_id {
task.children.push(task_ids[i].clone());
break;
}
}
}
}
tasks
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_numbered_steps() {
let content = r#"
1. Create the user model
2. Add authentication endpoints
3. Implement JWT token handling
"#;
let steps = parse_plan_steps(content);
assert_eq!(steps.len(), 3);
assert_eq!(steps[0].description, "Create the user model");
assert_eq!(steps[1].description, "Add authentication endpoints");
}
#[test]
fn test_parse_step_colon_format() {
let content = r#"
Step 1: Initialize the project
Step 2: Configure dependencies
"#;
let steps = parse_plan_steps(content);
assert_eq!(steps.len(), 2);
assert_eq!(steps[0].description, "Initialize the project");
}
#[test]
fn test_parse_indented_steps() {
let content = r#"
1. Setup phase
1. Install dependencies
2. Configure environment
2. Implementation phase
"#;
let steps = parse_plan_steps(content);
assert_eq!(steps.len(), 4);
}
#[test]
fn test_steps_to_tasks() {
let content = "1. First step\n2. Second step";
let steps = parse_plan_steps(content);
let tasks = steps_to_tasks(&steps, "plan-12345678");
assert_eq!(tasks.len(), 2);
assert!(tasks[0].plan_id.is_some());
assert_eq!(tasks[0].plan_id.as_ref().unwrap(), "plan-12345678");
}
#[test]
fn test_priority_detection() {
let content = "1. Important: Fix critical bug!\n2. Normal task";
let steps = parse_plan_steps(content);
assert!(steps[0].is_priority);
assert!(!steps[1].is_priority);
}
}