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 serde::{Deserialize, Serialize};
9use std::fmt;
10use regex::Regex;
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", "pt", "pr", "pb", "pl", "px", "py", "m", "mt", "mr", "mb", "ml", "mx", "my",
76        "space-x", "space-y", "gap", "gap-x", "gap-y",
77        // Sizing
78        "w", "h", "min-w", "max-w", "min-h", "max-h",
79        // Typography
80        "text", "font", "leading", "tracking", "indent", "text-indent",
81        // Colors
82        "bg", "text", "border", "ring", "accent", "caret", "fill", "stroke",
83        // Borders
84        "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        // Effects
88        "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        // Transforms
93        "scale", "scale-x", "scale-y", "rotate", "translate-x", "translate-y", "skew-x", "skew-y",
94        // Positioning
95        "top", "right", "bottom", "left", "inset", "inset-x", "inset-y",
96        // Z-index
97        "z",
98        // Grid
99        "grid-cols", "grid-rows", "col-start", "col-end", "row-start", "row-end",
100        "col-span", "row-span", "auto-cols", "auto-rows",
101        // Flexbox
102        "flex", "flex-grow", "flex-shrink", "flex-basis", "grow", "shrink", "basis",
103        "order", "self", "place-self", "justify-self", "justify-items",
104        // Animation
105        "animate", "transition", "duration", "delay", "ease",
106        // Interactivity
107        "cursor", "select", "resize", "scroll", "touch", "will-change", "overscroll",
108        // Accessibility
109        "sr", "motion", "forced-color-adjust",
110    ];
111
112    valid_properties.contains(&property)
113}
114
115/// Check if an arbitrary value is valid
116fn is_valid_arbitrary_value(value: &str) -> bool {
117    // Common patterns for arbitrary values
118    let patterns = [
119        // Length values (px, rem, em, %, vw, vh, etc.)
120        r"^-?\d+(\.\d+)?(px|rem|em|%|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc)$",
121        // Color values (hex, rgb, rgba, hsl, hsla, named colors)
122        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        // Fractional values
128        r"^-?\d+(\.\d+)?$",
129        // CSS custom properties
130        r"^var\(--[a-zA-Z0-9-_]+\)$",
131        // CSS functions
132        r"^(calc|min|max|clamp)\(.+\)$",
133        // Keywords
134        r"^(auto|none|inherit|initial|unset|revert|currentColor|transparent)$",
135        // Viewport units
136        r"^\d+(\.\d+)?(dvw|dvh|dvi|dvb|svw|svh|svi|svb|lvw|lvh|lvi|lvb)$",
137        // Container units
138        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
152/// Trait for adding arbitrary value utilities to a class builder
153pub trait ArbitraryValueUtilities {
154    /// Add an arbitrary value class
155    fn arbitrary_value(self, property: impl Into<String>, value: impl Into<String>) -> Self;
156    
157    /// Add arbitrary width
158    fn w_arbitrary(self, value: impl Into<String>) -> Self;
159    
160    /// Add arbitrary height
161    fn h_arbitrary(self, value: impl Into<String>) -> Self;
162    
163    /// Add arbitrary padding
164    fn p_arbitrary(self, value: impl Into<String>) -> Self;
165    
166    /// Add arbitrary margin
167    fn m_arbitrary(self, value: impl Into<String>) -> Self;
168    
169    /// Add arbitrary background color
170    fn bg_arbitrary(self, value: impl Into<String>) -> Self;
171    
172    /// Add arbitrary text color
173    fn text_arbitrary(self, value: impl Into<String>) -> Self;
174    
175    /// Add arbitrary border color
176    fn border_arbitrary(self, value: impl Into<String>) -> Self;
177    
178    /// Add arbitrary font size
179    fn text_size_arbitrary(self, value: impl Into<String>) -> Self;
180    
181    /// Add arbitrary line height
182    fn leading_arbitrary(self, value: impl Into<String>) -> Self;
183    
184    /// Add arbitrary letter spacing
185    fn tracking_arbitrary(self, value: impl Into<String>) -> Self;
186    
187    /// Add arbitrary border radius
188    fn rounded_arbitrary(self, value: impl Into<String>) -> Self;
189    
190    /// Add arbitrary shadow
191    fn shadow_arbitrary(self, value: impl Into<String>) -> Self;
192    
193    /// Add arbitrary opacity
194    fn opacity_arbitrary(self, value: impl Into<String>) -> Self;
195    
196    /// Add arbitrary z-index
197    fn z_arbitrary(self, value: impl Into<String>) -> Self;
198    
199    /// Add arbitrary top position
200    fn top_arbitrary(self, value: impl Into<String>) -> Self;
201    
202    /// Add arbitrary right position
203    fn right_arbitrary(self, value: impl Into<String>) -> Self;
204    
205    /// Add arbitrary bottom position
206    fn bottom_arbitrary(self, value: impl Into<String>) -> Self;
207    
208    /// Add arbitrary left position
209    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        // Valid values
304        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        // Invalid values
311        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        // Length values
365        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        // Color values
372        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        // Fractional values
380        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        // CSS custom properties
385        assert!(is_valid_arbitrary_value("var(--my-color)"));
386        assert!(is_valid_arbitrary_value("var(--spacing-lg)"));
387        
388        // CSS functions
389        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        // Keywords
395        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        // Viewport units
402        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        // Container units
410        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        // Invalid values
418        assert!(!is_valid_arbitrary_value(""));
419        assert!(!is_valid_arbitrary_value("invalid"));
420        assert!(!is_valid_arbitrary_value("abc"));
421    }
422}