piki_core/
plugin.rs

1#![allow(dead_code)]
2
3use crate::document::DocumentStore;
4use std::collections::HashMap;
5
6/// Trait for plugins that dynamically generate page content
7pub trait Plugin: Send + Sync {
8    /// Generate content for this plugin based on the current wiki state
9    fn generate_content(&self, store: &DocumentStore) -> Result<String, String>;
10}
11
12/// Registry for managing wiki plugins
13pub struct PluginRegistry {
14    plugins: HashMap<String, Box<dyn Plugin>>,
15}
16
17impl PluginRegistry {
18    /// Create a new empty plugin registry
19    pub fn new() -> Self {
20        PluginRegistry {
21            plugins: HashMap::new(),
22        }
23    }
24
25    /// Register a plugin with a given name
26    pub fn register(&mut self, name: impl Into<String>, plugin: Box<dyn Plugin>) {
27        self.plugins.insert(name.into(), plugin);
28    }
29
30    /// Check if a plugin exists with the given name
31    pub fn has_plugin(&self, name: &str) -> bool {
32        self.plugins.contains_key(name)
33    }
34
35    /// Generate content using the named plugin
36    pub fn generate(&self, name: &str, store: &DocumentStore) -> Result<String, String> {
37        self.plugins
38            .get(name)
39            .ok_or_else(|| format!("Plugin '{}' not found", name))
40            .and_then(|plugin| plugin.generate_content(store))
41    }
42}
43
44impl Default for PluginRegistry {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50/// Built-in plugin that generates a sorted index of all pages
51pub struct IndexPlugin;
52
53impl Plugin for IndexPlugin {
54    fn generate_content(&self, store: &DocumentStore) -> Result<String, String> {
55        let mut all_docs = store.list_all_documents()?;
56        all_docs.sort();
57
58        let mut content = String::from("# Index\n\n");
59        content.push_str(&format!(
60            "*Dynamically generated index of all {} pages*\n\n",
61            all_docs.len()
62        ));
63
64        if all_docs.is_empty() {
65            content.push_str("No pages found.\n");
66            return Ok(content);
67        }
68
69        // Group by top-level directory
70        let mut grouped: HashMap<String, Vec<String>> = HashMap::new();
71
72        for doc in &all_docs {
73            if let Some(slash_pos) = doc.find('/') {
74                let category = &doc[..slash_pos];
75                grouped
76                    .entry(category.to_string())
77                    .or_default()
78                    .push(doc.clone());
79            } else {
80                grouped
81                    .entry("Root".to_string())
82                    .or_default()
83                    .push(doc.clone());
84            }
85        }
86
87        // Sort categories
88        let mut categories: Vec<_> = grouped.keys().cloned().collect();
89        categories.sort();
90
91        // Always put "Root" first if it exists
92        if let Some(pos) = categories.iter().position(|c| c == "Root") {
93            let root = categories.remove(pos);
94            categories.insert(0, root);
95        }
96
97        // Generate grouped output
98        for category in &categories {
99            if let Some(docs) = grouped.get(category) {
100                if category == "Root" && categories.len() > 1 {
101                    content.push_str("## Root Pages\n\n");
102                } else if category != "Root" {
103                    content.push_str(&format!("## {}\n\n", category));
104                }
105
106                for doc in docs {
107                    content.push_str(&format!("- [[{}]]\n", doc));
108                }
109                content.push('\n');
110            }
111        }
112
113        content.push_str("---\n\n");
114        content.push_str("*This page is generated by the `index` plugin*\n");
115
116        Ok(content)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use std::path::PathBuf;
124
125    #[test]
126    fn test_plugin_registry() {
127        let mut registry = PluginRegistry::new();
128
129        assert!(!registry.has_plugin("index"));
130
131        registry.register("index", Box::new(IndexPlugin));
132
133        assert!(registry.has_plugin("index"));
134        assert!(!registry.has_plugin("nonexistent"));
135    }
136
137    #[test]
138    fn test_index_plugin_empty() {
139        use std::env;
140        use std::fs;
141
142        let temp_dir = env::temp_dir().join("piki-test-plugin-empty");
143        let _ = fs::remove_dir_all(&temp_dir);
144        fs::create_dir_all(&temp_dir).unwrap();
145
146        let store = DocumentStore::new(temp_dir.clone());
147        let plugin = IndexPlugin;
148
149        // Should handle empty directory gracefully
150        let result = plugin.generate_content(&store);
151        assert!(result.is_ok());
152
153        let content = result.unwrap();
154        assert!(content.contains("# Index"));
155        assert!(content.contains("No pages found"));
156
157        // Cleanup
158        fs::remove_dir_all(&temp_dir).ok();
159    }
160
161    #[test]
162    fn test_index_plugin_with_pages() {
163        let store = DocumentStore::new(PathBuf::from("example-wiki"));
164        let plugin = IndexPlugin;
165
166        let content = plugin.generate_content(&store).unwrap();
167
168        // Should contain header
169        assert!(content.contains("# Index"));
170        // Should be markdown
171        assert!(content.contains("[["));
172    }
173}