1mod claude;
6mod copilot;
7mod cursor;
8mod generic;
9mod windsurf;
10
11pub use claude::ClaudeEmitter;
12pub use copilot::CopilotEmitter;
13pub use cursor::CursorEmitter;
14pub use generic::GenericEmitter;
15pub use windsurf::WindsurfEmitter;
16
17use crate::{DrivenError, Editor, Result, parser::UnifiedRule};
18use std::path::Path;
19
20pub trait RuleEmitter {
22 fn emit_file(&self, rules: &[UnifiedRule], path: &Path) -> Result<()>;
24
25 fn emit_string(&self, rules: &[UnifiedRule]) -> Result<String>;
27
28 fn editor(&self) -> Editor;
30}
31
32pub struct Emitter {
34 inner: Box<dyn RuleEmitter + Send + Sync>,
35}
36
37impl std::fmt::Debug for Emitter {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("Emitter")
40 .field("editor", &self.inner.editor())
41 .finish()
42 }
43}
44
45impl Emitter {
46 pub fn for_editor(editor: Editor) -> Self {
48 let inner: Box<dyn RuleEmitter + Send + Sync> = match editor {
49 Editor::Cursor => Box::new(CursorEmitter::new()),
50 Editor::Copilot => Box::new(CopilotEmitter::new()),
51 Editor::Windsurf => Box::new(WindsurfEmitter::new()),
52 Editor::Claude => Box::new(ClaudeEmitter::new()),
53 Editor::Aider => Box::new(GenericEmitter::new()),
54 Editor::Cline => Box::new(GenericEmitter::new()),
55 };
56 Self { inner }
57 }
58
59 pub fn emit(&self, rules: &[UnifiedRule], path: impl AsRef<Path>) -> Result<()> {
61 self.inner.emit_file(rules, path.as_ref())
62 }
63
64 pub fn emit_string(&self, rules: &[UnifiedRule]) -> Result<String> {
66 self.inner.emit_string(rules)
67 }
68
69 pub fn editor(&self) -> Editor {
71 self.inner.editor()
72 }
73}
74
75pub(crate) fn ensure_parent_dir(path: &Path) -> Result<()> {
77 if let Some(parent) = path.parent() {
78 if !parent.exists() {
79 std::fs::create_dir_all(parent).map_err(|e| {
80 DrivenError::Io(std::io::Error::other(format!(
81 "Failed to create directory {}: {}",
82 parent.display(),
83 e
84 )))
85 })?;
86 }
87 }
88 Ok(())
89}
90
91pub(crate) fn format_heading(level: usize, text: &str) -> String {
93 let hashes = "#".repeat(level.min(6));
94 format!("{} {}\n\n", hashes, text)
95}
96
97pub(crate) fn format_bullet_list(items: &[String]) -> String {
99 items.iter().map(|item| format!("- {}\n", item)).collect()
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn test_format_heading() {
108 assert_eq!(format_heading(1, "Test"), "# Test\n\n");
109 assert_eq!(format_heading(2, "Test"), "## Test\n\n");
110 }
111
112 #[test]
113 fn test_format_bullet_list() {
114 let items = vec!["Item 1".to_string(), "Item 2".to_string()];
115 let result = format_bullet_list(&items);
116 assert!(result.contains("- Item 1"));
117 assert!(result.contains("- Item 2"));
118 }
119}