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 Default for PluginRegistry {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl PluginRegistry {
75    /// Create a new plugin registry
76    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    /// Register a plugin
89    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        // Initialize the plugin
100        let mut plugin_box = plugin;
101        plugin_box.initialize(&mut self.context)?;
102
103        // Register the plugin
104        self.plugins.insert(name.clone(), plugin_box);
105
106        // Register default hooks
107        self.register_default_hooks(&name);
108
109        Ok(())
110    }
111
112    /// Unregister a plugin
113    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        // Remove from plugins
122        self.plugins.remove(name);
123
124        // Remove from hooks
125        for hook_list in self.hooks.values_mut() {
126            hook_list.retain(|plugin_name| plugin_name != name);
127        }
128
129        Ok(())
130    }
131
132    /// Get a plugin by name
133    pub fn get_plugin(&self, name: &str) -> Option<&dyn Plugin> {
134        self.plugins.get(name).map(|p| p.as_ref())
135    }
136
137    /// Get a mutable plugin by name
138    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    /// List all registered plugins
147    pub fn list_plugins(&self) -> Vec<String> {
148        self.plugins.keys().cloned().collect()
149    }
150
151    /// Execute a hook for all registered plugins
152    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    /// Set plugin configuration
164    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    /// Get plugin configuration
178    pub fn get_plugin_config(&self, plugin_name: &str) -> Option<&serde_json::Value> {
179        self.context.config.get(plugin_name)
180    }
181
182    /// Set plugin data
183    pub fn set_plugin_data(&mut self, key: String, value: serde_json::Value) {
184        self.context.data.insert(key, value);
185    }
186
187    /// Get plugin data
188    pub fn get_plugin_data(&self, key: &str) -> Option<&serde_json::Value> {
189        self.context.data.get(key)
190    }
191
192    /// Update the CSS generator
193    pub fn update_generator(&mut self, generator: CssGenerator) {
194        self.context.generator = Arc::new(generator);
195    }
196
197    /// Get the current CSS generator
198    pub fn get_generator(&self) -> Arc<CssGenerator> {
199        self.context.generator.clone()
200    }
201
202    /// Register default hooks for a plugin
203    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/// Example plugin: Custom utilities
222#[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    /// Create a new custom utilities plugin
238    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    /// Add a custom utility
248    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        // Add some default custom utilities
268        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                // Add custom utilities to the generator
289                // Note: This is a simplified implementation
290                // In a real implementation, we would need to modify the generator
291                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/// Example plugin: CSS minifier
333#[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    /// Create a new minifier plugin
349    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        // This should not panic
484        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}