nargo_style_processor/
lib.rs1#![warn(missing_docs)]
2
3use nargo_types::{Error, Result, Span};
4use notify::{self, Watcher};
5use std::path::Path;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub enum PreprocessorType {
10 Css,
12 Scss,
14 Less,
16 Stylus,
18}
19
20use std::collections::HashMap;
21
22#[derive(Default)]
24pub struct StyleProcessor {
25 minify: bool,
27 remove_unused: bool,
29 preprocessor: Option<PreprocessorType>,
31 cache: HashMap<(String, PreprocessorType), String>,
33}
34
35impl StyleProcessor {
36 pub fn new() -> Self {
38 Self { minify: false, remove_unused: false, preprocessor: None, cache: HashMap::new() }
39 }
40
41 pub fn with_minify(mut self, minify: bool) -> Self {
43 self.minify = minify;
44 self
45 }
46
47 pub fn with_remove_unused(mut self, remove_unused: bool) -> Self {
49 self.remove_unused = remove_unused;
50 self
51 }
52
53 pub fn with_preprocessor(mut self, preprocessor: PreprocessorType) -> Self {
55 self.preprocessor = Some(preprocessor);
56 self
57 }
58
59 pub fn process(&mut self, css: &str, used_selectors: Option<&Vec<String>>) -> Result<String> {
68 let mut processed = css.to_string();
69
70 if let Some(preprocessor) = self.preprocessor.clone() {
72 processed = self.process_preprocessor(&processed, &preprocessor)?;
73 }
74
75 if self.remove_unused && used_selectors.is_some() {
77 processed = self.remove_unused_styles(&processed, used_selectors.unwrap())?;
78 }
79
80 if self.minify {
82 processed = self.minify_css(&processed);
83 }
84 else {
85 processed = format!("/* Processed by Nargo Style Processor */\n{}", processed);
87 }
88
89 Ok(processed)
90 }
91
92 fn process_preprocessor(&mut self, code: &str, preprocessor: &PreprocessorType) -> Result<String> {
94 let cache_key = (code.to_string(), preprocessor.clone());
96 if let Some(cached) = self.cache.get(&cache_key) {
97 return Ok(cached.clone());
98 }
99
100 let processed = match preprocessor {
101 PreprocessorType::Css => code.to_string(),
102 PreprocessorType::Scss => code.to_string(),
103 PreprocessorType::Less => code.to_string(),
104 PreprocessorType::Stylus => code.to_string(),
105 };
106
107 self.cache.insert(cache_key, processed.clone());
109 Ok(processed)
110 }
111
112 fn remove_unused_styles(&self, css: &str, used_selectors: &Vec<String>) -> Result<String> {
114 let mut result = String::new();
116 let mut remaining = css.to_string();
117
118 while !remaining.is_empty() {
119 if let Some(start_idx) = remaining.find('{') {
121 let selector = remaining[..start_idx].trim().to_string();
123
124 let mut brace_count = 1;
126 let mut end_idx = start_idx + 1;
127
128 while end_idx < remaining.len() && brace_count > 0 {
129 if remaining.chars().nth(end_idx) == Some('{') {
130 brace_count += 1;
131 }
132 else if remaining.chars().nth(end_idx) == Some('}') {
133 brace_count -= 1;
134 }
135 end_idx += 1;
136 }
137
138 let rule = remaining[..end_idx].to_string();
140
141 if self.is_selector_used(&selector, used_selectors) {
143 result.push_str(&rule);
144 result.push_str("\n");
145 }
146
147 remaining = remaining[end_idx..].trim_start().to_string();
149 }
150 else {
151 result.push_str(&remaining);
153 break;
154 }
155 }
156
157 Ok(result)
158 }
159
160 fn is_selector_used(&self, selector: &str, used_selectors: &Vec<String>) -> bool {
162 let selectors = selector.split(',').map(|s| s.trim());
164
165 for s in selectors {
166 if used_selectors.contains(&s.to_string()) {
167 return true;
168 }
169 }
170
171 false
172 }
173
174 fn minify_css(&self, css: &str) -> String {
176 css.trim().split('\n').map(|s| s.trim()).filter(|s| !s.is_empty() && !s.starts_with("/*") && !s.ends_with("*/")).collect::<Vec<_>>().join("").replace(" {", "{").replace("{ ", "{").replace(" }", "}").replace("} ", "}").replace(": ", ":").replace("; ", ";").replace("/*", "").replace("*/", "")
177 }
178}
179
180pub fn process(css: &str) -> Result<String> {
188 let mut processor = StyleProcessor::new();
189 processor.process(css, None)
190}
191
192pub fn process_with_used_selectors(css: &str, used_selectors: &Vec<String>) -> Result<String> {
201 let mut processor = StyleProcessor::new().with_remove_unused(true);
202 processor.process(css, Some(used_selectors))
203}
204
205pub fn minify(css: &str) -> Result<String> {
213 let mut processor = StyleProcessor::new().with_minify(true);
214 processor.process(css, None)
215}
216
217pub fn optimize(css: &str, used_selectors: &Vec<String>) -> Result<String> {
226 let mut processor = StyleProcessor::new().with_remove_unused(true).with_minify(true);
227 processor.process(css, Some(used_selectors))
228}
229
230pub fn process_with_preprocessor(css: &str, preprocessor: PreprocessorType) -> Result<String> {
239 let mut processor = StyleProcessor::new().with_preprocessor(preprocessor);
240 processor.process(css, None)
241}
242
243pub fn watch(processor: &StyleProcessor, file_path: &str, output_path: &str, callback: Option<fn()>) -> Result<()> {
254 let minify = processor.minify;
256 let remove_unused = processor.remove_unused;
257 let preprocessor = processor.preprocessor.clone();
258 let file_path_str = file_path.to_string();
259 let file_path_closure = file_path_str.clone();
260 let output_path = output_path.to_string();
261
262 match notify::recommended_watcher(move |res: std::result::Result<notify::Event, notify::Error>| {
264 match res {
265 Ok(event) => {
266 if let notify::EventKind::Modify(_) = event.kind {
267 if let Ok(css) = std::fs::read_to_string(&file_path_closure) {
269 let mut processor = StyleProcessor::new().with_minify(minify).with_remove_unused(remove_unused).with_preprocessor(preprocessor.clone().unwrap_or(PreprocessorType::Css));
270
271 if let Ok(processed) = processor.process(&css, None) {
272 if std::fs::write(&output_path, processed).is_ok() {
273 println!("Style updated: {}", output_path);
274 if let Some(cb) = callback {
275 cb();
276 }
277 }
278 }
279 }
280 }
281 }
282 Err(e) => println!("watch error: {:?}", e),
283 }
284 }) {
285 Ok(mut watcher) => {
286 if let Err(err) = watcher.watch(Path::new(&file_path_str), notify::RecursiveMode::NonRecursive) {
288 return Err(Error::external_error("notify".to_string(), err.to_string(), Span::default()));
289 }
290 Ok(())
291 }
292 Err(err) => Err(Error::external_error("notify".to_string(), err.to_string(), Span::default())),
293 }
294}