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}