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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum PreprocessorType {
14 Css,
16 Scss,
18 Less,
20 Stylus,
22}
23
24use std::collections::HashMap;
25
26#[derive(Default)]
28pub struct StyleProcessor {
29 pub minify: bool,
34 pub remove_unused: bool,
39 pub preprocessor: Option<PreprocessorType>,
43 cache: HashMap<(String, PreprocessorType), String>,
47 minify_cache: HashMap<String, String>,
51}
52
53impl StyleProcessor {
54 pub fn new() -> Self {
56 Self { minify: false, remove_unused: false, preprocessor: None, cache: HashMap::new(), minify_cache: HashMap::new() }
57 }
58
59 pub fn with_minify(mut self, minify: bool) -> Self {
61 self.minify = minify;
62 self
63 }
64
65 pub fn with_remove_unused(mut self, remove_unused: bool) -> Self {
67 self.remove_unused = remove_unused;
68 self
69 }
70
71 pub fn with_preprocessor(mut self, preprocessor: PreprocessorType) -> Self {
73 self.preprocessor = Some(preprocessor);
74 self
75 }
76
77 pub fn process(&mut self, css: &str, used_selectors: Option<&Vec<String>>) -> Result<String> {
86 let mut processed = css.to_string();
87
88 if let Some(preprocessor) = self.preprocessor.clone() {
90 processed = self.process_preprocessor(&processed, &preprocessor)?;
91 }
92
93 if self.remove_unused && used_selectors.is_some() {
95 processed = self.remove_unused_styles(&processed, used_selectors.unwrap())?;
96 }
97
98 if self.minify {
100 processed = self.minify_css(&processed);
101 }
102 else {
103 processed = format!("/* Processed by Nargo Style Processor */\n{}", processed);
105 }
106
107 Ok(processed)
108 }
109
110 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 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 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 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 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 fn remove_unused_styles(&self, css: &str, used_selectors: &Vec<String>) -> Result<String> {
175 let mut result = String::new();
177 let mut remaining = css.to_string();
178
179 while !remaining.is_empty() {
180 if let Some(start_idx) = remaining.find('{') {
182 let selector = remaining[..start_idx].trim().to_string();
184
185 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 let rule = remaining[..end_idx].to_string();
201
202 if self.is_selector_used(&selector, used_selectors) {
204 result.push_str(&rule);
205 result.push_str("\n");
206 }
207
208 remaining = remaining[end_idx..].trim_start().to_string();
210 }
211 else {
212 result.push_str(&remaining);
214 break;
215 }
216 }
217
218 Ok(result)
219 }
220
221 fn is_selector_used(&self, selector: &str, used_selectors: &Vec<String>) -> bool {
223 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 fn minify_css(&mut self, css: &str) -> String {
243 if let Some(cached) = self.minify_cache.get(css) {
245 return cached.clone();
246 }
247
248 let minified = self.perform_minification(css);
249
250 self.minify_cache.insert(css.to_string(), minified.clone());
252
253 minified
254 }
255
256 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 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 result.push(c);
280 if c == string_delimiter {
281 let is_escaped = self.is_escaped(&result);
283 if !is_escaped {
284 in_string = false;
285 }
286 }
287 continue;
288 }
289
290 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 }
314 '#' => {
315 result.push('#');
316 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 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 let trimmed = self.remove_unnecessary_semicolons(&result);
355
356 trimmed
357 }
358
359 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 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 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
408pub fn process(css: &str) -> Result<String> {
416 let mut processor = StyleProcessor::new();
417 processor.process(css, None)
418}
419
420pub 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
433pub fn minify(css: &str) -> Result<String> {
441 let mut processor = StyleProcessor::new().with_minify(true);
442 processor.process(css, None)
443}
444
445pub 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
458pub 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
471pub fn watch(processor: &StyleProcessor, file_path: &str, output_path: &str, callback: Option<fn()>) -> Result<()> {
482 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 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 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 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}