1use std::{collections::BTreeMap, io::Write, path::Path};
7
8use lightningcss::{
9 stylesheet::{ParserOptions, PrinterOptions},
10 visitor::Visit,
11};
12use rand::{distributions::Distribution, seq::SliceRandom, Rng, SeedableRng};
13use rcss_at_rule::{RcssAtRuleConfig, RcssAtRuleParser};
14
15pub mod rcss_at_rule;
16pub mod visitor;
17pub use visitor::Error;
18pub mod interpolate;
19
20pub type Result<T> = std::result::Result<T, Error>;
21
22#[derive(Debug)]
23pub struct CssProcessor<'i> {
24 style: lightningcss::stylesheet::StyleSheet<'i, 'i, RcssAtRuleConfig>,
25 random_ident: [char; 7],
27}
28impl<'src> CssProcessor<'src> {
29 fn new(style: &'src str) -> Result<Self> {
31 let this = Self {
32 random_ident: Self::init_random_class(style),
33 style: lightningcss::stylesheet::StyleSheet::parse_with(
34 style,
35 ParserOptions::default(),
36 &mut RcssAtRuleParser,
37 )
38 .map_err(|e| e.into_owned())?,
39 };
40 Ok(this)
41 }
42 pub fn process_style(style: &str) -> Result<CssOutput> {
43 let (interpolate, result) = crate::interpolate::handle_interpolate(&style);
45 let style = interpolate.unwrap_literals(result.as_ref());
46 let mut this = CssProcessor::new(&style)?;
47 this.process_style_inner()
48 }
49
50 fn process_style_inner<'a>(&mut self) -> Result<CssOutput> {
51 let suffix = self.get_class_suffix();
53 let mut visitor = visitor::SelectorVisitor {
54 append_class: self.get_scoped_class(),
55 class_modify: Box::new(move |class| format!("{class}-{suffix}")),
56 collect_classes: BTreeMap::new(),
57 declare: None,
58 extend: None,
59 state: Default::default(),
60 };
61 self.style.visit(&mut visitor)?;
62 let changed_classes = visitor
63 .collect_classes
64 .into_iter()
65 .map(|(k, v)| {
66 (
67 k,
68 ClassInfo {
69 class_name: v,
70 original_span: None,
71 },
72 )
73 })
74 .collect::<BTreeMap<_, _>>();
75 Ok(CssOutput {
76 uniq_class: visitor.append_class,
77 css_data: self
78 .style
79 .to_css(PrinterOptions {
80 minify: true,
81 ..Default::default()
82 })
83 .unwrap()
84 .code,
85 declare: visitor.declare,
86 extend: visitor.extend,
87 changed_classes,
88 })
89 }
90 #[doc(hidden)]
91 pub fn init_random_class(style: &str) -> [char; 7] {
92 struct CssIdentChars;
93 impl Distribution<char> for CssIdentChars {
94 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> char {
95 const ALLOWED_CHARS: &str =
96 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
97 let chars: Vec<char> = ALLOWED_CHARS.chars().collect();
98 *chars.choose(rng).unwrap()
99 }
100 }
101
102 let mut seed = [0xdeu8; 32];
103 style
104 .bytes()
105 .filter(|c| !c.is_ascii_whitespace())
106 .enumerate()
107 .for_each(|(i, c)| seed[i % 32] ^= c);
108
109 let rng = rand_chacha::ChaCha8Rng::from_seed(seed);
110
111 let ident_vec = std::iter::once('_')
112 .chain(rng.sample_iter(CssIdentChars).take(6))
113 .collect::<Vec<_>>();
114 std::array::from_fn(|i| ident_vec[i])
115 }
116 fn get_scoped_class(&self) -> String {
120 self.random_ident.iter().collect::<String>()
121 }
122
123 fn get_class_suffix(&self) -> String {
127 self.random_ident[1..=4].iter().collect::<String>()
128 }
129}
130
131#[derive(Clone, Debug)]
132pub struct ClassInfo {
133 pub class_name: String,
134 pub original_span: Option<proc_macro2::Span>,
135}
136impl From<String> for ClassInfo {
137 fn from(class_name: String) -> Self {
138 Self {
139 class_name,
140 original_span: None,
141 }
142 }
143}
144
145#[derive(Debug)]
146pub struct CssOutput {
147 uniq_class: String,
148 css_data: String,
149 declare: Option<syn::ItemStruct>,
150 extend: Option<syn::Path>,
151 changed_classes: BTreeMap<String, ClassInfo>,
152}
153
154impl CssOutput {
155 #[doc(hidden)]
156 pub fn create_from_fields(
157 uniq_class: String,
158 css_data: String,
159 declare: Option<syn::ItemStruct>,
160 extend: Option<syn::Path>,
161 changed_classes: BTreeMap<String, ClassInfo>,
162 ) -> Self {
163 Self {
164 uniq_class,
165 css_data,
166 declare,
167 extend,
168 changed_classes,
169 }
170 }
171 pub fn clear_styles(&mut self) {
173 self.css_data.clear();
174 }
175
176 #[doc(hidden)]
177 pub fn classes_list(&self) -> impl Iterator<Item = &str> {
178 self.changed_classes.keys().map(|k| k.as_str())
179 }
180 pub fn classes_map(&self) -> &BTreeMap<String, ClassInfo> {
182 &self.changed_classes
183 }
184
185 pub fn declare(&self) -> Option<syn::ItemStruct> {
187 self.declare.clone()
188 }
189 pub fn extend(&self) -> Option<syn::Path> {
191 self.extend.clone()
192 }
193
194 pub fn style_string(&self) -> String {
195 self.css_data.clone()
196 }
197
198 pub fn class_name(&self) -> &str {
199 &self.uniq_class
200 }
201 pub fn class_suffix(&self) -> &str {
202 &self.uniq_class[1..=4]
203 }
204
205 pub fn merge_to_string(styles: &[Self]) -> String {
206 let mut result = String::new();
207 for style in styles {
208 result.push_str(&style.css_data);
209 }
210 result
211 }
212 pub fn merge_to_file(styles: &[Self], file: impl AsRef<Path>) -> std::io::Result<()> {
214 let mut file = std::fs::File::create(file)?;
215 for style in styles {
216 file.write_all(style.css_data.as_bytes())?;
217 }
218 Ok(())
219 }
220}
221
222#[cfg(test)]
223mod tests {
224
225 #[test]
226 fn check_process_class_names() {
227 let style = r#"
228 .my-class {
229 color: red;
230 }
231 "#;
232 let output = super::CssProcessor::process_style(style).unwrap();
233
234 assert!(output.changed_classes["my-class"]
235 .class_name
236 .contains("my-class"));
237 let output_css = format!(r#".my-class-{}{{color:red}}"#, output.class_suffix());
238 assert_eq!(output.css_data, output_css)
239 }
240 #[test]
241 fn check_global_selector() {
242 let style = r#"
243 :global(.my-class) {
244 color: red;
245 }
246 :global(b) {
247 color: red;
248 }
249 "#;
250 let output = super::CssProcessor::process_style(style).unwrap();
251 let mut output_css = String::new();
252 output_css.push_str(&r#".my-class{color:red}"#);
253 output_css.push_str(&r#"b{color:red}"#);
254 assert_eq!(output.css_data, output_css)
255 }
256 #[test]
257 fn check_deep_selector() {
258 let style = r#"
259 :deep(.my-class) {
260 color: red;
261 }
262 :deep(b) {
263 color: red;
264 }
265 "#;
266 let output = super::CssProcessor::process_style(style).unwrap();
267 let suffix = output.class_suffix();
268 let mut output_css = String::new();
269 output_css.push_str(&format!(r#".my-class-{suffix}{{color:red}}"#));
270 output_css.push_str(&r#"b{color:red}"#);
271 assert_eq!(output.css_data, output_css)
272 }
273 #[test]
274 fn check_process_types_ids() {
275 let style = r#"
276 element {
277 color: red;
278 }
279 #my-id {
280 color: red;
281 }
282 type#with-id {
283 color: red;
284 }
285 "#;
286 let output = super::CssProcessor::process_style(style).unwrap();
287 let uniq_class = output.class_name();
288 let mut output_css = String::new();
289 output_css.push_str(&format!(r#"element.{uniq_class}{{color:red}}"#));
290 output_css.push_str(&format!(r#"#my-id.{uniq_class}{{color:red}}"#));
291 output_css.push_str(&format!(r#"type#with-id.{uniq_class}{{color:red}}"#));
292 assert_eq!(output.css_data, output_css)
293 }
294
295 #[test]
296 fn check_child_class() {
297 let style = r#"
298 type#with-id .class1{
299 color: red;
300 }
301 element > .child {
302 color: red;
303 }
304 .parent > element2 {
305 color: red;
306 }
307 "#;
308 let output = super::CssProcessor::process_style(style).unwrap();
309 let uniq_class = output.class_name();
310 let suffix = output.class_suffix();
311 let mut output_css = String::new();
312 output_css.push_str(&format!(
313 r#"type#with-id.{uniq_class} .class1-{suffix}{{color:red}}"#
314 ));
315
316 output_css.push_str(&format!(
317 r#"element.{uniq_class}>.child-{suffix}{{color:red}}"#
318 ));
319
320 output_css.push_str(&format!(
321 r#".parent-{suffix}>element2.{uniq_class}{{color:red}}"#
322 ));
323
324 assert_eq!(output.css_data, output_css)
325 }
326
327 #[test]
328 fn check_components_parsing() {
329 let style = r#"
330 type#with-id.class1[attribute=value]{
331 color: red;
332 }
333 "#;
334 let output = super::CssProcessor::process_style(style).unwrap();
335 let suffix = output.class_suffix();
336 let mut output_css = String::new();
337 output_css.push_str(&format!(
338 r#"type#with-id.class1-{suffix}[attribute=value]{{color:red}}"#
339 ));
340
341 assert_eq!(output.css_data, output_css)
342 }
343 #[test]
344 fn check_child_class2() {
345 let style = r#"
346 .parent > element2 {
347 color: red;
348 }
349 "#;
350 let output = super::CssProcessor::process_style(style).unwrap();
351 let uniq_class = output.class_name();
352 let suffix = output.class_suffix();
353 let mut output_css = String::new();
354
355 output_css.push_str(&format!(
356 r#".parent-{suffix}>element2.{uniq_class}{{color:red}}"#
357 ));
358
359 assert_eq!(output.css_data, output_css)
360 }
361
362 #[test]
363 fn check_mixed_types_ids_classes() {
364 let style = r#"
365 element, .class1 {
366 color: red;
367 }
368 #my-id.class2 {
369 color: red;
370 }
371 type#with-id .class3{
372 color: red;
373 }
374 element2 {
375 color: red;
376 }
377 .my-class {
378 color: red;
379 }
380 "#;
381 let output = super::CssProcessor::process_style(style).unwrap();
382 let uniq_class = output.class_name();
383 let suffix = output.class_suffix();
384 let mut output_css = String::new();
385 output_css.push_str(&format!(
386 r#"element.{uniq_class},.class1-{suffix}{{color:red}}"#
387 ));
388
389 output_css.push_str(&format!(r#"#my-id.class2-{suffix}{{color:red}}"#));
390
391 output_css.push_str(&format!(
392 r#"type#with-id.{uniq_class} .class3-{suffix}{{color:red}}"#
393 ));
394 output_css.push_str(&format!(r#"element2.{uniq_class}{{color:red}}"#));
395 output_css.push_str(&format!(r#".my-class-{suffix}{{color:red}}"#));
396 assert_eq!(output.css_data, output_css)
397 }
398 #[test]
399 fn complex_deep_global_combination() {
400 let style = r#"
401 :global(.my-class) {
402 color: red;
403 }
404 :deep(.my-class2) {
405 color: red;
406 }
407 :global(:deep(.my-class3)) {
408 color: red;
409 }
410 :deep(:global(.my-class4)) {
411 color: red;
412 }
413 "#;
414 let output = super::CssProcessor::process_style(style).unwrap();
415 let suffix = output.class_suffix();
416 let output_css = format!(
417 r#".my-class{{color:red}}.my-class2-{suffix}{{color:red}}.my-class3{{color:red}}.my-class4{{color:red}}"#
418 );
419 assert_eq!(output.css_data, output_css)
420 }
421 #[test]
422 fn complex_selector_in_deep() {
423 let style = r#"
424 :deep(.my-class) {
425 color: red;
426 }
427 :deep(.my-class2 .my-class3) {
428 color: red;
429 }
430 :deep(.my-class4 > .my-class5) {
431 color: red;
432 }
433 "#;
434 let output = super::CssProcessor::process_style(style).unwrap();
435 let suffix = output.class_suffix();
436 let output_css = format!(
437 r#".my-class-{suffix}{{color:red}}.my-class2-{suffix} .my-class3-{suffix}{{color:red}}.my-class4-{suffix}>.my-class5-{suffix}{{color:red}}"#
438 );
439 assert_eq!(output.css_data, output_css)
440 }
441 #[test]
442 fn id_after_global() {
443 let style = r#"
444 :global(.my-class)#my-id {
445 color: red;
446 }
447 "#;
448 let output = super::CssProcessor::process_style(style).unwrap();
449 let mut output_css = String::new();
450 output_css.push_str(&format!(r#".my-class#my-id{{color:red}}"#));
451 assert_eq!(output.css_data, output_css)
452 }
453 #[test]
454 fn id_after_deep() {
455 let style = r#"
456 :deep(.my-class)#my-id {
457 color: red;
458 }
459 "#;
460 let output = super::CssProcessor::process_style(style).unwrap();
461 let suffix = output.class_suffix();
462 let mut output_css = String::new();
463 output_css.push_str(&format!(r#".my-class-{suffix}#my-id{{color:red}}"#));
464 assert_eq!(output.css_data, output_css)
465 }
466
467 #[test]
468 fn complex_selector_in_global() {
469 let style = r#"
470 :global(.my-class) {
471 color: red;
472 }
473 :global(.my-class2 .my-class3) {
474 color: red;
475 }
476 :global(.my-class4 > .my-class5) {
477 color: red;
478 }
479 "#;
480 let output = super::CssProcessor::process_style(style).unwrap();
481 let output_css = format!(
482 r#".my-class{{color:red}}.my-class2 .my-class3{{color:red}}.my-class4>.my-class5{{color:red}}"#
483 );
484 assert_eq!(output.css_data, output_css)
485 }
486}