use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::backgrounds;
use crate::borders;
use crate::colors;
use crate::effects;
use crate::interactivity;
use crate::layout;
use crate::misc;
use crate::sizing;
use crate::spacing;
use crate::tables;
use crate::transforms;
use crate::typography;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CssProperty {
pub property: String,
pub value: String,
}
impl CssProperty {
pub fn new(property: &str, value: &str) -> Self {
Self {
property: property.to_string(),
value: value.to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Variant {
None,
Responsive(String),
State(String),
Dark,
Combined(Vec<Variant>),
}
impl Variant {
pub fn is_none(&self) -> bool {
matches!(self, Variant::None)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TailwindOutput {
pub base: Vec<CssProperty>,
pub variants: HashMap<String, Vec<CssProperty>>,
}
impl TailwindOutput {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, variant: Variant, property: CssProperty) {
match variant {
Variant::None => self.base.push(property),
Variant::Responsive(bp) => {
self.variants
.entry(format!("@{}", bp))
.or_default()
.push(property);
}
Variant::State(state) => {
self.variants
.entry(format!(":{}", state))
.or_default()
.push(property);
}
Variant::Dark => {
self.variants
.entry("dark".to_string())
.or_default()
.push(property);
}
Variant::Combined(variants) => {
let key = variants
.iter()
.map(|v| match v {
Variant::Responsive(bp) => format!("@{}", bp),
Variant::State(state) => format!(":{}", state),
Variant::Dark => "dark".to_string(),
_ => String::new(),
})
.collect::<Vec<_>>()
.join("");
self.variants.entry(key).or_default().push(property);
}
}
}
pub fn to_props(&self) -> HashMap<String, String> {
let mut props = HashMap::new();
for prop in &self.base {
props.insert(prop.property.clone(), prop.value.clone());
}
for (variant, properties) in &self.variants {
for prop in properties {
let key = format!("{}{}", prop.property, variant);
props.insert(key, prop.value.clone());
}
}
props
}
}
pub fn parse_classes(input: &str) -> TailwindOutput {
let mut output = TailwindOutput::new();
for class in input.split_whitespace() {
if let Some((variant, properties)) = parse_class(class) {
for prop in properties {
output.add(variant.clone(), prop);
}
}
}
output
}
pub fn parse_class(class: &str) -> Option<(Variant, Vec<CssProperty>)> {
let (variant, utility) = extract_variant(class);
let properties = parse_utility(utility)?;
Some((variant, properties))
}
fn extract_variant(class: &str) -> (Variant, &str) {
let mut parts: Vec<&str> = Vec::new();
let mut start = 0;
let mut bracket_depth: usize = 0;
for (i, ch) in class.char_indices() {
match ch {
'[' => bracket_depth += 1,
']' => bracket_depth = bracket_depth.saturating_sub(1),
':' if bracket_depth == 0 => {
parts.push(&class[start..i]);
start = i + 1;
}
_ => {}
}
}
parts.push(&class[start..]);
if parts.len() == 1 {
return (Variant::None, class);
}
let utility = parts.last().unwrap();
let variant_parts = &parts[..parts.len() - 1];
if variant_parts.len() == 1 {
let v = parse_variant_name(variant_parts[0]);
(v, utility)
} else {
let variants: Vec<Variant> = variant_parts
.iter()
.map(|p| parse_variant_name(p))
.filter(|v| !v.is_none())
.collect();
if variants.is_empty() {
(Variant::None, utility)
} else if variants.len() == 1 {
(variants.into_iter().next().unwrap(), utility)
} else {
(Variant::Combined(variants), utility)
}
}
}
fn parse_variant_name(name: &str) -> Variant {
match name {
"sm" => Variant::Responsive("sm".to_string()),
"md" => Variant::Responsive("md".to_string()),
"lg" => Variant::Responsive("lg".to_string()),
"xl" => Variant::Responsive("xl".to_string()),
"2xl" => Variant::Responsive("2xl".to_string()),
"hover" => Variant::State("hover".to_string()),
"focus" => Variant::State("focus".to_string()),
"focus-within" => Variant::State("focus-within".to_string()),
"focus-visible" => Variant::State("focus-visible".to_string()),
"active" => Variant::State("active".to_string()),
"disabled" => Variant::State("disabled".to_string()),
"visited" => Variant::State("visited".to_string()),
"checked" => Variant::State("checked".to_string()),
"required" => Variant::State("required".to_string()),
"placeholder" => Variant::State(":placeholder".to_string()),
"first" => Variant::State("first-child".to_string()),
"last" => Variant::State("last-child".to_string()),
"only" => Variant::State("only-child".to_string()),
"odd" => Variant::State("nth-child(odd)".to_string()),
"even" => Variant::State("nth-child(even)".to_string()),
"first-of-type" => Variant::State("first-of-type".to_string()),
"last-of-type" => Variant::State("last-of-type".to_string()),
"empty" => Variant::State("empty".to_string()),
"group-hover" => Variant::State("group-hover".to_string()),
"group-focus" => Variant::State("group-focus".to_string()),
"dark" => Variant::Dark,
_ => Variant::None,
}
}
fn parse_utility(utility: &str) -> Option<Vec<CssProperty>> {
None.or_else(|| spacing::parse(utility))
.or_else(|| sizing::parse(utility))
.or_else(|| colors::parse(utility))
.or_else(|| typography::parse(utility))
.or_else(|| layout::parse(utility))
.or_else(|| borders::parse(utility))
.or_else(|| effects::parse(utility))
.or_else(|| transforms::parse(utility))
.or_else(|| backgrounds::parse(utility))
.or_else(|| tables::parse(utility))
.or_else(|| interactivity::parse(utility))
.or_else(|| misc::parse(utility))
.or_else(|| parse_arbitrary(utility))
}
fn parse_arbitrary(utility: &str) -> Option<Vec<CssProperty>> {
let bracket_start = utility.find('[')?;
if !utility.ends_with(']') {
return None;
}
let value = &utility[bracket_start + 1..utility.len() - 1];
if value.is_empty() {
return None;
}
let prefix = &utility[..bracket_start.checked_sub(1)?]; if utility.as_bytes()[bracket_start - 1] != b'-' {
return None;
}
let is_negative = prefix.starts_with('-');
let bare_prefix = if is_negative { &prefix[1..] } else { prefix };
let negated_value;
let neg_val = if is_negative {
negated_value = format!("-{}", value);
negated_value.as_str()
} else {
value
};
None.or_else(|| spacing::parse_arbitrary(prefix, value))
.or_else(|| if is_negative { None } else { sizing::parse_arbitrary(prefix, value) })
.or_else(|| if is_negative { None } else { typography::parse_arbitrary(prefix, value) })
.or_else(|| layout::parse_arbitrary(bare_prefix, neg_val))
.or_else(|| if is_negative { None } else { borders::parse_arbitrary(prefix, value) })
.or_else(|| effects::parse_arbitrary(bare_prefix, neg_val))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_class() {
let output = parse_classes("p-4");
assert_eq!(output.base.len(), 1);
assert_eq!(output.base[0].property, "padding");
assert_eq!(output.base[0].value, "1rem");
}
#[test]
fn test_parse_with_variant() {
let output = parse_classes("md:p-4");
assert!(output.base.is_empty());
assert!(output.variants.contains_key("@md"));
let md_props = output.variants.get("@md").unwrap();
assert_eq!(md_props[0].property, "padding");
}
#[test]
fn test_parse_multiple_classes() {
let output = parse_classes("p-4 m-2 text-blue-500");
assert_eq!(output.base.len(), 3);
}
#[test]
fn test_parse_hover_variant() {
let output = parse_classes("hover:bg-white");
assert!(output.variants.contains_key(":hover"));
}
#[test]
fn test_to_props() {
let output = parse_classes("p-4 md:p-8");
let props = output.to_props();
assert_eq!(props.get("padding"), Some(&"1rem".to_string()));
assert_eq!(props.get("padding@md"), Some(&"2rem".to_string()));
}
}