hypen_tailwind_parse/
parser.rs1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::backgrounds;
7use crate::borders;
8use crate::colors;
9use crate::effects;
10use crate::interactivity;
11use crate::layout;
12use crate::misc;
13use crate::sizing;
14use crate::spacing;
15use crate::tables;
16use crate::transforms;
17use crate::typography;
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct CssProperty {
22 pub property: String,
23 pub value: String,
24}
25
26impl CssProperty {
27 pub fn new(property: &str, value: &str) -> Self {
28 Self {
29 property: property.to_string(),
30 value: value.to_string(),
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum Variant {
38 None,
40 Responsive(String),
42 State(String),
44 Dark,
46 Combined(Vec<Variant>),
48}
49
50impl Variant {
51 pub fn is_none(&self) -> bool {
52 matches!(self, Variant::None)
53 }
54}
55
56#[derive(Debug, Clone, Default, Serialize, Deserialize)]
58pub struct TailwindOutput {
59 pub base: Vec<CssProperty>,
61 pub variants: HashMap<String, Vec<CssProperty>>,
63}
64
65impl TailwindOutput {
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn add(&mut self, variant: Variant, property: CssProperty) {
71 match variant {
72 Variant::None => self.base.push(property),
73 Variant::Responsive(bp) => {
74 self.variants.entry(format!("@{}", bp)).or_default().push(property);
75 }
76 Variant::State(state) => {
77 self.variants.entry(format!(":{}", state)).or_default().push(property);
78 }
79 Variant::Dark => {
80 self.variants.entry("dark".to_string()).or_default().push(property);
81 }
82 Variant::Combined(variants) => {
83 let key = variants.iter().map(|v| match v {
85 Variant::Responsive(bp) => format!("@{}", bp),
86 Variant::State(state) => format!(":{}", state),
87 Variant::Dark => "dark".to_string(),
88 _ => String::new(),
89 }).collect::<Vec<_>>().join("");
90 self.variants.entry(key).or_default().push(property);
91 }
92 }
93 }
94
95 pub fn to_props(&self) -> HashMap<String, String> {
98 let mut props = HashMap::new();
99
100 for prop in &self.base {
101 props.insert(prop.property.clone(), prop.value.clone());
102 }
103
104 for (variant, properties) in &self.variants {
105 for prop in properties {
106 let key = format!("{}{}", prop.property, variant);
107 props.insert(key, prop.value.clone());
108 }
109 }
110
111 props
112 }
113}
114
115pub fn parse_classes(input: &str) -> TailwindOutput {
117 let mut output = TailwindOutput::new();
118
119 for class in input.split_whitespace() {
120 if let Some((variant, properties)) = parse_class(class) {
121 for prop in properties {
122 output.add(variant.clone(), prop);
123 }
124 }
125 }
126
127 output
128}
129
130pub fn parse_class(class: &str) -> Option<(Variant, Vec<CssProperty>)> {
133 let (variant, utility) = extract_variant(class);
135
136 let properties = parse_utility(utility)?;
138
139 Some((variant, properties))
140}
141
142fn extract_variant(class: &str) -> (Variant, &str) {
144 let parts: Vec<&str> = class.split(':').collect();
145
146 if parts.len() == 1 {
147 return (Variant::None, class);
148 }
149
150 let utility = parts.last().unwrap();
151 let variant_parts = &parts[..parts.len() - 1];
152
153 if variant_parts.len() == 1 {
154 let v = parse_variant_name(variant_parts[0]);
155 (v, utility)
156 } else {
157 let variants: Vec<Variant> = variant_parts.iter()
158 .map(|p| parse_variant_name(p))
159 .filter(|v| !v.is_none())
160 .collect();
161
162 if variants.is_empty() {
163 (Variant::None, utility)
164 } else if variants.len() == 1 {
165 (variants.into_iter().next().unwrap(), utility)
166 } else {
167 (Variant::Combined(variants), utility)
168 }
169 }
170}
171
172fn parse_variant_name(name: &str) -> Variant {
173 match name {
174 "sm" => Variant::Responsive("sm".to_string()),
176 "md" => Variant::Responsive("md".to_string()),
177 "lg" => Variant::Responsive("lg".to_string()),
178 "xl" => Variant::Responsive("xl".to_string()),
179 "2xl" => Variant::Responsive("2xl".to_string()),
180 "hover" => Variant::State("hover".to_string()),
182 "focus" => Variant::State("focus".to_string()),
183 "active" => Variant::State("active".to_string()),
184 "disabled" => Variant::State("disabled".to_string()),
185 "visited" => Variant::State("visited".to_string()),
186 "first" => Variant::State("first-child".to_string()),
187 "last" => Variant::State("last-child".to_string()),
188 "odd" => Variant::State("nth-child(odd)".to_string()),
189 "even" => Variant::State("nth-child(even)".to_string()),
190 "dark" => Variant::Dark,
192 _ => Variant::None,
193 }
194}
195
196fn parse_utility(utility: &str) -> Option<Vec<CssProperty>> {
198 None
200 .or_else(|| spacing::parse(utility))
201 .or_else(|| sizing::parse(utility))
202 .or_else(|| colors::parse(utility))
203 .or_else(|| typography::parse(utility))
204 .or_else(|| layout::parse(utility))
205 .or_else(|| borders::parse(utility))
206 .or_else(|| effects::parse(utility))
207 .or_else(|| transforms::parse(utility))
208 .or_else(|| backgrounds::parse(utility))
209 .or_else(|| tables::parse(utility))
210 .or_else(|| interactivity::parse(utility))
211 .or_else(|| misc::parse(utility))
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_parse_simple_class() {
220 let output = parse_classes("p-4");
221 assert_eq!(output.base.len(), 1);
222 assert_eq!(output.base[0].property, "padding");
223 assert_eq!(output.base[0].value, "1rem");
224 }
225
226 #[test]
227 fn test_parse_with_variant() {
228 let output = parse_classes("md:p-4");
229 assert!(output.base.is_empty());
230 assert!(output.variants.contains_key("@md"));
231 let md_props = output.variants.get("@md").unwrap();
232 assert_eq!(md_props[0].property, "padding");
233 }
234
235 #[test]
236 fn test_parse_multiple_classes() {
237 let output = parse_classes("p-4 m-2 text-blue-500");
238 assert_eq!(output.base.len(), 3);
239 }
240
241 #[test]
242 fn test_parse_hover_variant() {
243 let output = parse_classes("hover:bg-white");
244 assert!(output.variants.contains_key(":hover"));
245 }
246
247 #[test]
248 fn test_to_props() {
249 let output = parse_classes("p-4 md:p-8");
250 let props = output.to_props();
251 assert_eq!(props.get("padding"), Some(&"1rem".to_string()));
252 assert_eq!(props.get("padding@md"), Some(&"2rem".to_string()));
253 }
254}