xiss/
class_map.rs

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    // @classmap NAME { ... }
33    pub name: JsWord,
34    // @static static-class-a static-class-b;
35    pub static_classes: String,
36    // disabled: class-disabled-1 class-disabled-2;
37    pub states: Vec<ClassMapState>,
38    // @exclude active and disabled;
39    // => maps to state indexes
40    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            // table
65            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            // inline
94            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    /// Checks index bitmask in excluded constraints.
180    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
190/// Joins strings [a] and [b] with ' ' separator.
191fn 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}