Skip to main content

hypen_tailwind_parse/
colors.rs

1//! Color utilities: text, background, border colors
2
3use crate::parser::CssProperty;
4use std::collections::HashMap;
5use std::sync::LazyLock;
6
7/// Tailwind color palette
8pub static COLORS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
9    let mut m = HashMap::new();
10
11    // Basic colors
12    m.insert("transparent", "transparent");
13    m.insert("current", "currentColor");
14    m.insert("black", "#000000");
15    m.insert("white", "#ffffff");
16
17    // Slate
18    m.insert("slate-50", "#f8fafc");
19    m.insert("slate-100", "#f1f5f9");
20    m.insert("slate-200", "#e2e8f0");
21    m.insert("slate-300", "#cbd5e1");
22    m.insert("slate-400", "#94a3b8");
23    m.insert("slate-500", "#64748b");
24    m.insert("slate-600", "#475569");
25    m.insert("slate-700", "#334155");
26    m.insert("slate-800", "#1e293b");
27    m.insert("slate-900", "#0f172a");
28    m.insert("slate-950", "#020617");
29
30    // Gray
31    m.insert("gray-50", "#f9fafb");
32    m.insert("gray-100", "#f3f4f6");
33    m.insert("gray-200", "#e5e7eb");
34    m.insert("gray-300", "#d1d5db");
35    m.insert("gray-400", "#9ca3af");
36    m.insert("gray-500", "#6b7280");
37    m.insert("gray-600", "#4b5563");
38    m.insert("gray-700", "#374151");
39    m.insert("gray-800", "#1f2937");
40    m.insert("gray-900", "#111827");
41    m.insert("gray-950", "#030712");
42
43    // Zinc
44    m.insert("zinc-50", "#fafafa");
45    m.insert("zinc-100", "#f4f4f5");
46    m.insert("zinc-200", "#e4e4e7");
47    m.insert("zinc-300", "#d4d4d8");
48    m.insert("zinc-400", "#a1a1aa");
49    m.insert("zinc-500", "#71717a");
50    m.insert("zinc-600", "#52525b");
51    m.insert("zinc-700", "#3f3f46");
52    m.insert("zinc-800", "#27272a");
53    m.insert("zinc-900", "#18181b");
54    m.insert("zinc-950", "#09090b");
55
56    // Red
57    m.insert("red-50", "#fef2f2");
58    m.insert("red-100", "#fee2e2");
59    m.insert("red-200", "#fecaca");
60    m.insert("red-300", "#fca5a5");
61    m.insert("red-400", "#f87171");
62    m.insert("red-500", "#ef4444");
63    m.insert("red-600", "#dc2626");
64    m.insert("red-700", "#b91c1c");
65    m.insert("red-800", "#991b1b");
66    m.insert("red-900", "#7f1d1d");
67    m.insert("red-950", "#450a0a");
68
69    // Orange
70    m.insert("orange-50", "#fff7ed");
71    m.insert("orange-100", "#ffedd5");
72    m.insert("orange-200", "#fed7aa");
73    m.insert("orange-300", "#fdba74");
74    m.insert("orange-400", "#fb923c");
75    m.insert("orange-500", "#f97316");
76    m.insert("orange-600", "#ea580c");
77    m.insert("orange-700", "#c2410c");
78    m.insert("orange-800", "#9a3412");
79    m.insert("orange-900", "#7c2d12");
80    m.insert("orange-950", "#431407");
81
82    // Yellow
83    m.insert("yellow-50", "#fefce8");
84    m.insert("yellow-100", "#fef9c3");
85    m.insert("yellow-200", "#fef08a");
86    m.insert("yellow-300", "#fde047");
87    m.insert("yellow-400", "#facc15");
88    m.insert("yellow-500", "#eab308");
89    m.insert("yellow-600", "#ca8a04");
90    m.insert("yellow-700", "#a16207");
91    m.insert("yellow-800", "#854d0e");
92    m.insert("yellow-900", "#713f12");
93    m.insert("yellow-950", "#422006");
94
95    // Green
96    m.insert("green-50", "#f0fdf4");
97    m.insert("green-100", "#dcfce7");
98    m.insert("green-200", "#bbf7d0");
99    m.insert("green-300", "#86efac");
100    m.insert("green-400", "#4ade80");
101    m.insert("green-500", "#22c55e");
102    m.insert("green-600", "#16a34a");
103    m.insert("green-700", "#15803d");
104    m.insert("green-800", "#166534");
105    m.insert("green-900", "#14532d");
106    m.insert("green-950", "#052e16");
107
108    // Blue
109    m.insert("blue-50", "#eff6ff");
110    m.insert("blue-100", "#dbeafe");
111    m.insert("blue-200", "#bfdbfe");
112    m.insert("blue-300", "#93c5fd");
113    m.insert("blue-400", "#60a5fa");
114    m.insert("blue-500", "#3b82f6");
115    m.insert("blue-600", "#2563eb");
116    m.insert("blue-700", "#1d4ed8");
117    m.insert("blue-800", "#1e40af");
118    m.insert("blue-900", "#1e3a8a");
119    m.insert("blue-950", "#172554");
120
121    // Indigo
122    m.insert("indigo-50", "#eef2ff");
123    m.insert("indigo-100", "#e0e7ff");
124    m.insert("indigo-200", "#c7d2fe");
125    m.insert("indigo-300", "#a5b4fc");
126    m.insert("indigo-400", "#818cf8");
127    m.insert("indigo-500", "#6366f1");
128    m.insert("indigo-600", "#4f46e5");
129    m.insert("indigo-700", "#4338ca");
130    m.insert("indigo-800", "#3730a3");
131    m.insert("indigo-900", "#312e81");
132    m.insert("indigo-950", "#1e1b4b");
133
134    // Purple
135    m.insert("purple-50", "#faf5ff");
136    m.insert("purple-100", "#f3e8ff");
137    m.insert("purple-200", "#e9d5ff");
138    m.insert("purple-300", "#d8b4fe");
139    m.insert("purple-400", "#c084fc");
140    m.insert("purple-500", "#a855f7");
141    m.insert("purple-600", "#9333ea");
142    m.insert("purple-700", "#7e22ce");
143    m.insert("purple-800", "#6b21a8");
144    m.insert("purple-900", "#581c87");
145    m.insert("purple-950", "#3b0764");
146
147    // Pink
148    m.insert("pink-50", "#fdf2f8");
149    m.insert("pink-100", "#fce7f3");
150    m.insert("pink-200", "#fbcfe8");
151    m.insert("pink-300", "#f9a8d4");
152    m.insert("pink-400", "#f472b6");
153    m.insert("pink-500", "#ec4899");
154    m.insert("pink-600", "#db2777");
155    m.insert("pink-700", "#be185d");
156    m.insert("pink-800", "#9d174d");
157    m.insert("pink-900", "#831843");
158    m.insert("pink-950", "#500724");
159
160    // Rose
161    m.insert("rose-50", "#fff1f2");
162    m.insert("rose-100", "#ffe4e6");
163    m.insert("rose-200", "#fecdd3");
164    m.insert("rose-300", "#fda4af");
165    m.insert("rose-400", "#fb7185");
166    m.insert("rose-500", "#f43f5e");
167    m.insert("rose-600", "#e11d48");
168    m.insert("rose-700", "#be123c");
169    m.insert("rose-800", "#9f1239");
170    m.insert("rose-900", "#881337");
171    m.insert("rose-950", "#4c0519");
172
173    // Amber
174    m.insert("amber-50", "#fffbeb");
175    m.insert("amber-100", "#fef3c7");
176    m.insert("amber-200", "#fde68a");
177    m.insert("amber-300", "#fcd34d");
178    m.insert("amber-400", "#fbbf24");
179    m.insert("amber-500", "#f59e0b");
180    m.insert("amber-600", "#d97706");
181    m.insert("amber-700", "#b45309");
182    m.insert("amber-800", "#92400e");
183    m.insert("amber-900", "#78350f");
184    m.insert("amber-950", "#451a03");
185
186    // Lime
187    m.insert("lime-50", "#f7fee7");
188    m.insert("lime-100", "#ecfccb");
189    m.insert("lime-200", "#d9f99d");
190    m.insert("lime-300", "#bef264");
191    m.insert("lime-400", "#a3e635");
192    m.insert("lime-500", "#84cc16");
193    m.insert("lime-600", "#65a30d");
194    m.insert("lime-700", "#4d7c0f");
195    m.insert("lime-800", "#3f6212");
196    m.insert("lime-900", "#365314");
197    m.insert("lime-950", "#1a2e05");
198
199    // Emerald
200    m.insert("emerald-50", "#ecfdf5");
201    m.insert("emerald-100", "#d1fae5");
202    m.insert("emerald-200", "#a7f3d0");
203    m.insert("emerald-300", "#6ee7b7");
204    m.insert("emerald-400", "#34d399");
205    m.insert("emerald-500", "#10b981");
206    m.insert("emerald-600", "#059669");
207    m.insert("emerald-700", "#047857");
208    m.insert("emerald-800", "#065f46");
209    m.insert("emerald-900", "#064e3b");
210    m.insert("emerald-950", "#022c22");
211
212    // Teal
213    m.insert("teal-50", "#f0fdfa");
214    m.insert("teal-100", "#ccfbf1");
215    m.insert("teal-200", "#99f6e4");
216    m.insert("teal-300", "#5eead4");
217    m.insert("teal-400", "#2dd4bf");
218    m.insert("teal-500", "#14b8a6");
219    m.insert("teal-600", "#0d9488");
220    m.insert("teal-700", "#0f766e");
221    m.insert("teal-800", "#115e59");
222    m.insert("teal-900", "#134e4a");
223    m.insert("teal-950", "#042f2e");
224
225    // Cyan
226    m.insert("cyan-50", "#ecfeff");
227    m.insert("cyan-100", "#cffafe");
228    m.insert("cyan-200", "#a5f3fc");
229    m.insert("cyan-300", "#67e8f9");
230    m.insert("cyan-400", "#22d3ee");
231    m.insert("cyan-500", "#06b6d4");
232    m.insert("cyan-600", "#0891b2");
233    m.insert("cyan-700", "#0e7490");
234    m.insert("cyan-800", "#155e75");
235    m.insert("cyan-900", "#164e63");
236    m.insert("cyan-950", "#083344");
237
238    // Sky
239    m.insert("sky-50", "#f0f9ff");
240    m.insert("sky-100", "#e0f2fe");
241    m.insert("sky-200", "#bae6fd");
242    m.insert("sky-300", "#7dd3fc");
243    m.insert("sky-400", "#38bdf8");
244    m.insert("sky-500", "#0ea5e9");
245    m.insert("sky-600", "#0284c7");
246    m.insert("sky-700", "#0369a1");
247    m.insert("sky-800", "#075985");
248    m.insert("sky-900", "#0c4a6e");
249    m.insert("sky-950", "#082f49");
250
251    // Violet
252    m.insert("violet-50", "#f5f3ff");
253    m.insert("violet-100", "#ede9fe");
254    m.insert("violet-200", "#ddd6fe");
255    m.insert("violet-300", "#c4b5fd");
256    m.insert("violet-400", "#a78bfa");
257    m.insert("violet-500", "#8b5cf6");
258    m.insert("violet-600", "#7c3aed");
259    m.insert("violet-700", "#6d28d9");
260    m.insert("violet-800", "#5b21b6");
261    m.insert("violet-900", "#4c1d95");
262    m.insert("violet-950", "#2e1065");
263
264    // Fuchsia
265    m.insert("fuchsia-50", "#fdf4ff");
266    m.insert("fuchsia-100", "#fae8ff");
267    m.insert("fuchsia-200", "#f5d0fe");
268    m.insert("fuchsia-300", "#f0abfc");
269    m.insert("fuchsia-400", "#e879f9");
270    m.insert("fuchsia-500", "#d946ef");
271    m.insert("fuchsia-600", "#c026d3");
272    m.insert("fuchsia-700", "#a21caf");
273    m.insert("fuchsia-800", "#86198f");
274    m.insert("fuchsia-900", "#701a75");
275    m.insert("fuchsia-950", "#4a044e");
276
277    // Stone
278    m.insert("stone-50", "#fafaf9");
279    m.insert("stone-100", "#f5f5f4");
280    m.insert("stone-200", "#e7e5e4");
281    m.insert("stone-300", "#d6d3d1");
282    m.insert("stone-400", "#a8a29e");
283    m.insert("stone-500", "#78716c");
284    m.insert("stone-600", "#57534e");
285    m.insert("stone-700", "#44403c");
286    m.insert("stone-800", "#292524");
287    m.insert("stone-900", "#1c1917");
288    m.insert("stone-950", "#0c0a09");
289
290    // Neutral
291    m.insert("neutral-50", "#fafafa");
292    m.insert("neutral-100", "#f5f5f5");
293    m.insert("neutral-200", "#e5e5e5");
294    m.insert("neutral-300", "#d4d4d4");
295    m.insert("neutral-400", "#a3a3a3");
296    m.insert("neutral-500", "#737373");
297    m.insert("neutral-600", "#525252");
298    m.insert("neutral-700", "#404040");
299    m.insert("neutral-800", "#262626");
300    m.insert("neutral-900", "#171717");
301    m.insert("neutral-950", "#0a0a0a");
302
303    m
304});
305
306fn get_color(name: &str) -> Option<&'static str> {
307    COLORS.get(name).copied().or_else(|| {
308        // Bare color names (e.g. "red", "blue") default to the -500 shade,
309        // matching Tailwind CSS behavior.
310        let with_shade = format!("{}-500", name);
311        COLORS.get(with_shade.as_str()).copied()
312    })
313}
314
315pub fn parse(utility: &str) -> Option<Vec<CssProperty>> {
316    // Text color: text-blue-500
317    if let Some(color_name) = utility.strip_prefix("text-") {
318        // Skip typography classes like text-xl
319        if !color_name
320            .chars()
321            .next()
322            .map(|c| c.is_ascii_digit())
323            .unwrap_or(false)
324            && ![
325                "xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl",
326                "9xl",
327            ]
328            .contains(&color_name)
329            && !["left", "center", "right", "justify", "start", "end"].contains(&color_name)
330        {
331            if let Some(color) = get_color(color_name) {
332                return Some(vec![CssProperty::new("color", color)]);
333            }
334        }
335    }
336
337    // Background color: bg-white
338    if let Some(color_name) = utility.strip_prefix("bg-") {
339        if let Some(color) = get_color(color_name) {
340            return Some(vec![CssProperty::new("background-color", color)]);
341        }
342    }
343
344    // Border color: border-gray-300
345    if let Some(color_name) = utility.strip_prefix("border-") {
346        // Skip border width classes like border-2
347        if !color_name
348            .chars()
349            .next()
350            .map(|c| c.is_ascii_digit())
351            .unwrap_or(false)
352        {
353            if let Some(color) = get_color(color_name) {
354                return Some(vec![CssProperty::new("border-color", color)]);
355            }
356        }
357    }
358
359    // Opacity: opacity-50
360    if let Some(val) = utility.strip_prefix("opacity-") {
361        let opacity = match val {
362            "0" => "0",
363            "5" => "0.05",
364            "10" => "0.1",
365            "15" => "0.15",
366            "20" => "0.2",
367            "25" => "0.25",
368            "30" => "0.3",
369            "35" => "0.35",
370            "40" => "0.4",
371            "45" => "0.45",
372            "50" => "0.5",
373            "55" => "0.55",
374            "60" => "0.6",
375            "65" => "0.65",
376            "70" => "0.7",
377            "75" => "0.75",
378            "80" => "0.8",
379            "85" => "0.85",
380            "90" => "0.9",
381            "95" => "0.95",
382            "100" => "1",
383            _ => return None,
384        };
385        return Some(vec![CssProperty::new("opacity", opacity)]);
386    }
387
388    None
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn test_text_color() {
397        let props = parse("text-blue-500").unwrap();
398        assert_eq!(props[0].property, "color");
399        assert_eq!(props[0].value, "#3b82f6");
400    }
401
402    #[test]
403    fn test_bg_color() {
404        let props = parse("bg-white").unwrap();
405        assert_eq!(props[0].property, "background-color");
406        assert_eq!(props[0].value, "#ffffff");
407    }
408
409    #[test]
410    fn test_border_color() {
411        let props = parse("border-gray-300").unwrap();
412        assert_eq!(props[0].property, "border-color");
413        assert_eq!(props[0].value, "#d1d5db");
414    }
415
416    #[test]
417    fn test_opacity() {
418        let props = parse("opacity-50").unwrap();
419        assert_eq!(props[0].property, "opacity");
420        assert_eq!(props[0].value, "0.5");
421    }
422
423    #[test]
424    fn test_bare_color_names_default_to_500() {
425        // Bare color names should resolve to the -500 shade
426        let props = parse("bg-red").unwrap();
427        assert_eq!(props[0].property, "background-color");
428        assert_eq!(props[0].value, "#ef4444"); // red-500
429
430        let props = parse("bg-blue").unwrap();
431        assert_eq!(props[0].value, "#3b82f6"); // blue-500
432
433        let props = parse("text-green").unwrap();
434        assert_eq!(props[0].property, "color");
435        assert_eq!(props[0].value, "#22c55e"); // green-500
436
437        let props = parse("border-purple").unwrap();
438        assert_eq!(props[0].property, "border-color");
439        assert_eq!(props[0].value, "#a855f7"); // purple-500
440    }
441
442    #[test]
443    fn test_new_colors() {
444        // Cyan
445        let props = parse("text-cyan-500").unwrap();
446        assert_eq!(props[0].value, "#06b6d4");
447
448        // Teal
449        let props = parse("bg-teal-600").unwrap();
450        assert_eq!(props[0].value, "#0d9488");
451
452        // Emerald
453        let props = parse("border-emerald-400").unwrap();
454        assert_eq!(props[0].value, "#34d399");
455
456        // Rose
457        let props = parse("text-rose-500").unwrap();
458        assert_eq!(props[0].value, "#f43f5e");
459
460        // Amber
461        let props = parse("bg-amber-300").unwrap();
462        assert_eq!(props[0].value, "#fcd34d");
463
464        // Sky
465        let props = parse("text-sky-500").unwrap();
466        assert_eq!(props[0].value, "#0ea5e9");
467
468        // Violet
469        let props = parse("bg-violet-600").unwrap();
470        assert_eq!(props[0].value, "#7c3aed");
471
472        // Fuchsia
473        let props = parse("text-fuchsia-500").unwrap();
474        assert_eq!(props[0].value, "#d946ef");
475
476        // Lime
477        let props = parse("bg-lime-400").unwrap();
478        assert_eq!(props[0].value, "#a3e635");
479
480        // Stone
481        let props = parse("text-stone-700").unwrap();
482        assert_eq!(props[0].value, "#44403c");
483
484        // Neutral
485        let props = parse("bg-neutral-800").unwrap();
486        assert_eq!(props[0].value, "#262626");
487    }
488}