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}