1use super::{RuleParser, UnifiedRule, extract_bullet_points, parse_markdown_sections};
4use crate::{DrivenError, Editor, Result};
5use std::path::Path;
6
7#[derive(Debug, Default)]
9pub struct CursorParser;
10
11impl CursorParser {
12 pub fn new() -> Self {
14 Self
15 }
16
17 fn detect_section_type(heading: &str) -> SectionKind {
19 let lower = heading.to_lowercase();
20
21 if lower.contains("persona")
22 || lower.contains("role")
23 || lower.contains("who you are")
24 || lower.contains("identity")
25 {
26 SectionKind::Persona
27 } else if lower.contains("context")
28 || lower.contains("project")
29 || lower.contains("codebase")
30 {
31 SectionKind::Context
32 } else if lower.contains("workflow") || lower.contains("process") || lower.contains("steps")
33 {
34 SectionKind::Workflow
35 } else if lower.contains("style")
36 || lower.contains("convention")
37 || lower.contains("standard")
38 || lower.contains("rule")
39 || lower.contains("guideline")
40 {
41 SectionKind::Standards
42 } else {
43 SectionKind::Other
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49enum SectionKind {
50 Persona,
51 Context,
52 Workflow,
53 Standards,
54 Other,
55}
56
57impl RuleParser for CursorParser {
58 fn parse_file(&self, path: &Path) -> Result<Vec<UnifiedRule>> {
59 let content = std::fs::read_to_string(path)
60 .map_err(|e| DrivenError::Parse(format!("Failed to read {}: {}", path.display(), e)))?;
61 self.parse_content(&content)
62 }
63
64 fn parse_content(&self, content: &str) -> Result<Vec<UnifiedRule>> {
65 let mut rules = Vec::new();
66 let sections = parse_markdown_sections(content);
67
68 if sections.is_empty() {
69 if !content.trim().is_empty() {
71 rules.push(UnifiedRule::raw(content.trim()));
72 }
73 return Ok(rules);
74 }
75
76 for (heading, body) in sections {
77 let kind = Self::detect_section_type(&heading);
78
79 match kind {
80 SectionKind::Persona => {
81 let points = extract_bullet_points(&body);
82 let (traits, principles): (Vec<_>, Vec<_>) = points
83 .into_iter()
84 .partition(|p| !p.to_lowercase().contains("principle"));
85
86 rules.push(UnifiedRule::Persona {
87 name: heading.clone(),
88 role: body.lines().next().unwrap_or("").to_string(),
89 identity: None,
90 style: None,
91 traits,
92 principles,
93 });
94 }
95 SectionKind::Context => {
96 let points = extract_bullet_points(&body);
97 let (includes, rest): (Vec<_>, Vec<_>) = points
98 .into_iter()
99 .partition(|p| p.contains("**") || p.contains("src/"));
100
101 rules.push(UnifiedRule::Context {
102 includes,
103 excludes: Vec::new(),
104 focus: rest,
105 });
106 }
107 SectionKind::Workflow => {
108 let points = extract_bullet_points(&body);
109 let steps = points
110 .into_iter()
111 .map(|p| super::WorkflowStepData {
112 name: p.clone(),
113 description: p,
114 condition: None,
115 actions: Vec::new(),
116 })
117 .collect();
118
119 rules.push(UnifiedRule::Workflow {
120 name: heading,
121 steps,
122 });
123 }
124 SectionKind::Standards => {
125 let points = extract_bullet_points(&body);
126 for (i, point) in points.into_iter().enumerate() {
127 rules.push(UnifiedRule::Standard {
128 category: crate::format::RuleCategory::Style,
129 priority: i as u8,
130 description: point,
131 pattern: None,
132 });
133 }
134 }
135 SectionKind::Other => {
136 if !body.trim().is_empty() {
138 rules.push(UnifiedRule::raw(format!("# {}\n{}", heading, body)));
139 }
140 }
141 }
142 }
143
144 Ok(rules)
145 }
146
147 fn editor(&self) -> Editor {
148 Editor::Cursor
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_parse_simple_cursorrules() {
158 let content = r#"# Coding Guidelines
159
160- Use snake_case for functions
161- Use PascalCase for types
162- Keep functions under 50 lines
163
164# Project Context
165
166- Focus on src/ directory
167- This is a Rust project
168"#;
169
170 let parser = CursorParser::new();
171 let rules = parser.parse_content(content).unwrap();
172
173 assert!(!rules.is_empty());
174 }
175
176 #[test]
177 fn test_parse_empty() {
178 let parser = CursorParser::new();
179 let rules = parser.parse_content("").unwrap();
180 assert!(rules.is_empty());
181 }
182
183 #[test]
184 fn test_parse_no_structure() {
185 let content = "Just some plain text without markdown structure.";
186 let parser = CursorParser::new();
187 let rules = parser.parse_content(content).unwrap();
188
189 assert_eq!(rules.len(), 1);
190 assert!(matches!(rules[0], UnifiedRule::Raw { .. }));
191 }
192
193 #[test]
194 fn test_detect_section_types() {
195 assert_eq!(
196 CursorParser::detect_section_type("AI Persona"),
197 SectionKind::Persona
198 );
199 assert_eq!(
200 CursorParser::detect_section_type("Coding Conventions"),
201 SectionKind::Standards
202 );
203 assert_eq!(
204 CursorParser::detect_section_type("Project Context"),
205 SectionKind::Context
206 );
207 assert_eq!(
208 CursorParser::detect_section_type("Development Workflow"),
209 SectionKind::Workflow
210 );
211 }
212}