1use crate::parser::CssProperty;
4use std::collections::HashMap;
5use std::sync::LazyLock;
6
7pub static COLORS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
9 let mut m = HashMap::new();
10
11 m.insert("transparent", "transparent");
13 m.insert("current", "currentColor");
14 m.insert("black", "#000000");
15 m.insert("white", "#ffffff");
16
17 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let with_shade = format!("{}-500", name);
311 COLORS.get(with_shade.as_str()).copied()
312 })
313}
314
315fn resolve_color_with_opacity(name: &str) -> Option<String> {
318 if let Some(slash_pos) = name.rfind('/') {
319 let color_name = &name[..slash_pos];
320 let opacity_str = &name[slash_pos + 1..];
321 let opacity: u32 = opacity_str.parse().ok()?;
322 if opacity > 100 {
323 return None;
324 }
325 let color = get_color(color_name)?;
326 if color.starts_with('#') && color.len() == 7 {
327 let r = u8::from_str_radix(&color[1..3], 16).ok()?;
328 let g = u8::from_str_radix(&color[3..5], 16).ok()?;
329 let b = u8::from_str_radix(&color[5..7], 16).ok()?;
330 let alpha = opacity as f64 / 100.0;
331 Some(format!("rgba({}, {}, {}, {})", r, g, b, alpha))
332 } else {
333 Some(color.to_string())
334 }
335 } else {
336 get_color(name).map(|s| s.to_string())
337 }
338}
339
340pub fn parse(utility: &str) -> Option<Vec<CssProperty>> {
341 if let Some(color_name) = utility.strip_prefix("text-") {
343 if !color_name
345 .chars()
346 .next()
347 .map(|c| c.is_ascii_digit())
348 .unwrap_or(false)
349 && ![
350 "xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl",
351 "9xl",
352 ]
353 .contains(&color_name)
354 && !["left", "center", "right", "justify", "start", "end"].contains(&color_name)
355 {
356 if let Some(color) = resolve_color_with_opacity(color_name) {
357 return Some(vec![CssProperty::new("color", &color)]);
358 }
359 }
360 }
361
362 if let Some(color_name) = utility.strip_prefix("bg-") {
364 if let Some(color) = resolve_color_with_opacity(color_name) {
365 return Some(vec![CssProperty::new("background-color", &color)]);
366 }
367 }
368
369 if let Some(color_name) = utility.strip_prefix("border-") {
371 if !color_name
373 .chars()
374 .next()
375 .map(|c| c.is_ascii_digit())
376 .unwrap_or(false)
377 {
378 if let Some(color) = resolve_color_with_opacity(color_name) {
379 return Some(vec![CssProperty::new("border-color", &color)]);
380 }
381 }
382 }
383
384 if let Some(val) = utility.strip_prefix("opacity-") {
386 let opacity = match val {
387 "0" => "0",
388 "5" => "0.05",
389 "10" => "0.1",
390 "15" => "0.15",
391 "20" => "0.2",
392 "25" => "0.25",
393 "30" => "0.3",
394 "35" => "0.35",
395 "40" => "0.4",
396 "45" => "0.45",
397 "50" => "0.5",
398 "55" => "0.55",
399 "60" => "0.6",
400 "65" => "0.65",
401 "70" => "0.7",
402 "75" => "0.75",
403 "80" => "0.8",
404 "85" => "0.85",
405 "90" => "0.9",
406 "95" => "0.95",
407 "100" => "1",
408 _ => return None,
409 };
410 return Some(vec![CssProperty::new("opacity", opacity)]);
411 }
412
413 None
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn test_text_color() {
422 let props = parse("text-blue-500").unwrap();
423 assert_eq!(props[0].property, "color");
424 assert_eq!(props[0].value, "#3b82f6");
425 }
426
427 #[test]
428 fn test_bg_color() {
429 let props = parse("bg-white").unwrap();
430 assert_eq!(props[0].property, "background-color");
431 assert_eq!(props[0].value, "#ffffff");
432 }
433
434 #[test]
435 fn test_border_color() {
436 let props = parse("border-gray-300").unwrap();
437 assert_eq!(props[0].property, "border-color");
438 assert_eq!(props[0].value, "#d1d5db");
439 }
440
441 #[test]
442 fn test_opacity() {
443 let props = parse("opacity-50").unwrap();
444 assert_eq!(props[0].property, "opacity");
445 assert_eq!(props[0].value, "0.5");
446 }
447
448 #[test]
449 fn test_bare_color_names_default_to_500() {
450 let props = parse("bg-red").unwrap();
452 assert_eq!(props[0].property, "background-color");
453 assert_eq!(props[0].value, "#ef4444"); let props = parse("bg-blue").unwrap();
456 assert_eq!(props[0].value, "#3b82f6"); let props = parse("text-green").unwrap();
459 assert_eq!(props[0].property, "color");
460 assert_eq!(props[0].value, "#22c55e"); let props = parse("border-purple").unwrap();
463 assert_eq!(props[0].property, "border-color");
464 assert_eq!(props[0].value, "#a855f7"); }
466
467 #[test]
468 fn test_new_colors() {
469 let props = parse("text-cyan-500").unwrap();
471 assert_eq!(props[0].value, "#06b6d4");
472
473 let props = parse("bg-teal-600").unwrap();
475 assert_eq!(props[0].value, "#0d9488");
476
477 let props = parse("border-emerald-400").unwrap();
479 assert_eq!(props[0].value, "#34d399");
480
481 let props = parse("text-rose-500").unwrap();
483 assert_eq!(props[0].value, "#f43f5e");
484
485 let props = parse("bg-amber-300").unwrap();
487 assert_eq!(props[0].value, "#fcd34d");
488
489 let props = parse("text-sky-500").unwrap();
491 assert_eq!(props[0].value, "#0ea5e9");
492
493 let props = parse("bg-violet-600").unwrap();
495 assert_eq!(props[0].value, "#7c3aed");
496
497 let props = parse("text-fuchsia-500").unwrap();
499 assert_eq!(props[0].value, "#d946ef");
500
501 let props = parse("bg-lime-400").unwrap();
503 assert_eq!(props[0].value, "#a3e635");
504
505 let props = parse("text-stone-700").unwrap();
507 assert_eq!(props[0].value, "#44403c");
508
509 let props = parse("bg-neutral-800").unwrap();
511 assert_eq!(props[0].value, "#262626");
512 }
513
514 #[test]
517 fn test_text_color_with_opacity() {
518 let props = parse("text-red-500/50").unwrap();
519 assert_eq!(props[0].property, "color");
520 assert_eq!(props[0].value, "rgba(239, 68, 68, 0.5)");
521 }
522
523 #[test]
524 fn test_bg_color_with_opacity() {
525 let props = parse("bg-blue-500/75").unwrap();
526 assert_eq!(props[0].property, "background-color");
527 assert_eq!(props[0].value, "rgba(59, 130, 246, 0.75)");
528 }
529
530 #[test]
531 fn test_border_color_with_opacity() {
532 let props = parse("border-green-400/25").unwrap();
533 assert_eq!(props[0].property, "border-color");
534 assert_eq!(props[0].value, "rgba(74, 222, 128, 0.25)");
535 }
536
537 #[test]
538 fn test_opacity_zero_produces_alpha_zero() {
539 let props = parse("bg-red-500/0").unwrap();
540 assert_eq!(props[0].value, "rgba(239, 68, 68, 0)");
541 }
542
543 #[test]
544 fn test_opacity_100_produces_alpha_one() {
545 let props = parse("bg-red-500/100").unwrap();
546 assert_eq!(props[0].value, "rgba(239, 68, 68, 1)");
547 }
548
549 #[test]
550 fn test_opacity_over_100_returns_none() {
551 assert!(parse("text-red-500/101").is_none());
552 }
553
554 #[test]
555 fn test_opacity_non_numeric_returns_none() {
556 assert!(parse("text-red-500/xx").is_none());
557 }
558
559 #[test]
560 fn test_opacity_empty_suffix_returns_none() {
561 assert!(parse("text-red-500/").is_none());
563 }
564
565 #[test]
566 fn test_resolve_color_with_opacity_direct() {
567 let r = resolve_color_with_opacity("blue-500/50").unwrap();
569 assert_eq!(r, "rgba(59, 130, 246, 0.5)");
570
571 let r = resolve_color_with_opacity("blue-500").unwrap();
573 assert_eq!(r, "#3b82f6");
574
575 let r = resolve_color_with_opacity("transparent/50").unwrap();
577 assert_eq!(r, "transparent");
578
579 assert!(resolve_color_with_opacity("nonexistent/50").is_none());
581 }
582}