1use crate::classes::ClassBuilder;
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct ArbitraryValue {
15 pub property: String,
17 pub value: String,
19}
20
21impl ArbitraryValue {
22 pub fn new(property: impl Into<String>, value: impl Into<String>) -> Self {
24 Self {
25 property: property.into(),
26 value: value.into(),
27 }
28 }
29
30 pub fn to_class_name(&self) -> String {
32 format!("{}-[{}]", self.property, self.value)
33 }
34
35 pub fn validate(&self) -> Result<(), ArbitraryValueError> {
37 if !is_valid_property(&self.property) {
39 return Err(ArbitraryValueError::InvalidProperty(self.property.clone()));
40 }
41
42 if !is_valid_arbitrary_value(&self.value) {
44 return Err(ArbitraryValueError::InvalidValue(self.value.clone()));
45 }
46
47 Ok(())
48 }
49}
50
51#[derive(Debug, thiserror::Error)]
53pub enum ArbitraryValueError {
54 #[error("Invalid property: {0}")]
55 InvalidProperty(String),
56
57 #[error("Invalid arbitrary value: {0}")]
58 InvalidValue(String),
59
60 #[error("Unsupported property for arbitrary values: {0}")]
61 UnsupportedProperty(String),
62}
63
64impl fmt::Display for ArbitraryValue {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 write!(f, "{}", self.to_class_name())
67 }
68}
69
70fn is_valid_property(property: &str) -> bool {
72 let valid_properties = [
74 "p",
76 "pt",
77 "pr",
78 "pb",
79 "pl",
80 "px",
81 "py",
82 "m",
83 "mt",
84 "mr",
85 "mb",
86 "ml",
87 "mx",
88 "my",
89 "space-x",
90 "space-y",
91 "gap",
92 "gap-x",
93 "gap-y",
94 "w",
96 "h",
97 "min-w",
98 "max-w",
99 "min-h",
100 "max-h",
101 "text",
103 "font",
104 "leading",
105 "tracking",
106 "indent",
107 "text-indent",
108 "bg",
110 "text",
111 "border",
112 "ring",
113 "accent",
114 "caret",
115 "fill",
116 "stroke",
117 "border",
119 "border-t",
120 "border-r",
121 "border-b",
122 "border-l",
123 "border-x",
124 "border-y",
125 "rounded",
126 "rounded-t",
127 "rounded-r",
128 "rounded-b",
129 "rounded-l",
130 "rounded-tl",
131 "rounded-tr",
132 "rounded-br",
133 "rounded-bl",
134 "shadow",
136 "opacity",
137 "backdrop-blur",
138 "backdrop-brightness",
139 "backdrop-contrast",
140 "backdrop-grayscale",
141 "backdrop-hue-rotate",
142 "backdrop-invert",
143 "backdrop-opacity",
144 "backdrop-saturate",
145 "backdrop-sepia",
146 "blur",
147 "brightness",
148 "contrast",
149 "drop-shadow",
150 "grayscale",
151 "hue-rotate",
152 "invert",
153 "saturate",
154 "sepia",
155 "scale",
157 "scale-x",
158 "scale-y",
159 "rotate",
160 "translate-x",
161 "translate-y",
162 "skew-x",
163 "skew-y",
164 "top",
166 "right",
167 "bottom",
168 "left",
169 "inset",
170 "inset-x",
171 "inset-y",
172 "z",
174 "grid-cols",
176 "grid-rows",
177 "col-start",
178 "col-end",
179 "row-start",
180 "row-end",
181 "col-span",
182 "row-span",
183 "auto-cols",
184 "auto-rows",
185 "flex",
187 "flex-grow",
188 "flex-shrink",
189 "flex-basis",
190 "grow",
191 "shrink",
192 "basis",
193 "order",
194 "self",
195 "place-self",
196 "justify-self",
197 "justify-items",
198 "animate",
200 "transition",
201 "duration",
202 "delay",
203 "ease",
204 "cursor",
206 "select",
207 "resize",
208 "scroll",
209 "touch",
210 "will-change",
211 "overscroll",
212 "sr",
214 "motion",
215 "forced-color-adjust",
216 ];
217
218 valid_properties.contains(&property)
219}
220
221fn is_valid_arbitrary_value(value: &str) -> bool {
223 let patterns = [
225 r"^-?\d+(\.\d+)?(px|rem|em|%|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc)$",
227 r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
229 r"^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$",
230 r"^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$",
231 r"^hsl\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\)$",
232 r"^hsla\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*,\s*[\d.]+\s*\)$",
233 r"^-?\d+(\.\d+)?$",
235 r"^var\(--[a-zA-Z0-9-_]+\)$",
237 r"^(calc|min|max|clamp)\(.+\)$",
239 r"^(auto|none|inherit|initial|unset|revert|currentColor|transparent)$",
241 r"^\d+(\.\d+)?(dvw|dvh|dvi|dvb|svw|svh|svi|svb|lvw|lvh|lvi|lvb)$",
243 r"^\d+(\.\d+)?(cqw|cqh|cqi|cqb|cqmin|cqmax)$",
245 ];
246
247 for pattern in &patterns {
248 if let Ok(regex) = Regex::new(pattern) {
249 if regex.is_match(value) {
250 return true;
251 }
252 }
253 }
254
255 false
256}
257
258pub trait ArbitraryValueUtilities {
260 fn arbitrary_value(self, property: impl Into<String>, value: impl Into<String>) -> Self;
262
263 fn w_arbitrary(self, value: impl Into<String>) -> Self;
265
266 fn h_arbitrary(self, value: impl Into<String>) -> Self;
268
269 fn p_arbitrary(self, value: impl Into<String>) -> Self;
271
272 fn m_arbitrary(self, value: impl Into<String>) -> Self;
274
275 fn bg_arbitrary(self, value: impl Into<String>) -> Self;
277
278 fn text_arbitrary(self, value: impl Into<String>) -> Self;
280
281 fn border_arbitrary(self, value: impl Into<String>) -> Self;
283
284 fn text_size_arbitrary(self, value: impl Into<String>) -> Self;
286
287 fn leading_arbitrary(self, value: impl Into<String>) -> Self;
289
290 fn tracking_arbitrary(self, value: impl Into<String>) -> Self;
292
293 fn rounded_arbitrary(self, value: impl Into<String>) -> Self;
295
296 fn shadow_arbitrary(self, value: impl Into<String>) -> Self;
298
299 fn opacity_arbitrary(self, value: impl Into<String>) -> Self;
301
302 fn z_arbitrary(self, value: impl Into<String>) -> Self;
304
305 fn top_arbitrary(self, value: impl Into<String>) -> Self;
307
308 fn right_arbitrary(self, value: impl Into<String>) -> Self;
310
311 fn bottom_arbitrary(self, value: impl Into<String>) -> Self;
313
314 fn left_arbitrary(self, value: impl Into<String>) -> Self;
316}
317
318impl ArbitraryValueUtilities for ClassBuilder {
319 fn arbitrary_value(self, property: impl Into<String>, value: impl Into<String>) -> Self {
320 let arbitrary = ArbitraryValue::new(property, value);
321 self.class(arbitrary.to_class_name())
322 }
323
324 fn w_arbitrary(self, value: impl Into<String>) -> Self {
325 self.arbitrary_value("w", value)
326 }
327
328 fn h_arbitrary(self, value: impl Into<String>) -> Self {
329 self.arbitrary_value("h", value)
330 }
331
332 fn p_arbitrary(self, value: impl Into<String>) -> Self {
333 self.arbitrary_value("p", value)
334 }
335
336 fn m_arbitrary(self, value: impl Into<String>) -> Self {
337 self.arbitrary_value("m", value)
338 }
339
340 fn bg_arbitrary(self, value: impl Into<String>) -> Self {
341 self.arbitrary_value("bg", value)
342 }
343
344 fn text_arbitrary(self, value: impl Into<String>) -> Self {
345 self.arbitrary_value("text", value)
346 }
347
348 fn border_arbitrary(self, value: impl Into<String>) -> Self {
349 self.arbitrary_value("border", value)
350 }
351
352 fn text_size_arbitrary(self, value: impl Into<String>) -> Self {
353 self.arbitrary_value("text", value)
354 }
355
356 fn leading_arbitrary(self, value: impl Into<String>) -> Self {
357 self.arbitrary_value("leading", value)
358 }
359
360 fn tracking_arbitrary(self, value: impl Into<String>) -> Self {
361 self.arbitrary_value("tracking", value)
362 }
363
364 fn rounded_arbitrary(self, value: impl Into<String>) -> Self {
365 self.arbitrary_value("rounded", value)
366 }
367
368 fn shadow_arbitrary(self, value: impl Into<String>) -> Self {
369 self.arbitrary_value("shadow", value)
370 }
371
372 fn opacity_arbitrary(self, value: impl Into<String>) -> Self {
373 self.arbitrary_value("opacity", value)
374 }
375
376 fn z_arbitrary(self, value: impl Into<String>) -> Self {
377 self.arbitrary_value("z", value)
378 }
379
380 fn top_arbitrary(self, value: impl Into<String>) -> Self {
381 self.arbitrary_value("top", value)
382 }
383
384 fn right_arbitrary(self, value: impl Into<String>) -> Self {
385 self.arbitrary_value("right", value)
386 }
387
388 fn bottom_arbitrary(self, value: impl Into<String>) -> Self {
389 self.arbitrary_value("bottom", value)
390 }
391
392 fn left_arbitrary(self, value: impl Into<String>) -> Self {
393 self.arbitrary_value("left", value)
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_arbitrary_value_creation() {
403 let arbitrary = ArbitraryValue::new("w", "123px");
404 assert_eq!(arbitrary.to_class_name(), "w-[123px]");
405 }
406
407 #[test]
408 fn test_arbitrary_value_validation() {
409 assert!(ArbitraryValue::new("w", "123px").validate().is_ok());
411 assert!(ArbitraryValue::new("bg", "#ff0000").validate().is_ok());
412 assert!(ArbitraryValue::new("text", "14px").validate().is_ok());
413 assert!(ArbitraryValue::new("p", "1.5rem").validate().is_ok());
414 assert!(ArbitraryValue::new("opacity", "0.5").validate().is_ok());
415
416 assert!(ArbitraryValue::new("invalid", "123px").validate().is_err());
418 assert!(ArbitraryValue::new("w", "invalid-value")
419 .validate()
420 .is_err());
421 }
422
423 #[test]
424 fn test_arbitrary_value_utilities() {
425 let classes = ClassBuilder::new()
426 .w_arbitrary("123px")
427 .h_arbitrary("456px")
428 .bg_arbitrary("#ff0000")
429 .text_arbitrary("#ffffff")
430 .p_arbitrary("1.5rem")
431 .m_arbitrary("2rem")
432 .build();
433
434 let css_classes = classes.to_css_classes();
435 assert!(css_classes.contains("w-[123px]"));
436 assert!(css_classes.contains("h-[456px]"));
437 assert!(css_classes.contains("bg-[#ff0000]"));
438 assert!(css_classes.contains("text-[#ffffff]"));
439 assert!(css_classes.contains("p-[1.5rem]"));
440 assert!(css_classes.contains("m-[2rem]"));
441 }
442
443 #[test]
444 fn test_arbitrary_value_display() {
445 let arbitrary = ArbitraryValue::new("w", "123px");
446 assert_eq!(format!("{}", arbitrary), "w-[123px]");
447 }
448
449 #[test]
450 fn test_valid_properties() {
451 assert!(is_valid_property("w"));
452 assert!(is_valid_property("h"));
453 assert!(is_valid_property("bg"));
454 assert!(is_valid_property("text"));
455 assert!(is_valid_property("p"));
456 assert!(is_valid_property("m"));
457 assert!(is_valid_property("rounded"));
458 assert!(is_valid_property("shadow"));
459 assert!(is_valid_property("opacity"));
460 assert!(is_valid_property("z"));
461 assert!(is_valid_property("top"));
462 assert!(is_valid_property("right"));
463 assert!(is_valid_property("bottom"));
464 assert!(is_valid_property("left"));
465
466 assert!(!is_valid_property("invalid"));
467 assert!(!is_valid_property(""));
468 }
469
470 #[test]
471 fn test_valid_arbitrary_values() {
472 assert!(is_valid_arbitrary_value("123px"));
474 assert!(is_valid_arbitrary_value("1.5rem"));
475 assert!(is_valid_arbitrary_value("50%"));
476 assert!(is_valid_arbitrary_value("100vw"));
477 assert!(is_valid_arbitrary_value("100vh"));
478
479 assert!(is_valid_arbitrary_value("#ff0000"));
481 assert!(is_valid_arbitrary_value("#f00"));
482 assert!(is_valid_arbitrary_value("rgb(255, 0, 0)"));
483 assert!(is_valid_arbitrary_value("rgba(255, 0, 0, 0.5)"));
484 assert!(is_valid_arbitrary_value("hsl(0, 100%, 50%)"));
485 assert!(is_valid_arbitrary_value("hsla(0, 100%, 50%, 0.5)"));
486
487 assert!(is_valid_arbitrary_value("0.5"));
489 assert!(is_valid_arbitrary_value("1.25"));
490 assert!(is_valid_arbitrary_value("-0.5"));
491
492 assert!(is_valid_arbitrary_value("var(--my-color)"));
494 assert!(is_valid_arbitrary_value("var(--spacing-lg)"));
495
496 assert!(is_valid_arbitrary_value("calc(100% - 20px)"));
498 assert!(is_valid_arbitrary_value("min(100px, 50%)"));
499 assert!(is_valid_arbitrary_value("max(100px, 50%)"));
500 assert!(is_valid_arbitrary_value("clamp(100px, 50%, 200px)"));
501
502 assert!(is_valid_arbitrary_value("auto"));
504 assert!(is_valid_arbitrary_value("none"));
505 assert!(is_valid_arbitrary_value("inherit"));
506 assert!(is_valid_arbitrary_value("currentColor"));
507 assert!(is_valid_arbitrary_value("transparent"));
508
509 assert!(is_valid_arbitrary_value("100dvw"));
511 assert!(is_valid_arbitrary_value("100dvh"));
512 assert!(is_valid_arbitrary_value("100svw"));
513 assert!(is_valid_arbitrary_value("100svh"));
514 assert!(is_valid_arbitrary_value("100lvw"));
515 assert!(is_valid_arbitrary_value("100lvh"));
516
517 assert!(is_valid_arbitrary_value("100cqw"));
519 assert!(is_valid_arbitrary_value("100cqh"));
520 assert!(is_valid_arbitrary_value("100cqi"));
521 assert!(is_valid_arbitrary_value("100cqb"));
522 assert!(is_valid_arbitrary_value("100cqmin"));
523 assert!(is_valid_arbitrary_value("100cqmax"));
524
525 assert!(!is_valid_arbitrary_value(""));
527 assert!(!is_valid_arbitrary_value("invalid"));
528 assert!(!is_valid_arbitrary_value("abc"));
529 }
530}