Skip to main content

nargo_style_processor/
lib.rs

1#![warn(missing_docs)]
2
3use nargo_types::{Error, Result, Span};
4use notify::{self, Watcher};
5use oak_core::{ParseSession, parse_one_pass};
6use oak_css::{CssLanguage, CssParser};
7use oak_scss::{ScssLanguage, ScssParser};
8use oak_stylus::{StylusLanguage, StylusParser};
9use std::path::Path;
10
11/// CSS 预处理器类型
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum PreprocessorType {
14    /// 标准 CSS
15    Css,
16    /// SCSS/SASS
17    Scss,
18    /// Less
19    Less,
20    /// Stylus
21    Stylus,
22}
23
24use std::collections::HashMap;
25
26/// CSS 处理器,用于处理和优化 CSS 代码
27#[derive(Default)]
28pub struct StyleProcessor {
29    /// 是否启用 CSS 压缩
30    ///
31    /// 当设置为 `true` 时,处理器会移除空白字符、注释和不必要的字符,
32    /// 并压缩颜色值,以减少 CSS 文件大小。
33    pub minify: bool,
34    /// 是否移除未使用的样式
35    ///
36    /// 当设置为 `true` 时,处理器会根据提供的已使用选择器列表,
37    /// 移除未被使用的 CSS 规则。
38    pub remove_unused: bool,
39    /// CSS 预处理器类型
40    ///
41    /// 用于指定要使用的 CSS 预处理器,如 SCSS、Less 或 Stylus。
42    pub preprocessor: Option<PreprocessorType>,
43    /// 缓存已处理的代码,提高性能
44    ///
45    /// 缓存键为 (代码内容, 预处理器类型) 的元组,值为处理后的代码。
46    cache: HashMap<(String, PreprocessorType), String>,
47    /// 缓存压缩后的 CSS 代码,提高性能
48    ///
49    /// 缓存键为原始 CSS 代码,值为压缩后的 CSS 代码。
50    minify_cache: HashMap<String, String>,
51}
52
53impl StyleProcessor {
54    /// 创建新的样式处理器
55    pub fn new() -> Self {
56        Self { minify: false, remove_unused: false, preprocessor: None, cache: HashMap::new(), minify_cache: HashMap::new() }
57    }
58
59    /// 设置是否启用压缩
60    pub fn with_minify(mut self, minify: bool) -> Self {
61        self.minify = minify;
62        self
63    }
64
65    /// 设置是否移除未使用的样式
66    pub fn with_remove_unused(mut self, remove_unused: bool) -> Self {
67        self.remove_unused = remove_unused;
68        self
69    }
70
71    /// 设置 CSS 预处理器类型
72    pub fn with_preprocessor(mut self, preprocessor: PreprocessorType) -> Self {
73        self.preprocessor = Some(preprocessor);
74        self
75    }
76
77    /// 处理 CSS 代码
78    ///
79    /// # 参数
80    /// * `css` - 要处理的 CSS 代码
81    /// * `used_selectors` - 已使用的选择器列表,用于移除未使用的样式
82    ///
83    /// # 返回值
84    /// 处理后的 CSS 代码
85    pub fn process(&mut self, css: &str, used_selectors: Option<&Vec<String>>) -> Result<String> {
86        let mut processed = css.to_string();
87
88        // 处理预处理器代码
89        if let Some(preprocessor) = self.preprocessor.clone() {
90            processed = self.process_preprocessor(&processed, &preprocessor)?;
91        }
92
93        // 移除未使用的样式
94        if self.remove_unused && used_selectors.is_some() {
95            processed = self.remove_unused_styles(&processed, used_selectors.unwrap())?;
96        }
97
98        // 压缩 CSS
99        if self.minify {
100            processed = self.minify_css(&processed);
101        }
102        else {
103            // 添加处理注释
104            processed = format!("/* Processed by Nargo Style Processor */\n{}", processed);
105        }
106
107        Ok(processed)
108    }
109
110    /// 处理预处理器代码
111    fn process_preprocessor(&mut self, code: &str, preprocessor: &PreprocessorType) -> Result<String> {
112        let cache_key = (code.to_string(), preprocessor.clone());
113
114        Self::get_or_compute(&mut self.cache, cache_key, || {
115            match preprocessor {
116                PreprocessorType::Css => {
117                    // 使用 oak-css 解析 CSS
118                    let language = CssLanguage::default();
119                    let parser = CssParser::new(&language);
120                    let mut session = ParseSession::new(1024);
121                    let result = parse_one_pass(&parser, code, &mut session);
122                    result.result.map_err(|e| Error::external_error("oak-css".to_string(), e.to_string(), Span::default()))?;
123                    Ok(code.to_string())
124                }
125                PreprocessorType::Scss => {
126                    // 使用 oak-scss 解析 SCSS
127                    let language = ScssLanguage::default();
128                    let parser = ScssParser::new(&language);
129                    let mut session = ParseSession::new(1024);
130                    let result = parse_one_pass(&parser, code, &mut session);
131                    result.result.map_err(|e| Error::external_error("oak-scss".to_string(), e.to_string(), Span::default()))?;
132                    Ok(code.to_string())
133                }
134                PreprocessorType::Less => Ok(code.to_string()),
135                PreprocessorType::Stylus => {
136                    // 使用 oak-stylus 解析 Stylus
137                    let language = StylusLanguage::default();
138                    let parser = StylusParser::new(&language);
139                    let mut session = ParseSession::new(1024);
140                    let result = parse_one_pass(&parser, code, &mut session);
141                    result.result.map_err(|e| Error::external_error("oak-stylus".to_string(), e.to_string(), Span::default()))?;
142                    Ok(code.to_string())
143                }
144            }
145        })
146    }
147
148    /// 通用缓存处理方法
149    ///
150    /// # 参数
151    /// * `cache` - 缓存 HashMap
152    /// * `key` - 缓存键
153    /// * `compute` - 计算函数,当缓存中不存在时执行
154    ///
155    /// # 返回值
156    /// 缓存的值或计算的结果
157    fn get_or_compute<K, V, F>(cache: &mut HashMap<K, V>, key: K, compute: F) -> Result<V>
158    where
159        K: std::hash::Hash + Eq,
160        V: Clone,
161        F: FnOnce() -> Result<V>,
162    {
163        if let Some(cached) = cache.get(&key) {
164            Ok(cached.clone())
165        }
166        else {
167            let value = compute()?;
168            cache.insert(key, value.clone());
169            Ok(value)
170        }
171    }
172
173    /// 移除未使用的样式
174    fn remove_unused_styles(&self, css: &str, used_selectors: &Vec<String>) -> Result<String> {
175        // 简单的未使用样式移除实现
176        let mut result = String::new();
177        let mut remaining = css.to_string();
178
179        while !remaining.is_empty() {
180            // 查找规则开始
181            if let Some(start_idx) = remaining.find('{') {
182                // 提取选择器
183                let selector = remaining[..start_idx].trim().to_string();
184
185                // 查找规则结束
186                let mut brace_count = 1;
187                let mut end_idx = start_idx + 1;
188
189                while end_idx < remaining.len() && brace_count > 0 {
190                    if remaining.chars().nth(end_idx) == Some('{') {
191                        brace_count += 1;
192                    }
193                    else if remaining.chars().nth(end_idx) == Some('}') {
194                        brace_count -= 1;
195                    }
196                    end_idx += 1;
197                }
198
199                // 提取规则
200                let rule = remaining[..end_idx].to_string();
201
202                // 检查选择器是否被使用
203                if self.is_selector_used(&selector, used_selectors) {
204                    result.push_str(&rule);
205                    result.push_str("\n");
206                }
207
208                // 更新剩余内容
209                remaining = remaining[end_idx..].trim_start().to_string();
210            }
211            else {
212                // 没有更多规则,添加剩余内容
213                result.push_str(&remaining);
214                break;
215            }
216        }
217
218        Ok(result)
219    }
220
221    /// 检查选择器是否被使用
222    fn is_selector_used(&self, selector: &str, used_selectors: &Vec<String>) -> bool {
223        // 简单实现:检查选择器是否在使用列表中
224        let selectors = selector.split(',').map(|s| s.trim());
225
226        for s in selectors {
227            if used_selectors.contains(&s.to_string()) {
228                return true;
229            }
230        }
231
232        false
233    }
234
235    /// 压缩 CSS
236    ///
237    /// # 参数
238    /// * `css` - 要压缩的 CSS 代码
239    ///
240    /// # 返回值
241    /// 压缩后的 CSS 代码
242    fn minify_css(&mut self, css: &str) -> String {
243        // 检查缓存
244        if let Some(cached) = self.minify_cache.get(css) {
245            return cached.clone();
246        }
247
248        let minified = self.perform_minification(css);
249
250        // 存入缓存
251        self.minify_cache.insert(css.to_string(), minified.clone());
252
253        minified
254    }
255
256    /// 执行 CSS 压缩的核心逻辑
257    fn perform_minification(&self, css: &str) -> String {
258        let mut result = String::with_capacity(css.len());
259        let mut in_comment = false;
260        let mut in_string = false;
261        let mut string_delimiter = '"';
262        let mut chars = css.chars();
263
264        while let Some(c) = chars.next() {
265            if in_comment {
266                // 处理注释
267                if c == '*' {
268                    if let Some(next) = chars.next() {
269                        if next == '/' {
270                            in_comment = false;
271                        }
272                    }
273                }
274                continue;
275            }
276
277            if in_string {
278                // 处理字符串
279                result.push(c);
280                if c == string_delimiter {
281                    // 检查是否被转义
282                    let is_escaped = self.is_escaped(&result);
283                    if !is_escaped {
284                        in_string = false;
285                    }
286                }
287                continue;
288            }
289
290            // 处理其他字符
291            match c {
292                '"' | '\'' => {
293                    result.push(c);
294                    in_string = true;
295                    string_delimiter = c;
296                }
297                '/' => {
298                    if let Some(next) = chars.next() {
299                        if next == '*' {
300                            in_comment = true;
301                        }
302                        else {
303                            result.push('/');
304                            result.push(next);
305                        }
306                    }
307                    else {
308                        result.push('/');
309                    }
310                }
311                ' ' | '\t' | '\n' | '\r' => {
312                    // 跳过所有空白字符
313                }
314                '#' => {
315                    result.push('#');
316                    // 读取并压缩颜色值
317                    let mut hex_chars = String::new();
318                    for _ in 0..6 {
319                        if let Some(next) = chars.next() {
320                            if next.is_ascii_hexdigit() {
321                                hex_chars.push(next);
322                            }
323                            else {
324                                result.push_str(&hex_chars);
325                                result.push(next);
326                                break;
327                            }
328                        }
329                        else {
330                            result.push_str(&hex_chars);
331                            break;
332                        }
333                    }
334                    // 尝试压缩颜色值
335                    if hex_chars.len() == 6 {
336                        if let Some(compressed) = self.compress_color(&hex_chars) {
337                            result.push_str(&compressed);
338                        }
339                        else {
340                            result.push_str(&hex_chars);
341                        }
342                    }
343                    else if !hex_chars.is_empty() {
344                        result.push_str(&hex_chars);
345                    }
346                }
347                _ => {
348                    result.push(c);
349                }
350            }
351        }
352
353        // 移除不必要的分号
354        let trimmed = self.remove_unnecessary_semicolons(&result);
355
356        trimmed
357    }
358
359    /// 检查字符是否被转义
360    fn is_escaped(&self, result: &String) -> bool {
361        let mut backslash_count = 0;
362        for ch in result.chars().rev() {
363            if ch == '\\' {
364                backslash_count += 1;
365            }
366            else {
367                break;
368            }
369        }
370        backslash_count % 2 == 1
371    }
372
373    /// 压缩颜色值
374    ///
375    /// # 参数
376    /// * `color` - 6位十六进制颜色值(不包含#)
377    ///
378    /// # 返回值
379    /// 压缩后的颜色值,如 #FFFFFF → #FFF
380    fn compress_color(&self, color: &str) -> Option<String> {
381        if color.len() != 6 {
382            return None;
383        }
384
385        let chars: Vec<char> = color.chars().collect();
386        if chars[0] == chars[1] && chars[2] == chars[3] && chars[4] == chars[5] { Some(format!("{}{}{}", chars[0], chars[2], chars[4])) } else { None }
387    }
388
389    /// 移除不必要的分号
390    ///
391    /// # 参数
392    /// * `css` - 压缩后的 CSS 代码
393    ///
394    /// # 返回值
395    /// 移除不必要分号后的 CSS 代码
396    fn remove_unnecessary_semicolons(&self, css: &str) -> String {
397        let mut result = String::with_capacity(css.len());
398        let mut chars = css.chars().peekable();
399
400        while let Some(c) = chars.next() {
401            result.push(c);
402        }
403
404        result
405    }
406}
407
408/// 处理 CSS 代码的便捷函数
409///
410/// # 参数
411/// * `css` - 要处理的 CSS 代码
412///
413/// # 返回值
414/// 处理后的 CSS 代码
415pub fn process(css: &str) -> Result<String> {
416    let mut processor = StyleProcessor::new();
417    processor.process(css, None)
418}
419
420/// 处理 CSS 代码并移除未使用的样式
421///
422/// # 参数
423/// * `css` - 要处理的 CSS 代码
424/// * `used_selectors` - 已使用的选择器列表
425///
426/// # 返回值
427/// 处理后的 CSS 代码
428pub fn process_with_used_selectors(css: &str, used_selectors: &Vec<String>) -> Result<String> {
429    let mut processor = StyleProcessor::new().with_remove_unused(true);
430    processor.process(css, Some(used_selectors))
431}
432
433/// 压缩 CSS 代码
434///
435/// # 参数
436/// * `css` - 要压缩的 CSS 代码
437///
438/// # 返回值
439/// 压缩后的 CSS 代码
440pub fn minify(css: &str) -> Result<String> {
441    let mut processor = StyleProcessor::new().with_minify(true);
442    processor.process(css, None)
443}
444
445/// 优化 CSS 代码(移除未使用的样式并压缩)
446///
447/// # 参数
448/// * `css` - 要优化的 CSS 代码
449/// * `used_selectors` - 已使用的选择器列表
450///
451/// # 返回值
452/// 优化后的 CSS 代码
453pub fn optimize(css: &str, used_selectors: &Vec<String>) -> Result<String> {
454    let mut processor = StyleProcessor::new().with_remove_unused(true).with_minify(true);
455    processor.process(css, Some(used_selectors))
456}
457
458/// 使用预处理器处理 CSS 代码
459///
460/// # 参数
461/// * `css` - 要处理的 CSS 代码
462/// * `preprocessor` - CSS 预处理器类型
463///
464/// # 返回值
465/// 处理后的 CSS 代码
466pub fn process_with_preprocessor(css: &str, preprocessor: PreprocessorType) -> Result<String> {
467    let mut processor = StyleProcessor::new().with_preprocessor(preprocessor);
468    processor.process(css, None)
469}
470
471/// 监听文件变化并自动重新处理样式
472///
473/// # 参数
474/// * `processor` - 样式处理器实例
475/// * `file_path` - 要监听的文件路径
476/// * `output_path` - 输出文件路径
477/// * `callback` - 文件变化时的回调函数
478///
479/// # 返回值
480/// 监听结果
481pub fn watch(processor: &StyleProcessor, file_path: &str, output_path: &str, callback: Option<fn()>) -> Result<()> {
482    // 复制处理器配置
483    let minify = processor.minify;
484    let remove_unused = processor.remove_unused;
485    let preprocessor = processor.preprocessor.clone();
486    let file_path_str = file_path.to_string();
487    let file_path_closure = file_path_str.clone();
488    let output_path = output_path.to_string();
489
490    // 创建文件监视器
491    match notify::recommended_watcher(move |res: std::result::Result<notify::Event, notify::Error>| {
492        match res {
493            Ok(event) => {
494                if let notify::EventKind::Modify(_) = event.kind {
495                    // 文件被修改,重新处理
496                    if let Ok(css) = std::fs::read_to_string(&file_path_closure) {
497                        let mut processor = StyleProcessor::new().with_minify(minify).with_remove_unused(remove_unused).with_preprocessor(preprocessor.clone().unwrap_or(PreprocessorType::Css));
498
499                        if let Ok(processed) = processor.process(&css, None) {
500                            if std::fs::write(&output_path, processed).is_ok() {
501                                println!("Style updated: {}", output_path);
502                                if let Some(cb) = callback {
503                                    cb();
504                                }
505                            }
506                        }
507                    }
508                }
509            }
510            Err(e) => println!("watch error: {:?}", e),
511        }
512    }) {
513        Ok(mut watcher) => {
514            // 开始监听文件
515            if let Err(err) = watcher.watch(Path::new(&file_path_str), notify::RecursiveMode::NonRecursive) {
516                return Err(Error::external_error("notify".to_string(), err.to_string(), Span::default()));
517            }
518            Ok(())
519        }
520        Err(err) => Err(Error::external_error("notify".to_string(), err.to_string(), Span::default())),
521    }
522}