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, utils::split_ignore_arbitrary};
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/// --en-bg-opacity: 1;
34/// background-color: rgb(254 202 202 / var(--en-bg-opacity));
35/// }
36///
37/// .text-blue-300 {
38/// --en-text-opacity: 1;
39/// color: rgb(147 197 253 / var(--en-text-opacity));
40/// }
41///
42/// .underline {
43/// -webkit-text-decoration-line: underline;
44/// text-decoration-line: underline;
45/// }"));
46/// ```
47///
48/// [`split_ignore_arbitrary`]: crate::utils::split_ignore_arbitrary
49#[allow(missing_debug_implementations)]
50#[allow(clippy::type_complexity)]
51#[derive(Clone)]
52pub struct Scanner {
53 scan_fn: Arc<dyn Fn(&str) -> BTreeSet<&str> + Send + Sync>,
54}
55
56impl Scanner {
57 /// Build a [`Scanner`] from a closure taking some content and returning a list of possible
58 /// classes.
59 pub fn from_fn<T: 'static + Fn(&str) -> BTreeSet<&str> + Send + Sync>(scan_fn: T) -> Self {
60 Self {
61 scan_fn: Arc::new(scan_fn),
62 }
63 }
64
65 pub(crate) fn scan<'a>(&self, val: &'a str) -> BTreeSet<&'a str> {
66 (self.scan_fn)(val)
67 }
68}
69
70impl Default for Scanner {
71 fn default() -> Self {
72 Self {
73 scan_fn: Arc::new(|val| {
74 let mut is_arbitrary = false;
75
76 val.split(|ch| {
77 // Escape all characters in arbitrary values prefixed by a dash (used to avoid
78 // ignoring values in, for example, JS arrays, given that they are defined
79 // using square brackets)
80 match ch {
81 '[' => {
82 is_arbitrary = true;
83 false
84 }
85 ']' => {
86 is_arbitrary = false;
87 false
88 }
89 _ => {
90 ch == ' '
91 || (!is_arbitrary
92 && (ch == '\'' || ch == '"' || ch == '`' || ch == '\n'))
93 }
94 }
95 })
96 .collect::<BTreeSet<&str>>()
97 }),
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 use std::collections::BTreeSet;
107
108 #[test]
109 fn default_scanner_test() {
110 assert_eq!(
111 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)]"),
112 BTreeSet::from([
113 "",
114 "test",
115 "bg-red-500",
116 "hello",
117 "content-[some_[_square]_brackets]",
118 "foo-bar",
119 "sm:focus:ring",
120 "hover:bg-black",
121 "border-[#333]",
122 "text-[color:var(--hello)]",
123 ])
124 );
125 }
126
127 #[test]
128 fn custom_scanner_test() {
129 let scanner = Scanner::from_fn(|val| val.split(|ch| ch == '|').collect::<BTreeSet<&str>>());
130
131 assert_eq!(
132 scanner.scan("test|bg-red-500|'hello'"),
133 BTreeSet::from(["test", "bg-red-500", "'hello'"])
134 );
135 }
136
137 #[test]
138 fn utf8_scan() {
139 assert_eq!(
140 Scanner::default().scan("<div class=\"before:content-[J\u{e4}s\u{f8}n_Doe] content-[\u{2192}]\">\u{306}</div>"),
141 BTreeSet::from([
142 "<div",
143 ">\u{306}</div>",
144 "before:content-[J\u{e4}s\u{f8}n_Doe]",
145 "class=",
146 "content-[\u{2192}]",
147 ])
148 );
149 }
150
151 #[test]
152 fn scan_prevent_splitting_arbitrary_values() {
153 assert_eq!(
154 Scanner::default().scan(r#"<div class="bg-red-300 content-['hello:>"']"></div>"#),
155 BTreeSet::from([
156 "<div",
157 "></div>",
158 "bg-red-300",
159 "class=",
160 "content-['hello:>\"']",
161 ])
162 );
163 }
164
165 #[test]
166 fn scan_with_arbitrary_variant() {
167 assert_eq!(
168 Scanner::default().scan(r#"<div class="[input[type='text']]:block"></div>"#),
169 BTreeSet::from([
170 "<div",
171 "></div>",
172 "class=",
173 "[input[type='text']]:block",
174 ])
175 );
176 }
177}