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