css_knife/
starter.rs

1use std::{
2    collections::{BTreeMap, HashMap},
3    fs::{self, read_to_string},
4    path::Path,
5};
6
7use crate::{
8    config::{read_config, Config},
9    visit_class::check_html,
10    visit_macros::check_macro,
11    visit_map::check_js,
12    visit_selectors::ClassVisitor,
13};
14use lightningcss::{
15    stylesheet::{ParserOptions, PrinterOptions, StyleSheet},
16    visitor::Visit,
17};
18
19pub fn start_all() {
20    let config = read_config();
21    match config {
22        Ok(config) => {
23            let validate = config.validate();
24            if config.validate().is_ok() {
25                let mut css_walker = CSSWalker::new(&config.ignored_files);
26
27                for dir in &config.css_dir {
28                    css_walker.walk_tree(dir, &config);
29                }
30
31                let mut macro_class_walker =
32                    MacroClassWalker::new(&css_walker.class_visitor);
33
34                if let Some(macros) = &config.macro_classes {
35                    for dir in macros {
36                        macro_class_walker.walk_tree(dir, &config);
37                    }
38                }
39
40                let mut html_walker = HTMLWalker::new(
41                    &css_walker.class_visitor,
42                    &macro_class_walker,
43                );
44
45                for dir in &config.html_dir {
46                    html_walker.walk_tree(dir, &config);
47                }
48
49                let mut js_walker = JSWalker::new(&css_walker.class_visitor);
50                if let Some(js) = &config.js_map {
51                    for dir in js {
52                        js_walker.walk_tree(dir, &config);
53                    }
54                }
55
56                if let Some(assets_dir) = &config.assets_dir {
57                    for dir in assets_dir {
58                        html_walker.walk_tree(dir, &config);
59                    }
60                }
61            } else {
62                println!("{}", validate.err().unwrap());
63            }
64        }
65        Err(e) => {
66            println!("{}", e);
67        }
68    }
69}
70
71fn handle_path(path: &Path) -> &Path {
72    if path.starts_with("./") {
73        if let Some(path2) = path.to_str() {
74            let mut path2 = path2.chars();
75            path2.next();
76            path2.next();
77            return Path::new(path2.as_str());
78        }
79    }
80    path
81}
82
83trait TreeWalker {
84    fn walk(&mut self, old_content: String, path: &Path) -> Option<String>;
85
86    fn write(&self, path: &Path, new_content: Option<&str>, config: &Config) {
87        let output_path = Path::new(&config.output_dir);
88        let new_path = output_path.join(handle_path(path));
89        if let Some(parent) = new_path.parent() {
90            let _ = std::fs::create_dir_all(parent);
91            if let Some(new_content) = new_content {
92                let _ = std::fs::write(new_path, new_content);
93            } else {
94                let _ = fs::copy(path, new_path);
95            }
96        }
97    }
98
99    fn walk_tree(&mut self, dir: &String, config: &Config) {
100        if let Ok(paths) = std::fs::read_dir(dir) {
101            for path in paths.flatten() {
102                if let Ok(meta) = path.metadata() {
103                    if meta.is_file() {
104                        if let Ok(old_content) = read_to_string(path.path()) {
105                            if let Some(new_content) =
106                                self.walk(old_content, &path.path())
107                            {
108                                self.write(
109                                    &path.path(),
110                                    Some(&new_content),
111                                    config,
112                                );
113                            }
114                        } else {
115                            self.write(&path.path(), None, config)
116                        }
117                    } else if meta.is_dir() {
118                        if let Some(path) = path.path().to_str() {
119                            self.walk_tree(&path.to_string(), config);
120                        }
121                    }
122                }
123            }
124        }
125    }
126}
127
128struct CSSWalker<'a> {
129    pub class_visitor: ClassVisitor,
130    pub ignored: &'a Option<Vec<String>>,
131}
132
133impl<'a> CSSWalker<'a> {
134    pub fn new(ignored: &'a Option<Vec<String>>) -> Self {
135        Self {
136            class_visitor: ClassVisitor::default(),
137            ignored,
138        }
139    }
140
141    pub fn is_ignored(&self, path: &Path) -> bool {
142        if let Some(ignored) = self.ignored {
143            if let Some(path) = path.to_str() {
144                return ignored.contains(&path.to_string());
145            }
146        }
147        false
148    }
149}
150
151impl<'a> TreeWalker for CSSWalker<'a> {
152    fn walk(&mut self, old_content: String, path: &Path) -> Option<String> {
153        if self.is_ignored(path) {
154            return None;
155        }
156        let a = StyleSheet::parse(&old_content, ParserOptions::default());
157        if let Ok(mut stylesheet) = a {
158            let _ = stylesheet.visit(&mut self.class_visitor);
159            let opt = PrinterOptions {
160                minify: true,
161                ..Default::default()
162            };
163            if let Ok(f) = stylesheet.to_css(opt) {
164                return Some(f.code);
165            }
166        }
167        None
168    }
169}
170
171struct HTMLWalker<'a> {
172    pub class_visitor: &'a ClassVisitor,
173    pub macros_walker: &'a MacroClassWalker<'a>,
174}
175
176impl<'a> HTMLWalker<'a> {
177    pub fn new(
178        class_visitor: &'a ClassVisitor,
179        macros_walker: &'a MacroClassWalker,
180    ) -> Self {
181        Self {
182            class_visitor,
183            macros_walker,
184        }
185    }
186}
187
188impl<'a> TreeWalker for HTMLWalker<'a> {
189    fn walk(&mut self, mut old_content: String, path: &Path) -> Option<String> {
190        if let Some(updated_macros) =
191            self.macros_walker.macros.get(path.to_str().unwrap())
192        {
193            for m in updated_macros {
194                old_content = old_content.replace(m.0, m.1);
195            }
196        }
197        if let Ok(html) = check_html(&old_content, self.class_visitor) {
198            return Some(html);
199        }
200        None
201    }
202}
203
204struct JSWalker<'a> {
205    pub class_visitor: &'a ClassVisitor,
206}
207
208impl<'a> JSWalker<'a> {
209    pub fn new(class_visitor: &'a ClassVisitor) -> Self {
210        Self { class_visitor }
211    }
212}
213
214impl<'a> TreeWalker for JSWalker<'a> {
215    fn walk(&mut self, old_content: String, _: &Path) -> Option<String> {
216        if let Some(html) = check_js(&old_content, self.class_visitor) {
217            if let Ok(html) = String::from_utf8(html) {
218                return Some(html);
219            }
220        }
221        None
222    }
223}
224
225pub struct MacroClassWalker<'a> {
226    pub class_visitor: &'a ClassVisitor,
227    pub macros: HashMap<String, BTreeMap<String, String>>,
228}
229
230impl<'a> MacroClassWalker<'a> {
231    pub fn new(class_visitor: &'a ClassVisitor) -> Self {
232        Self {
233            class_visitor,
234            macros: HashMap::new(),
235        }
236    }
237
238    pub fn insert(&mut self, path: String, map: BTreeMap<String, String>) {
239        self.macros.insert(path, map);
240    }
241}
242
243impl<'a> TreeWalker for MacroClassWalker<'a> {
244    fn walk(&mut self, old_content: String, path: &Path) -> Option<String> {
245        check_macro(old_content, self.class_visitor, self, path)
246    }
247}