Skip to main content

encre_css/utils/
mod.rs

1//! Define some utility functions for quickly doing things.
2use crate::{
3    config::Config,
4    error::{ParseError, ParseErrorKind},
5    selector::{
6        Selector,
7        parser::{ARBITRARY_END, ARBITRARY_START, ESCAPE, GROUP_END, GROUP_START, parse},
8    },
9};
10
11use std::{cmp::Ordering, iter, str::CharIndices};
12
13pub mod buffer;
14pub mod color;
15pub mod shadow;
16pub mod spacing;
17pub mod value_matchers;
18
19#[cfg(test)]
20pub(crate) mod testing;
21
22/// While <https://github.com/rust-lang/rust/issues/27721> is pending we need to define
23/// our own minimal [`Pattern`] trait.
24///
25/// | Pattern type             | Match condition                           |
26/// |--------------------------|-------------------------------------------|
27/// | `char`                   | is contained in string                    |
28/// | `&[char]`                | any char in slice is contained in string  |
29/// | `F: FnMut(char) -> bool` | `F` returns `true` for a char in string   |
30///
31/// [`Pattern`]: std::str::pattern::Pattern
32pub trait Pattern {
33    /// Returns whether the character is matching the pattern.
34    fn is_matching(&self, val: char) -> bool;
35}
36
37impl Pattern for char {
38    fn is_matching(&self, val: char) -> bool {
39        val == *self
40    }
41}
42
43impl Pattern for &[char] {
44    fn is_matching(&self, val: char) -> bool {
45        #[allow(clippy::manual_contains)]
46        self.iter().any(|ch| val == *ch)
47    }
48}
49
50impl<F: Fn(char) -> bool> Pattern for F {
51    fn is_matching(&self, val: char) -> bool {
52        self(val)
53    }
54}
55
56/// An iterator ignoring values wrapped in parenthesis and brackets.
57///
58/// This structure is created by the [`split_ignore_arbitrary`] function. See its documentation for
59/// more.
60#[derive(Debug)]
61pub struct SplitIgnoreArbitrary<'a, P: Pattern> {
62    val: &'a str,
63    iter: CharIndices<'a>,
64    searched_pattern: P,
65    ignore_parenthesis: bool,
66    is_next_escaped: bool,
67    last_slice_returned: bool,
68    parenthesis_level: usize,
69    bracket_level: usize,
70    last_index: usize,
71    seek_index: usize,
72}
73
74impl<'a, P: Pattern> Iterator for SplitIgnoreArbitrary<'a, P> {
75    type Item = (usize, &'a str);
76
77    fn next(&mut self) -> Option<Self::Item> {
78        loop {
79            if self.is_next_escaped {
80                let _ = self.iter.next()?;
81                self.is_next_escaped = false;
82                continue;
83            }
84
85            let ch = self.iter.next();
86
87            if let Some(ch) = ch {
88                match ch.1 {
89                    ESCAPE => self.is_next_escaped = true,
90                    GROUP_START if self.ignore_parenthesis && self.bracket_level == 0 => {
91                        self.parenthesis_level += 1;
92                    }
93                    GROUP_END if self.ignore_parenthesis && self.bracket_level == 0 => {
94                        if self.parenthesis_level > 0 {
95                            self.parenthesis_level -= 1;
96                            self.seek_index = ch.0 + 1;
97                        }
98                    }
99                    ARBITRARY_START => self.bracket_level += 1,
100                    ARBITRARY_END => {
101                        if self.bracket_level > 0 {
102                            self.bracket_level -= 1;
103                            self.seek_index = ch.0 + 1;
104                        }
105                    }
106                    _ => {
107                        if self.searched_pattern.is_matching(ch.1)
108                            && self.bracket_level == 0
109                            && !(self.ignore_parenthesis && self.parenthesis_level > 0)
110                        {
111                            let last_index = self.last_index;
112                            self.last_index = ch.0 + ch.1.len_utf8();
113                            self.seek_index = self.last_index;
114                            return Some((last_index, &self.val[last_index..ch.0]));
115                        }
116                    }
117                }
118            } else if !self.last_slice_returned {
119                // The characters are all handled, return the last slice
120                let last_index = self.last_index;
121                self.last_index = self.val.len();
122                self.last_slice_returned = true;
123                return Some((last_index, &self.val[last_index..self.val.len()]));
124            } else {
125                // The characters are all handled, and the last slice was returned if no character is
126                // searched, return `None`
127                return None;
128            }
129        }
130    }
131}
132
133/// Split a value while avoiding arbitrary values/variants (wrapped in brackets) from being split.
134///
135/// The last argument indicates whether variant groups (wrapped in parentheses) are also ignored.
136///
137/// # Example
138///
139/// ```
140/// use encre_css::utils::split_ignore_arbitrary;
141///
142/// let value = "bg-red-500 content-[wrapped in `[]`, will not be split] (words wrapped in parenthesis are not split too)";
143/// assert_eq!(split_ignore_arbitrary(value, ' ', true).collect::<Vec<(usize, &str)>>(), vec![(0, "bg-red-500"), (11, "content-[wrapped in `[]`, will not be split]"), (56, "(words wrapped in parenthesis are not split too)")]);
144/// ```
145pub fn split_ignore_arbitrary<P: Pattern>(
146    val: &str,
147    searched_pattern: P,
148    ignore_parenthesis: bool,
149) -> impl Iterator<Item = (usize, &str)> {
150    SplitIgnoreArbitrary {
151        val,
152        iter: val.char_indices(),
153        searched_pattern,
154        ignore_parenthesis,
155        is_next_escaped: false,
156        last_slice_returned: false,
157        parenthesis_level: 0,
158        bracket_level: 0,
159        last_index: 0,
160        seek_index: 0,
161    }
162}
163
164fn sort_selectors_recursive<'a>(
165    val: impl Iterator<Item = &'a str>,
166    separator: &str,
167    config: &Config,
168) -> String {
169    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
170    enum FoundSelector<'a> {
171        UnknownSelector(&'a str),
172        KnownSelector(Selector<'a>),
173        Group(String),
174    }
175
176    fn split_map_closure(s: (usize, &str)) -> &str {
177        s.1
178    }
179
180    fn dedup_key<'a>(s: &'a FoundSelector<'a>) -> &'a str {
181        match s {
182            FoundSelector::KnownSelector(s) => s.full,
183            FoundSelector::UnknownSelector(s) => s,
184            FoundSelector::Group(g) => g,
185        }
186    }
187
188    let trie = crate::selector::trie::build_trie(&config.custom_plugins);
189    let config_derived_variants = config.get_derived_variants();
190    let mut selectors = val
191        .filter_map(|v| {
192            let selectors = parse(
193                v.trim(),
194                None,
195                None,
196                config,
197                &config_derived_variants,
198                &trie,
199            );
200
201            if selectors.len() > 1 {
202                // Sort variant groups
203                let start = split_ignore_arbitrary(v.trim(), '(', false)
204                    .nth(1)
205                    .map(|(n, _s)| n)
206                    .unwrap_or_default();
207
208                Some(FoundSelector::Group(format!(
209                    "{}{})",
210                    &v[..start],
211                    sort_selectors_recursive(
212                        split_ignore_arbitrary(v[start..v.len() - 1].trim(), ',', true)
213                            .map(split_map_closure),
214                        ",",
215                        config,
216                    )
217                )))
218            } else {
219                match selectors.into_iter().next()? {
220                    Ok(selector) => Some(FoundSelector::KnownSelector(selector)),
221                    Err(ParseError {
222                        kind:
223                            ParseErrorKind::TooShort(selector)
224                            | ParseErrorKind::VariantsWithoutModifier(selector)
225                            | ParseErrorKind::UnknownPlugin(selector)
226                            | ParseErrorKind::UnknownVariant(_, selector),
227                        ..
228                    }) => Some(FoundSelector::UnknownSelector(selector)),
229                }
230            }
231        })
232        .collect::<Vec<FoundSelector>>();
233
234    // Sort selectors
235    selectors.sort_unstable_by(|a, b| match (a, b) {
236        (FoundSelector::KnownSelector(_), FoundSelector::UnknownSelector(_))
237        | (FoundSelector::Group(_), _) => Ordering::Greater,
238        (FoundSelector::UnknownSelector(_), FoundSelector::KnownSelector(_))
239        | (_, FoundSelector::Group(_)) => Ordering::Less,
240        (FoundSelector::KnownSelector(a), FoundSelector::KnownSelector(b)) => a.cmp(b),
241        (FoundSelector::UnknownSelector(a), FoundSelector::UnknownSelector(b)) => a.cmp(b),
242    });
243
244    // Deduplicate selectors
245    selectors.dedup_by(|a, b| dedup_key(&*a) == dedup_key(&*b));
246
247    selectors
248        .iter()
249        .map(|s| match s {
250            FoundSelector::KnownSelector(s) => s.full,
251            FoundSelector::UnknownSelector(s) => s,
252            FoundSelector::Group(g) => g,
253        })
254        .collect::<Vec<&str>>()
255        .join(separator)
256}
257
258/// Sort a list of selectors (separated by spaces) according to `encre-css` rules.
259///
260/// Note: selectors are also deduplicated.
261///
262/// # Example
263///
264/// ```
265/// use encre_css::{Config, utils::sort_selectors};
266///
267/// let value = "foo text-white px-4 sm:px-8 py-2 qux:(bg-green-500,dark:bar:foo) sm:py-3 bar bg-sky-700 foo focus:(md:text-white,lg:text-gray-500) hover:bg-sky-800";
268/// assert_eq!(sort_selectors(value, &Config::default()), "bar foo qux:(bg-green-500,dark:bar:foo) bg-sky-700 px-4 py-2 text-white hover:bg-sky-800 sm:py-3 sm:px-8 focus:(lg:text-gray-500,md:text-white)".to_string());
269/// ```
270pub fn sort_selectors(val: &str, config: &Config) -> String {
271    sort_selectors_recursive(val.split_whitespace(), " ", config)
272}
273
274/// Return the list of errors encountered when parsing a list of selectors
275///
276/// # Example
277///
278/// ```
279/// use encre_css::{Config, error::{ParseError, ParseErrorKind}, utils::check_selectors};
280///
281/// let value = "bg text-red hover:a lg: focus:() dark:(md:,shadow-8xl) bar:text-black md:foo:flex";
282/// assert_eq!(check_selectors(value, &Config::default()), vec![
283///     ParseError { span: 0..2, kind: ParseErrorKind::TooShort("bg") },
284///     ParseError { span: 3..11, kind: ParseErrorKind::UnknownPlugin("text-red") },
285///     ParseError { span: 12..19, kind: ParseErrorKind::UnknownPlugin("hover:a") },
286///     ParseError { span: 20..23, kind: ParseErrorKind::VariantsWithoutModifier("lg:") },
287///     ParseError { span: 24..32, kind: ParseErrorKind::VariantsWithoutModifier("focus:()") },
288///     ParseError { span: 39..42, kind: ParseErrorKind::VariantsWithoutModifier("md:") },
289///     ParseError { span: 43..53, kind: ParseErrorKind::UnknownPlugin("shadow-8xl") },
290///     ParseError { span: 55..69, kind: ParseErrorKind::UnknownVariant("bar", "bar:text-black") },
291///     ParseError { span: 70..81, kind: ParseErrorKind::UnknownVariant("foo", "md:foo:flex") }
292/// ]);
293/// ```
294pub fn check_selectors<'a>(val: &'a str, config: &Config) -> Vec<ParseError<'a>> {
295    let trie = crate::selector::trie::build_trie(&config.custom_plugins);
296    let config_derived_variants = config.get_derived_variants();
297    val.char_indices()
298        .chain(iter::once((val.len(), ' ')))
299        .filter(|(_, ch)| ch.is_whitespace())
300        .scan(0, |last_i, (i, _)| {
301            let old_i = *last_i;
302            *last_i = i + 1;
303            Some((old_i..i, &val[old_i..i]))
304        })
305        .filter(|(_, v)| !v.is_empty())
306        .flat_map(|(span, v)| {
307            parse(
308                v.trim(),
309                Some(span),
310                None,
311                config,
312                &config_derived_variants,
313                &trie,
314            )
315        })
316        .filter_map(Result::err)
317        .collect::<Vec<ParseError>>()
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::error::ParseErrorKind;
324
325    #[test]
326    fn sort_selectors_with_variant_groups() {
327        assert_eq!(
328            sort_selectors(
329                "hover:(text-white,bg-sky-800) focus-within:bg-red-100 text-blue-500 md:flex [()())):]:checked:([))]:text-white,[))]:bg-red-500) hover:(focus:(focus-within:bg-red-500,checked:text-black),active:bg-red-500)",
330                &Config::default()
331            ),
332            "text-blue-500 focus-within:bg-red-100 md:flex hover:(bg-sky-800,text-white) [()())):]:checked:([))]:bg-red-500,[))]:text-white) hover:(active:bg-red-500,focus:(checked:text-black,focus-within:bg-red-500))"
333                .to_string()
334        );
335    }
336
337    #[test]
338    fn sort_selectors_deduplicate() {
339        assert_eq!(sort_selectors("text-blue-100 text-blue-100 md:flex lg:block content-['hover:(md:text-white)'] md:flex focus:(hover:md:flex,lg:flex)", &Config::default()), "text-blue-100 content-['hover:(md:text-white)'] lg:block md:flex focus:(hover:md:flex,lg:flex)".to_string());
340    }
341
342    #[test]
343    fn check_selectors_ignore_newlines_and_spaces() {
344        assert_eq!(
345            check_selectors(
346                "text-blue-100   text-blue-100  md:flex   lg:block
347content-['hover:(md:text-white)'] md:blue-flex
348
349focus:(hover:md:flex,lg:flex)
350  lg:bg-red-500",
351                &Config::default()
352            ),
353            vec![ParseError {
354                span: 84..96,
355                kind: ParseErrorKind::UnknownPlugin("md:blue-flex")
356            }]
357        );
358    }
359}