Skip to main content

manganis_core/
css_module.rs

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