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