#[derive(Debug, Clone)]
pub(crate) struct TypedClassSlot {
pub end: usize,
pub options: Vec<String>,
}
pub(crate) fn parse_typed_class_slot(input: &str, start: usize) -> Option<TypedClassSlot> {
if start >= input.len() || !input[start..].starts_with("@{") {
return None;
}
let body_start = start + 2;
let body_end = find_slot_body_end(input, body_start)?;
let body = &input[body_start..body_end];
let parts = split_slot_segments(body);
if parts.len() < 2 {
return None;
}
let path = parts[0].trim();
if path.is_empty() {
return None;
}
let mut options: Vec<String> = Vec::new();
for segment in parts.iter().skip(1) {
let seg = segment.trim();
if seg.is_empty() {
return None;
}
if let Some(rhs) = seg.strip_prefix("default=") {
let value = parse_slot_option(rhs.trim())?;
if !options
.iter()
.any(|existing| normalize_value(existing) == normalize_value(&value))
{
options.push(value);
}
continue;
}
let value = parse_slot_option(seg)?;
options.push(value);
}
if options.is_empty() {
return None;
}
Some(TypedClassSlot {
end: body_end + 1,
options,
})
}
pub(crate) fn extract_tailwind_tokens_from_class_value(class_value: &str) -> Vec<String> {
let mut out = Vec::new();
for pattern in split_class_patterns(class_value) {
let expanded = expand_class_pattern(&pattern);
for candidate in expanded {
for token in candidate.split_whitespace() {
let token = token.trim();
if !token.is_empty() {
out.push(token.to_string());
}
}
}
}
out
}
fn expand_class_pattern(pattern: &str) -> Vec<String> {
let Some(start) = pattern.find("@{") else {
return vec![pattern.to_string()];
};
let Some(slot) = parse_typed_class_slot(pattern, start) else {
return vec![pattern.to_string()];
};
let prefix = &pattern[..start];
let suffix = &pattern[slot.end..];
let suffix_expanded = expand_class_pattern(suffix);
let mut out = Vec::new();
for option in &slot.options {
for tail in &suffix_expanded {
out.push(format!("{prefix}{option}{tail}"));
}
}
out
}
fn split_class_patterns(class_value: &str) -> Vec<String> {
let mut out = Vec::new();
let mut buf = String::new();
let mut cursor = 0usize;
while cursor < class_value.len() {
if let Some(slot) = parse_typed_class_slot(class_value, cursor) {
buf.push_str(&class_value[cursor..slot.end]);
cursor = slot.end;
continue;
}
let Some(ch) = class_value[cursor..].chars().next() else {
break;
};
if ch.is_whitespace() {
if !buf.trim().is_empty() {
out.push(buf.trim().to_string());
}
buf.clear();
cursor += ch.len_utf8();
continue;
}
let next = cursor + ch.len_utf8();
buf.push_str(&class_value[cursor..next]);
cursor = next;
}
if !buf.trim().is_empty() {
out.push(buf.trim().to_string());
}
out
}
fn find_slot_body_end(input: &str, body_start: usize) -> Option<usize> {
let mut idx = body_start;
let mut bracket_depth = 0usize;
while idx < input.len() {
let ch = input[idx..].chars().next()?;
match ch {
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.saturating_sub(1),
'}' if bracket_depth == 0 => return Some(idx),
_ => {}
}
idx += ch.len_utf8();
}
None
}
fn split_slot_segments(body: &str) -> Vec<String> {
let mut out = Vec::new();
let mut buf = String::new();
let mut bracket_depth = 0usize;
let mut idx = 0usize;
while idx < body.len() {
let Some(ch) = body[idx..].chars().next() else {
break;
};
match ch {
'[' => {
bracket_depth += 1;
buf.push(ch);
}
']' => {
bracket_depth = bracket_depth.saturating_sub(1);
buf.push(ch);
}
'|' if bracket_depth == 0 => {
out.push(buf.trim().to_string());
buf.clear();
}
_ => buf.push(ch),
}
idx += ch.len_utf8();
}
if !buf.trim().is_empty() {
out.push(buf.trim().to_string());
}
out
}
fn parse_slot_option(raw: &str) -> Option<String> {
if raw.starts_with('[') && raw.ends_with(']') && raw.len() >= 2 {
let inner = raw[1..raw.len() - 1].trim();
if inner.is_empty() {
return None;
}
return Some(inner.to_string());
}
let token = raw.trim();
if token.is_empty() {
return None;
}
Some(token.to_string())
}
fn normalize_value(value: &str) -> String {
value.split_whitespace().collect::<Vec<_>>().join(" ")
}