1use std::fmt::{self, Write};
2
3use clap::ValueEnum;
4use swc_atoms::JsWord;
5
6#[derive(Debug, Clone, Copy, clap::ValueEnum)]
7pub enum ClassMapOutput {
8 Inline,
9 Table,
10}
11
12impl fmt::Display for ClassMapOutput {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 self.to_possible_value().unwrap().get_name().fmt(f)
15 }
16}
17
18#[derive(Debug, Clone)]
19pub struct ClassMapState {
20 pub name: JsWord,
21 pub classes: String,
22}
23
24impl ClassMapState {
25 pub fn new(name: JsWord, classes: String) -> Self {
26 Self { name, classes }
27 }
28}
29
30#[derive(Debug, Clone)]
31pub struct ClassMap {
32 pub name: JsWord,
34 pub static_classes: String,
36 pub states: Vec<ClassMapState>,
38 pub exclude_constraints: Vec<usize>,
41}
42
43impl ClassMap {
44 pub fn new(
45 name: JsWord,
46 static_classes: String,
47 states: Vec<ClassMapState>,
48 exclude_constraints: Vec<usize>,
49 ) -> Self {
50 Self {
51 name,
52 static_classes,
53 states,
54 exclude_constraints,
55 }
56 }
57
58 pub fn emit_js<W: Write>(
59 &self,
60 output: &mut W,
61 kind: ClassMapOutput,
62 ) -> Result<(), std::fmt::Error> {
63 if let ClassMapOutput::Table = kind {
64 let mut table = self.create_empty_table();
66 self.populate_table(&mut table, 0, &self.static_classes, 0);
67
68 write!(output, "const __CLASS_MAP_{} = [\n", self.name)?;
69 for entry in table {
70 write!(output, " \"{}\",\n", entry)?;
71 }
72 write!(output, "];\n\n")?;
73 write!(output, "/** classmap {{@link {}}} */\n", self.name)?;
74 write!(output, "export function {}(", self.name)?;
75 let mut iter = self.states.iter();
76 if let Some(s) = iter.next() {
77 write!(output, "{}", s.name)?;
78 for s in iter {
79 write!(output, ", {}", s.name)?;
80 }
81 }
82 write!(output, ") {{\n return __CLASS_MAP_{}[", self.name)?;
83 let mut iter = self.states.iter().enumerate();
84 if let Some((_, s)) = iter.next() {
85 write!(output, "({} ? 1 : 0)", s.name)?;
86
87 for (i, s) in iter {
88 write!(output, " | ({} ? {} : 0)", s.name, 1 << i)?;
89 }
90 }
91 write!(output, "];\n}}\n")?;
92 } else {
93 write!(output, "/** classmap {{@link {}}} */\n", self.name)?;
95 write!(output, "export function {}(", self.name)?;
96 let mut iter = self.states.iter();
97 if let Some(s) = iter.next() {
98 write!(output, "{}", s.name)?;
99 for s in iter {
100 write!(output, ", {}", s.name)?;
101 }
102 }
103 write!(output, ") {{\n return ")?;
104 self.write_inline_cond_expr(output, 0, &self.static_classes, 0)?;
105 write!(output, ";\n}}\n")?;
106 }
107
108 Ok(())
109 }
110
111 pub fn emit_ts<W: Write>(&self, output: &mut W) -> Result<(), std::fmt::Error> {
112 write!(output, "/** classmap {{@link {}}} */\n", self.name)?;
113 write!(output, "export function {}(", self.name)?;
114 let mut iter = self.states.iter();
115 if let Some(s) = iter.next() {
116 write!(output, "{}: boolean", s.name)?;
117 for s in iter {
118 write!(output, ", {}: boolean", s.name)?;
119 }
120 }
121 write!(output, "): string;\n")?;
122 Ok(())
123 }
124
125 fn write_inline_cond_expr<W: Write>(
126 &self,
127 output: &mut W,
128 mut i: usize,
129 prev_state: &str,
130 prev_state_mask: usize,
131 ) -> Result<(), std::fmt::Error> {
132 while i < self.states.len() {
133 let s = &self.states[i];
134 let next_state = join_strings(prev_state, &s.classes);
135 let next_state_mask = prev_state_mask | (1 << i);
136 i += 1;
137 if self.is_constraints_satisfied(next_state_mask) {
138 write!(output, "({} ? ", s.name)?;
139 self.write_inline_cond_expr(output, i, &next_state, next_state_mask)?;
140 write!(output, " : ")?;
141 self.write_inline_cond_expr(output, i, prev_state, prev_state_mask)?;
142 write!(output, ")")?;
143
144 return Ok(());
145 }
146 }
147 write!(output, "\"{}\"", prev_state)
148 }
149
150 fn create_empty_table(&self) -> Vec<String> {
151 vec![String::new(); 2_usize.pow(self.states.len() as u32)]
152 }
153
154 fn populate_table(
155 &self,
156 result: &mut Vec<String>,
157 i: usize,
158 prev_state: &str,
159 prev_state_mask: usize,
160 ) {
161 if !self.is_constraints_satisfied(prev_state_mask) {
162 return;
163 }
164
165 if i == self.states.len() {
166 result[prev_state_mask] = prev_state.into();
167 } else {
168 let next_i = i + 1;
169 self.populate_table(result, next_i, prev_state, prev_state_mask);
170 self.populate_table(
171 result,
172 next_i,
173 &join_strings(prev_state, &self.states[i].classes),
174 prev_state_mask | (1 << i),
175 )
176 }
177 }
178
179 fn is_constraints_satisfied(&self, index: usize) -> bool {
181 for constraint in self.exclude_constraints.iter() {
182 if (index & *constraint) == *constraint {
183 return false;
184 }
185 }
186 true
187 }
188}
189
190fn join_strings(a: &str, b: &str) -> String {
192 if a.is_empty() {
193 b.to_string()
194 } else {
195 let mut s = String::with_capacity(a.len() + b.len() + 1);
196 s.push_str(a);
197 s.push(' ');
198 s.push_str(b);
199 s
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn concat_class_names_empty_a() {
209 assert_eq!(join_strings("", "b"), "b");
210 }
211
212 #[test]
213 fn concat_class_names_empty_a_b() {
214 assert_eq!(join_strings("", ""), "");
215 }
216
217 #[test]
218 fn concat_class_names_empty_b() {
219 assert_eq!(join_strings("a", ""), "a ");
220 }
221
222 #[test]
223 fn concat_class_names_a_b() {
224 assert_eq!(join_strings("a", "b"), "a b");
225 }
226
227 #[test]
228 fn is_constraints_satisfied_empty() {
229 let cm = ClassMap::new("".into(), "".into(), vec![], vec![]);
230 assert!(cm.is_constraints_satisfied(0b1))
231 }
232
233 #[test]
234 fn is_constraints_satisfied_exclude_one_rule() {
235 let cm = ClassMap::new("".into(), "".into(), vec![], vec![0b01]);
236 assert!(cm.is_constraints_satisfied(0b10));
237
238 assert!(!cm.is_constraints_satisfied(0b01));
239 }
240
241 #[test]
242 fn is_constraints_satisfied_exclude_two_rules() {
243 let cm = ClassMap::new("".into(), "".into(), vec![], vec![0b101, 0b110]);
244 assert!(cm.is_constraints_satisfied(0b100));
245 assert!(cm.is_constraints_satisfied(0b010));
246 assert!(cm.is_constraints_satisfied(0b001));
247
248 assert!(!cm.is_constraints_satisfied(0b101));
249 assert!(!cm.is_constraints_satisfied(0b110));
250 }
251
252 #[test]
253 fn write_class_map_inline_cond_expr_1() {
254 let cm = ClassMap::new(
255 "".into(),
256 "".into(),
257 vec![ClassMapState::new("a".into(), "A".into())],
258 vec![],
259 );
260
261 let mut result = String::new();
262 cm.write_inline_cond_expr(&mut result, 0, "", 0).unwrap();
263 assert_eq!(result, "(a ? \"A\" : \"\")");
264 }
265
266 #[test]
267 fn write_class_map_inline_cond_expr_2() {
268 let cm = ClassMap::new(
269 "".into(),
270 "".into(),
271 vec![
272 ClassMapState::new("a".into(), "A".into()),
273 ClassMapState::new("b".into(), "B".into()),
274 ],
275 vec![],
276 );
277
278 let mut result = String::new();
279 cm.write_inline_cond_expr(&mut result, 0, "", 0).unwrap();
280 assert_eq!(result, "(a ? (b ? \"A B\" : \"A\") : (b ? \"B\" : \"\"))");
281 }
282
283 #[test]
284 fn write_class_map_inline_cond_expr_exclude_0b11() {
285 let cm = ClassMap::new(
286 "".into(),
287 "".into(),
288 vec![
289 ClassMapState::new("a".into(), "A".into()),
290 ClassMapState::new("b".into(), "B".into()),
291 ],
292 vec![0b11],
293 );
294
295 let mut result = String::new();
296 cm.write_inline_cond_expr(&mut result, 0, "", 0).unwrap();
297 assert_eq!(result, "(a ? \"A\" : (b ? \"B\" : \"\"))");
298 }
299
300 #[test]
301 fn generate_class_map_table_0() {
302 let cm = ClassMap::new("".into(), "".into(), vec![], vec![]);
303
304 let mut result = cm.create_empty_table();
305 cm.populate_table(&mut result, 0, "", 0);
306 assert_eq!(result, vec![""]);
307 }
308
309 #[test]
310 fn generate_class_map_table_1() {
311 let cm = ClassMap::new(
312 "".into(),
313 "".into(),
314 vec![ClassMapState::new("a".into(), "A".into())],
315 vec![],
316 );
317
318 let mut result = cm.create_empty_table();
319 cm.populate_table(&mut result, 0, "", 0);
320 assert_eq!(result, vec!["", "A"]);
321 }
322
323 #[test]
324 fn generate_class_map_table_2() {
325 let cm = ClassMap::new(
326 "".into(),
327 "".into(),
328 vec![
329 ClassMapState::new("a".into(), "A".into()),
330 ClassMapState::new("b".into(), "B".into()),
331 ],
332 vec![],
333 );
334
335 let mut result = cm.create_empty_table();
336 cm.populate_table(&mut result, 0, "", 0);
337 assert_eq!(result, vec!["", "A", "B", "A B"]);
338 }
339
340 #[test]
341 fn generate_class_map_table_3() {
342 let cm = ClassMap::new(
343 "".into(),
344 "".into(),
345 vec![
346 ClassMapState::new("a".into(), "A".into()),
347 ClassMapState::new("b".into(), "B".into()),
348 ClassMapState::new("c".into(), "C".into()),
349 ],
350 vec![],
351 );
352
353 let mut result = cm.create_empty_table();
354 cm.populate_table(&mut result, 0, "", 0);
355 assert_eq!(
356 result,
357 vec!["", "A", "B", "A B", "C", "A C", "B C", "A B C"]
358 );
359 }
360
361 #[test]
362 fn generate_class_map_table_3_exclude_0b011() {
363 let cm = ClassMap::new(
364 "".into(),
365 "".into(),
366 vec![
367 ClassMapState::new("a".into(), "A".into()),
368 ClassMapState::new("b".into(), "B".into()),
369 ClassMapState::new("c".into(), "C".into()),
370 ],
371 vec![0b011],
372 );
373
374 let mut result = cm.create_empty_table();
375 cm.populate_table(&mut result, 0, "", 0);
376 assert_eq!(result, vec!["", "A", "B", "", "C", "A C", "B C", ""]);
377 }
378
379 #[test]
380 fn generate_class_map_table_3_exclude_0b011_and_0b101() {
381 let cm = ClassMap::new(
382 "".into(),
383 "".into(),
384 vec![
385 ClassMapState::new("a".into(), "A".into()),
386 ClassMapState::new("b".into(), "B".into()),
387 ClassMapState::new("c".into(), "C".into()),
388 ],
389 vec![0b011, 0b101],
390 );
391
392 let mut result = cm.create_empty_table();
393 cm.populate_table(&mut result, 0, "", 0);
394 assert_eq!(result, vec!["", "A", "B", "", "C", "", "B C", ""]);
395 }
396}