encre_css/
scanner.rs

1//! Define a structure used to scan content.
2use std::{collections::BTreeSet, sync::Arc};
3
4/// A structure responsible for scanning some content and returning a list of possible classes.
5///
6/// By default, it splits the content by spaces, double quotes, single quotes, backticks and new
7/// lines, while ignoring arbitrary the content inside values/variants and variant groups
8/// by using [`split_ignore_arbitrary`].
9/// It is recommended to use this function when splitting classes with characters which can be
10/// included inside arbitrary strings.
11///
12/// # Example
13///
14/// The following code snippet defines a scanner for extracting classes listed in the `data-en`
15/// HTML attribute.
16///
17/// ```
18/// use encre_css::{Config, Scanner};
19/// use std::collections::BTreeSet;
20///
21/// let mut config = Config::default();
22/// config.scanner = Scanner::from_fn(|content| content.split(r#"data-en=""#)
23///     .filter_map(|v| v.split_once("\"").map(|(classes, _)| classes.split_whitespace()))
24///     .flatten()
25///     .collect::<BTreeSet<&str>>());
26///
27/// let generated = encre_css::generate(
28///     [r#"<h1 data-en="underline"></h1><p data-en="bg-red-200 text-blue-300"></p>"#],
29///     &config,
30/// );
31///
32/// assert!(generated.ends_with(".bg-red-200 {
33///   background-color: oklch(88.5% .062 18.334);
34/// }
35///
36/// .text-blue-300 {
37///   color: oklch(80.9% .105 251.813);
38/// }
39///
40/// .underline {
41///   -webkit-text-decoration-line: underline;
42///   text-decoration-line: underline;
43/// }"));
44/// ```
45///
46/// [`split_ignore_arbitrary`]: crate::utils::split_ignore_arbitrary
47#[allow(missing_debug_implementations)]
48#[allow(clippy::type_complexity)]
49#[derive(Clone)]
50pub struct Scanner {
51    scan_fn: Arc<dyn Fn(&str) -> BTreeSet<&str> + Send + Sync>,
52}
53
54impl Scanner {
55    /// Build a [`Scanner`] from a closure taking some content and returning a list of possible
56    /// classes.
57    pub fn from_fn<T: 'static + Fn(&str) -> BTreeSet<&str> + Send + Sync>(scan_fn: T) -> Self {
58        Self {
59            scan_fn: Arc::new(scan_fn),
60        }
61    }
62
63    pub(crate) fn scan<'a>(&self, val: &'a str) -> BTreeSet<&'a str> {
64        (self.scan_fn)(val)
65    }
66}
67
68impl Default for Scanner {
69    fn default() -> Self {
70        Self {
71            scan_fn: Arc::new(|val| {
72                val.split([' ', '\n', '\'', '"', '`', '\\'])
73                    .collect::<BTreeSet<&str>>()
74            }),
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    use std::collections::BTreeSet;
84
85    #[test]
86    fn default_scanner_test() {
87        assert_eq!(
88            Scanner::default().scan("test bg-red-500 'hello' content-[some_[_square]_brackets] foo-bar sm:focus:ring hover:bg-black border-[#333] text-[color:var(--hello)]"),
89            BTreeSet::from([
90                "",
91                "test",
92                "bg-red-500",
93                "hello",
94                "content-[some_[_square]_brackets]",
95                "foo-bar",
96                "sm:focus:ring",
97                "hover:bg-black",
98                "border-[#333]",
99                "text-[color:var(--hello)]",
100            ])
101        );
102    }
103
104    #[test]
105    fn custom_scanner_test() {
106        let scanner = Scanner::from_fn(|val| val.split(|ch| ch == '|').collect::<BTreeSet<&str>>());
107
108        assert_eq!(
109            scanner.scan("test|bg-red-500|'hello'"),
110            BTreeSet::from(["test", "bg-red-500", "'hello'"])
111        );
112    }
113
114    #[test]
115    fn utf8_scan() {
116        assert_eq!(
117            Scanner::default().scan("<div class=\"before:content-[J\u{e4}s\u{f8}n_Doe] content-[\u{2192}]\">\u{306}</div>"),
118            BTreeSet::from([
119                "<div",
120                ">\u{306}</div>",
121                "before:content-[J\u{e4}s\u{f8}n_Doe]",
122                "class=",
123                "content-[\u{2192}]",
124            ])
125        );
126    }
127
128    #[test]
129    fn scan_prevent_splitting_arbitrary_values() {
130        assert_eq!(
131            Scanner::default().scan(r#"<div class="bg-red-300 content-[&#39;hello:>&#34;&#39;]"></div>"#),
132            BTreeSet::from([
133                "<div",
134                "></div>",
135                "bg-red-300",
136                "class=",
137                "content-[&#39;hello:>&#34;&#39;]",
138            ])
139        );
140    }
141
142    #[test]
143    fn scan_with_arbitrary_variant() {
144        assert_eq!(
145            Scanner::default().scan(r#"<div class="[input[type=&#39;text&#39;]]:block"></div>"#),
146            BTreeSet::from(["<div", "></div>", "class=", "[input[type=&#39;text&#39;]]:block",])
147        );
148    }
149}