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-['hello:>"']"></div>"#),
132 BTreeSet::from([
133 "<div",
134 "></div>",
135 "bg-red-300",
136 "class=",
137 "content-['hello:>"']",
138 ])
139 );
140 }
141
142 #[test]
143 fn scan_with_arbitrary_variant() {
144 assert_eq!(
145 Scanner::default().scan(r#"<div class="[input[type='text']]:block"></div>"#),
146 BTreeSet::from(["<div", "></div>", "class=", "[input[type='text']]:block",])
147 );
148 }
149}