1#![allow(dead_code)]
2
3use crate::document::DocumentStore;
4use std::collections::HashMap;
5
6pub trait Plugin: Send + Sync {
8 fn generate_content(&self, store: &DocumentStore) -> Result<String, String>;
10}
11
12pub struct PluginRegistry {
14 plugins: HashMap<String, Box<dyn Plugin>>,
15}
16
17impl PluginRegistry {
18 pub fn new() -> Self {
20 PluginRegistry {
21 plugins: HashMap::new(),
22 }
23 }
24
25 pub fn register(&mut self, name: impl Into<String>, plugin: Box<dyn Plugin>) {
27 self.plugins.insert(name.into(), plugin);
28 }
29
30 pub fn has_plugin(&self, name: &str) -> bool {
32 self.plugins.contains_key(name)
33 }
34
35 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
50pub 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 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 let mut categories: Vec<_> = grouped.keys().cloned().collect();
89 categories.sort();
90
91 if let Some(pos) = categories.iter().position(|c| c == "Root") {
93 let root = categories.remove(pos);
94 categories.insert(0, root);
95 }
96
97 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 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 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 assert!(content.contains("# Index"));
170 assert!(content.contains("[["));
172 }
173}