tailwind_rs_core/
arbitrary.rs

1//! Arbitrary values support for tailwind-rs
2//!
3//! This module provides support for Tailwind CSS arbitrary values,
4//! allowing users to specify custom values using square bracket notation.
5//! Examples: w-[123px], bg-[#ff0000], text-[14px], etc.
6
7use crate::classes::ClassBuilder;
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12/// Represents an arbitrary value in Tailwind CSS
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct ArbitraryValue {
15    /// The property prefix (e.g., "w", "bg", "text")
16    pub property: String,
17    /// The arbitrary value content (e.g., "123px", "#ff0000", "14px")
18    pub value: String,
19}
20
21impl ArbitraryValue {
22    /// Create a new arbitrary value
23    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    /// Convert to Tailwind CSS class name
31    pub fn to_class_name(&self) -> String {
32        format!("{}-[{}]", self.property, self.value)
33    }
34
35    /// Validate the arbitrary value
36    pub fn validate(&self) -> Result<(), ArbitraryValueError> {
37        // Validate property name
38        if !is_valid_property(&self.property) {
39            return Err(ArbitraryValueError::InvalidProperty(self.property.clone()));
40        }
41
42        // Validate value format
43        if !is_valid_arbitrary_value(&self.value) {
44            return Err(ArbitraryValueError::InvalidValue(self.value.clone()));
45        }
46
47        Ok(())
48    }
49}
50
51/// Errors that can occur when working with arbitrary values
52#[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
70/// Check if a property name is valid for arbitrary values
71fn is_valid_property(property: &str) -> bool {
72    // Common Tailwind CSS properties that support arbitrary values
73    let valid_properties = [
74        // Spacing
75        "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        // Sizing
95        "w",
96        "h",
97        "min-w",
98        "max-w",
99        "min-h",
100        "max-h",
101        // Typography
102        "text",
103        "font",
104        "leading",
105        "tracking",
106        "indent",
107        "text-indent",
108        // Colors
109        "bg",
110        "text",
111        "border",
112        "ring",
113        "accent",
114        "caret",
115        "fill",
116        "stroke",
117        // Borders
118        "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        // Effects
135        "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        // Transforms
156        "scale",
157        "scale-x",
158        "scale-y",
159        "rotate",
160        "translate-x",
161        "translate-y",
162        "skew-x",
163        "skew-y",
164        // Positioning
165        "top",
166        "right",
167        "bottom",
168        "left",
169        "inset",
170        "inset-x",
171        "inset-y",
172        // Z-index
173        "z",
174        // Grid
175        "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        // Flexbox
186        "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        // Animation
199        "animate",
200        "transition",
201        "duration",
202        "delay",
203        "ease",
204        // Interactivity
205        "cursor",
206        "select",
207        "resize",
208        "scroll",
209        "touch",
210        "will-change",
211        "overscroll",
212        // Accessibility
213        "sr",
214        "motion",
215        "forced-color-adjust",
216    ];
217
218    valid_properties.contains(&property)
219}
220
221/// Check if an arbitrary value is valid
222fn is_valid_arbitrary_value(value: &str) -> bool {
223    // Common patterns for arbitrary values
224    let patterns = [
225        // Length values (px, rem, em, %, vw, vh, etc.)
226        r"^-?\d+(\.\d+)?(px|rem|em|%|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc)$",
227        // Color values (hex, rgb, rgba, hsl, hsla, named colors)
228        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        // Fractional values
234        r"^-?\d+(\.\d+)?$",
235        // CSS custom properties
236        r"^var\(--[a-zA-Z0-9-_]+\)$",
237        // CSS functions
238        r"^(calc|min|max|clamp)\(.+\)$",
239        // Keywords
240        r"^(auto|none|inherit|initial|unset|revert|currentColor|transparent)$",
241        // Viewport units
242        r"^\d+(\.\d+)?(dvw|dvh|dvi|dvb|svw|svh|svi|svb|lvw|lvh|lvi|lvb)$",
243        // Container units
244        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
258/// Trait for adding arbitrary value utilities to a class builder
259pub trait ArbitraryValueUtilities {
260    /// Add an arbitrary value class
261    fn arbitrary_value(self, property: impl Into<String>, value: impl Into<String>) -> Self;
262
263    /// Add arbitrary width
264    fn w_arbitrary(self, value: impl Into<String>) -> Self;
265
266    /// Add arbitrary height
267    fn h_arbitrary(self, value: impl Into<String>) -> Self;
268
269    /// Add arbitrary padding
270    fn p_arbitrary(self, value: impl Into<String>) -> Self;
271
272    /// Add arbitrary margin
273    fn m_arbitrary(self, value: impl Into<String>) -> Self;
274
275    /// Add arbitrary background color
276    fn bg_arbitrary(self, value: impl Into<String>) -> Self;
277
278    /// Add arbitrary text color
279    fn text_arbitrary(self, value: impl Into<String>) -> Self;
280
281    /// Add arbitrary border color
282    fn border_arbitrary(self, value: impl Into<String>) -> Self;
283
284    /// Add arbitrary font size
285    fn text_size_arbitrary(self, value: impl Into<String>) -> Self;
286
287    /// Add arbitrary line height
288    fn leading_arbitrary(self, value: impl Into<String>) -> Self;
289
290    /// Add arbitrary letter spacing
291    fn tracking_arbitrary(self, value: impl Into<String>) -> Self;
292
293    /// Add arbitrary border radius
294    fn rounded_arbitrary(self, value: impl Into<String>) -> Self;
295
296    /// Add arbitrary shadow
297    fn shadow_arbitrary(self, value: impl Into<String>) -> Self;
298
299    /// Add arbitrary opacity
300    fn opacity_arbitrary(self, value: impl Into<String>) -> Self;
301
302    /// Add arbitrary z-index
303    fn z_arbitrary(self, value: impl Into<String>) -> Self;
304
305    /// Add arbitrary top position
306    fn top_arbitrary(self, value: impl Into<String>) -> Self;
307
308    /// Add arbitrary right position
309    fn right_arbitrary(self, value: impl Into<String>) -> Self;
310
311    /// Add arbitrary bottom position
312    fn bottom_arbitrary(self, value: impl Into<String>) -> Self;
313
314    /// Add arbitrary left position
315    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        // Valid values
410        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        // Invalid values
417        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        // Length values
473        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        // Color values
480        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        // Fractional values
488        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        // CSS custom properties
493        assert!(is_valid_arbitrary_value("var(--my-color)"));
494        assert!(is_valid_arbitrary_value("var(--spacing-lg)"));
495
496        // CSS functions
497        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        // Keywords
503        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        // Viewport units
510        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        // Container units
518        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        // Invalid values
526        assert!(!is_valid_arbitrary_value(""));
527        assert!(!is_valid_arbitrary_value("invalid"));
528        assert!(!is_valid_arbitrary_value("abc"));
529    }
530}