1use crate::css_generator::{CssGenerator, CssRule, CssProperty};
7use crate::error::{Result, TailwindError};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum PluginHook {
14 BeforeGenerate,
16 AfterGenerate,
18 OnClassAdd,
20 OnRuleCreate,
22 OnOptimize,
24}
25
26#[derive(Debug, Clone)]
28pub struct PluginContext {
29 pub generator: Arc<CssGenerator>,
31 pub data: HashMap<String, serde_json::Value>,
33 pub config: HashMap<String, serde_json::Value>,
35}
36
37pub trait Plugin: Send + Sync {
39 fn name(&self) -> &str;
41
42 fn version(&self) -> &str;
44
45 fn description(&self) -> &str;
47
48 fn initialize(&mut self, context: &mut PluginContext) -> Result<()>;
50
51 fn handle_hook(&mut self, hook: PluginHook, context: &mut PluginContext) -> Result<()>;
53
54 fn get_config_schema(&self) -> Option<serde_json::Value>;
56
57 fn validate_config(&self, config: &serde_json::Value) -> Result<()>;
59}
60
61pub struct PluginRegistry {
63 plugins: HashMap<String, Box<dyn Plugin>>,
64 hooks: HashMap<PluginHook, Vec<String>>,
65 context: PluginContext,
66}
67
68impl PluginRegistry {
69 pub fn new() -> Self {
71 Self {
72 plugins: HashMap::new(),
73 hooks: HashMap::new(),
74 context: PluginContext {
75 generator: Arc::new(CssGenerator::new()),
76 data: HashMap::new(),
77 config: HashMap::new(),
78 },
79 }
80 }
81
82 pub fn register_plugin(&mut self, plugin: Box<dyn Plugin>) -> Result<()> {
84 let name = plugin.name().to_string();
85
86 if self.plugins.contains_key(&name) {
87 return Err(TailwindError::build(format!("Plugin '{}' is already registered", name)));
88 }
89
90 let mut plugin_box = plugin;
92 plugin_box.initialize(&mut self.context)?;
93
94 self.plugins.insert(name.clone(), plugin_box);
96
97 self.register_default_hooks(&name);
99
100 Ok(())
101 }
102
103 pub fn unregister_plugin(&mut self, name: &str) -> Result<()> {
105 if !self.plugins.contains_key(name) {
106 return Err(TailwindError::build(format!("Plugin '{}' is not registered", name)));
107 }
108
109 self.plugins.remove(name);
111
112 for hook_list in self.hooks.values_mut() {
114 hook_list.retain(|plugin_name| plugin_name != name);
115 }
116
117 Ok(())
118 }
119
120 pub fn get_plugin(&self, name: &str) -> Option<&dyn Plugin> {
122 self.plugins.get(name).map(|p| p.as_ref())
123 }
124
125 pub fn get_plugin_mut(&mut self, name: &str) -> Option<&mut (dyn Plugin + '_)> {
127 if let Some(plugin) = self.plugins.get_mut(name) {
128 Some(plugin.as_mut())
129 } else {
130 None
131 }
132 }
133
134 pub fn list_plugins(&self) -> Vec<String> {
136 self.plugins.keys().cloned().collect()
137 }
138
139 pub fn execute_hook(&mut self, hook: PluginHook) -> Result<()> {
141 if let Some(plugin_names) = self.hooks.get(&hook) {
142 for plugin_name in plugin_names {
143 if let Some(plugin) = self.plugins.get_mut(plugin_name) {
144 plugin.handle_hook(hook.clone(), &mut self.context)?;
145 }
146 }
147 }
148 Ok(())
149 }
150
151 pub fn set_plugin_config(&mut self, plugin_name: &str, config: serde_json::Value) -> Result<()> {
153 if let Some(plugin) = self.plugins.get(plugin_name) {
154 plugin.validate_config(&config)?;
155 }
156
157 self.context.config.insert(plugin_name.to_string(), config);
158 Ok(())
159 }
160
161 pub fn get_plugin_config(&self, plugin_name: &str) -> Option<&serde_json::Value> {
163 self.context.config.get(plugin_name)
164 }
165
166 pub fn set_plugin_data(&mut self, key: String, value: serde_json::Value) {
168 self.context.data.insert(key, value);
169 }
170
171 pub fn get_plugin_data(&self, key: &str) -> Option<&serde_json::Value> {
173 self.context.data.get(key)
174 }
175
176 pub fn update_generator(&mut self, generator: CssGenerator) {
178 self.context.generator = Arc::new(generator);
179 }
180
181 pub fn get_generator(&self) -> Arc<CssGenerator> {
183 self.context.generator.clone()
184 }
185
186 fn register_default_hooks(&mut self, plugin_name: &str) {
188 let default_hooks = vec![
189 PluginHook::BeforeGenerate,
190 PluginHook::AfterGenerate,
191 PluginHook::OnClassAdd,
192 PluginHook::OnRuleCreate,
193 PluginHook::OnOptimize,
194 ];
195
196 for hook in default_hooks {
197 self.hooks.entry(hook).or_default().push(plugin_name.to_string());
198 }
199 }
200}
201
202
203#[derive(Debug)]
205pub struct CustomUtilitiesPlugin {
206 name: String,
207 version: String,
208 description: String,
209 custom_utilities: HashMap<String, CssRule>,
210}
211
212impl CustomUtilitiesPlugin {
213 pub fn new() -> Self {
215 Self {
216 name: "custom-utilities".to_string(),
217 version: "1.0.0".to_string(),
218 description: "Adds custom utility classes".to_string(),
219 custom_utilities: HashMap::new(),
220 }
221 }
222
223 pub fn add_utility(&mut self, class_name: String, rule: CssRule) {
225 self.custom_utilities.insert(class_name, rule);
226 }
227}
228
229impl Plugin for CustomUtilitiesPlugin {
230 fn name(&self) -> &str {
231 &self.name
232 }
233
234 fn version(&self) -> &str {
235 &self.version
236 }
237
238 fn description(&self) -> &str {
239 &self.description
240 }
241
242 fn initialize(&mut self, _context: &mut PluginContext) -> Result<()> {
243 self.add_utility("custom-shadow".to_string(), CssRule {
245 selector: ".custom-shadow".to_string(),
246 properties: vec![
247 CssProperty {
248 name: "box-shadow".to_string(),
249 value: "0 4px 6px -1px rgba(0, 0, 0, 0.1)".to_string(),
250 important: false,
251 },
252 ],
253 media_query: None,
254 specificity: 10,
255 });
256
257 Ok(())
258 }
259
260 fn handle_hook(&mut self, hook: PluginHook, _context: &mut PluginContext) -> Result<()> {
261 match hook {
262 PluginHook::BeforeGenerate => {
263 println!("Custom utilities plugin: Adding {} custom utilities", self.custom_utilities.len());
267 }
268 PluginHook::AfterGenerate => {
269 println!("Custom utilities plugin: CSS generation completed");
270 }
271 _ => {}
272 }
273 Ok(())
274 }
275
276 fn get_config_schema(&self) -> Option<serde_json::Value> {
277 Some(serde_json::json!({
278 "type": "object",
279 "properties": {
280 "utilities": {
281 "type": "array",
282 "items": {
283 "type": "object",
284 "properties": {
285 "name": {"type": "string"},
286 "properties": {"type": "object"}
287 }
288 }
289 }
290 }
291 }))
292 }
293
294 fn validate_config(&self, config: &serde_json::Value) -> Result<()> {
295 if !config.is_object() {
296 return Err(TailwindError::build("Plugin config must be an object".to_string()));
297 }
298 Ok(())
299 }
300}
301
302#[derive(Debug)]
304pub struct MinifierPlugin {
305 name: String,
306 version: String,
307 description: String,
308 minify: bool,
309}
310
311impl MinifierPlugin {
312 pub fn new() -> Self {
314 Self {
315 name: "minifier".to_string(),
316 version: "1.0.0".to_string(),
317 description: "Minifies CSS output".to_string(),
318 minify: true,
319 }
320 }
321}
322
323impl Plugin for MinifierPlugin {
324 fn name(&self) -> &str {
325 &self.name
326 }
327
328 fn version(&self) -> &str {
329 &self.version
330 }
331
332 fn description(&self) -> &str {
333 &self.description
334 }
335
336 fn initialize(&mut self, _context: &mut PluginContext) -> Result<()> {
337 Ok(())
338 }
339
340 fn handle_hook(&mut self, hook: PluginHook, _context: &mut PluginContext) -> Result<()> {
341 match hook {
342 PluginHook::OnOptimize => {
343 if self.minify {
344 println!("Minifier plugin: Applying minification");
345 }
346 }
347 _ => {}
348 }
349 Ok(())
350 }
351
352 fn get_config_schema(&self) -> Option<serde_json::Value> {
353 Some(serde_json::json!({
354 "type": "object",
355 "properties": {
356 "enabled": {"type": "boolean"}
357 }
358 }))
359 }
360
361 fn validate_config(&self, config: &serde_json::Value) -> Result<()> {
362 if let Some(enabled) = config.get("enabled") {
363 if !enabled.is_boolean() {
364 return Err(TailwindError::build("Minifier enabled must be a boolean".to_string()));
365 }
366 }
367 Ok(())
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn test_plugin_registry_creation() {
377 let registry = PluginRegistry::new();
378 assert!(registry.list_plugins().is_empty());
379 }
380
381 #[test]
382 fn test_register_plugin() {
383 let mut registry = PluginRegistry::new();
384 let plugin = Box::new(CustomUtilitiesPlugin::new());
385
386 registry.register_plugin(plugin).unwrap();
387
388 assert_eq!(registry.list_plugins().len(), 1);
389 assert!(registry.list_plugins().contains(&"custom-utilities".to_string()));
390 }
391
392 #[test]
393 fn test_duplicate_plugin_registration() {
394 let mut registry = PluginRegistry::new();
395 let plugin1 = Box::new(CustomUtilitiesPlugin::new());
396 let plugin2 = Box::new(CustomUtilitiesPlugin::new());
397
398 registry.register_plugin(plugin1).unwrap();
399 let result = registry.register_plugin(plugin2);
400
401 assert!(result.is_err());
402 }
403
404 #[test]
405 fn test_unregister_plugin() {
406 let mut registry = PluginRegistry::new();
407 let plugin = Box::new(CustomUtilitiesPlugin::new());
408
409 registry.register_plugin(plugin).unwrap();
410 assert_eq!(registry.list_plugins().len(), 1);
411
412 registry.unregister_plugin("custom-utilities").unwrap();
413 assert!(registry.list_plugins().is_empty());
414 }
415
416 #[test]
417 fn test_plugin_config() {
418 let mut registry = PluginRegistry::new();
419 let plugin = Box::new(MinifierPlugin::new());
420
421 registry.register_plugin(plugin).unwrap();
422
423 let config = serde_json::json!({"enabled": true});
424 registry.set_plugin_config("minifier", config.clone()).unwrap();
425
426 assert_eq!(registry.get_plugin_config("minifier"), Some(&config));
427 }
428
429 #[test]
430 fn test_plugin_data() {
431 let mut registry = PluginRegistry::new();
432
433 let data = serde_json::json!({"key": "value"});
434 registry.set_plugin_data("test_key".to_string(), data.clone());
435
436 assert_eq!(registry.get_plugin_data("test_key"), Some(&data));
437 }
438
439 #[test]
440 fn test_execute_hook() {
441 let mut registry = PluginRegistry::new();
442 let plugin = Box::new(MinifierPlugin::new());
443
444 registry.register_plugin(plugin).unwrap();
445
446 registry.execute_hook(PluginHook::OnOptimize).unwrap();
448 }
449
450 #[test]
451 fn test_custom_utilities_plugin() {
452 let mut plugin = CustomUtilitiesPlugin::new();
453 let mut context = PluginContext {
454 generator: Arc::new(CssGenerator::new()),
455 data: HashMap::new(),
456 config: HashMap::new(),
457 };
458
459 plugin.initialize(&mut context).unwrap();
460 assert_eq!(plugin.name(), "custom-utilities");
461 assert_eq!(plugin.version(), "1.0.0");
462 }
463
464 #[test]
465 fn test_minifier_plugin() {
466 let mut plugin = MinifierPlugin::new();
467 let mut context = PluginContext {
468 generator: Arc::new(CssGenerator::new()),
469 data: HashMap::new(),
470 config: HashMap::new(),
471 };
472
473 plugin.initialize(&mut context).unwrap();
474 assert_eq!(plugin.name(), "minifier");
475 assert_eq!(plugin.version(), "1.0.0");
476 }
477}