swc_css_utils/
lib.rs

1#![deny(clippy::all)]
2
3use std::{borrow::Cow, f64::consts::PI, str};
4
5use once_cell::sync::Lazy;
6use rustc_hash::FxHashMap;
7use serde::{Deserialize, Serialize};
8use swc_atoms::{Atom, StaticString};
9use swc_css_ast::*;
10use swc_css_visit::{VisitMut, VisitMutWith};
11
12pub struct IdentReplacer<'a> {
13    from: &'a str,
14    to: &'a str,
15}
16
17impl VisitMut for IdentReplacer<'_> {
18    fn visit_mut_ident(&mut self, n: &mut Ident) {
19        n.visit_mut_children_with(self);
20
21        if n.value.eq_ignore_ascii_case(self.from) {
22            n.value = self.to.into();
23            n.raw = None;
24        }
25    }
26}
27
28pub fn replace_ident<N>(node: &mut N, from: &str, to: &str)
29where
30    N: for<'aa> VisitMutWith<IdentReplacer<'aa>>,
31{
32    node.visit_mut_with(&mut IdentReplacer { from, to });
33}
34
35pub struct FunctionNameReplacer<'a> {
36    from: &'a str,
37    to: &'a str,
38}
39
40impl VisitMut for FunctionNameReplacer<'_> {
41    fn visit_mut_function(&mut self, n: &mut Function) {
42        n.visit_mut_children_with(self);
43
44        match &mut n.name {
45            FunctionName::Ident(name) if name.value.eq_ignore_ascii_case(self.from) => {
46                name.value = self.to.into();
47                name.raw = None;
48            }
49            FunctionName::DashedIdent(name) if name.value.eq_ignore_ascii_case(self.from) => {
50                name.value = self.to.into();
51                name.raw = None;
52            }
53            _ => {}
54        }
55    }
56}
57
58pub fn replace_function_name<N>(node: &mut N, from: &str, to: &str)
59where
60    N: for<'aa> VisitMutWith<FunctionNameReplacer<'aa>>,
61{
62    node.visit_mut_with(&mut FunctionNameReplacer { from, to });
63}
64
65pub struct PseudoClassSelectorNameReplacer<'a> {
66    from: &'a str,
67    to: &'a str,
68}
69
70impl VisitMut for PseudoClassSelectorNameReplacer<'_> {
71    fn visit_mut_pseudo_class_selector(&mut self, n: &mut PseudoClassSelector) {
72        n.visit_mut_children_with(self);
73
74        if &*n.name.value == self.from {
75            n.name.value = self.to.into();
76            n.name.raw = None;
77        }
78    }
79}
80
81pub fn replace_pseudo_class_selector_name<N>(node: &mut N, from: &str, to: &str)
82where
83    N: for<'aa> VisitMutWith<PseudoClassSelectorNameReplacer<'aa>>,
84{
85    node.visit_mut_with(&mut PseudoClassSelectorNameReplacer { from, to });
86}
87
88pub struct PseudoElementSelectorNameReplacer<'a> {
89    from: &'a str,
90    to: &'a str,
91}
92
93impl VisitMut for PseudoElementSelectorNameReplacer<'_> {
94    fn visit_mut_pseudo_element_selector(&mut self, n: &mut PseudoElementSelector) {
95        n.visit_mut_children_with(self);
96
97        if &*n.name.value == self.from {
98            n.name.value = self.to.into();
99            n.name.raw = None;
100        }
101    }
102}
103
104pub fn replace_pseudo_element_selector_name<N>(node: &mut N, from: &str, to: &str)
105where
106    N: for<'aa> VisitMutWith<PseudoElementSelectorNameReplacer<'aa>>,
107{
108    node.visit_mut_with(&mut PseudoElementSelectorNameReplacer { from, to });
109}
110
111pub struct PseudoElementOnPseudoClassReplacer<'a> {
112    from: &'a str,
113    to: &'a str,
114}
115
116impl VisitMut for PseudoElementOnPseudoClassReplacer<'_> {
117    fn visit_mut_subclass_selector(&mut self, n: &mut SubclassSelector) {
118        n.visit_mut_children_with(self);
119
120        match n {
121            SubclassSelector::PseudoElement(PseudoElementSelector { name, span, .. })
122                if &*name.value == self.from =>
123            {
124                *n = SubclassSelector::PseudoClass(PseudoClassSelector {
125                    span: *span,
126                    name: Ident {
127                        span: name.span,
128                        value: self.to.into(),
129                        raw: None,
130                    },
131                    children: None,
132                })
133            }
134            _ => {}
135        }
136    }
137}
138
139pub fn replace_pseudo_class_selector_on_pseudo_element_selector<N>(
140    node: &mut N,
141    from: &str,
142    to: &str,
143) where
144    N: for<'aa> VisitMutWith<PseudoElementOnPseudoClassReplacer<'aa>>,
145{
146    node.visit_mut_with(&mut PseudoElementOnPseudoClassReplacer { from, to });
147}
148
149#[derive(Serialize, Deserialize, Debug)]
150pub struct NamedColor {
151    pub hex: String,
152    pub rgb: Vec<u8>,
153}
154
155pub static NAMED_COLORS: Lazy<FxHashMap<StaticString, NamedColor>> = Lazy::new(|| {
156    serde_json::from_str(include_str!("./named-colors.json"))
157        .expect("failed to parse named-colors.json for html entities")
158});
159
160#[inline]
161fn is_escape_not_required(value: &str) -> bool {
162    if value.is_empty() {
163        return true;
164    }
165
166    if value.as_bytes()[0].is_ascii_digit() {
167        return false;
168    }
169
170    if value.len() == 1 && value.as_bytes()[0] == b'-' {
171        return false;
172    }
173
174    if value.len() >= 2 && value.as_bytes()[0] == b'-' && value.as_bytes()[1].is_ascii_digit() {
175        return false;
176    }
177
178    value.chars().all(|c| {
179        match c {
180            '\x00' => false,
181            '\x01'..='\x1f' | '\x7F' => false,
182            '-' | '_' => true,
183            _ if !c.is_ascii()
184                || c.is_ascii_digit()
185                || c.is_ascii_uppercase()
186                || c.is_ascii_lowercase() =>
187            {
188                true
189            }
190            // Otherwise, the escaped character.
191            _ => false,
192        }
193    })
194}
195
196// https://drafts.csswg.org/cssom/#serialize-an-identifier
197pub fn serialize_ident(value: &str, minify: bool) -> Cow<'_, str> {
198    // Fast-path
199    if is_escape_not_required(value) {
200        return Cow::Borrowed(value);
201    }
202
203    let mut result = String::with_capacity(value.len());
204
205    //
206    // To escape a character means to create a string of "\" (U+005C), followed by
207    // the character.
208    //
209    // To escape a character as code point means to create a string of "\" (U+005C),
210    // followed by the Unicode code point as the smallest possible number of
211    // hexadecimal digits in the range 0-9 a-f (U+0030 to U+0039 and U+0061 to
212    // U+0066) to represent the code point in base 16, followed by a single SPACE
213    // (U+0020).
214    //
215    // To serialize an identifier means to create a string represented
216    // by the concatenation of, for each character of the identifier:
217    for (i, c) in value.chars().enumerate() {
218        match c {
219            // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD).
220            '\x00' => {
221                result.push(char::REPLACEMENT_CHARACTER);
222            }
223            // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F, then the
224            // character escaped as code point.
225            '\x01'..='\x1f' | '\x7F' => {
226                result.push_str(&hex_escape(c as u8, minify));
227            }
228            // If the character is the first character and is in the range [0-9] (U+0030 to U+0039),
229            // then the character escaped as code point.
230            '0'..='9' if i == 0 => {
231                result.push_str(&hex_escape(c as u8, minify));
232            }
233            // If the character is the second character and is in the range [0-9] (U+0030 to U+0039)
234            // and the first character is a "-" (U+002D), then the character escaped as code point.
235            '0'..='9' if i == 1 && &value[0..1] == "-" => {
236                result.push_str(&hex_escape(c as u8, minify));
237            }
238            // If the character is the first character and is a "-" (U+002D), and there is no second
239            // character, then the escaped character.
240            '-' if i == 0 && value.len() == 1 => {
241                result.push_str(&hex_escape(c as u8, minify));
242            }
243            // If the character is not handled by one of the above rules and is greater than or
244            // equal to U+0080, is "-" (U+002D) or "_" (U+005F), or is in one of the ranges [0-9]
245            // (U+0030 to U+0039), [A-Z] (U+0041 to U+005A), or \[a-z] (U+0061 to U+007A), then the
246            // character itself.
247            _ if !c.is_ascii()
248                || c == '-'
249                || c == '_'
250                || c.is_ascii_digit()
251                || c.is_ascii_uppercase()
252                || c.is_ascii_lowercase() =>
253            {
254                result.push(c);
255            }
256            // Otherwise, the escaped character.
257            _ => {
258                let bytes = [b'\\', c as u8];
259
260                // SAFETY: We know it's valid to convert bytes to &str 'cause it's all valid
261                // ASCII
262                result.push_str(unsafe { str::from_utf8_unchecked(&bytes) });
263            }
264        }
265    }
266
267    Cow::Owned(result)
268}
269
270// https://github.com/servo/rust-cssparser/blob/4c5d065798ea1be649412532bde481dbd404f44a/src/serializer.rs#L166
271fn hex_escape(ascii_byte: u8, _minify: bool) -> String {
272    static HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
273
274    if ascii_byte > 0x0f {
275        let high = (ascii_byte >> 4) as usize;
276        let low = (ascii_byte & 0x0f) as usize;
277        unsafe { str::from_utf8_unchecked(&[b'\\', HEX_DIGITS[high], HEX_DIGITS[low], b' ']) }
278            .to_string()
279    } else {
280        unsafe { str::from_utf8_unchecked(&[b'\\', HEX_DIGITS[ascii_byte as usize], b' ']) }
281            .to_string()
282    }
283}
284
285pub fn hwb_to_rgb(hwb: [f64; 3]) -> [f64; 3] {
286    let [h, w, b] = hwb;
287
288    if w + b >= 1.0 {
289        let gray = w / (w + b);
290
291        return [gray, gray, gray];
292    }
293
294    let mut rgb = hsl_to_rgb([h, 1.0, 0.5]);
295
296    for item in &mut rgb {
297        *item *= 1.0 - w - b;
298        *item += w;
299    }
300
301    [rgb[0], rgb[1], rgb[2]]
302}
303
304pub fn hsl_to_rgb(hsl: [f64; 3]) -> [f64; 3] {
305    let [h, s, l] = hsl;
306
307    let r;
308    let g;
309    let b;
310
311    if s == 0.0 {
312        r = l;
313        g = l;
314        b = l;
315    } else {
316        let f = |n: f64| -> f64 {
317            let k = (n + h / 30.0) % 12.0;
318            let a = s * f64::min(l, 1.0 - l);
319
320            l - a * f64::min(k - 3.0, 9.0 - k).clamp(-1.0, 1.0)
321        };
322
323        r = f(0.0);
324        g = f(8.0);
325        b = f(4.0);
326    }
327
328    [r, g, b]
329}
330
331pub fn to_rgb255(abc: [f64; 3]) -> [f64; 3] {
332    let mut abc255 = abc;
333
334    for item in &mut abc255 {
335        *item *= 255.0;
336    }
337
338    abc255
339}
340
341pub fn clamp_unit_f64(val: f64) -> u8 {
342    (val * 255.).round().clamp(0., 255.) as u8
343}
344
345pub fn round_alpha(alpha: f64) -> f64 {
346    let mut rounded_alpha = (alpha * 100.).round() / 100.;
347
348    if clamp_unit_f64(rounded_alpha) != clamp_unit_f64(alpha) {
349        rounded_alpha = (alpha * 1000.).round() / 1000.;
350    }
351
352    rounded_alpha
353}
354
355#[inline]
356fn from_hex(c: u8) -> u8 {
357    match c {
358        b'0'..=b'9' => c - b'0',
359        b'a'..=b'f' => c - b'a' + 10,
360        b'A'..=b'F' => c - b'A' + 10,
361        _ => {
362            unreachable!();
363        }
364    }
365}
366
367pub fn hex_to_rgba(hex: &str) -> (u8, u8, u8, f64) {
368    let hex = hex.as_bytes();
369
370    match hex.len() {
371        8 => {
372            let r = from_hex(hex[0]) * 16 + from_hex(hex[1]);
373            let g = from_hex(hex[2]) * 16 + from_hex(hex[3]);
374            let b = from_hex(hex[4]) * 16 + from_hex(hex[5]);
375            let a = (from_hex(hex[6]) * 16 + from_hex(hex[7])) as f64 / 255.0;
376
377            (r, g, b, a)
378        }
379        4 => {
380            let r = from_hex(hex[0]) * 17;
381            let g = from_hex(hex[1]) * 17;
382            let b = from_hex(hex[2]) * 17;
383            let a = (from_hex(hex[3]) * 17) as f64 / 255.0;
384
385            (r, g, b, a)
386        }
387
388        _ => {
389            unreachable!()
390        }
391    }
392}
393
394pub fn angle_to_deg(value: f64, from: &Atom) -> f64 {
395    match &*from.to_ascii_lowercase() {
396        "deg" => value,
397        "grad" => value * 180.0 / 200.0,
398        "turn" => value * 360.0,
399        "rad" => value * 180.0 / PI,
400        _ => {
401            unreachable!("Unknown angle type: {:?}", from);
402        }
403    }
404}