fallow_extract/
tailwind.rs1use std::sync::LazyLock;
13
14static ARBITRARY_VALUE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
23 crate::static_regex(r#"[a-z][a-z0-9]*(?:-[a-z0-9]+)*-\[[^\]\[\s"'`]{1,100}\]"#)
24});
25
26#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct TailwindArbitraryUse {
30 pub value: String,
32 pub line: u32,
34}
35
36#[must_use]
40pub fn scan_tailwind_arbitrary_values(source: &str) -> Vec<TailwindArbitraryUse> {
41 let mut out = Vec::new();
42 for m in ARBITRARY_VALUE_RE.find_iter(source) {
43 if is_arbitrary_variant_match(source.as_bytes(), m.end()) {
44 continue;
45 }
46 let line = 1 + source[..m.start()].bytes().filter(|&b| b == b'\n').count();
47 out.push(TailwindArbitraryUse {
48 value: m.as_str().to_owned(),
49 line: u32::try_from(line).unwrap_or(u32::MAX),
50 });
51 }
52 out
53}
54
55fn is_arbitrary_variant_match(source: &[u8], end: usize) -> bool {
56 if source.get(end) == Some(&b':') {
57 return true;
58 }
59 if source.get(end) != Some(&b'/') {
60 return false;
61 }
62 for &byte in &source[end + 1..] {
63 match byte {
64 b':' => return true,
65 b' ' | b'\n' | b'\r' | b'\t' | b'"' | b'\'' | b'`' => return false,
66 _ => {}
67 }
68 }
69 false
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 fn values(source: &str) -> Vec<String> {
77 scan_tailwind_arbitrary_values(source)
78 .into_iter()
79 .map(|u| u.value)
80 .collect()
81 }
82
83 #[test]
84 fn matches_common_arbitrary_value_shapes() {
85 let v = values(r#"<div class="w-[13px] bg-[#abc] grid-cols-[1fr_2fr] top-[7px]">x</div>"#);
86 assert_eq!(
87 v,
88 vec!["w-[13px]", "bg-[#abc]", "grid-cols-[1fr_2fr]", "top-[7px]"]
89 );
90 }
91
92 #[test]
93 fn ignores_plain_scale_utilities() {
94 let v = values(r#"<div class="w-4 bg-red-500 grid-cols-3">x</div>"#);
96 assert!(v.is_empty(), "got {v:?}");
97 }
98
99 #[test]
100 fn does_not_match_attribute_selectors() {
101 let v = values("a[href] { color: red; } [data-state] { color: blue; }");
103 assert!(v.is_empty(), "got {v:?}");
104 }
105
106 #[test]
107 fn reports_one_based_line() {
108 let uses = scan_tailwind_arbitrary_values("\n\n<i class=\"h-[3px]\"></i>");
109 assert_eq!(uses.len(), 1);
110 assert_eq!(uses[0].line, 3);
111 }
112
113 #[test]
114 fn captures_utility_prefix_not_variant() {
115 let v = values(r#"<a class="hover:w-[20px]">x</a>"#);
118 assert_eq!(v, vec!["w-[20px]"]);
119 }
120
121 #[test]
122 fn ignores_arbitrary_variants() {
123 let v = values(
124 r#"<div class="data-[side=left]:slide-in min-[320px]:text-sm group-data-[collapsible=icon]:hidden group-data-[size=sm]/dialog:grid peer-data-[size=lg]/button:top-2 hover:w-[20px]">x</div>"#,
125 );
126 assert_eq!(v, vec!["w-[20px]"]);
127 }
128
129 #[test]
130 fn keeps_arbitrary_value_modifiers() {
131 let v = values(r#"<div class="bg-[#fff]/50 ring-[3px]">x</div>"#);
132 assert_eq!(v, vec!["bg-[#fff]", "ring-[3px]"]);
133 }
134}