use std::sync::LazyLock;
static ARBITRARY_VALUE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
crate::static_regex(r#"[a-z][a-z0-9]*(?:-[a-z0-9]+)*-\[[^\]\[\s"'`]{1,100}\]"#)
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TailwindArbitraryUse {
pub value: String,
pub line: u32,
}
#[must_use]
pub fn scan_tailwind_arbitrary_values(source: &str) -> Vec<TailwindArbitraryUse> {
let mut out = Vec::new();
for m in ARBITRARY_VALUE_RE.find_iter(source) {
let line = 1 + source[..m.start()].bytes().filter(|&b| b == b'\n').count();
out.push(TailwindArbitraryUse {
value: m.as_str().to_owned(),
line: u32::try_from(line).unwrap_or(u32::MAX),
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn values(source: &str) -> Vec<String> {
scan_tailwind_arbitrary_values(source)
.into_iter()
.map(|u| u.value)
.collect()
}
#[test]
fn matches_common_arbitrary_value_shapes() {
let v = values(r#"<div class="w-[13px] bg-[#abc] grid-cols-[1fr_2fr] top-[7px]">x</div>"#);
assert_eq!(
v,
vec!["w-[13px]", "bg-[#abc]", "grid-cols-[1fr_2fr]", "top-[7px]"]
);
}
#[test]
fn ignores_plain_scale_utilities() {
let v = values(r#"<div class="w-4 bg-red-500 grid-cols-3">x</div>"#);
assert!(v.is_empty(), "got {v:?}");
}
#[test]
fn does_not_match_attribute_selectors() {
let v = values("a[href] { color: red; } [data-state] { color: blue; }");
assert!(v.is_empty(), "got {v:?}");
}
#[test]
fn reports_one_based_line() {
let uses = scan_tailwind_arbitrary_values("\n\n<i class=\"h-[3px]\"></i>");
assert_eq!(uses.len(), 1);
assert_eq!(uses[0].line, 3);
}
#[test]
fn captures_utility_prefix_not_variant() {
let v = values(r#"<a class="hover:w-[20px]">x</a>"#);
assert_eq!(v, vec!["w-[20px]"]);
}
}