rcss_core/
lib.rs

1// TODO:
2// - [ ] Add support for css interpolation (usefull for theming, can be used with css custom properties).
3// - [ ] Procss preprocessor.
4// - [ ] :deep pseudo-elemenet support
5
6use 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    // use array instead of string to avoid heap allocation.
26    random_ident: [char; 7],
27}
28impl<'src> CssProcessor<'src> {
29    // TODO: Handle error
30    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        // Hide interpolation for now
44        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        // Create visitor that will modify class names, but will not modify css rules.
52        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    // Returns random class identifier.
117    // Each call return same result for same preprocessor input.
118    // Return 7 symbol with first symbol as underscore.
119    fn get_scoped_class(&self) -> String {
120        self.random_ident.iter().collect::<String>()
121    }
122
123    // Returns random class suffix.
124    // Each call return same result for same preprocessor input.
125    // Return 4 symbol.
126    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    /// Removes styles from output.
172    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    /// Returns map of changed classes.
181    pub fn classes_map(&self) -> &BTreeMap<String, ClassInfo> {
182        &self.changed_classes
183    }
184
185    /// Returns mod name if css should emit mod instead of inline struct.
186    pub fn declare(&self) -> Option<syn::ItemStruct> {
187        self.declare.clone()
188    }
189    /// Returns path to mod if css should extend existing css in mod instead of creating one from scratch.
190    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    /// Save multiple outputs to a single file.
213    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}