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