mod claude;
mod copilot;
mod cursor;
mod generic;
mod windsurf;
pub use claude::ClaudeEmitter;
pub use copilot::CopilotEmitter;
pub use cursor::CursorEmitter;
pub use generic::GenericEmitter;
pub use windsurf::WindsurfEmitter;
use crate::{DrivenError, Editor, Result, parser::UnifiedRule};
use std::path::Path;
pub trait RuleEmitter {
fn emit_file(&self, rules: &[UnifiedRule], path: &Path) -> Result<()>;
fn emit_string(&self, rules: &[UnifiedRule]) -> Result<String>;
fn editor(&self) -> Editor;
}
pub struct Emitter {
inner: Box<dyn RuleEmitter + Send + Sync>,
}
impl std::fmt::Debug for Emitter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Emitter")
.field("editor", &self.inner.editor())
.finish()
}
}
impl Emitter {
pub fn for_editor(editor: Editor) -> Self {
let inner: Box<dyn RuleEmitter + Send + Sync> = match editor {
Editor::Cursor => Box::new(CursorEmitter::new()),
Editor::Copilot => Box::new(CopilotEmitter::new()),
Editor::Windsurf => Box::new(WindsurfEmitter::new()),
Editor::Claude => Box::new(ClaudeEmitter::new()),
Editor::Aider => Box::new(GenericEmitter::new()),
Editor::Cline => Box::new(GenericEmitter::new()),
};
Self { inner }
}
pub fn emit(&self, rules: &[UnifiedRule], path: impl AsRef<Path>) -> Result<()> {
self.inner.emit_file(rules, path.as_ref())
}
pub fn emit_string(&self, rules: &[UnifiedRule]) -> Result<String> {
self.inner.emit_string(rules)
}
pub fn editor(&self) -> Editor {
self.inner.editor()
}
}
pub(crate) fn ensure_parent_dir(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent).map_err(|e| {
DrivenError::Io(std::io::Error::other(format!(
"Failed to create directory {}: {}",
parent.display(),
e
)))
})?;
}
}
Ok(())
}
pub(crate) fn format_heading(level: usize, text: &str) -> String {
let hashes = "#".repeat(level.min(6));
format!("{} {}\n\n", hashes, text)
}
pub(crate) fn format_bullet_list(items: &[String]) -> String {
items.iter().map(|item| format!("- {}\n", item)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_heading() {
assert_eq!(format_heading(1, "Test"), "# Test\n\n");
assert_eq!(format_heading(2, "Test"), "## Test\n\n");
}
#[test]
fn test_format_bullet_list() {
let items = vec!["Item 1".to_string(), "Item 2".to_string()];
let result = format_bullet_list(&items);
assert!(result.contains("- Item 1"));
assert!(result.contains("- Item 2"));
}
}