manganis_core/
css_module.rs

1use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
2use const_serialize::SerializeConst;
3use std::collections::HashSet;
4
5/// Options for a css module asset
6#[derive(
7    Debug,
8    Eq,
9    PartialEq,
10    PartialOrd,
11    Clone,
12    Copy,
13    Hash,
14    SerializeConst,
15    serde::Serialize,
16    serde::Deserialize,
17)]
18#[non_exhaustive]
19#[doc(hidden)]
20pub struct CssModuleAssetOptions {
21    minify: bool,
22    preload: bool,
23}
24
25impl Default for CssModuleAssetOptions {
26    fn default() -> Self {
27        Self::default()
28    }
29}
30
31impl CssModuleAssetOptions {
32    /// Create a new css asset using the builder
33    pub const fn new() -> AssetOptionsBuilder<CssModuleAssetOptions> {
34        AssetOptions::css_module()
35    }
36
37    /// Create a default css module asset options
38    pub const fn default() -> Self {
39        Self {
40            preload: false,
41            minify: true,
42        }
43    }
44
45    /// Check if the asset is minified
46    pub const fn minified(&self) -> bool {
47        self.minify
48    }
49
50    /// Check if the asset is preloaded
51    pub const fn preloaded(&self) -> bool {
52        self.preload
53    }
54}
55
56impl AssetOptions {
57    /// Create a new css module asset builder
58    ///
59    /// ```rust
60    /// # use manganis::{asset, Asset, AssetOptions};
61    /// const _: Asset = asset!("/assets/style.css", AssetOptions::css_module());
62    /// ```
63    pub const fn css_module() -> AssetOptionsBuilder<CssModuleAssetOptions> {
64        AssetOptionsBuilder::variant(CssModuleAssetOptions::default())
65    }
66}
67
68impl AssetOptionsBuilder<CssModuleAssetOptions> {
69    /// Sets whether the css should be minified (default: true)
70    ///
71    /// Minifying the css can make your site load faster by loading less data
72    pub const fn with_minify(mut self, minify: bool) -> Self {
73        self.variant.minify = minify;
74        self
75    }
76
77    /// Make the asset preloaded
78    ///
79    /// Preloading css will make the image start to load as soon as possible. This is useful for css that is used soon after the page loads or css that may not be used immediately, but should start loading sooner
80    pub const fn with_preload(mut self, preload: bool) -> Self {
81        self.variant.preload = preload;
82        self
83    }
84
85    /// Convert the options into options for a generic asset
86    pub const fn into_asset_options(self) -> AssetOptions {
87        AssetOptions {
88            add_hash: self.add_hash,
89            variant: AssetVariant::CssModule(self.variant),
90        }
91    }
92}
93
94/// Collect CSS classes & ids.
95///
96/// This is a rudementary css classes & ids collector.
97/// Idents used only in media queries will not be collected. (not support yet)
98///
99/// There are likely a number of edge cases that will show up.
100///
101/// Returns `(HashSet<Classes>, HashSet<Ids>)`
102pub fn collect_css_idents(css: &str) -> (HashSet<String>, HashSet<String>) {
103    const ALLOWED: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
104
105    let mut classes = HashSet::new();
106    let mut ids = HashSet::new();
107
108    // Collected ident name and true for ids.
109    let mut start: Option<(String, bool)> = None;
110
111    // True if we have the first comment start delimiter `/`
112    let mut comment_start = false;
113    // True if we have the first comment end delimiter '*'
114    let mut comment_end = false;
115    // True if we're in a comment scope.
116    let mut in_comment_scope = false;
117
118    // True if we're in a block scope: `#hi { this is block scope }`
119    let mut in_block_scope = false;
120
121    // If we are currently collecting an ident:
122    // - Check if the char is allowed, put it into the ident string.
123    // - If not allowed, finalize the ident string and reset start.
124    // Otherwise:
125    // Check if character is a `.` or `#` representing a class or string, and start collecting.
126    for (_byte_index, c) in css.char_indices() {
127        if let Some(ident) = start.as_mut() {
128            if ALLOWED.find(c).is_some() {
129                // CSS ignore idents that start with a number.
130                // 1. Difficult to process
131                // 2. Avoid false positives (transition: 0.5s)
132                if ident.0.is_empty() && c.is_numeric() {
133                    start = None;
134                    continue;
135                }
136
137                ident.0.push(c);
138            } else {
139                match ident.1 {
140                    true => ids.insert(ident.0.clone()),
141                    false => classes.insert(ident.0.clone()),
142                };
143
144                start = None;
145            }
146        } else {
147            // Handle entering an exiting scopede.
148            match c {
149                // Mark as comment scope if we have comment start: /*
150                '*' if comment_start => {
151                    comment_start = false;
152                    in_comment_scope = true;
153                }
154                // Mark start of comment end if in comment scope: */
155                '*' if in_comment_scope => comment_end = true,
156                // Mark as comment start if not in comment scope and no comment start, mark comment_start
157                '/' if !in_comment_scope => {
158                    comment_start = true;
159                }
160                // If we get the closing delimiter, mark as non-comment scope.
161                '/' if comment_end => {
162                    in_comment_scope = false;
163                    comment_start = false;
164                    comment_end = false;
165                }
166                // Entering & Exiting block scope.
167                '{' => in_block_scope = true,
168                '}' => in_block_scope = false,
169                // Any other character, reset comment start and end if not in scope.
170                _ => {
171                    comment_start = false;
172                    comment_end = false;
173                }
174            }
175
176            // No need to process this char if in bad scope.
177            if in_comment_scope || in_block_scope {
178                continue;
179            }
180
181            match c {
182                '.' => start = Some((String::new(), false)),
183                '#' => start = Some((String::new(), true)),
184                _ => {}
185            }
186        }
187    }
188
189    (classes, ids)
190}