use crate::cursor;
use crate::extractor::machine::Span;
use candidate_machine::CandidateMachine;
use css_variable_machine::CssVariableMachine;
use machine::{Machine, MachineState};
use std::fmt;
pub mod arbitrary_property_machine;
pub mod arbitrary_value_machine;
pub mod arbitrary_variable_machine;
mod boundary;
pub mod bracket_stack;
pub mod candidate_machine;
pub mod css_variable_machine;
pub mod machine;
pub mod modifier_machine;
pub mod named_utility_machine;
pub mod named_variant_machine;
pub mod pre_processors;
pub mod string_machine;
pub mod utility_machine;
pub mod variant_machine;
#[derive(Debug)]
pub enum Extracted<'a> {
Candidate(&'a [u8]),
CssVariable(&'a [u8]),
}
impl fmt::Display for Extracted<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Extracted::Candidate(candidate) => {
write!(f, "Candidate({})", std::str::from_utf8(candidate).unwrap())
}
Extracted::CssVariable(candidate) => {
write!(
f,
"CssVariable({})",
std::str::from_utf8(candidate).unwrap()
)
}
}
}
}
#[derive(Debug)]
pub struct Extractor<'a> {
cursor: cursor::Cursor<'a>,
css_variable_machine: CssVariableMachine,
candidate_machine: CandidateMachine,
}
impl<'a> Extractor<'a> {
pub fn new(input: &'a [u8]) -> Self {
Self {
cursor: cursor::Cursor::new(input),
css_variable_machine: Default::default(),
candidate_machine: Default::default(),
}
}
pub fn extract(&mut self) -> Vec<Extracted<'a>> {
let mut in_flight_spans: Vec<Span> = Vec::with_capacity(15);
let mut extracted = Vec::with_capacity(100);
let len = self.cursor.input.len();
{
let cursor = &mut self.cursor.clone();
while cursor.pos < len {
if cursor.curr.is_ascii_whitespace() {
cursor.advance();
continue;
}
if let MachineState::Done(span) = self.css_variable_machine.next(cursor) {
extracted.push(Extracted::CssVariable(span.slice(self.cursor.input)));
}
cursor.advance();
}
}
{
let cursor = &mut self.cursor.clone();
while cursor.pos < len {
if cursor.curr.is_ascii_whitespace() {
cursor.advance();
continue;
}
let before = cursor.pos;
match self.candidate_machine.next(cursor) {
MachineState::Done(span) => {
in_flight_spans.push(span);
extract_sub_candidates(before..span.start, cursor, &mut in_flight_spans);
}
MachineState::Idle => {
extract_sub_candidates(
before..cursor.pos.min(cursor.input.len()),
cursor,
&mut in_flight_spans,
);
}
}
cursor.advance();
}
if !in_flight_spans.is_empty() {
extracted.extend(
drop_covered_spans(in_flight_spans)
.iter()
.map(|span| Extracted::Candidate(span.slice(self.cursor.input))),
);
}
}
extracted
}
pub fn extract_variables_from_css(&mut self) -> Vec<Extracted<'a>> {
let mut extracted = Vec::with_capacity(100);
let len = self.cursor.input.len();
let cursor = &mut self.cursor.clone();
while cursor.pos < len {
if cursor.curr.is_ascii_whitespace() {
cursor.advance();
continue;
}
if let MachineState::Done(span) = self.css_variable_machine.next(cursor) {
if span.start < 4 {
cursor.advance();
continue;
}
let slice_before = Span::new(span.start - 4, span.start - 1);
if !slice_before.slice(self.cursor.input).starts_with(b"var(") {
cursor.advance();
continue;
}
extracted.push(Extracted::CssVariable(span.slice(self.cursor.input)));
}
cursor.advance();
}
extracted
}
}
#[inline(always)]
fn extract_sub_candidates(
range: std::ops::Range<usize>,
cursor: &cursor::Cursor<'_>,
in_flight_spans: &mut Vec<Span>,
) {
let end = range.end;
for i in range {
if cursor.input[i] == b'[' {
let mut cursor = cursor.clone();
cursor.move_to(i + 1);
let mut machine = CandidateMachine::default();
while cursor.pos < end {
if let MachineState::Done(span) = machine.next(&mut cursor) {
in_flight_spans.push(span);
}
cursor.advance();
}
}
}
}
fn drop_covered_spans(mut spans: Vec<Span>) -> Vec<Span> {
if spans.len() <= 1 {
return spans;
}
spans.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
let mut result = Vec::with_capacity(spans.len());
let mut max_end = None;
for span in spans {
if max_end.is_none_or(|end| span.end > end) {
result.push(span);
max_end = Some(span.end);
}
}
result
}
#[cfg(test)]
mod tests {
use super::{Extracted, Extractor};
use crate::throughput::Throughput;
use pretty_assertions::assert_eq;
use std::hint::black_box;
fn pre_process_input(input: &str, extension: &str) -> String {
let input = crate::scanner::pre_process_input(input.as_bytes(), extension);
String::from_utf8(input).unwrap()
}
fn extract_sorted_candidates(input: &str) -> Vec<&str> {
let mut machine = Extractor::new(input.as_bytes());
let mut actual = machine
.extract()
.iter()
.filter_map(|x| match x {
Extracted::Candidate(candidate) => std::str::from_utf8(candidate).ok(),
Extracted::CssVariable(_) => None,
})
.collect::<Vec<_>>();
actual.sort();
actual
}
fn extract_sorted_css_variables(input: &str) -> Vec<&str> {
let mut machine = Extractor::new(input.as_bytes());
let mut actual = machine
.extract()
.iter()
.filter_map(|x| match x {
Extracted::Candidate(_) => None,
Extracted::CssVariable(bytes) => std::str::from_utf8(bytes).ok(),
})
.collect::<Vec<_>>();
actual.sort();
actual
}
fn assert_extract_sorted_candidates(input: &str, expected: Vec<&str>) {
let mut actual = extract_sorted_candidates(input);
actual.sort();
actual.dedup();
let mut expected = expected;
expected.sort();
expected.dedup();
if actual != expected {
dbg!(&input);
}
assert_eq!(actual, expected);
}
fn assert_extract_candidates_contains(input: &str, expected: Vec<&str>) {
let actual = extract_sorted_candidates(input);
let mut missing = vec![];
for item in &expected {
if !actual.contains(item) {
missing.push(item);
}
}
if !missing.is_empty() {
dbg!(&actual, &missing);
panic!("Missing some items");
}
}
fn assert_extract_sorted_css_variables(input: &str, expected: Vec<&str>) {
let actual = extract_sorted_css_variables(input);
let mut expected = expected;
expected.sort();
if actual != expected {
dbg!(&input);
}
assert_eq!(actual, expected);
}
#[test]
#[ignore]
fn test_extract_performance() {
if true {
let iterations = 50_000;
let input = include_bytes!("../fixtures/example.html");
let throughput = Throughput::compute(iterations, input.len(), || {
let mut extractor = Extractor::new(input);
_ = black_box(extractor.extract());
});
eprintln!("Extractor throughput: {:}", throughput);
let mut extractor = Extractor::new(input);
let start = std::time::Instant::now();
_ = black_box(extractor.extract().len());
let end = start.elapsed();
eprintln!("Extractor took: {:?}", end);
todo!();
}
}
#[test]
fn test_candidates_extraction() {
for (input, expected) in [
("flex", vec!["flex"]),
("a", vec!["a"]),
("hover:flex", vec!["hover:flex"]),
("flex block", vec!["flex", "block"]),
("items-center", vec!["items-center"]),
("items--center", vec!["items--center"]),
("px-2.5", vec!["px-2.5"]),
("[color:red]", vec!["[color:red]"]),
("![color:red]", vec!["![color:red]"]),
("[color:red]!", vec!["[color:red]!"]),
("[color:red]/20", vec!["[color:red]/20"]),
("![color:red]/20", vec!["![color:red]/20"]),
("[color:red]/20!", vec!["[color:red]/20!"]),
(
r#"<div class="flex items-center px-2.5 bg-[#0088cc] text-(--my-color)"></div>"#,
vec![
"class",
"flex",
"items-center",
"px-2.5",
"bg-[#0088cc]",
"text-(--my-color)",
],
),
(r#"["flex"]"#, vec!["flex"]),
(r#"["p-2.5"]"#, vec!["p-2.5"]),
(r#"["flex","p-2.5"]"#, vec!["flex", "p-2.5"]),
(r#"["flex", "p-2.5"]"#, vec!["flex", "p-2.5"]),
(
r#"[CssClass("[&:hover]:flex",'italic')]"#,
vec!["[&:hover]:flex", "italic"],
),
(
r#"["flex",["italic",["underline"]]]"#,
vec!["flex", "italic", "underline"],
),
(r#"[:is(italic):is(underline)]"#, vec![]),
(
r#"[:is(italic):is(underline)]:flex"#,
vec!["[:is(italic):is(underline)]:flex"],
),
(r#"[:div {:class ["p-2""#, vec!["p-2"]),
(r#" "text-green"]}"#, vec!["text-green"]),
(r#"[:div.p-2]"#, vec!["p-2"]),
(
"[&>[data-slot=icon]:last-child]:right-2.5",
vec!["[&>[data-slot=icon]:last-child]:right-2.5"],
),
(
"sm:[&>[data-slot=icon]:last-child]:right-2.5",
vec!["sm:[&>[data-slot=icon]:last-child]:right-2.5"],
),
("{ underline: true }", vec!["underline", "true"]),
(
r#" <CheckIcon className={clsx('h-4 w-4', { invisible: index !== 0 })} />"#,
vec!["className", "h-4", "w-4", "invisible", "index"],
),
(
"{ 'hover:underline': true }",
vec!["hover:underline", "true"],
),
("!flex!", vec![]),
("bg-red-500!/20", vec![]),
("<div", vec![]),
("</div>", vec![]),
("<div></div>", vec![]),
("bg-red-500/20/20", vec![]),
("bg-red-500/20/[20%]", vec![]),
("bg-red-500/20/(--my-opacity)", vec![]),
("bg-red-500/[20%]/20", vec![]),
("bg-red-500/[20%]/[20%]", vec![]),
("bg-red-500/[20%]/(--my-opacity)", vec![]),
("bg-red-500/(--my-opacity)/20", vec![]),
("bg-red-500/(--my-opacity)/[20%]", vec![]),
("bg-red-500/(--my-opacity)/(--my-opacity)", vec![]),
("bg-[red]-[blue]", vec![]),
("bg-[red][blue]", vec![]),
("bg-[red]-(--my-color)", vec![]),
("bg-[red](--my-color)", vec![]),
("flex!block", vec![]),
("[foo]/bar:flex", vec![]),
("_blank", vec![]),
("hover:_blank", vec![]),
("hover:focus:_blank", vec![]),
] {
assert_extract_sorted_candidates(input, expected);
}
}
#[test]
fn test_extractor_extract_candidates() {
for (input, expected) in [
("flex", vec!["flex"]),
("@container", vec!["@container"]),
("a", vec!["a"]),
("items-center", vec!["items-center"]),
("px-2.5", vec!["px-2.5"]),
("hover:flex", vec!["hover:flex"]),
("[color:red]", vec!["[color:red]"]),
("![color:red]", vec!["![color:red]"]),
("[color:red]!", vec!["[color:red]!"]),
("[color:red]/20", vec!["[color:red]/20"]),
("![color:red]/20", vec!["![color:red]/20"]),
("[color:red]/20!", vec!["[color:red]/20!"]),
("hover:focus:flex", vec!["hover:focus:flex"]),
("{ underline: true }", vec!["underline", "true"]),
(
"[&>[data-slot=icon]:last-child]:right-2.5",
vec!["[&>[data-slot=icon]:last-child]:right-2.5"],
),
(
"[&>[data-slot=icon]:last-child]:sm:right-2.5",
vec!["[&>[data-slot=icon]:last-child]:sm:right-2.5"],
),
(
"sm:[&>[data-slot=icon]:last-child]:right-2.5",
vec!["sm:[&>[data-slot=icon]:last-child]:right-2.5"],
),
("flex!block", vec![]),
] {
for (wrapper, additional) in [
("{}", vec![]),
(" {}", vec![]),
(" {}", vec![]),
(" {}", vec![]),
("{} ", vec![]),
("{} ", vec![]),
("{} ", vec![]),
(" {} ", vec![]),
("'{}'", vec![]),
("fn('{}')", vec![]),
("fn1(fn2('{}'))", vec![]),
(r#"<div class="{}"></div>"#, vec!["class"]),
(r#"<div class="{} foo"></div>"#, vec!["class", "foo"]),
(r#"<div class="foo {}"></div>"#, vec!["class", "foo"]),
(
r#"<div class="foo {} bar"></div>"#,
vec!["class", "foo", "bar"],
),
(r#"let classes = '{}';"#, vec!["let", "classes"]),
(
r#"let classes = { '{}': true };"#,
vec!["let", "classes", "true"],
),
(r#"let classes = {'{}':true};"#, vec!["let", "classes"]),
(
r#"let classes = { primary: '{}' };"#,
vec!["let", "classes", "primary"],
),
(r#"let classes = {primary:'{}'};"#, vec!["let", "classes"]),
(r#"let classes = ['{}'];"#, vec!["let", "classes"]),
] {
let input = &wrapper.replace("{}", input);
let mut expected = expected.clone();
expected.extend(additional);
assert_extract_sorted_candidates(input, expected);
}
}
}
#[test]
fn test_ruby_syntax() {
for (input, expected) in [
(r#"%w[flex]"#, vec!["flex"]),
(r#"%w[flex items-center]"#, vec!["flex", "items-center"]),
(r#"%w[[color:red]]"#, vec!["[color:red]"]),
(
r#"def call = tag.span "Foo", class: %w[rounded-full h-0.75 w-0.75]"#,
vec![
"def",
"call",
"span",
"class",
"rounded-full",
"h-0.75",
"w-0.75",
],
),
(
r#"def call = tag.span "Foo", class: %w[rounded-full w-0.75 h-0.75]"#,
vec![
"def",
"call",
"span",
"class",
"rounded-full",
"h-0.75",
"w-0.75",
],
),
(
r#"def call = tag.span "Foo", class: %w[w-0.75 h-0.75 rounded-full]"#,
vec![
"def",
"call",
"span",
"class",
"rounded-full",
"h-0.75",
"w-0.75",
],
),
(r#"%w[flex]"#, vec!["flex"]),
(r#"%w(flex)"#, vec!["flex"]),
] {
assert_extract_sorted_candidates(&pre_process_input(input, "rb"), expected);
}
}
#[test]
fn test_pug_syntax() {
for (input, expected) in [
(
".bg-green-300.2xl:bg-red-500",
vec!["bg-green-300", "2xl:bg-red-500"],
),
(
".2xl:bg-red-500.bg-green-300",
vec!["bg-green-300", "2xl:bg-red-500"],
),
(".xl:col-span-2.xl:pr-8", vec!["xl:col-span-2", "xl:pr-8"]),
(
"div.2xl:bg-red-500.bg-green-300",
vec!["div", "bg-green-300", "2xl:bg-red-500"],
),
(
r#"input(type="checkbox" class="px-2.5")"#,
vec!["input", "type", "checkbox", "class", "px-2.5"],
),
] {
assert_extract_sorted_candidates(&pre_process_input(input, "pug"), expected);
}
}
#[test]
fn test_slim_syntax() {
for (input, expected) in [
(
".bg-blue-100.2xl:bg-red-100",
vec!["bg-blue-100", "2xl:bg-red-100"],
),
(
".2xl:bg-red-100.bg-blue-100",
vec!["bg-blue-100", "2xl:bg-red-100"],
),
(r#"div class="px-2.5""#, vec!["div", "class", "px-2.5"]),
] {
assert_extract_sorted_candidates(&pre_process_input(input, "slim"), expected);
}
}
#[test]
fn test_csharp_syntax() {
for (input, expected) in [
(r#"[CssClass("gap-y-4")]"#, vec!["gap-y-4"]),
(r#"[CssClass("hover:gap-y-4")]"#, vec!["hover:gap-y-4"]),
(
r#"[CssClass("gap-y-4")]:flex"#,
vec![r#"[CssClass("gap-y-4")]:flex"#],
),
] {
assert_extract_sorted_candidates(input, expected);
}
}
#[test]
fn test_clojure_syntax() {
for (input, expected) in [
(r#"[:div {:class ["p-2"]}"#, vec!["p-2"]),
(
r#"[:div {:class ["p-2" "text-green"]}"#,
vec!["p-2", "text-green"],
),
(r#"[:div {:class ["p-2""#, vec!["p-2"]),
(r#" "text-green"]}"#, vec!["text-green"]),
(r#"[:div.p-2]"#, vec!["p-2"]),
(r#"[:div {:class ["p-2"]}"#, vec!["p-2"]),
(
r#"[:div {:class ["p-2" "text-green"]}"#,
vec!["p-2", "text-green"],
),
] {
assert_extract_candidates_contains(&pre_process_input(input, "cljs"), expected);
}
}
#[test]
fn test_gleam_syntax() {
for (input, expected) in [
(r#"html.div([attribute.class("py-10")], [])"#, vec!["py-10"]),
(
r#"html.div([attribute.class("hover:py-10")], [])"#,
vec!["hover:py-10"],
),
] {
assert_extract_sorted_candidates(input, expected);
}
}
#[test]
fn test_overlapping_candidates() {
for (input, expected) in [
(
r#"[CssClass("[&:hover]:flex",'italic')]"#,
vec!["[&:hover]:flex", "italic"],
),
(
r#"["flex",["italic",["underline"]]]"#,
vec!["flex", "italic", "underline"],
),
(r#"[:is(italic):is(underline)]"#, vec![]),
(
r#"[:is(italic):is(underline)]:flex"#,
vec!["[:is(italic):is(underline)]:flex"],
),
] {
assert_extract_sorted_candidates(input, expected);
}
}
#[test]
fn test_js_syntax() {
for (input, expected) in [
(
r#"let classes = 'flex items-center';"#,
vec!["let", "classes", "flex", "items-center"],
),
(
r#"let classes = ['flex', 'items-center'];"#,
vec!["let", "classes", "flex", "items-center"],
),
(
r#"let classes = ['flex','items-center'];"#,
vec!["let", "classes", "flex", "items-center"],
),
(
r#"let classes = something('flex');"#,
vec!["let", "classes", "flex"],
),
(
r#"let classes = [wrapper('flex')]"#,
vec!["let", "classes", "flex"],
),
] {
assert_extract_sorted_candidates(input, expected);
}
}
#[test]
fn test_js_tuple_syntax() {
for (input, expected) in [
(
r#"["h-[calc(100vh-(var(--spacing)*8)-(var(--spacing)*14))]",\n true],"#,
vec![
"h-[calc(100vh-(var(--spacing)*8)-(var(--spacing)*14))]",
"true",
],
),
(
r#"["h-[calc(100vh-(var(--spacing)*8)-(var(--spacing)*14))]", true],"#,
vec![
"h-[calc(100vh-(var(--spacing)*8)-(var(--spacing)*14))]",
"true",
],
),
(
r#"[ "h-[calc(100vh-(var(--spacing)*8)-(var(--spacing)*14))]",\n true],"#,
vec![
"h-[calc(100vh-(var(--spacing)*8)-(var(--spacing)*14))]",
"true",
],
),
] {
assert_extract_sorted_candidates(input, expected);
}
}
#[test]
fn test_angular_binding_syntax() {
for (input, expected) in [
(
r#"'[ngClass]': `{"variant": variant(), "no-variant": !variant() }`"#,
vec!["variant", "no-variant"],
),
(
r#"'[class]': '"bg-gradient-to-b px-6 py-3 rounded-3xl from-5%"',"#,
vec!["bg-gradient-to-b", "px-6", "py-3", "rounded-3xl", "from-5%"],
),
(
r#"'[class.from-secondary-light]': `variant() === 'secondary'`,"#,
vec!["from-secondary-light", "secondary"],
),
(
r#"'[class.to-secondary]': `variant() === 'secondary'`,"#,
vec!["to-secondary", "secondary"],
),
(
r#"'[class.from-5%]': `variant() === 'secondary'`,"#,
vec!["from-5%", "secondary"],
),
(
r#"'[class.from-1%]': `variant() === 'primary'`,"#,
vec!["from-1%", "primary"],
),
(
r#"'[class.from-light-blue]': `variant() === 'primary'`,"#,
vec!["from-light-blue", "primary"],
),
(
r#"'[class.to-primary]': `variant() === 'primary'`,"#,
vec!["to-primary", "primary"],
),
] {
assert_extract_sorted_candidates(input, expected);
}
}
#[test]
fn test_angular_binding_attribute_syntax() {
for (input, expected) in [
(
r#"<div [class.underline]="bool"></div>"#,
vec!["underline", "bool"],
),
(
r#"<div [class.px-2.5]="bool"></div>"#,
vec!["px-2.5", "bool"],
),
(
r#"<div [class.bg-[#0088cc]]="bool"></div>"#,
vec!["bg-[#0088cc]", "bool"],
),
] {
assert_extract_sorted_candidates(input, expected);
}
}
#[test]
fn test_svelte_shorthand_syntax() {
assert_extract_sorted_candidates(
&pre_process_input(r#"<div class:px-4='condition'></div>"#, "svelte"),
vec!["class", "px-4", "condition"],
);
assert_extract_sorted_candidates(
&pre_process_input(r#"<div class:flex='condition'></div>"#, "svelte"),
vec!["class", "flex", "condition"],
);
}
#[test]
fn test_twig_syntax() {
assert_extract_candidates_contains(
r#"<div class="flex items-center mx-4{% if session.isValid %}{% else %} h-4{% endif %}"></div>"#,
vec!["flex", "items-center", "mx-4", "h-4"],
);
assert_extract_candidates_contains(
r#"<div class="{% if true %}flex{% else %}block{% endif %}">"#,
vec!["flex", "block"],
);
}
#[test]
fn test_haml_syntax() {
for (input, expected) in [
(
"%body.flex.flex-col.items-center.justify-center",
vec!["flex", "flex-col", "items-center", "justify-center"],
),
(
".text-slate-500.xl:text-gray-500",
vec!["text-slate-500", "xl:text-gray-500"],
),
(
".text-black.xl:text-red-500{ data: { tailwind: 'css' } }",
vec!["text-black", "xl:text-red-500"],
),
(
".text-green-500.xl:text-blue-500(data-sidebar)",
vec!["text-green-500", "xl:text-blue-500"],
),
(
".text-yellow-500.xl:text-purple-500= 'Element with interpreted content'",
vec!["text-yellow-500", "xl:text-purple-500"],
),
(
".text-orange-500.xl:text-pink-500{ class: 'bg-slate-100' }",
vec!["text-orange-500", "xl:text-pink-500", "bg-slate-100"],
),
(
".text-teal-500.xl:text-indigo-500[@user, :greeting]",
vec!["text-teal-500", "xl:text-indigo-500"],
),
(
".text-lime-500.xl:text-emerald-500#root",
vec!["text-lime-500", "xl:text-emerald-500"],
),
] {
assert_extract_candidates_contains(&pre_process_input(input, "haml"), expected);
}
}
#[test]
fn test_fluid_template_syntax() {
let input = r#"
<f:variable name="bgStyle">
<f:switch expression="{data.layout}">
<f:case value="0">from-blue-900 to-cyan-200</f:case>
<f:case value="1">from-cyan-600 to-teal-200</f:case>
<f:defaultCase>from-blue-300 to-cyan-100</f:defaultCase>
</f:switch>
</f:variable>
"#;
assert_extract_candidates_contains(
input,
vec![
"from-blue-900",
"to-cyan-200",
"from-cyan-600",
"to-teal-200",
"from-blue-300",
"to-cyan-100",
],
);
}
#[test]
fn test_arbitrary_container_queries_syntax() {
assert_extract_sorted_candidates(
r#"<div class="@md:flex @max-md:flex @-[36rem]:flex @[36rem]:flex"></div>"#,
vec![
"class",
"@md:flex",
"@max-md:flex",
"@-[36rem]:flex",
"@[36rem]:flex",
],
);
}
#[test]
fn test_js_embedded_in_php_syntax() {
let input = r#"
@php
if ($sidebarIsStashable) {
$attributes = $attributes->merge([
'x-init' => '$el.classList.add(\'-translate-x-full\'); $el.classList.add(\'transition-transform\')',
]);
}
@endphp
"#;
assert_extract_candidates_contains(
input,
vec!["-translate-x-full", "transition-transform"],
);
let input = r#"
@php
if ($sidebarIsStashable) {
$attributes = $attributes->merge([
'x-init' => "\$el.classList.add('-translate-x-full'); \$el.classList.add('transition-transform')",
]);
}
@endphp
"#;
assert_extract_candidates_contains(
input,
vec!["-translate-x-full", "transition-transform"],
);
}
#[test]
fn test_classes_containing_number_followed_by_dash_or_underscore() {
assert_extract_sorted_candidates(
r#"<div class="text-Title1_Strong"></div>"#,
vec!["class", "text-Title1_Strong"],
);
}
#[test]
fn test_arbitrary_variable_with_data_type() {
assert_extract_sorted_candidates(
r#"<div class="bg-(length:--my-length) bg-[color:var(--my-color)]"></div>"#,
vec![
"class",
"bg-(length:--my-length)",
"bg-[color:var(--my-color)]",
],
);
}
#[test]
fn test_leptos_rs_view_class_colon_syntax() {
for (input, expected) in [
(r#"<div class:px-6=true>"#, vec!["class", "px-6"]),
(
r#"view! { <div class:px-6=true> }"#,
vec!["class", "px-6", "view!"],
),
] {
assert_extract_sorted_candidates(&pre_process_input(input, "svelte"), expected);
}
}
#[test]
fn test_extract_css_variables() {
for (input, expected) in [
("--foo", vec!["--foo"]),
("--my-variable", vec!["--my-variable"]),
(
"calc(var(--first) + var(--second))",
vec!["--first", "--second"],
),
(r#"--spacing-1\/2"#, vec![r#"--spacing-1\/2"#]),
(r#"--my-\ variable"#, vec![]),
] {
for wrapper in [
"{}",
" {}",
"{} ",
" {} ",
"'{}'",
"fn({})",
"fn1(fn2({}))",
r#"<div class="{}"></div>"#,
r#"<div class="{} foo"></div>"#,
r#"<div class="foo {}"></div>"#,
r#"<div class="foo {} bar"></div>"#,
r#"<div class="[{}:red]"></div>"#,
r#"let classes = '{}';"#,
r#"let classes = { '{}': true };"#,
r#"let classes = {'{}':true};"#,
r#"let classes = { primary: '{}' };"#,
r#"let classes = {primary:'{}'};"#,
r#"let classes = ['{}'];"#,
] {
let input = wrapper.replace("{}", input);
assert_extract_sorted_css_variables(&input, expected.clone());
}
}
}
}