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 ClaudeParser;
10
11impl ClaudeParser {
12 pub fn new() -> Self {
14 Self
15 }
16}
17
18impl RuleParser for ClaudeParser {
19 fn parse_file(&self, path: &Path) -> Result<Vec<UnifiedRule>> {
20 if path.is_dir() {
22 return self.parse_directory(path);
23 }
24
25 let content = std::fs::read_to_string(path)
26 .map_err(|e| DrivenError::Parse(format!("Failed to read {}: {}", path.display(), e)))?;
27 self.parse_content(&content)
28 }
29
30 fn parse_content(&self, content: &str) -> Result<Vec<UnifiedRule>> {
31 let mut rules = Vec::new();
32 let sections = parse_markdown_sections(content);
33
34 if sections.is_empty() {
35 if !content.trim().is_empty() {
36 rules.push(UnifiedRule::raw(content.trim()));
37 }
38 return Ok(rules);
39 }
40
41 for (heading, body) in sections {
42 let lower = heading.to_lowercase();
43
44 if lower.contains("instructions") || lower.contains("guidelines") {
45 let points = extract_bullet_points(&body);
46 for (i, point) in points.into_iter().enumerate() {
47 rules.push(UnifiedRule::Standard {
48 category: crate::format::RuleCategory::Style,
49 priority: i as u8,
50 description: point,
51 pattern: None,
52 });
53 }
54 } else if lower.contains("context") || lower.contains("about") {
55 let points = extract_bullet_points(&body);
56 rules.push(UnifiedRule::Context {
57 includes: Vec::new(),
58 excludes: Vec::new(),
59 focus: points,
60 });
61 } else if lower.contains("memory") || lower.contains("remember") {
62 let points = extract_bullet_points(&body);
64 for (i, point) in points.into_iter().enumerate() {
65 rules.push(UnifiedRule::Standard {
66 category: crate::format::RuleCategory::Other,
67 priority: (100 + i) as u8,
68 description: point,
69 pattern: None,
70 });
71 }
72 } else if !body.trim().is_empty() {
73 rules.push(UnifiedRule::raw(format!("# {}\n{}", heading, body)));
74 }
75 }
76
77 Ok(rules)
78 }
79
80 fn editor(&self) -> Editor {
81 Editor::Claude
82 }
83}
84
85impl ClaudeParser {
86 fn parse_directory(&self, path: &Path) -> Result<Vec<UnifiedRule>> {
87 let mut rules = Vec::new();
88
89 let files_to_check = ["settings.json", "CLAUDE.md", "instructions.md"];
91
92 for file_name in files_to_check {
93 let file_path = path.join(file_name);
94 if file_path.exists() {
95 if file_name.ends_with(".json") {
96 let content = std::fs::read_to_string(&file_path)?;
98 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
99 rules.extend(self.parse_json_settings(&json)?);
100 }
101 } else {
102 let content = std::fs::read_to_string(&file_path)?;
104 rules.extend(self.parse_content(&content)?);
105 }
106 }
107 }
108
109 Ok(rules)
110 }
111
112 fn parse_json_settings(&self, json: &serde_json::Value) -> Result<Vec<UnifiedRule>> {
113 let mut rules = Vec::new();
114
115 if let Some(obj) = json.as_object() {
116 if let Some(instructions) = obj.get("instructions") {
117 if let Some(text) = instructions.as_str() {
118 rules.push(UnifiedRule::raw(text));
119 } else if let Some(arr) = instructions.as_array() {
120 for (i, item) in arr.iter().enumerate() {
121 if let Some(text) = item.as_str() {
122 rules.push(UnifiedRule::Standard {
123 category: crate::format::RuleCategory::Style,
124 priority: i as u8,
125 description: text.to_string(),
126 pattern: None,
127 });
128 }
129 }
130 }
131 }
132 }
133
134 Ok(rules)
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_parse_claude_md() {
144 let content = r#"# CLAUDE.md
145
146## Instructions
147
148- Always explain your reasoning
149- Use clear, concise language
150- Follow the coding standards
151
152## Project Context
153
154- This is a Rust project
155- Focus on performance
156"#;
157
158 let parser = ClaudeParser::new();
159 let rules = parser.parse_content(content).unwrap();
160
161 assert!(!rules.is_empty());
162 }
163}