use crate::ast::CSSNode;
use crate::error::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginConfig {
pub name: String,
pub version: Option<String>,
pub options: serde_json::Value,
}
impl PluginConfig {
pub fn requires_js(&self) -> bool {
match self.name.as_str() {
"autoprefixer" | "cssnano" | "postcss-preset-env" | "postcss-import" => true,
_ => {
self.name.contains('/') || self.name.starts_with('@')
}
}
}
}
#[derive(Debug)]
pub struct PluginLoader;
#[derive(Debug)]
pub enum PluginResult {
Native(NativePlugin),
JavaScript(JSPlugin),
}
#[derive(Debug)]
pub struct NativePlugin;
impl NativePlugin {
pub fn transform(&self, ast: CSSNode) -> Result<CSSNode> {
Ok(ast)
}
}
#[derive(Debug)]
pub struct JSPlugin {
pub name: String,
}
impl PluginLoader {
pub fn new() -> Self {
Self
}
pub async fn load_plugin(&self, config: &PluginConfig) -> Result<PluginResult> {
if config.requires_js() {
Ok(PluginResult::JavaScript(JSPlugin {
name: config.name.clone(),
}))
} else {
Ok(PluginResult::Native(NativePlugin))
}
}
pub async fn load_plugins(&self, configs: &[PluginConfig]) -> Result<Vec<PluginResult>> {
let mut results = Vec::new();
for config in configs {
let result = self.load_plugin(config).await?;
results.push(result);
}
Ok(results)
}
pub fn validate_config(&self, config: &PluginConfig) -> Result<()> {
if config.name.is_empty() {
return Err(crate::error::PostCSSError::config(
"Plugin name cannot be empty",
));
}
if !self.is_valid_plugin_name(&config.name) {
return Err(crate::error::PostCSSError::config(&format!(
"Invalid plugin name: {}",
config.name
)));
}
Ok(())
}
fn is_valid_plugin_name(&self, name: &str) -> bool {
!name.is_empty()
&& name.len() <= 100
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '/' || c == '@')
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_loader_creation() {
let _loader = PluginLoader::new();
assert!(true); }
}