use crate::ast::{CSSNode, CSSRule};
use crate::error::{PostCSSError, Result};
use crate::js_bridge::JSBridge;
use crate::parser::{CSSParser, ParseOptions};
use crate::plugin_loader::{PluginConfig, PluginLoader, PluginResult};
use crate::source_map::{SourceMap, SourceMapGenerator};
use crate::transformer::{CSSTransformer, TransformOptions};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug)]
pub struct PostCSSEngine {
config: PostCSSConfig,
parser: CSSParser,
transformer: CSSTransformer,
js_bridge: Option<JSBridge>,
plugin_loader: PluginLoader,
source_map_generator: SourceMapGenerator,
cache: Arc<RwLock<HashMap<String, ProcessedCSS>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostCSSConfig {
pub plugins: Vec<PluginConfig>,
pub source_map: bool,
pub source_map_options: SourceMapOptions,
pub parser_options: ParseOptions,
pub transform_options: TransformOptions,
pub performance: PerformanceOptions,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceMapOptions {
pub inline: bool,
pub file: Option<String>,
pub source_root: Option<String>,
pub sources_content: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceOptions {
pub enable_cache: bool,
pub cache_size_limit: usize,
pub parallel_processing: bool,
pub memory_optimization: bool,
}
#[derive(Debug, Clone)]
pub struct ProcessedCSS {
pub css: String,
pub source_map: Option<SourceMap>,
pub warnings: Vec<ProcessingWarning>,
pub metrics: ProcessingMetrics,
}
#[derive(Debug, Clone)]
pub struct ProcessingWarning {
pub message: String,
pub line: Option<usize>,
pub column: Option<usize>,
pub severity: WarningSeverity,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WarningSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct ProcessingMetrics {
pub parse_time: std::time::Duration,
pub transform_time: std::time::Duration,
pub generate_time: std::time::Duration,
pub total_time: std::time::Duration,
pub memory_usage: usize,
pub rules_processed: usize,
pub plugins_executed: usize,
}
impl Default for PostCSSConfig {
fn default() -> Self {
Self {
plugins: Vec::new(),
source_map: true,
source_map_options: SourceMapOptions::default(),
parser_options: ParseOptions::default(),
transform_options: TransformOptions::default(),
performance: PerformanceOptions::default(),
}
}
}
impl Default for SourceMapOptions {
fn default() -> Self {
Self {
inline: false,
file: None,
source_root: None,
sources_content: true,
}
}
}
impl Default for PerformanceOptions {
fn default() -> Self {
Self {
enable_cache: true,
cache_size_limit: 1000,
parallel_processing: true,
memory_optimization: true,
}
}
}
impl PostCSSEngine {
pub fn new(config: PostCSSConfig) -> Result<Self> {
let parser = CSSParser::new(config.parser_options.clone());
let transformer = CSSTransformer::new(config.transform_options.clone());
let plugin_loader = PluginLoader::new();
let source_map_generator = SourceMapGenerator::new();
let js_bridge = if config.plugins.iter().any(|p| p.requires_js()) {
Some(JSBridge::new()?)
} else {
None
};
Ok(Self {
config,
parser,
transformer,
js_bridge,
plugin_loader,
source_map_generator,
cache: Arc::new(RwLock::new(HashMap::new())),
})
}
pub async fn process_css(&self, input: &str) -> Result<ProcessedCSS> {
let start_time = std::time::Instant::now();
if self.config.performance.enable_cache {
if let Some(cached) = self.get_cached_result(input).await {
return Ok(cached);
}
}
let parse_start = std::time::Instant::now();
let ast = self.parser.parse(input)?;
let parse_time = parse_start.elapsed();
let transform_start = std::time::Instant::now();
let transformed_ast = self.apply_transformations(ast).await?;
let transform_time = transform_start.elapsed();
let generate_start = std::time::Instant::now();
let css = self.generate_css(&transformed_ast)?;
let generate_time = generate_start.elapsed();
let source_map = if self.config.source_map {
let source_map_options = crate::source_map::SourceMapOptions {
inline: self.config.source_map_options.inline,
file: self.config.source_map_options.file.clone(),
source_root: self.config.source_map_options.source_root.clone(),
sources_content: self.config.source_map_options.sources_content,
};
Some(
self.source_map_generator
.generate(input, &css, &source_map_options)?,
)
} else {
None
};
let total_time = start_time.elapsed();
let metrics = ProcessingMetrics {
parse_time,
transform_time,
generate_time,
total_time,
memory_usage: self.get_memory_usage(),
rules_processed: self.count_rules(&transformed_ast),
plugins_executed: self.config.plugins.len(),
};
let result = ProcessedCSS {
css,
source_map,
warnings: Vec::new(), metrics,
};
if self.config.performance.enable_cache {
self.cache_result(input, &result).await;
}
Ok(result)
}
async fn apply_transformations(&self, mut ast: CSSNode) -> Result<CSSNode> {
for plugin_config in &self.config.plugins {
let plugin_result = self.plugin_loader.load_plugin(plugin_config).await?;
match plugin_result {
PluginResult::Native(plugin) => {
ast = plugin.transform(ast)?;
}
PluginResult::JavaScript(js_plugin) => {
if let Some(js_bridge) = &self.js_bridge {
ast = js_bridge.execute_plugin(&js_plugin.name, ast).await?;
} else {
return Err(PostCSSError::JavaScriptBridgeNotAvailable);
}
}
}
}
ast = self.transformer.transform(ast)?;
Ok(ast)
}
fn generate_css(&self, ast: &CSSNode) -> Result<String> {
match ast {
CSSNode::Stylesheet(rules) => {
let mut css = String::new();
for rule in rules {
css.push_str(&self.rule_to_css(rule)?);
css.push('\n');
}
Ok(css)
}
_ => Err(PostCSSError::InvalidAST("Expected stylesheet".to_string())),
}
}
fn rule_to_css(&self, rule: &CSSRule) -> Result<String> {
let mut css = String::new();
css.push_str(&rule.selector);
css.push_str(" {\n");
for declaration in &rule.declarations {
css.push_str(" ");
css.push_str(&declaration.property);
css.push_str(": ");
css.push_str(&declaration.value);
if declaration.important {
css.push_str(" !important");
}
css.push_str(";\n");
}
css.push('}');
Ok(css)
}
async fn get_cached_result(&self, input: &str) -> Option<ProcessedCSS> {
let cache = self.cache.read().await;
cache.get(input).cloned()
}
async fn cache_result(&self, input: &str, result: &ProcessedCSS) {
let mut cache = self.cache.write().await;
if cache.len() >= self.config.performance.cache_size_limit {
let keys_to_remove: Vec<String> = cache.keys().take(cache.len() / 2).cloned().collect();
for key in keys_to_remove {
cache.remove(&key);
}
}
cache.insert(input.to_string(), result.clone());
}
fn get_memory_usage(&self) -> usize {
std::mem::size_of::<Self>() + self.cache.try_read().map(|c| c.len() * 1024).unwrap_or(0)
}
fn count_rules(&self, ast: &CSSNode) -> usize {
match ast {
CSSNode::Stylesheet(rules) => rules.len(),
_ => 0,
}
}
pub async fn get_metrics(&self) -> EngineMetrics {
let cache = self.cache.read().await;
EngineMetrics {
cache_size: cache.len(),
memory_usage: self.get_memory_usage(),
plugins_loaded: self.config.plugins.len(),
js_bridge_available: self.js_bridge.is_some(),
}
}
pub async fn clear_cache(&self) {
let mut cache = self.cache.write().await;
cache.clear();
}
}
#[derive(Debug, Clone)]
pub struct EngineMetrics {
pub cache_size: usize,
pub memory_usage: usize,
pub plugins_loaded: usize,
pub js_bridge_available: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_engine_creation() {
let config = PostCSSConfig::default();
let engine = PostCSSEngine::new(config);
assert!(engine.is_ok());
}
#[tokio::test]
async fn test_css_processing() {
let engine = PostCSSEngine::new(PostCSSConfig::default()).unwrap();
let input = ".test { color: red; }";
let result = engine.process_css(input).await;
assert!(result.is_ok());
let css = result.unwrap();
assert!(css.css.contains(".test"));
assert!(css.css.contains("color: red"));
}
#[tokio::test]
async fn test_caching() {
let mut config = PostCSSConfig::default();
config.performance.enable_cache = true;
let engine = PostCSSEngine::new(config).unwrap();
let input = ".test { color: red; }";
let result1 = engine.process_css(input).await.unwrap();
let result2 = engine.process_css(input).await.unwrap();
assert_eq!(result1.css, result2.css);
}
}