ricecoder_keybinds/
parser.rs

1//! Keybind configuration parsers for JSON and Markdown formats
2
3use crate::error::ParseError;
4use crate::models::Keybind;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8/// Trait for parsing keybind configurations
9pub trait KeybindParser: Send + Sync {
10    /// Parse keybind configuration from content
11    fn parse(&self, content: &str) -> Result<Vec<Keybind>, ParseError>;
12}
13
14/// Registry for keybind parsers supporting multiple formats
15pub struct ParserRegistry {
16    parsers: HashMap<String, Arc<dyn KeybindParser>>,
17}
18
19impl ParserRegistry {
20    /// Create a new parser registry with default parsers
21    pub fn new() -> Self {
22        let mut parsers = HashMap::new();
23        parsers.insert("json".to_string(), Arc::new(JsonKeybindParser) as Arc<dyn KeybindParser>);
24        parsers.insert("markdown".to_string(), Arc::new(MarkdownKeybindParser) as Arc<dyn KeybindParser>);
25        parsers.insert("md".to_string(), Arc::new(MarkdownKeybindParser) as Arc<dyn KeybindParser>);
26        
27        ParserRegistry { parsers }
28    }
29
30    /// Register a custom parser for a format
31    pub fn register(&mut self, format: impl Into<String>, parser: Arc<dyn KeybindParser>) {
32        self.parsers.insert(format.into(), parser);
33    }
34
35    /// Get a parser for a specific format
36    pub fn get_parser(&self, format: &str) -> Option<Arc<dyn KeybindParser>> {
37        self.parsers.get(format).cloned()
38    }
39
40    /// Auto-detect format and parse content
41    pub fn parse_auto(&self, content: &str) -> Result<Vec<Keybind>, ParseError> {
42        // Try JSON first
43        if let Ok(keybinds) = self.get_parser("json")
44            .ok_or_else(|| ParseError::InvalidJson("No JSON parser available".to_string()))?
45            .parse(content) {
46            return Ok(keybinds);
47        }
48
49        // Fall back to Markdown
50        self.get_parser("markdown")
51            .ok_or_else(|| ParseError::InvalidMarkdown("No Markdown parser available".to_string()))?
52            .parse(content)
53    }
54
55    /// Parse content with explicit format
56    pub fn parse(&self, content: &str, format: &str) -> Result<Vec<Keybind>, ParseError> {
57        let parser = self.get_parser(format)
58            .ok_or_else(|| ParseError::InvalidJson(format!("Unknown format: {}", format)))?;
59        parser.parse(content)
60    }
61}
62
63impl Default for ParserRegistry {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69/// JSON keybind parser
70pub struct JsonKeybindParser;
71
72impl KeybindParser for JsonKeybindParser {
73    fn parse(&self, content: &str) -> Result<Vec<Keybind>, ParseError> {
74        let value: serde_json::Value = serde_json::from_str(content)
75            .map_err(|e| ParseError::InvalidJson(e.to_string()))?;
76
77        let keybinds_array = value
78            .get("keybinds")
79            .and_then(|v| v.as_array())
80            .ok_or_else(|| ParseError::MissingField("keybinds".to_string()))?;
81
82        let mut keybinds = Vec::new();
83        for (idx, item) in keybinds_array.iter().enumerate() {
84            let keybind: Keybind = serde_json::from_value(item.clone()).map_err(|e| {
85                ParseError::LineError {
86                    line: idx + 1,
87                    message: e.to_string(),
88                }
89            })?;
90
91            // Validate required fields
92            if keybind.action_id.is_empty() {
93                return Err(ParseError::LineError {
94                    line: idx + 1,
95                    message: "Missing action_id".to_string(),
96                });
97            }
98            if keybind.key.is_empty() {
99                return Err(ParseError::LineError {
100                    line: idx + 1,
101                    message: "Missing key".to_string(),
102                });
103            }
104
105            keybinds.push(keybind);
106        }
107
108        Ok(keybinds)
109    }
110}
111
112/// Markdown keybind parser
113pub struct MarkdownKeybindParser;
114
115impl KeybindParser for MarkdownKeybindParser {
116    fn parse(&self, content: &str) -> Result<Vec<Keybind>, ParseError> {
117        let mut keybinds = Vec::new();
118        let mut current_category = String::new();
119
120        for (line_num, line) in content.lines().enumerate() {
121            let trimmed = line.trim();
122
123            // Skip empty lines and code blocks
124            if trimmed.is_empty() || trimmed.starts_with("```") {
125                continue;
126            }
127
128            // Extract category from headers
129            if let Some(category) = trimmed.strip_prefix("## ") {
130                current_category = category.trim().to_string();
131                continue;
132            }
133
134            // Parse keybind entries: `action_id`: key - description
135            if trimmed.starts_with("- `") {
136                let keybind = parse_markdown_entry(trimmed, &current_category, line_num + 1)?;
137                keybinds.push(keybind);
138            }
139        }
140
141        Ok(keybinds)
142    }
143}
144
145/// Parse a single markdown keybind entry
146/// Format: - `action_id`: key - description
147fn parse_markdown_entry(
148    line: &str,
149    category: &str,
150    line_num: usize,
151) -> Result<Keybind, ParseError> {
152    // Remove leading "- `"
153    let content = line.strip_prefix("- `").ok_or_else(|| ParseError::LineError {
154        line: line_num,
155        message: "Invalid markdown format".to_string(),
156    })?;
157
158    // Find the closing backtick
159    let backtick_pos = content.find('`').ok_or_else(|| ParseError::LineError {
160        line: line_num,
161        message: "Missing closing backtick".to_string(),
162    })?;
163
164    let action_id = content[..backtick_pos].to_string();
165
166    // Find the colon
167    let rest = &content[backtick_pos + 1..];
168    let colon_pos = rest.find(':').ok_or_else(|| ParseError::LineError {
169        line: line_num,
170        message: "Missing colon after action_id".to_string(),
171    })?;
172
173    // Find the dash separator
174    let key_part = &rest[colon_pos + 1..];
175    let dash_pos = key_part.find(" - ").ok_or_else(|| ParseError::LineError {
176        line: line_num,
177        message: "Missing ' - ' separator".to_string(),
178    })?;
179
180    let key = key_part[..dash_pos].trim().to_string();
181    let description = key_part[dash_pos + 3..].trim().to_string();
182
183    Ok(Keybind {
184        action_id,
185        key,
186        category: category.to_string(),
187        description,
188        is_default: false,
189    })
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_parser_registry_creation() {
198        let registry = ParserRegistry::new();
199        assert!(registry.get_parser("json").is_some());
200        assert!(registry.get_parser("markdown").is_some());
201        assert!(registry.get_parser("md").is_some());
202    }
203
204    #[test]
205    fn test_parser_registry_parse_json() {
206        let registry = ParserRegistry::new();
207        let json = r#"{
208            "version": "1.0",
209            "keybinds": [
210                {
211                    "action_id": "editor.save",
212                    "key": "Ctrl+S",
213                    "category": "editing",
214                    "description": "Save file",
215                    "is_default": true
216                }
217            ]
218        }"#;
219
220        let keybinds = registry.parse(json, "json").unwrap();
221        assert_eq!(keybinds.len(), 1);
222        assert_eq!(keybinds[0].action_id, "editor.save");
223    }
224
225    #[test]
226    fn test_parser_registry_parse_markdown() {
227        let registry = ParserRegistry::new();
228        let markdown = r#"# Keybinds
229
230## Editing
231
232- `editor.save`: Ctrl+S - Save file
233"#;
234
235        let keybinds = registry.parse(markdown, "markdown").unwrap();
236        assert_eq!(keybinds.len(), 1);
237        assert_eq!(keybinds[0].action_id, "editor.save");
238    }
239
240    #[test]
241    fn test_parser_registry_auto_detect_json() {
242        let registry = ParserRegistry::new();
243        let json = r#"{
244            "version": "1.0",
245            "keybinds": [
246                {
247                    "action_id": "editor.save",
248                    "key": "Ctrl+S",
249                    "category": "editing",
250                    "description": "Save file",
251                    "is_default": true
252                }
253            ]
254        }"#;
255
256        let keybinds = registry.parse_auto(json).unwrap();
257        assert_eq!(keybinds.len(), 1);
258    }
259
260    #[test]
261    fn test_parser_registry_auto_detect_markdown() {
262        let registry = ParserRegistry::new();
263        let markdown = r#"# Keybinds
264
265## Editing
266
267- `editor.save`: Ctrl+S - Save file
268"#;
269
270        let keybinds = registry.parse_auto(markdown).unwrap();
271        assert_eq!(keybinds.len(), 1);
272    }
273
274    #[test]
275    fn test_json_parser_valid() {
276        let json = r#"{
277            "version": "1.0",
278            "keybinds": [
279                {
280                    "action_id": "editor.save",
281                    "key": "Ctrl+S",
282                    "category": "editing",
283                    "description": "Save file",
284                    "is_default": true
285                }
286            ]
287        }"#;
288
289        let parser = JsonKeybindParser;
290        let keybinds = parser.parse(json).unwrap();
291        assert_eq!(keybinds.len(), 1);
292        assert_eq!(keybinds[0].action_id, "editor.save");
293        assert_eq!(keybinds[0].key, "Ctrl+S");
294        assert!(keybinds[0].is_default);
295    }
296
297    #[test]
298    fn test_json_parser_invalid() {
299        let json = "invalid json";
300        let parser = JsonKeybindParser;
301        assert!(parser.parse(json).is_err());
302    }
303
304    #[test]
305    fn test_json_parser_missing_keybinds() {
306        let json = r#"{"version": "1.0"}"#;
307        let parser = JsonKeybindParser;
308        assert!(parser.parse(json).is_err());
309    }
310
311    #[test]
312    fn test_markdown_parser_valid() {
313        let markdown = r#"# Keybinds
314
315## Editing
316
317- `editor.save`: Ctrl+S - Save file
318- `editor.undo`: Ctrl+Z - Undo
319
320## Navigation
321
322- `nav.next`: Tab - Next item
323"#;
324
325        let parser = MarkdownKeybindParser;
326        let keybinds = parser.parse(markdown).unwrap();
327        assert_eq!(keybinds.len(), 3);
328        assert_eq!(keybinds[0].action_id, "editor.save");
329        assert_eq!(keybinds[0].category, "Editing");
330        assert_eq!(keybinds[2].category, "Navigation");
331    }
332
333    #[test]
334    fn test_markdown_parser_empty() {
335        let markdown = "# Keybinds\n\n## Editing\n";
336        let parser = MarkdownKeybindParser;
337        let keybinds = parser.parse(markdown).unwrap();
338        assert_eq!(keybinds.len(), 0);
339    }
340}