tailwind_rs_core/
plugin_system.rs

1//! Plugin system for extending Tailwind-RS functionality
2//!
3//! This module provides a plugin system that allows users to extend Tailwind-RS
4//! with custom utilities, components, and optimizations.
5
6use crate::css_generator::{CssGenerator, CssProperty, CssRule};
7use crate::error::{Result, TailwindError};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Plugin hook types
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum PluginHook {
14    /// Hook called before CSS generation
15    BeforeGenerate,
16    /// Hook called after CSS generation
17    AfterGenerate,
18    /// Hook called when a class is added
19    OnClassAdd,
20    /// Hook called when a rule is created
21    OnRuleCreate,
22    /// Hook called during optimization
23    OnOptimize,
24}
25
26/// Plugin context containing current state
27#[derive(Debug, Clone)]
28pub struct PluginContext {
29    /// Current CSS generator
30    pub generator: Arc<CssGenerator>,
31    /// Plugin data storage
32    pub data: HashMap<String, serde_json::Value>,
33    /// Configuration
34    pub config: HashMap<String, serde_json::Value>,
35}
36
37/// Plugin trait that all plugins must implement
38pub trait Plugin: Send + Sync {
39    /// Get the plugin name
40    fn name(&self) -> &str;
41
42    /// Get the plugin version
43    fn version(&self) -> &str;
44
45    /// Get the plugin description
46    fn description(&self) -> &str;
47
48    /// Initialize the plugin
49    fn initialize(&mut self, context: &mut PluginContext) -> Result<()>;
50
51    /// Handle plugin hooks
52    fn handle_hook(&mut self, hook: PluginHook, context: &mut PluginContext) -> Result<()>;
53
54    /// Get plugin configuration schema
55    fn get_config_schema(&self) -> Option<serde_json::Value>;
56
57    /// Validate plugin configuration
58    fn validate_config(&self, config: &serde_json::Value) -> Result<()>;
59}
60
61/// Plugin registry for managing plugins
62pub struct PluginRegistry {
63    plugins: HashMap<String, Box<dyn Plugin>>,
64    hooks: HashMap<PluginHook, Vec<String>>,
65    context: PluginContext,
66}
67
68impl PluginRegistry {
69    /// Create a new plugin registry
70    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    /// Register a plugin
83    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        // Initialize the plugin
94        let mut plugin_box = plugin;
95        plugin_box.initialize(&mut self.context)?;
96
97        // Register the plugin
98        self.plugins.insert(name.clone(), plugin_box);
99
100        // Register default hooks
101        self.register_default_hooks(&name);
102
103        Ok(())
104    }
105
106    /// Unregister a plugin
107    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        // Remove from plugins
116        self.plugins.remove(name);
117
118        // Remove from hooks
119        for hook_list in self.hooks.values_mut() {
120            hook_list.retain(|plugin_name| plugin_name != name);
121        }
122
123        Ok(())
124    }
125
126    /// Get a plugin by name
127    pub fn get_plugin(&self, name: &str) -> Option<&dyn Plugin> {
128        self.plugins.get(name).map(|p| p.as_ref())
129    }
130
131    /// Get a mutable plugin by name
132    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    /// List all registered plugins
141    pub fn list_plugins(&self) -> Vec<String> {
142        self.plugins.keys().cloned().collect()
143    }
144
145    /// Execute a hook for all registered plugins
146    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    /// Set plugin configuration
158    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    /// Get plugin configuration
172    pub fn get_plugin_config(&self, plugin_name: &str) -> Option<&serde_json::Value> {
173        self.context.config.get(plugin_name)
174    }
175
176    /// Set plugin data
177    pub fn set_plugin_data(&mut self, key: String, value: serde_json::Value) {
178        self.context.data.insert(key, value);
179    }
180
181    /// Get plugin data
182    pub fn get_plugin_data(&self, key: &str) -> Option<&serde_json::Value> {
183        self.context.data.get(key)
184    }
185
186    /// Update the CSS generator
187    pub fn update_generator(&mut self, generator: CssGenerator) {
188        self.context.generator = Arc::new(generator);
189    }
190
191    /// Get the current CSS generator
192    pub fn get_generator(&self) -> Arc<CssGenerator> {
193        self.context.generator.clone()
194    }
195
196    /// Register default hooks for a plugin
197    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/// Example plugin: Custom utilities
216#[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    /// Create a new custom utilities plugin
226    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    /// Add a custom utility
236    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        // Add some default custom utilities
256        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                // Add custom utilities to the generator
277                // Note: This is a simplified implementation
278                // In a real implementation, we would need to modify the generator
279                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/// Example plugin: CSS minifier
321#[derive(Debug)]
322pub struct MinifierPlugin {
323    name: String,
324    version: String,
325    description: String,
326    minify: bool,
327}
328
329impl MinifierPlugin {
330    /// Create a new minifier plugin
331    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        // This should not panic
471        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}