1use crate::classes::ClassBuilder;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use regex::Regex;
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", "pt", "pr", "pb", "pl", "px", "py", "m", "mt", "mr", "mb", "ml", "mx", "my",
76 "space-x", "space-y", "gap", "gap-x", "gap-y",
77 "w", "h", "min-w", "max-w", "min-h", "max-h",
79 "text", "font", "leading", "tracking", "indent", "text-indent",
81 "bg", "text", "border", "ring", "accent", "caret", "fill", "stroke",
83 "border", "border-t", "border-r", "border-b", "border-l", "border-x", "border-y",
85 "rounded", "rounded-t", "rounded-r", "rounded-b", "rounded-l",
86 "rounded-tl", "rounded-tr", "rounded-br", "rounded-bl",
87 "shadow", "opacity", "backdrop-blur", "backdrop-brightness", "backdrop-contrast",
89 "backdrop-grayscale", "backdrop-hue-rotate", "backdrop-invert", "backdrop-opacity",
90 "backdrop-saturate", "backdrop-sepia", "blur", "brightness", "contrast", "drop-shadow",
91 "grayscale", "hue-rotate", "invert", "saturate", "sepia",
92 "scale", "scale-x", "scale-y", "rotate", "translate-x", "translate-y", "skew-x", "skew-y",
94 "top", "right", "bottom", "left", "inset", "inset-x", "inset-y",
96 "z",
98 "grid-cols", "grid-rows", "col-start", "col-end", "row-start", "row-end",
100 "col-span", "row-span", "auto-cols", "auto-rows",
101 "flex", "flex-grow", "flex-shrink", "flex-basis", "grow", "shrink", "basis",
103 "order", "self", "place-self", "justify-self", "justify-items",
104 "animate", "transition", "duration", "delay", "ease",
106 "cursor", "select", "resize", "scroll", "touch", "will-change", "overscroll",
108 "sr", "motion", "forced-color-adjust",
110 ];
111
112 valid_properties.contains(&property)
113}
114
115fn is_valid_arbitrary_value(value: &str) -> bool {
117 let patterns = [
119 r"^-?\d+(\.\d+)?(px|rem|em|%|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc)$",
121 r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$",
123 r"^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$",
124 r"^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$",
125 r"^hsl\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\)$",
126 r"^hsla\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*,\s*[\d.]+\s*\)$",
127 r"^-?\d+(\.\d+)?$",
129 r"^var\(--[a-zA-Z0-9-_]+\)$",
131 r"^(calc|min|max|clamp)\(.+\)$",
133 r"^(auto|none|inherit|initial|unset|revert|currentColor|transparent)$",
135 r"^\d+(\.\d+)?(dvw|dvh|dvi|dvb|svw|svh|svi|svb|lvw|lvh|lvi|lvb)$",
137 r"^\d+(\.\d+)?(cqw|cqh|cqi|cqb|cqmin|cqmax)$",
139 ];
140
141 for pattern in &patterns {
142 if let Ok(regex) = Regex::new(pattern) {
143 if regex.is_match(value) {
144 return true;
145 }
146 }
147 }
148
149 false
150}
151
152pub trait ArbitraryValueUtilities {
154 fn arbitrary_value(self, property: impl Into<String>, value: impl Into<String>) -> Self;
156
157 fn w_arbitrary(self, value: impl Into<String>) -> Self;
159
160 fn h_arbitrary(self, value: impl Into<String>) -> Self;
162
163 fn p_arbitrary(self, value: impl Into<String>) -> Self;
165
166 fn m_arbitrary(self, value: impl Into<String>) -> Self;
168
169 fn bg_arbitrary(self, value: impl Into<String>) -> Self;
171
172 fn text_arbitrary(self, value: impl Into<String>) -> Self;
174
175 fn border_arbitrary(self, value: impl Into<String>) -> Self;
177
178 fn text_size_arbitrary(self, value: impl Into<String>) -> Self;
180
181 fn leading_arbitrary(self, value: impl Into<String>) -> Self;
183
184 fn tracking_arbitrary(self, value: impl Into<String>) -> Self;
186
187 fn rounded_arbitrary(self, value: impl Into<String>) -> Self;
189
190 fn shadow_arbitrary(self, value: impl Into<String>) -> Self;
192
193 fn opacity_arbitrary(self, value: impl Into<String>) -> Self;
195
196 fn z_arbitrary(self, value: impl Into<String>) -> Self;
198
199 fn top_arbitrary(self, value: impl Into<String>) -> Self;
201
202 fn right_arbitrary(self, value: impl Into<String>) -> Self;
204
205 fn bottom_arbitrary(self, value: impl Into<String>) -> Self;
207
208 fn left_arbitrary(self, value: impl Into<String>) -> Self;
210}
211
212impl ArbitraryValueUtilities for ClassBuilder {
213 fn arbitrary_value(self, property: impl Into<String>, value: impl Into<String>) -> Self {
214 let arbitrary = ArbitraryValue::new(property, value);
215 self.class(arbitrary.to_class_name())
216 }
217
218 fn w_arbitrary(self, value: impl Into<String>) -> Self {
219 self.arbitrary_value("w", value)
220 }
221
222 fn h_arbitrary(self, value: impl Into<String>) -> Self {
223 self.arbitrary_value("h", value)
224 }
225
226 fn p_arbitrary(self, value: impl Into<String>) -> Self {
227 self.arbitrary_value("p", value)
228 }
229
230 fn m_arbitrary(self, value: impl Into<String>) -> Self {
231 self.arbitrary_value("m", value)
232 }
233
234 fn bg_arbitrary(self, value: impl Into<String>) -> Self {
235 self.arbitrary_value("bg", value)
236 }
237
238 fn text_arbitrary(self, value: impl Into<String>) -> Self {
239 self.arbitrary_value("text", value)
240 }
241
242 fn border_arbitrary(self, value: impl Into<String>) -> Self {
243 self.arbitrary_value("border", value)
244 }
245
246 fn text_size_arbitrary(self, value: impl Into<String>) -> Self {
247 self.arbitrary_value("text", value)
248 }
249
250 fn leading_arbitrary(self, value: impl Into<String>) -> Self {
251 self.arbitrary_value("leading", value)
252 }
253
254 fn tracking_arbitrary(self, value: impl Into<String>) -> Self {
255 self.arbitrary_value("tracking", value)
256 }
257
258 fn rounded_arbitrary(self, value: impl Into<String>) -> Self {
259 self.arbitrary_value("rounded", value)
260 }
261
262 fn shadow_arbitrary(self, value: impl Into<String>) -> Self {
263 self.arbitrary_value("shadow", value)
264 }
265
266 fn opacity_arbitrary(self, value: impl Into<String>) -> Self {
267 self.arbitrary_value("opacity", value)
268 }
269
270 fn z_arbitrary(self, value: impl Into<String>) -> Self {
271 self.arbitrary_value("z", value)
272 }
273
274 fn top_arbitrary(self, value: impl Into<String>) -> Self {
275 self.arbitrary_value("top", value)
276 }
277
278 fn right_arbitrary(self, value: impl Into<String>) -> Self {
279 self.arbitrary_value("right", value)
280 }
281
282 fn bottom_arbitrary(self, value: impl Into<String>) -> Self {
283 self.arbitrary_value("bottom", value)
284 }
285
286 fn left_arbitrary(self, value: impl Into<String>) -> Self {
287 self.arbitrary_value("left", value)
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_arbitrary_value_creation() {
297 let arbitrary = ArbitraryValue::new("w", "123px");
298 assert_eq!(arbitrary.to_class_name(), "w-[123px]");
299 }
300
301 #[test]
302 fn test_arbitrary_value_validation() {
303 assert!(ArbitraryValue::new("w", "123px").validate().is_ok());
305 assert!(ArbitraryValue::new("bg", "#ff0000").validate().is_ok());
306 assert!(ArbitraryValue::new("text", "14px").validate().is_ok());
307 assert!(ArbitraryValue::new("p", "1.5rem").validate().is_ok());
308 assert!(ArbitraryValue::new("opacity", "0.5").validate().is_ok());
309
310 assert!(ArbitraryValue::new("invalid", "123px").validate().is_err());
312 assert!(ArbitraryValue::new("w", "invalid-value").validate().is_err());
313 }
314
315 #[test]
316 fn test_arbitrary_value_utilities() {
317 let classes = ClassBuilder::new()
318 .w_arbitrary("123px")
319 .h_arbitrary("456px")
320 .bg_arbitrary("#ff0000")
321 .text_arbitrary("#ffffff")
322 .p_arbitrary("1.5rem")
323 .m_arbitrary("2rem")
324 .build();
325
326 let css_classes = classes.to_css_classes();
327 assert!(css_classes.contains("w-[123px]"));
328 assert!(css_classes.contains("h-[456px]"));
329 assert!(css_classes.contains("bg-[#ff0000]"));
330 assert!(css_classes.contains("text-[#ffffff]"));
331 assert!(css_classes.contains("p-[1.5rem]"));
332 assert!(css_classes.contains("m-[2rem]"));
333 }
334
335 #[test]
336 fn test_arbitrary_value_display() {
337 let arbitrary = ArbitraryValue::new("w", "123px");
338 assert_eq!(format!("{}", arbitrary), "w-[123px]");
339 }
340
341 #[test]
342 fn test_valid_properties() {
343 assert!(is_valid_property("w"));
344 assert!(is_valid_property("h"));
345 assert!(is_valid_property("bg"));
346 assert!(is_valid_property("text"));
347 assert!(is_valid_property("p"));
348 assert!(is_valid_property("m"));
349 assert!(is_valid_property("rounded"));
350 assert!(is_valid_property("shadow"));
351 assert!(is_valid_property("opacity"));
352 assert!(is_valid_property("z"));
353 assert!(is_valid_property("top"));
354 assert!(is_valid_property("right"));
355 assert!(is_valid_property("bottom"));
356 assert!(is_valid_property("left"));
357
358 assert!(!is_valid_property("invalid"));
359 assert!(!is_valid_property(""));
360 }
361
362 #[test]
363 fn test_valid_arbitrary_values() {
364 assert!(is_valid_arbitrary_value("123px"));
366 assert!(is_valid_arbitrary_value("1.5rem"));
367 assert!(is_valid_arbitrary_value("50%"));
368 assert!(is_valid_arbitrary_value("100vw"));
369 assert!(is_valid_arbitrary_value("100vh"));
370
371 assert!(is_valid_arbitrary_value("#ff0000"));
373 assert!(is_valid_arbitrary_value("#f00"));
374 assert!(is_valid_arbitrary_value("rgb(255, 0, 0)"));
375 assert!(is_valid_arbitrary_value("rgba(255, 0, 0, 0.5)"));
376 assert!(is_valid_arbitrary_value("hsl(0, 100%, 50%)"));
377 assert!(is_valid_arbitrary_value("hsla(0, 100%, 50%, 0.5)"));
378
379 assert!(is_valid_arbitrary_value("0.5"));
381 assert!(is_valid_arbitrary_value("1.25"));
382 assert!(is_valid_arbitrary_value("-0.5"));
383
384 assert!(is_valid_arbitrary_value("var(--my-color)"));
386 assert!(is_valid_arbitrary_value("var(--spacing-lg)"));
387
388 assert!(is_valid_arbitrary_value("calc(100% - 20px)"));
390 assert!(is_valid_arbitrary_value("min(100px, 50%)"));
391 assert!(is_valid_arbitrary_value("max(100px, 50%)"));
392 assert!(is_valid_arbitrary_value("clamp(100px, 50%, 200px)"));
393
394 assert!(is_valid_arbitrary_value("auto"));
396 assert!(is_valid_arbitrary_value("none"));
397 assert!(is_valid_arbitrary_value("inherit"));
398 assert!(is_valid_arbitrary_value("currentColor"));
399 assert!(is_valid_arbitrary_value("transparent"));
400
401 assert!(is_valid_arbitrary_value("100dvw"));
403 assert!(is_valid_arbitrary_value("100dvh"));
404 assert!(is_valid_arbitrary_value("100svw"));
405 assert!(is_valid_arbitrary_value("100svh"));
406 assert!(is_valid_arbitrary_value("100lvw"));
407 assert!(is_valid_arbitrary_value("100lvh"));
408
409 assert!(is_valid_arbitrary_value("100cqw"));
411 assert!(is_valid_arbitrary_value("100cqh"));
412 assert!(is_valid_arbitrary_value("100cqi"));
413 assert!(is_valid_arbitrary_value("100cqb"));
414 assert!(is_valid_arbitrary_value("100cqmin"));
415 assert!(is_valid_arbitrary_value("100cqmax"));
416
417 assert!(!is_valid_arbitrary_value(""));
419 assert!(!is_valid_arbitrary_value("invalid"));
420 assert!(!is_valid_arbitrary_value("abc"));
421 }
422}