tailwind_rs_postcss/
engine.rs

1//! PostCSS Engine implementation
2//!
3//! This module provides the core PostCSS processing engine with plugin support,
4//! AST manipulation, and source map generation.
5
6use crate::ast::{CSSNode, CSSRule};
7use crate::error::{PostCSSError, Result};
8use crate::js_bridge::JSBridge;
9use crate::parser::{CSSParser, ParseOptions};
10use crate::plugin_loader::{PluginConfig, PluginLoader, PluginResult};
11use crate::source_map::{SourceMap, SourceMapGenerator};
12use crate::transformer::{CSSTransformer, TransformOptions};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::Arc;
16use tokio::sync::RwLock;
17
18/// PostCSS processing engine
19#[derive(Debug)]
20pub struct PostCSSEngine {
21    config: PostCSSConfig,
22    parser: CSSParser,
23    transformer: CSSTransformer,
24    js_bridge: Option<JSBridge>,
25    plugin_loader: PluginLoader,
26    source_map_generator: SourceMapGenerator,
27    cache: Arc<RwLock<HashMap<String, ProcessedCSS>>>,
28}
29
30/// PostCSS configuration
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct PostCSSConfig {
33    /// Plugins to use for processing
34    pub plugins: Vec<PluginConfig>,
35    /// Source map generation
36    pub source_map: bool,
37    /// Source map options
38    pub source_map_options: SourceMapOptions,
39    /// Parser options
40    pub parser_options: ParseOptions,
41    /// Transformer options
42    pub transform_options: TransformOptions,
43    /// Performance options
44    pub performance: PerformanceOptions,
45}
46
47/// Source map configuration
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SourceMapOptions {
50    /// Generate inline source maps
51    pub inline: bool,
52    /// Source map file path
53    pub file: Option<String>,
54    /// Source root
55    pub source_root: Option<String>,
56    /// Include sources content
57    pub sources_content: bool,
58}
59
60/// Performance configuration
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct PerformanceOptions {
63    /// Enable caching
64    pub enable_cache: bool,
65    /// Cache size limit
66    pub cache_size_limit: usize,
67    /// Parallel processing
68    pub parallel_processing: bool,
69    /// Memory optimization
70    pub memory_optimization: bool,
71}
72
73/// Processed CSS result
74#[derive(Debug, Clone)]
75pub struct ProcessedCSS {
76    /// Generated CSS
77    pub css: String,
78    /// Source map (if enabled)
79    pub source_map: Option<SourceMap>,
80    /// Processing warnings
81    pub warnings: Vec<ProcessingWarning>,
82    /// Processing metrics
83    pub metrics: ProcessingMetrics,
84}
85
86/// Processing warning
87#[derive(Debug, Clone)]
88pub struct ProcessingWarning {
89    pub message: String,
90    pub line: Option<usize>,
91    pub column: Option<usize>,
92    pub severity: WarningSeverity,
93}
94
95/// Warning severity levels
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum WarningSeverity {
98    Info,
99    Warning,
100    Error,
101}
102
103/// Processing metrics
104#[derive(Debug, Clone)]
105pub struct ProcessingMetrics {
106    pub parse_time: std::time::Duration,
107    pub transform_time: std::time::Duration,
108    pub generate_time: std::time::Duration,
109    pub total_time: std::time::Duration,
110    pub memory_usage: usize,
111    pub rules_processed: usize,
112    pub plugins_executed: usize,
113}
114
115impl Default for PostCSSConfig {
116    fn default() -> Self {
117        Self {
118            plugins: Vec::new(),
119            source_map: true,
120            source_map_options: SourceMapOptions::default(),
121            parser_options: ParseOptions::default(),
122            transform_options: TransformOptions::default(),
123            performance: PerformanceOptions::default(),
124        }
125    }
126}
127
128impl Default for SourceMapOptions {
129    fn default() -> Self {
130        Self {
131            inline: false,
132            file: None,
133            source_root: None,
134            sources_content: true,
135        }
136    }
137}
138
139impl Default for PerformanceOptions {
140    fn default() -> Self {
141        Self {
142            enable_cache: true,
143            cache_size_limit: 1000,
144            parallel_processing: true,
145            memory_optimization: true,
146        }
147    }
148}
149
150impl PostCSSEngine {
151    /// Create a new PostCSS engine with configuration
152    pub fn new(config: PostCSSConfig) -> Result<Self> {
153        let parser = CSSParser::new(config.parser_options.clone());
154        let transformer = CSSTransformer::new(config.transform_options.clone());
155        let plugin_loader = PluginLoader::new();
156        let source_map_generator = SourceMapGenerator::new();
157
158        // Initialize JavaScript bridge if needed
159        let js_bridge = if config.plugins.iter().any(|p| p.requires_js()) {
160            Some(JSBridge::new()?)
161        } else {
162            None
163        };
164
165        Ok(Self {
166            config,
167            parser,
168            transformer,
169            js_bridge,
170            plugin_loader,
171            source_map_generator,
172            cache: Arc::new(RwLock::new(HashMap::new())),
173        })
174    }
175
176    /// Process CSS input through PostCSS pipeline
177    pub async fn process_css(&self, input: &str) -> Result<ProcessedCSS> {
178        let start_time = std::time::Instant::now();
179
180        // Check cache first
181        if self.config.performance.enable_cache {
182            if let Some(cached) = self.get_cached_result(input).await {
183                return Ok(cached);
184            }
185        }
186
187        // Parse CSS into AST
188        let parse_start = std::time::Instant::now();
189        let ast = self.parser.parse(input)?;
190        let parse_time = parse_start.elapsed();
191
192        // Apply transformations
193        let transform_start = std::time::Instant::now();
194        let transformed_ast = self.apply_transformations(ast).await?;
195        let transform_time = transform_start.elapsed();
196
197        // Generate output CSS
198        let generate_start = std::time::Instant::now();
199        let css = self.generate_css(&transformed_ast)?;
200        let generate_time = generate_start.elapsed();
201
202        // Generate source map if enabled
203        let source_map = if self.config.source_map {
204            let source_map_options = crate::source_map::SourceMapOptions {
205                inline: self.config.source_map_options.inline,
206                file: self.config.source_map_options.file.clone(),
207                source_root: self.config.source_map_options.source_root.clone(),
208                sources_content: self.config.source_map_options.sources_content,
209            };
210            Some(
211                self.source_map_generator
212                    .generate(input, &css, &source_map_options)?,
213            )
214        } else {
215            None
216        };
217
218        let total_time = start_time.elapsed();
219        let metrics = ProcessingMetrics {
220            parse_time,
221            transform_time,
222            generate_time,
223            total_time,
224            memory_usage: self.get_memory_usage(),
225            rules_processed: self.count_rules(&transformed_ast),
226            plugins_executed: self.config.plugins.len(),
227        };
228
229        let result = ProcessedCSS {
230            css,
231            source_map,
232            warnings: Vec::new(), // TODO: Collect warnings during processing
233            metrics,
234        };
235
236        // Cache result if enabled
237        if self.config.performance.enable_cache {
238            self.cache_result(input, &result).await;
239        }
240
241        Ok(result)
242    }
243
244    /// Apply all configured transformations to the AST
245    async fn apply_transformations(&self, mut ast: CSSNode) -> Result<CSSNode> {
246        for plugin_config in &self.config.plugins {
247            let plugin_result = self.plugin_loader.load_plugin(plugin_config).await?;
248
249            match plugin_result {
250                PluginResult::Native(plugin) => {
251                    ast = plugin.transform(ast)?;
252                }
253                PluginResult::JavaScript(js_plugin) => {
254                    if let Some(js_bridge) = &self.js_bridge {
255                        ast = js_bridge.execute_plugin(&js_plugin.name, ast).await?;
256                    } else {
257                        return Err(PostCSSError::JavaScriptBridgeNotAvailable);
258                    }
259                }
260            }
261        }
262
263        // Apply built-in transformations
264        ast = self.transformer.transform(ast)?;
265
266        Ok(ast)
267    }
268
269    /// Generate CSS from transformed AST
270    fn generate_css(&self, ast: &CSSNode) -> Result<String> {
271        match ast {
272            CSSNode::Stylesheet(rules) => {
273                let mut css = String::new();
274                for rule in rules {
275                    css.push_str(&self.rule_to_css(rule)?);
276                    css.push('\n');
277                }
278                Ok(css)
279            }
280            _ => Err(PostCSSError::InvalidAST("Expected stylesheet".to_string())),
281        }
282    }
283
284    /// Convert a CSS rule to CSS string
285    fn rule_to_css(&self, rule: &CSSRule) -> Result<String> {
286        let mut css = String::new();
287
288        // Add selector
289        css.push_str(&rule.selector);
290        css.push_str(" {\n");
291
292        // Add declarations
293        for declaration in &rule.declarations {
294            css.push_str("  ");
295            css.push_str(&declaration.property);
296            css.push_str(": ");
297            css.push_str(&declaration.value);
298            if declaration.important {
299                css.push_str(" !important");
300            }
301            css.push_str(";\n");
302        }
303
304        css.push('}');
305        Ok(css)
306    }
307
308    /// Get cached result if available
309    async fn get_cached_result(&self, input: &str) -> Option<ProcessedCSS> {
310        let cache = self.cache.read().await;
311        cache.get(input).cloned()
312    }
313
314    /// Cache processing result
315    async fn cache_result(&self, input: &str, result: &ProcessedCSS) {
316        let mut cache = self.cache.write().await;
317
318        // Check cache size limit
319        if cache.len() >= self.config.performance.cache_size_limit {
320            // Remove oldest entries (simple LRU)
321            let keys_to_remove: Vec<String> = cache.keys().take(cache.len() / 2).cloned().collect();
322            for key in keys_to_remove {
323                cache.remove(&key);
324            }
325        }
326
327        cache.insert(input.to_string(), result.clone());
328    }
329
330    /// Get current memory usage
331    fn get_memory_usage(&self) -> usize {
332        // Simple memory usage estimation
333        // In a real implementation, this would use proper memory profiling
334        std::mem::size_of::<Self>() + self.cache.try_read().map(|c| c.len() * 1024).unwrap_or(0)
335    }
336
337    /// Count rules in AST
338    fn count_rules(&self, ast: &CSSNode) -> usize {
339        match ast {
340            CSSNode::Stylesheet(rules) => rules.len(),
341            _ => 0,
342        }
343    }
344
345    /// Get engine metrics
346    pub async fn get_metrics(&self) -> EngineMetrics {
347        let cache = self.cache.read().await;
348        EngineMetrics {
349            cache_size: cache.len(),
350            memory_usage: self.get_memory_usage(),
351            plugins_loaded: self.config.plugins.len(),
352            js_bridge_available: self.js_bridge.is_some(),
353        }
354    }
355
356    /// Clear cache
357    pub async fn clear_cache(&self) {
358        let mut cache = self.cache.write().await;
359        cache.clear();
360    }
361}
362
363/// Engine metrics
364#[derive(Debug, Clone)]
365pub struct EngineMetrics {
366    pub cache_size: usize,
367    pub memory_usage: usize,
368    pub plugins_loaded: usize,
369    pub js_bridge_available: bool,
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[tokio::test]
377    async fn test_engine_creation() {
378        let config = PostCSSConfig::default();
379        let engine = PostCSSEngine::new(config);
380        assert!(engine.is_ok());
381    }
382
383    #[tokio::test]
384    async fn test_css_processing() {
385        let engine = PostCSSEngine::new(PostCSSConfig::default()).unwrap();
386        let input = ".test { color: red; }";
387        let result = engine.process_css(input).await;
388        assert!(result.is_ok());
389
390        let css = result.unwrap();
391        assert!(css.css.contains(".test"));
392        assert!(css.css.contains("color: red"));
393    }
394
395    #[tokio::test]
396    async fn test_caching() {
397        let mut config = PostCSSConfig::default();
398        config.performance.enable_cache = true;
399
400        let engine = PostCSSEngine::new(config).unwrap();
401        let input = ".test { color: red; }";
402
403        // First processing
404        let result1 = engine.process_css(input).await.unwrap();
405
406        // Second processing (should use cache)
407        let result2 = engine.process_css(input).await.unwrap();
408
409        assert_eq!(result1.css, result2.css);
410    }
411}