1use crate::css_generator::{CssGenerator, CssProperty, CssRule};
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!(
88 "Plugin '{}' is already registered",
89 name
90 )));
91 }
92
93 let mut plugin_box = plugin;
95 plugin_box.initialize(&mut self.context)?;
96
97 self.plugins.insert(name.clone(), plugin_box);
99
100 self.register_default_hooks(&name);
102
103 Ok(())
104 }
105
106 pub fn unregister_plugin(&mut self, name: &str) -> Result<()> {
108 if !self.plugins.contains_key(name) {
109 return Err(TailwindError::build(format!(
110 "Plugin '{}' is not registered",
111 name
112 )));
113 }
114
115 self.plugins.remove(name);
117
118 for hook_list in self.hooks.values_mut() {
120 hook_list.retain(|plugin_name| plugin_name != name);
121 }
122
123 Ok(())
124 }
125
126 pub fn get_plugin(&self, name: &str) -> Option<&dyn Plugin> {
128 self.plugins.get(name).map(|p| p.as_ref())
129 }
130
131 pub fn get_plugin_mut(&mut self, name: &str) -> Option<&mut (dyn Plugin + '_)> {
133 if let Some(plugin) = self.plugins.get_mut(name) {
134 Some(plugin.as_mut())
135 } else {
136 None
137 }
138 }
139
140 pub fn list_plugins(&self) -> Vec<String> {
142 self.plugins.keys().cloned().collect()
143 }
144
145 pub fn execute_hook(&mut self, hook: PluginHook) -> Result<()> {
147 if let Some(plugin_names) = self.hooks.get(&hook) {
148 for plugin_name in plugin_names {
149 if let Some(plugin) = self.plugins.get_mut(plugin_name) {
150 plugin.handle_hook(hook.clone(), &mut self.context)?;
151 }
152 }
153 }
154 Ok(())
155 }
156
157 pub fn set_plugin_config(
159 &mut self,
160 plugin_name: &str,
161 config: serde_json::Value,
162 ) -> Result<()> {
163 if let Some(plugin) = self.plugins.get(plugin_name) {
164 plugin.validate_config(&config)?;
165 }
166
167 self.context.config.insert(plugin_name.to_string(), config);
168 Ok(())
169 }
170
171 pub fn get_plugin_config(&self, plugin_name: &str) -> Option<&serde_json::Value> {
173 self.context.config.get(plugin_name)
174 }
175
176 pub fn set_plugin_data(&mut self, key: String, value: serde_json::Value) {
178 self.context.data.insert(key, value);
179 }
180
181 pub fn get_plugin_data(&self, key: &str) -> Option<&serde_json::Value> {
183 self.context.data.get(key)
184 }
185
186 pub fn update_generator(&mut self, generator: CssGenerator) {
188 self.context.generator = Arc::new(generator);
189 }
190
191 pub fn get_generator(&self) -> Arc<CssGenerator> {
193 self.context.generator.clone()
194 }
195
196 fn register_default_hooks(&mut self, plugin_name: &str) {
198 let default_hooks = vec![
199 PluginHook::BeforeGenerate,
200 PluginHook::AfterGenerate,
201 PluginHook::OnClassAdd,
202 PluginHook::OnRuleCreate,
203 PluginHook::OnOptimize,
204 ];
205
206 for hook in default_hooks {
207 self.hooks
208 .entry(hook)
209 .or_default()
210 .push(plugin_name.to_string());
211 }
212 }
213}
214
215#[derive(Debug)]
217pub struct CustomUtilitiesPlugin {
218 name: String,
219 version: String,
220 description: String,
221 custom_utilities: HashMap<String, CssRule>,
222}
223
224impl CustomUtilitiesPlugin {
225 pub fn new() -> Self {
227 Self {
228 name: "custom-utilities".to_string(),
229 version: "1.0.0".to_string(),
230 description: "Adds custom utility classes".to_string(),
231 custom_utilities: HashMap::new(),
232 }
233 }
234
235 pub fn add_utility(&mut self, class_name: String, rule: CssRule) {
237 self.custom_utilities.insert(class_name, rule);
238 }
239}
240
241impl Plugin for CustomUtilitiesPlugin {
242 fn name(&self) -> &str {
243 &self.name
244 }
245
246 fn version(&self) -> &str {
247 &self.version
248 }
249
250 fn description(&self) -> &str {
251 &self.description
252 }
253
254 fn initialize(&mut self, _context: &mut PluginContext) -> Result<()> {
255 self.add_utility(
257 "custom-shadow".to_string(),
258 CssRule {
259 selector: ".custom-shadow".to_string(),
260 properties: vec![CssProperty {
261 name: "box-shadow".to_string(),
262 value: "0 4px 6px -1px rgba(0, 0, 0, 0.1)".to_string(),
263 important: false,
264 }],
265 media_query: None,
266 specificity: 10,
267 },
268 );
269
270 Ok(())
271 }
272
273 fn handle_hook(&mut self, hook: PluginHook, _context: &mut PluginContext) -> Result<()> {
274 match hook {
275 PluginHook::BeforeGenerate => {
276 println!(
280 "Custom utilities plugin: Adding {} custom utilities",
281 self.custom_utilities.len()
282 );
283 }
284 PluginHook::AfterGenerate => {
285 println!("Custom utilities plugin: CSS generation completed");
286 }
287 _ => {}
288 }
289 Ok(())
290 }
291
292 fn get_config_schema(&self) -> Option<serde_json::Value> {
293 Some(serde_json::json!({
294 "type": "object",
295 "properties": {
296 "utilities": {
297 "type": "array",
298 "items": {
299 "type": "object",
300 "properties": {
301 "name": {"type": "string"},
302 "properties": {"type": "object"}
303 }
304 }
305 }
306 }
307 }))
308 }
309
310 fn validate_config(&self, config: &serde_json::Value) -> Result<()> {
311 if !config.is_object() {
312 return Err(TailwindError::build(
313 "Plugin config must be an object".to_string(),
314 ));
315 }
316 Ok(())
317 }
318}
319
320#[derive(Debug)]
322pub struct MinifierPlugin {
323 name: String,
324 version: String,
325 description: String,
326 minify: bool,
327}
328
329impl MinifierPlugin {
330 pub fn new() -> Self {
332 Self {
333 name: "minifier".to_string(),
334 version: "1.0.0".to_string(),
335 description: "Minifies CSS output".to_string(),
336 minify: true,
337 }
338 }
339}
340
341impl Plugin for MinifierPlugin {
342 fn name(&self) -> &str {
343 &self.name
344 }
345
346 fn version(&self) -> &str {
347 &self.version
348 }
349
350 fn description(&self) -> &str {
351 &self.description
352 }
353
354 fn initialize(&mut self, _context: &mut PluginContext) -> Result<()> {
355 Ok(())
356 }
357
358 fn handle_hook(&mut self, hook: PluginHook, _context: &mut PluginContext) -> Result<()> {
359 match hook {
360 PluginHook::OnOptimize => {
361 if self.minify {
362 println!("Minifier plugin: Applying minification");
363 }
364 }
365 _ => {}
366 }
367 Ok(())
368 }
369
370 fn get_config_schema(&self) -> Option<serde_json::Value> {
371 Some(serde_json::json!({
372 "type": "object",
373 "properties": {
374 "enabled": {"type": "boolean"}
375 }
376 }))
377 }
378
379 fn validate_config(&self, config: &serde_json::Value) -> Result<()> {
380 if let Some(enabled) = config.get("enabled") {
381 if !enabled.is_boolean() {
382 return Err(TailwindError::build(
383 "Minifier enabled must be a boolean".to_string(),
384 ));
385 }
386 }
387 Ok(())
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_plugin_registry_creation() {
397 let registry = PluginRegistry::new();
398 assert!(registry.list_plugins().is_empty());
399 }
400
401 #[test]
402 fn test_register_plugin() {
403 let mut registry = PluginRegistry::new();
404 let plugin = Box::new(CustomUtilitiesPlugin::new());
405
406 registry.register_plugin(plugin).unwrap();
407
408 assert_eq!(registry.list_plugins().len(), 1);
409 assert!(registry
410 .list_plugins()
411 .contains(&"custom-utilities".to_string()));
412 }
413
414 #[test]
415 fn test_duplicate_plugin_registration() {
416 let mut registry = PluginRegistry::new();
417 let plugin1 = Box::new(CustomUtilitiesPlugin::new());
418 let plugin2 = Box::new(CustomUtilitiesPlugin::new());
419
420 registry.register_plugin(plugin1).unwrap();
421 let result = registry.register_plugin(plugin2);
422
423 assert!(result.is_err());
424 }
425
426 #[test]
427 fn test_unregister_plugin() {
428 let mut registry = PluginRegistry::new();
429 let plugin = Box::new(CustomUtilitiesPlugin::new());
430
431 registry.register_plugin(plugin).unwrap();
432 assert_eq!(registry.list_plugins().len(), 1);
433
434 registry.unregister_plugin("custom-utilities").unwrap();
435 assert!(registry.list_plugins().is_empty());
436 }
437
438 #[test]
439 fn test_plugin_config() {
440 let mut registry = PluginRegistry::new();
441 let plugin = Box::new(MinifierPlugin::new());
442
443 registry.register_plugin(plugin).unwrap();
444
445 let config = serde_json::json!({"enabled": true});
446 registry
447 .set_plugin_config("minifier", config.clone())
448 .unwrap();
449
450 assert_eq!(registry.get_plugin_config("minifier"), Some(&config));
451 }
452
453 #[test]
454 fn test_plugin_data() {
455 let mut registry = PluginRegistry::new();
456
457 let data = serde_json::json!({"key": "value"});
458 registry.set_plugin_data("test_key".to_string(), data.clone());
459
460 assert_eq!(registry.get_plugin_data("test_key"), Some(&data));
461 }
462
463 #[test]
464 fn test_execute_hook() {
465 let mut registry = PluginRegistry::new();
466 let plugin = Box::new(MinifierPlugin::new());
467
468 registry.register_plugin(plugin).unwrap();
469
470 registry.execute_hook(PluginHook::OnOptimize).unwrap();
472 }
473
474 #[test]
475 fn test_custom_utilities_plugin() {
476 let mut plugin = CustomUtilitiesPlugin::new();
477 let mut context = PluginContext {
478 generator: Arc::new(CssGenerator::new()),
479 data: HashMap::new(),
480 config: HashMap::new(),
481 };
482
483 plugin.initialize(&mut context).unwrap();
484 assert_eq!(plugin.name(), "custom-utilities");
485 assert_eq!(plugin.version(), "1.0.0");
486 }
487
488 #[test]
489 fn test_minifier_plugin() {
490 let mut plugin = MinifierPlugin::new();
491 let mut context = PluginContext {
492 generator: Arc::new(CssGenerator::new()),
493 data: HashMap::new(),
494 config: HashMap::new(),
495 };
496
497 plugin.initialize(&mut context).unwrap();
498 assert_eq!(plugin.name(), "minifier");
499 assert_eq!(plugin.version(), "1.0.0");
500 }
501}