Skip to main content

morph_ir/
tailwind.rs

1//! Tailwind CSS resolver — static map + arbitrary values
2//! Mirrors Python's `morph/style/tailwind.py` (500+ utilities, 3 tiers)
3
4use std::collections::HashMap;
5
6pub struct TailwindResolver;
7
8impl TailwindResolver {
9    pub fn new() -> Self { Self }
10
11    pub fn resolve(&self, class: &str) -> HashMap<String, String> {
12        let mut out = HashMap::new();
13        if let Some(mapped) = static_map(class) {
14            for (k, v) in mapped { out.insert(k.to_string(), v.to_string()); }
15            return out;
16        }
17        if let Some(mapped) = resolve_negative(class) {
18            for (k, v) in mapped { out.insert(k.to_string(), v.to_string()); }
19            return out;
20        }
21        if let Some(pairs) = parse_arbitrary(class) {
22            for (k, v) in pairs { out.insert(k, v); }
23            return out;
24        }
25        out
26    }
27
28    pub fn resolve_many(&self, class_name: &str) -> HashMap<String, String> {
29        let mut merged = HashMap::new();
30        for cls in class_name.split_whitespace() {
31            for (k, v) in self.resolve(cls) {
32                merged.insert(k, v);
33            }
34        }
35        merged
36    }
37}
38
39fn static_map(class: &str) -> Option<Vec<(&'static str, &'static str)>> {
40    Some(match class {
41        "absolute" => vec![("position", "absolute")],
42        "bg-black" => vec![("background-color", "#000000")],
43        "bg-blue-500" => vec![("background-color", "#3b82f6")],
44        "bg-blue-600" => vec![("background-color", "#2563eb")],
45        "bg-gray-100" => vec![("background-color", "#f3f4f6")],
46        "bg-gray-200" => vec![("background-color", "#e5e7eb")],
47        "bg-gray-300" => vec![("background-color", "#d1d5db")],
48        "bg-gray-400" => vec![("background-color", "#9ca3af")],
49        "bg-gray-50" => vec![("background-color", "#f9fafb")],
50        "bg-gray-500" => vec![("background-color", "#6b7280")],
51        "bg-gray-600" => vec![("background-color", "#4b5563")],
52        "bg-gray-700" => vec![("background-color", "#374151")],
53        "bg-gray-800" => vec![("background-color", "#1f2937")],
54        "bg-gray-900" => vec![("background-color", "#111827")],
55        "bg-green-500" => vec![("background-color", "#22c55e")],
56        "bg-indigo-500" => vec![("background-color", "#6366f1")],
57        "bg-indigo-600" => vec![("background-color", "#4f46e5")],
58        "bg-pink-500" => vec![("background-color", "#ec4899")],
59        "bg-purple-500" => vec![("background-color", "#a855f7")],
60        "bg-purple-600" => vec![("background-color", "#9333ea")],
61        "bg-red-500" => vec![("background-color", "#ef4444")],
62        "bg-transparent" => vec![("background-color", "transparent")],
63        "bg-white" => vec![("background-color", "#ffffff")],
64        "bg-yellow-500" => vec![("background-color", "#eab308")],
65        "block" => vec![("display", "block")],
66        "cursor-default" => vec![("cursor", "default")],
67        "cursor-not-allowed" => vec![("cursor", "not-allowed")],
68        "cursor-pointer" => vec![("cursor", "pointer")],
69        "fixed" => vec![("position", "fixed")],
70        "flex" => vec![("display", "flex")],
71        "flex-1" => vec![("flex-basis", "0%"), ("flex-grow", "1"), ("flex-shrink", "1")],
72        "flex-auto" => vec![("flex-basis", "auto"), ("flex-grow", "1"), ("flex-shrink", "1")],
73        "flex-col" => vec![("flex-direction", "column")],
74        "flex-col-reverse" => vec![("flex-direction", "column-reverse")],
75        "flex-none" => vec![("flex-basis", "auto"), ("flex-grow", "0"), ("flex-shrink", "0")],
76        "flex-nowrap" => vec![("flex-wrap", "nowrap")],
77        "flex-row" => vec![("flex-direction", "row")],
78        "flex-row-reverse" => vec![("flex-direction", "row-reverse")],
79        "flex-wrap" => vec![("flex-wrap", "wrap")],
80        "font-bold" => vec![("font-weight", "700")],
81        "font-extrabold" => vec![("font-weight", "800")],
82        "font-light" => vec![("font-weight", "300")],
83        "font-medium" => vec![("font-weight", "500")],
84        "font-normal" => vec![("font-weight", "400")],
85        "font-semibold" => vec![("font-weight", "600")],
86        "font-thin" => vec![("font-weight", "100")],
87        "gap-0" => vec![("gap", "0px")],
88        "gap-1" => vec![("gap", "4px")],
89        "gap-2" => vec![("gap", "8px")],
90        "gap-3" => vec![("gap", "12px")],
91        "gap-4" => vec![("gap", "16px")],
92        "gap-6" => vec![("gap", "24px")],
93        "gap-8" => vec![("gap", "32px")],
94        "h-0" => vec![("height", "0px")],
95        "h-1" => vec![("height", "4px")],
96        "h-10" => vec![("height", "40px")],
97        "h-12" => vec![("height", "48px")],
98        "h-16" => vec![("height", "64px")],
99        "h-2" => vec![("height", "8px")],
100        "h-20" => vec![("height", "80px")],
101        "h-4" => vec![("height", "16px")],
102        "h-6" => vec![("height", "24px")],
103        "h-8" => vec![("height", "32px")],
104        "h-auto" => vec![("height", "auto")],
105        "h-full" => vec![("height", "100%")],
106        "h-screen" => vec![("height", "100vh")],
107        "hidden" => vec![("display", "none")],
108        "inline" => vec![("display", "inline")],
109        "inline-block" => vec![("display", "inline-block")],
110        "inline-flex" => vec![("display", "inline-flex")],
111        "items-center" => vec![("align-items", "center")],
112        "items-end" => vec![("align-items", "flex-end")],
113        "items-start" => vec![("align-items", "flex-start")],
114        "items-stretch" => vec![("align-items", "stretch")],
115        "justify-around" => vec![("justify-content", "space-around")],
116        "justify-between" => vec![("justify-content", "space-between")],
117        "justify-center" => vec![("justify-content", "center")],
118        "justify-end" => vec![("justify-content", "flex-end")],
119        "justify-start" => vec![("justify-content", "flex-start")],
120        "m-0" => vec![("margin", "0px")],
121        "m-1" => vec![("margin", "4px")],
122        "m-2" => vec![("margin", "8px")],
123        "m-4" => vec![("margin", "16px")],
124        "m-6" => vec![("margin", "24px")],
125        "m-8" => vec![("margin", "32px")],
126        "m-auto" => vec![("margin", "auto")],
127        "mb-2" => vec![("margin-bottom", "8px")],
128        "mb-4" => vec![("margin-bottom", "16px")],
129        "mb-8" => vec![("margin-bottom", "32px")],
130        "min-h-0" => vec![("min-height", "0px")],
131        "min-w-0" => vec![("min-width", "0px")],
132        "ml-2" => vec![("margin-left", "8px")],
133        "ml-4" => vec![("margin-left", "16px")],
134        "mr-2" => vec![("margin-right", "8px")],
135        "mr-4" => vec![("margin-right", "16px")],
136        "mt-1" => vec![("margin-top", "4px")],
137        "mt-2" => vec![("margin-top", "8px")],
138        "mt-4" => vec![("margin-top", "16px")],
139        "mt-8" => vec![("margin-top", "32px")],
140        "mx-4" => vec![("margin-left", "16px"), ("margin-right", "16px")],
141        "mx-auto" => vec![("margin-left", "auto"), ("margin-right", "auto")],
142        "my-4" => vec![("margin-bottom", "16px"), ("margin-top", "16px")],
143        "opacity-0" => vec![("opacity", "0")],
144        "opacity-100" => vec![("opacity", "1")],
145        "opacity-25" => vec![("opacity", "0.25")],
146        "opacity-50" => vec![("opacity", "0.5")],
147        "opacity-75" => vec![("opacity", "0.75")],
148        "overflow-auto" => vec![("overflow", "auto")],
149        "overflow-hidden" => vec![("overflow", "hidden")],
150        "overflow-scroll" => vec![("overflow", "scroll")],
151        "overflow-visible" => vec![("overflow", "visible")],
152        "p-0" => vec![("padding", "0px")],
153        "p-1" => vec![("padding", "4px")],
154        "p-10" => vec![("padding", "40px")],
155        "p-12" => vec![("padding", "48px")],
156        "p-2" => vec![("padding", "8px")],
157        "p-3" => vec![("padding", "12px")],
158        "p-4" => vec![("padding", "16px")],
159        "p-5" => vec![("padding", "20px")],
160        "p-6" => vec![("padding", "24px")],
161        "p-8" => vec![("padding", "32px")],
162        "pb-4" => vec![("padding-bottom", "16px")],
163        "pl-4" => vec![("padding-left", "16px")],
164        "pr-4" => vec![("padding-right", "16px")],
165        "pt-4" => vec![("padding-top", "16px")],
166        "px-1" => vec![("padding-left", "4px"), ("padding-right", "4px")],
167        "px-2" => vec![("padding-left", "8px"), ("padding-right", "8px")],
168        "px-3" => vec![("padding-left", "12px"), ("padding-right", "12px")],
169        "px-4" => vec![("padding-left", "16px"), ("padding-right", "16px")],
170        "px-6" => vec![("padding-left", "24px"), ("padding-right", "24px")],
171        "px-8" => vec![("padding-left", "32px"), ("padding-right", "32px")],
172        "py-1" => vec![("padding-bottom", "4px"), ("padding-top", "4px")],
173        "py-2" => vec![("padding-bottom", "8px"), ("padding-top", "8px")],
174        "py-3" => vec![("padding-bottom", "12px"), ("padding-top", "12px")],
175        "py-4" => vec![("padding-bottom", "16px"), ("padding-top", "16px")],
176        "py-6" => vec![("padding-bottom", "24px"), ("padding-top", "24px")],
177        "relative" => vec![("position", "relative")],
178        "rotate-0" => vec![("transform", "rotate(0deg)")],
179        "rotate-1" => vec![("transform", "rotate(1deg)")],
180        "rotate-12" => vec![("transform", "rotate(12deg)")],
181        "rotate-180" => vec![("transform", "rotate(180deg)")],
182        "rotate-2" => vec![("transform", "rotate(2deg)")],
183        "rotate-3" => vec![("transform", "rotate(3deg)")],
184        "rotate-45" => vec![("transform", "rotate(45deg)")],
185        "rotate-6" => vec![("transform", "rotate(6deg)")],
186        "rotate-90" => vec![("transform", "rotate(90deg)")],
187        "rounded" => vec![("border-radius", "4px")],
188        "rounded-2xl" => vec![("border-radius", "16px")],
189        "rounded-3xl" => vec![("border-radius", "24px")],
190        "rounded-full" => vec![("border-radius", "9999px")],
191        "rounded-lg" => vec![("border-radius", "8px")],
192        "rounded-md" => vec![("border-radius", "6px")],
193        "rounded-none" => vec![("border-radius", "0px")],
194        "rounded-sm" => vec![("border-radius", "2px")],
195        "rounded-xl" => vec![("border-radius", "12px")],
196        "scale-0" => vec![("transform", "scale(0)")],
197        "scale-100" => vec![("transform", "scale(1)")],
198        "scale-105" => vec![("transform", "scale(1.05)")],
199        "scale-110" => vec![("transform", "scale(1.1)")],
200        "scale-125" => vec![("transform", "scale(1.25)")],
201        "scale-150" => vec![("transform", "scale(1.5)")],
202        "scale-50" => vec![("transform", "scale(0.5)")],
203        "scale-75" => vec![("transform", "scale(0.75)")],
204        "scale-90" => vec![("transform", "scale(0.9)")],
205        "scale-95" => vec![("transform", "scale(0.95)")],
206        "scale-x-0" => vec![("transform", "scaleX(0)")],
207        "scale-x-100" => vec![("transform", "scaleX(1)")],
208        "scale-x-105" => vec![("transform", "scaleX(1.05)")],
209        "scale-x-110" => vec![("transform", "scaleX(1.1)")],
210        "scale-x-125" => vec![("transform", "scaleX(1.25)")],
211        "scale-x-150" => vec![("transform", "scaleX(1.5)")],
212        "scale-x-50" => vec![("transform", "scaleX(0.5)")],
213        "scale-x-75" => vec![("transform", "scaleX(0.75)")],
214        "scale-x-90" => vec![("transform", "scaleX(0.9)")],
215        "scale-x-95" => vec![("transform", "scaleX(0.95)")],
216        "scale-y-0" => vec![("transform", "scaleY(0)")],
217        "scale-y-100" => vec![("transform", "scaleY(1)")],
218        "scale-y-105" => vec![("transform", "scaleY(1.05)")],
219        "scale-y-110" => vec![("transform", "scaleY(1.1)")],
220        "scale-y-125" => vec![("transform", "scaleY(1.25)")],
221        "scale-y-150" => vec![("transform", "scaleY(1.5)")],
222        "scale-y-50" => vec![("transform", "scaleY(0.5)")],
223        "scale-y-75" => vec![("transform", "scaleY(0.75)")],
224        "scale-y-90" => vec![("transform", "scaleY(0.9)")],
225        "scale-y-95" => vec![("transform", "scaleY(0.95)")],
226        "select-all" => vec![("user-select", "all")],
227        "select-none" => vec![("user-select", "none")],
228        "select-text" => vec![("user-select", "text")],
229        "skew-x-0" => vec![("transform", "skewX(0deg)")],
230        "skew-x-1" => vec![("transform", "skewX(1deg)")],
231        "skew-x-12" => vec![("transform", "skewX(12deg)")],
232        "skew-x-2" => vec![("transform", "skewX(2deg)")],
233        "skew-x-3" => vec![("transform", "skewX(3deg)")],
234        "skew-x-6" => vec![("transform", "skewX(6deg)")],
235        "skew-y-0" => vec![("transform", "skewY(0deg)")],
236        "skew-y-1" => vec![("transform", "skewY(1deg)")],
237        "skew-y-12" => vec![("transform", "skewY(12deg)")],
238        "skew-y-2" => vec![("transform", "skewY(2deg)")],
239        "skew-y-3" => vec![("transform", "skewY(3deg)")],
240        "skew-y-6" => vec![("transform", "skewY(6deg)")],
241        "static" => vec![("position", "static")],
242        "sticky" => vec![("position", "sticky")],
243        "text-2xl" => vec![("font-size", "24px"), ("line-height", "32px")],
244        "text-3xl" => vec![("font-size", "30px"), ("line-height", "36px")],
245        "text-4xl" => vec![("font-size", "36px"), ("line-height", "40px")],
246        "text-base" => vec![("font-size", "16px"), ("line-height", "24px")],
247        "text-black" => vec![("color", "#000000")],
248        "text-blue-500" => vec![("color", "#3b82f6")],
249        "text-center" => vec![("text-align", "center")],
250        "text-gray-400" => vec![("color", "#9ca3af")],
251        "text-gray-500" => vec![("color", "#6b7280")],
252        "text-gray-600" => vec![("color", "#4b5563")],
253        "text-gray-700" => vec![("color", "#374151")],
254        "text-gray-800" => vec![("color", "#1f2937")],
255        "text-gray-900" => vec![("color", "#111827")],
256        "text-green-500" => vec![("color", "#22c55e")],
257        "text-left" => vec![("text-align", "left")],
258        "text-lg" => vec![("font-size", "18px"), ("line-height", "28px")],
259        "text-purple-500" => vec![("color", "#a855f7")],
260        "text-red-500" => vec![("color", "#ef4444")],
261        "text-right" => vec![("text-align", "right")],
262        "text-sm" => vec![("font-size", "14px"), ("line-height", "20px")],
263        "text-white" => vec![("color", "#ffffff")],
264        "text-xl" => vec![("font-size", "20px"), ("line-height", "28px")],
265        "text-xs" => vec![("font-size", "12px"), ("line-height", "16px")],
266        "translate-x-0" => vec![("transform", "translateX(0px)")],
267        "translate-x-1" => vec![("transform", "translateX(4px)")],
268        "translate-x-10" => vec![("transform", "translateX(40px)")],
269        "translate-x-12" => vec![("transform", "translateX(48px)")],
270        "translate-x-16" => vec![("transform", "translateX(64px)")],
271        "translate-x-2" => vec![("transform", "translateX(8px)")],
272        "translate-x-20" => vec![("transform", "translateX(80px)")],
273        "translate-x-24" => vec![("transform", "translateX(96px)")],
274        "translate-x-3" => vec![("transform", "translateX(12px)")],
275        "translate-x-32" => vec![("transform", "translateX(128px)")],
276        "translate-x-4" => vec![("transform", "translateX(16px)")],
277        "translate-x-40" => vec![("transform", "translateX(160px)")],
278        "translate-x-48" => vec![("transform", "translateX(192px)")],
279        "translate-x-5" => vec![("transform", "translateX(20px)")],
280        "translate-x-56" => vec![("transform", "translateX(224px)")],
281        "translate-x-6" => vec![("transform", "translateX(24px)")],
282        "translate-x-64" => vec![("transform", "translateX(256px)")],
283        "translate-x-8" => vec![("transform", "translateX(32px)")],
284        "translate-y-0" => vec![("transform", "translateY(0px)")],
285        "translate-y-1" => vec![("transform", "translateY(4px)")],
286        "translate-y-10" => vec![("transform", "translateY(40px)")],
287        "translate-y-12" => vec![("transform", "translateY(48px)")],
288        "translate-y-16" => vec![("transform", "translateY(64px)")],
289        "translate-y-2" => vec![("transform", "translateY(8px)")],
290        "translate-y-20" => vec![("transform", "translateY(80px)")],
291        "translate-y-24" => vec![("transform", "translateY(96px)")],
292        "translate-y-3" => vec![("transform", "translateY(12px)")],
293        "translate-y-32" => vec![("transform", "translateY(128px)")],
294        "translate-y-4" => vec![("transform", "translateY(16px)")],
295        "translate-y-40" => vec![("transform", "translateY(160px)")],
296        "translate-y-48" => vec![("transform", "translateY(192px)")],
297        "translate-y-5" => vec![("transform", "translateY(20px)")],
298        "translate-y-56" => vec![("transform", "translateY(224px)")],
299        "translate-y-6" => vec![("transform", "translateY(24px)")],
300        "translate-y-64" => vec![("transform", "translateY(256px)")],
301        "translate-y-8" => vec![("transform", "translateY(32px)")],
302        "w-0" => vec![("width", "0px")],
303        "w-1" => vec![("width", "4px")],
304        "w-10" => vec![("width", "40px")],
305        "w-12" => vec![("width", "48px")],
306        "w-16" => vec![("width", "64px")],
307        "w-2" => vec![("width", "8px")],
308        "w-20" => vec![("width", "80px")],
309        "w-24" => vec![("width", "96px")],
310        "w-32" => vec![("width", "128px")],
311        "w-4" => vec![("width", "16px")],
312        "w-48" => vec![("width", "192px")],
313        "w-6" => vec![("width", "24px")],
314        "w-64" => vec![("width", "256px")],
315        "w-8" => vec![("width", "32px")],
316        "w-auto" => vec![("width", "auto")],
317        "w-full" => vec![("width", "100%")],
318        "w-screen" => vec![("width", "100vw")],
319        "z-0" => vec![("z-index", "0")],
320        "z-10" => vec![("z-index", "10")],
321        "z-20" => vec![("z-index", "20")],
322        "z-30" => vec![("z-index", "30")],
323        "z-40" => vec![("z-index", "40")],
324        "z-50" => vec![("z-index", "50")],
325        "z-auto" => vec![("z-index", "auto")],
326        _ => return None,
327    })
328}
329
330/// Negative utilities: `-z-10` → z-index -10, `-translate-x-4` →
331/// translateX(-16px), `-rotate-45` → rotate(-45deg). Mirrors Python
332/// `_resolve_one` tier 0 / `_resolve_negative_transform`.
333fn resolve_negative(class: &str) -> Option<Vec<(String, String)>> {
334    let pair = |k: &str, v: String| Some(vec![(k.to_string(), v)]);
335    if let Some(rest) = class.strip_prefix("-z-") {
336        if !rest.is_empty() && rest.trim_start_matches('-').chars().all(|c| c.is_ascii_digit()) {
337            let val: i64 = rest.parse().unwrap_or(0);
338            return pair("z-index", (-val).to_string());
339        }
340        return None;
341    }
342    let body = class.strip_prefix('-')?;
343    let (name, num) = body.rsplit_once('-')?;
344    let n: i64 = num.parse().ok()?;
345    match name {
346        "translate-x" => pair("transform", format!("translateX(-{}px)", n * 4)),
347        "translate-y" => pair("transform", format!("translateY(-{}px)", n * 4)),
348        "rotate" => pair("transform", format!("rotate(-{}deg)", n)),
349        "skew-x" => pair("transform", format!("skewX(-{}deg)", n)),
350        "skew-y" => pair("transform", format!("skewY(-{}deg)", n)),
351        _ => None,
352    }
353}
354
355/// Arbitrary values: `bg-[#ff0000]`, `w-[200px]`, `rotate-[45deg]`,
356/// `-translate-x-[50%]`. Mirrors Python `_resolve_arbitrary` including the
357/// `^([\w-]+)-\[(.+)\]$` anchor and the negation rule.
358fn parse_arbitrary(class: &str) -> Option<Vec<(String, String)>> {
359    let single = |k: &str, v: &str| Some(vec![(k.to_string(), v.to_string())]);
360    let dash = class.find("-[")?;
361    if !class.ends_with(']') {
362        return None;
363    }
364    let prefix = &class[..dash];
365    let value = &class[dash + 2..class.len() - 1];
366    if prefix.is_empty() || value.is_empty() {
367        return None;
368    }
369    if !prefix
370        .trim_start_matches('-')
371        .chars()
372        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
373    {
374        return None;
375    }
376    let (negated, base) = match prefix.strip_prefix('-') {
377        Some(rest) => (true, rest),
378        None => (false, prefix),
379    };
380    if let Some(func) = transform_fn(base) {
381        let val = if negated { negate_value(value) } else { value.to_string() };
382        return single("transform", &format!("{}({})", func, val));
383    }
384    if negated {
385        return None;
386    }
387    // NOTE: `px`/`py` intentionally expand to both axes. Python mapped them
388    // to a single side (padding-left / padding-top); that asymmetry is a bug,
389    // fixed here and mirrored back into morph/style/tailwind.py.
390    match base {
391        "bg" => single("background-color", value),
392        "text" => single("color", value),
393        "w" => single("width", value),
394        "h" => single("height", value),
395        "p" => single("padding", value),
396        "px" => Some(vec![
397            ("padding-left".to_string(), value.to_string()),
398            ("padding-right".to_string(), value.to_string()),
399        ]),
400        "py" => Some(vec![
401            ("padding-top".to_string(), value.to_string()),
402            ("padding-bottom".to_string(), value.to_string()),
403        ]),
404        "m" => single("margin", value),
405        "mt" => single("margin-top", value),
406        "mb" => single("margin-bottom", value),
407        "ml" => single("margin-left", value),
408        "mr" => single("margin-right", value),
409        "gap" => single("gap", value),
410        "rounded" => single("border-radius", value),
411        "opacity" => single("opacity", value),
412        "top" => single("top", value),
413        "bottom" => single("bottom", value),
414        "left" => single("left", value),
415        "right" => single("right", value),
416        "min-w" => single("min-width", value),
417        "min-h" => single("min-height", value),
418        "max-w" => single("max-width", value),
419        "max-h" => single("max-height", value),
420        "z" => single("z-index", value),
421        "font" => single("font-weight", value),
422        _ => None,
423    }
424}
425
426fn transform_fn(name: &str) -> Option<&'static str> {
427    Some(match name {
428        "translate-x" => "translateX",
429        "translate-y" => "translateY",
430        "rotate" => "rotate",
431        "scale-x" => "scaleX",
432        "scale-y" => "scaleY",
433        "scale" => "scale",
434        "skew-x" => "skewX",
435        "skew-y" => "skewY",
436        _ => return None,
437    })
438}
439
440/// Negate an arbitrary transform value: `45deg` → `-45deg`, `-50%` → `50%`.
441fn negate_value(value: &str) -> String {
442    let v = value.trim();
443    if let Some(rest) = v.strip_prefix('-') {
444        return rest.to_string();
445    }
446    if let Some(rest) = v.strip_prefix('+') {
447        return format!("-{}", rest);
448    }
449    format!("-{}", v)
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    fn resolved(class: &str) -> HashMap<String, String> {
457        TailwindResolver::new().resolve(class)
458    }
459
460    #[test]
461    fn static_entries_cover_all_tiers() {
462        // Multi-property entries keep every declaration.
463        let flex = resolved("flex-1");
464        assert_eq!(flex.get("flex-grow").map(String::as_str), Some("1"));
465        assert_eq!(flex.get("flex-shrink").map(String::as_str), Some("1"));
466        assert_eq!(flex.get("flex-basis").map(String::as_str), Some("0%"));
467        // Size ramps carry their line-height companions.
468        let sm = resolved("text-sm");
469        assert_eq!(sm.get("font-size").map(String::as_str), Some("14px"));
470        assert_eq!(sm.get("line-height").map(String::as_str), Some("20px"));
471        // Axis shorthands expand to both sides.
472        let px = resolved("px-4");
473        assert_eq!(px.get("padding-left").map(String::as_str), Some("16px"));
474        assert_eq!(px.get("padding-right").map(String::as_str), Some("16px"));
475        // Later classes win in a list.
476        let merged = TailwindResolver::new().resolve_many("text-white text-black");
477        assert_eq!(merged.get("color").map(String::as_str), Some("#000000"));
478    }
479
480    #[test]
481    fn negative_utilities_negate_numerically() {
482        assert_eq!(resolved("-z-10").get("z-index").map(String::as_str), Some("-10"));
483        assert_eq!(resolved("-z-0").get("z-index").map(String::as_str), Some("0"));
484        assert_eq!(
485            resolved("-translate-x-4").get("transform").map(String::as_str),
486            Some("translateX(-16px)")
487        );
488        assert_eq!(
489            resolved("-rotate-45").get("transform").map(String::as_str),
490            Some("rotate(-45deg)")
491        );
492        assert_eq!(
493            resolved("-skew-y-12").get("transform").map(String::as_str),
494            Some("skewY(-12deg)")
495        );
496        assert!(resolved("-mt-4").is_empty());
497    }
498
499    #[test]
500    fn arbitrary_values_cover_every_prefix() {
501        assert_eq!(
502            resolved("bg-[#ff0000]").get("background-color").map(String::as_str),
503            Some("#ff0000")
504        );
505        assert_eq!(resolved("w-[200px]").get("width").map(String::as_str), Some("200px"));
506        assert_eq!(resolved("text-[red]").get("color").map(String::as_str), Some("red"));
507        assert_eq!(resolved("z-[5]").get("z-index").map(String::as_str), Some("5"));
508        assert_eq!(resolved("font-[700]").get("font-weight").map(String::as_str), Some("700"));
509        assert_eq!(resolved("min-w-[10px]").get("min-width").map(String::as_str), Some("10px"));
510        // px/py expand to both axes (single-side was a ported Python bug).
511        let px = resolved("px-[3px]");
512        assert_eq!(px.get("padding-left").map(String::as_str), Some("3px"));
513        assert_eq!(px.get("padding-right").map(String::as_str), Some("3px"));
514        let py = resolved("py-[3px]");
515        assert_eq!(py.get("padding-top").map(String::as_str), Some("3px"));
516        assert_eq!(py.get("padding-bottom").map(String::as_str), Some("3px"));
517    }
518
519    #[test]
520    fn arbitrary_transforms_support_negation() {
521        assert_eq!(
522            resolved("rotate-[45deg]").get("transform").map(String::as_str),
523            Some("rotate(45deg)")
524        );
525        assert_eq!(
526            resolved("-translate-x-[50%]").get("transform").map(String::as_str),
527            Some("translateX(-50%)")
528        );
529        assert_eq!(
530            resolved("translate-y-[-3px]").get("transform").map(String::as_str),
531            Some("translateY(-3px)")
532        );
533    }
534
535    #[test]
536    fn unknown_and_malformed_classes_resolve_empty() {
537        for cls in ["bogus-class", "bg-", "w-[]", "bg-[unclosed", "-z-", "-rotate-", "px-[]"] {
538            assert!(resolved(cls).is_empty(), "{} should resolve empty", cls);
539        }
540    }
541}