tailwind_rs_core/
validation.rs

1//! Validation system for tailwind-rs
2//!
3//! This module provides class validation functionality to ensure that
4//! generated Tailwind CSS classes are valid and don't conflict.
5
6use crate::custom_variant::CustomVariantManager;
7use std::collections::{HashMap, HashSet};
8use thiserror::Error;
9
10/// Represents validation errors
11#[derive(Debug, Error)]
12pub enum ValidationError {
13    #[error("Invalid class name: {0}")]
14    InvalidClass(String),
15
16    #[error("Class conflict: {0} conflicts with {1}")]
17    ClassConflict(String, String),
18
19    #[error("Deprecated class: {0}")]
20    DeprecatedClass(String),
21
22    #[error("Unsupported class: {0}")]
23    UnsupportedClass(String),
24
25    #[error("Invalid custom variant: {0}")]
26    InvalidCustomVariant(String),
27
28    #[error("Custom variant validation failed: {0}")]
29    CustomVariantValidation(String),
30}
31
32/// Validation rules for class validation
33#[derive(Debug, Clone)]
34pub struct ValidationRules {
35    /// Allowed class patterns
36    pub allowed_patterns: Vec<regex::Regex>,
37    /// Forbidden class patterns
38    pub forbidden_patterns: Vec<regex::Regex>,
39    /// Deprecated classes
40    pub deprecated_classes: HashSet<String>,
41    /// Class conflicts (classes that can't be used together)
42    pub class_conflicts: HashMap<String, HashSet<String>>,
43    /// Required classes (classes that must be present when certain classes are used)
44    pub required_classes: HashMap<String, HashSet<String>>,
45}
46
47impl Default for ValidationRules {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl ValidationRules {
54    /// Create new validation rules with default Tailwind CSS patterns
55    pub fn new() -> Self {
56        let mut rules = Self {
57            allowed_patterns: Vec::new(),
58            forbidden_patterns: Vec::new(),
59            deprecated_classes: HashSet::new(),
60            class_conflicts: HashMap::new(),
61            required_classes: HashMap::new(),
62        };
63
64        // Add common Tailwind CSS class patterns
65        rules.add_allowed_patterns();
66        rules.add_class_conflicts();
67        rules.add_required_classes();
68
69        rules
70    }
71
72    /// Add allowed class patterns
73    fn add_allowed_patterns(&mut self) {
74        let patterns = vec![
75            // Layout
76            r"^(block|inline-block|inline|flex|inline-flex|table|inline-table|table-caption|table-cell|table-column|table-column-group|table-footer-group|table-header-group|table-row-group|table-row|flow-root|grid|inline-grid|contents|list-item|hidden)$",
77            // Flexbox
78            r"^(flex-row|flex-row-reverse|flex-col|flex-col-reverse|flex-wrap|flex-wrap-reverse|flex-nowrap|items-start|items-end|items-center|items-baseline|items-stretch|justify-start|justify-end|justify-center|justify-between|justify-around|justify-evenly|justify-stretch|content-start|content-end|content-center|content-between|content-around|content-evenly|content-stretch|self-auto|self-start|self-end|self-center|self-stretch|self-baseline)$",
79            // Grid
80            r"^(grid-cols-\d+|grid-cols-none|grid-cols-subgrid|col-auto|col-span-\d+|col-start-\d+|col-end-\d+|col-start-auto|col-end-auto|grid-rows-\d+|grid-rows-none|grid-rows-subgrid|row-auto|row-span-\d+|row-start-\d+|row-end-\d+|row-start-auto|row-end-auto|auto-cols-auto|auto-cols-min|auto-cols-max|auto-cols-fr|auto-rows-auto|auto-rows-min|auto-rows-max|auto-rows-fr|gap-\d+|gap-x-\d+|gap-y-\d+|justify-items-start|justify-items-end|justify-items-center|justify-items-stretch|justify-self-auto|justify-self-start|justify-self-end|justify-self-center|justify-self-stretch|place-content-start|place-content-end|place-content-center|place-content-between|place-content-around|place-content-evenly|place-content-stretch|place-items-start|place-items-end|place-items-center|place-items-stretch|place-self-auto|place-self-start|place-self-end|place-self-center|place-self-stretch)$",
81            // Spacing (Enhanced with fractional values and space/divide utilities)
82            r"^(p-\d+(\.\d+)?|pt-\d+(\.\d+)?|pr-\d+(\.\d+)?|pb-\d+(\.\d+)?|pl-\d+(\.\d+)?|px-\d+(\.\d+)?|py-\d+(\.\d+)?|ps-\d+(\.\d+)?|pe-\d+(\.\d+)?|m-\d+(\.\d+)?|mt-\d+(\.\d+)?|mr-\d+(\.\d+)?|mb-\d+(\.\d+)?|ml-\d+(\.\d+)?|mx-\d+(\.\d+)?|my-\d+(\.\d+)?|ms-\d+(\.\d+)?|me-\d+(\.\d+)?|-m-\d+(\.\d+)?|-mt-\d+(\.\d+)?|-mr-\d+(\.\d+)?|-mb-\d+(\.\d+)?|-ml-\d+(\.\d+)?|-mx-\d+(\.\d+)?|-my-\d+(\.\d+)?|space-x-\d+(\.\d+)?|space-y-\d+(\.\d+)?|space-x-reverse|space-y-reverse|divide-x-\d+(\.\d+)?|divide-y-\d+(\.\d+)?|divide-x-reverse|divide-y-reverse)$",
83            // Sizing
84            r"^(w-\d+|w-auto|w-px|w-0\.5|w-1\.5|w-2\.5|w-3\.5|w-1\/2|w-1\/3|w-2\/3|w-1\/4|w-2\/4|w-3\/4|w-1\/5|w-2\/5|w-3\/5|w-4\/5|w-1\/6|w-2\/6|w-3\/6|w-4\/6|w-5\/6|w-1\/12|w-2\/12|w-3\/12|w-4\/12|w-5\/12|w-6\/12|w-7\/12|w-8\/12|w-9\/12|w-10\/12|w-11\/12|w-full|w-screen|w-min|w-max|w-fit|h-\d+|h-auto|h-px|h-0\.5|h-1\.5|h-2\.5|h-3\.5|h-1\/2|h-1\/3|h-2\/3|h-1\/4|h-2\/4|h-3\/4|h-1\/5|h-2\/5|h-3\/5|h-4\/5|h-1\/6|h-2\/6|h-3\/6|h-4\/6|h-5\/6|h-1\/12|h-2\/12|h-3\/12|h-4\/12|h-5\/12|h-6\/12|h-7\/12|h-8\/12|h-9\/12|h-10\/12|h-11\/12|h-full|h-screen|h-min|h-max|h-fit|min-w-0|min-w-full|min-w-min|min-w-max|min-w-fit|max-w-0|max-w-none|max-w-xs|max-w-sm|max-w-md|max-w-lg|max-w-xl|max-w-2xl|max-w-3xl|max-w-4xl|max-w-5xl|max-w-6xl|max-w-7xl|max-w-full|max-w-min|max-w-max|max-w-fit|max-w-prose|max-w-screen-sm|max-w-screen-md|max-w-screen-lg|max-w-screen-xl|max-w-screen-2xl|min-h-0|min-h-full|min-h-screen|min-h-min|min-h-max|min-h-fit|max-h-0|max-h-px|max-h-0\.5|max-h-1|max-h-1\.5|max-h-2|max-h-2\.5|max-h-3|max-h-3\.5|max-h-4|max-h-5|max-h-6|max-h-7|max-h-8|max-h-9|max-h-10|max-h-11|max-h-12|max-h-14|max-h-16|max-h-20|max-h-24|max-h-28|max-h-32|max-h-36|max-h-40|max-h-44|max-h-48|max-h-52|max-h-56|max-h-60|max-h-64|max-h-72|max-h-80|max-h-96|max-h-px|max-h-0\.5|max-h-1|max-h-1\.5|max-h-2|max-h-2\.5|max-h-3|max-h-3\.5|max-h-4|max-h-5|max-h-6|max-h-7|max-h-8|max-h-9|max-h-10|max-h-11|max-h-12|max-h-14|max-h-16|max-h-20|max-h-24|max-h-28|max-h-32|max-h-36|max-h-40|max-h-44|max-h-48|max-h-52|max-h-56|max-h-60|max-h-64|max-h-72|max-h-80|max-h-96|max-h-full|max-h-screen)$",
85            // Typography
86            r"^(text-xs|text-sm|text-base|text-lg|text-xl|text-2xl|text-3xl|text-4xl|text-5xl|text-6xl|text-7xl|text-8xl|text-9xl|font-thin|font-extralight|font-light|font-normal|font-medium|font-semibold|font-bold|font-extrabold|font-black|italic|not-italic|leading-3|leading-4|leading-5|leading-6|leading-7|leading-8|leading-9|leading-10|leading-none|leading-tight|leading-snug|leading-normal|leading-relaxed|leading-loose|tracking-tighter|tracking-tight|tracking-normal|tracking-wide|tracking-wider|tracking-widest|text-left|text-center|text-right|text-justify|text-start|text-end|text-inherit|text-current|text-transparent|text-black|text-white|text-slate-\d+|text-gray-\d+|text-zinc-\d+|text-neutral-\d+|text-stone-\d+|text-red-\d+|text-orange-\d+|text-amber-\d+|text-yellow-\d+|text-lime-\d+|text-green-\d+|text-emerald-\d+|text-teal-\d+|text-cyan-\d+|text-sky-\d+|text-blue-\d+|text-indigo-\d+|text-violet-\d+|text-purple-\d+|text-fuchsia-\d+|text-pink-\d+|text-rose-\d+|underline|overline|line-through|no-underline|decoration-solid|decoration-double|decoration-dotted|decoration-dashed|decoration-wavy|decoration-auto|decoration-from-font|decoration-0|decoration-1|decoration-2|decoration-4|decoration-8|underline-offset-auto|underline-offset-0|underline-offset-1|underline-offset-2|underline-offset-4|underline-offset-8|uppercase|lowercase|capitalize|normal-case)$",
87            // Backgrounds
88            r"^(bg-inherit|bg-current|bg-transparent|bg-black|bg-white|bg-slate-\d+|bg-gray-\d+|bg-zinc-\d+|bg-neutral-\d+|bg-stone-\d+|bg-red-\d+|bg-orange-\d+|bg-amber-\d+|bg-yellow-\d+|bg-lime-\d+|bg-green-\d+|bg-emerald-\d+|bg-teal-\d+|bg-cyan-\d+|bg-sky-\d+|bg-blue-\d+|bg-indigo-\d+|bg-violet-\d+|bg-purple-\d+|bg-fuchsia-\d+|bg-pink-\d+|bg-rose-\d+)$",
89            // Borders
90            r"^(border-0|border-2|border-4|border-8|border|border-t-0|border-t-2|border-t-4|border-t-8|border-t|border-r-0|border-r-2|border-r-4|border-r-8|border-r|border-b-0|border-b-2|border-b-4|border-b-8|border-b|border-l-0|border-l-2|border-l-4|border-l-8|border-l|border-x-0|border-x-2|border-x-4|border-x-8|border-x|border-y-0|border-y-2|border-y-4|border-y-8|border-y|border-solid|border-dashed|border-dotted|border-double|border-none|border-inherit|border-current|border-transparent|border-black|border-white|border-slate-\d+|border-gray-\d+|border-zinc-\d+|border-neutral-\d+|border-stone-\d+|border-red-\d+|border-orange-\d+|border-amber-\d+|border-yellow-\d+|border-lime-\d+|border-green-\d+|border-emerald-\d+|border-teal-\d+|border-cyan-\d+|border-sky-\d+|border-blue-\d+|border-indigo-\d+|border-violet-\d+|border-purple-\d+|border-fuchsia-\d+|border-pink-\d+|border-rose-\d+|rounded-none|rounded-sm|rounded|rounded-md|rounded-lg|rounded-xl|rounded-2xl|rounded-3xl|rounded-full|rounded-t-none|rounded-t-sm|rounded-t|rounded-t-md|rounded-t-lg|rounded-t-xl|rounded-t-2xl|rounded-t-3xl|rounded-t-full|rounded-r-none|rounded-r-sm|rounded-r|rounded-r-md|rounded-r-lg|rounded-r-xl|rounded-r-2xl|rounded-r-3xl|rounded-r-full|rounded-b-none|rounded-b-sm|rounded-b|rounded-b-md|rounded-b-lg|rounded-b-xl|rounded-b-2xl|rounded-b-3xl|rounded-b-full|rounded-l-none|rounded-l-sm|rounded-l|rounded-l-md|rounded-l-lg|rounded-l-xl|rounded-l-2xl|rounded-l-3xl|rounded-l-full|rounded-tl-none|rounded-tl-sm|rounded-tl|rounded-tl-md|rounded-tl-lg|rounded-tl-xl|rounded-tl-2xl|rounded-tl-3xl|rounded-tl-full|rounded-tr-none|rounded-tr-sm|rounded-tr|rounded-tr-md|rounded-tr-lg|rounded-tr-xl|rounded-tr-2xl|rounded-tr-3xl|rounded-tr-full|rounded-br-none|rounded-br-sm|rounded-br|rounded-br-md|rounded-br-lg|rounded-br-xl|rounded-br-2xl|rounded-br-3xl|rounded-br-full|rounded-bl-none|rounded-bl-sm|rounded-bl|rounded-bl-md|rounded-bl-lg|rounded-bl-xl|rounded-bl-2xl|rounded-bl-3xl|rounded-bl-full)$",
91            // Effects
92            r"^(shadow-sm|shadow|shadow-md|shadow-lg|shadow-xl|shadow-2xl|shadow-inner|shadow-none|shadow-inherit|shadow-current|shadow-transparent|shadow-black|shadow-white|shadow-slate-\d+|shadow-gray-\d+|shadow-zinc-\d+|shadow-neutral-\d+|shadow-stone-\d+|shadow-red-\d+|shadow-orange-\d+|shadow-amber-\d+|shadow-yellow-\d+|shadow-lime-\d+|shadow-green-\d+|shadow-emerald-\d+|shadow-teal-\d+|shadow-cyan-\d+|shadow-sky-\d+|shadow-blue-\d+|shadow-indigo-\d+|shadow-violet-\d+|shadow-purple-\d+|shadow-fuchsia-\d+|shadow-pink-\d+|shadow-rose-\d+|opacity-0|opacity-5|opacity-10|opacity-20|opacity-25|opacity-30|opacity-40|opacity-50|opacity-60|opacity-70|opacity-75|opacity-80|opacity-90|opacity-95|opacity-100)$",
93            // Transforms
94            r"^(transform|transform-gpu|transform-none|origin-center|origin-top|origin-top-right|origin-right|origin-bottom-right|origin-bottom|origin-bottom-left|origin-left|origin-top-left|scale-0|scale-50|scale-75|scale-90|scale-95|scale-100|scale-105|scale-110|scale-125|scale-150|scale-x-0|scale-x-50|scale-x-75|scale-x-90|scale-x-95|scale-x-100|scale-x-105|scale-x-110|scale-x-125|scale-x-150|scale-y-0|scale-y-50|scale-y-75|scale-y-90|scale-y-95|scale-y-100|scale-y-105|scale-y-110|scale-y-125|scale-y-150|rotate-0|rotate-1|rotate-2|rotate-3|rotate-6|rotate-12|rotate-45|rotate-90|rotate-180|-rotate-180|-rotate-90|-rotate-45|-rotate-12|-rotate-6|-rotate-3|-rotate-2|-rotate-1|translate-x-0|translate-x-px|translate-x-0\.5|translate-x-1|translate-x-1\.5|translate-x-2|translate-x-2\.5|translate-x-3|translate-x-3\.5|translate-x-4|translate-x-5|translate-x-6|translate-x-7|translate-x-8|translate-x-9|translate-x-10|translate-x-11|translate-x-12|translate-x-14|translate-x-16|translate-x-20|translate-x-24|translate-x-28|translate-x-32|translate-x-36|translate-x-40|translate-x-44|translate-x-48|translate-x-52|translate-x-56|translate-x-60|translate-x-64|translate-x-72|translate-x-80|translate-x-96|translate-x-1\/2|translate-x-1\/3|translate-x-2\/3|translate-x-1\/4|translate-x-2\/4|translate-x-3\/4|translate-x-full|-translate-x-0|-translate-x-px|-translate-x-0\.5|-translate-x-1|-translate-x-1\.5|-translate-x-2|-translate-x-2\.5|-translate-x-3|-translate-x-3\.5|-translate-x-4|-translate-x-5|-translate-x-6|-translate-x-7|-translate-x-8|-translate-x-9|-translate-x-10|-translate-x-11|-translate-x-12|-translate-x-14|-translate-x-16|-translate-x-20|-translate-x-24|-translate-x-28|-translate-x-32|-translate-x-36|-translate-x-40|-translate-x-44|-translate-x-48|-translate-x-52|-translate-x-56|-translate-x-60|-translate-x-64|-translate-x-72|-translate-x-80|-translate-x-96|-translate-x-1\/2|-translate-x-1\/3|-translate-x-2\/3|-translate-x-1\/4|-translate-x-2\/4|-translate-x-3\/4|-translate-x-full|translate-y-0|translate-y-px|translate-y-0\.5|translate-y-1|translate-y-1\.5|translate-y-2|translate-y-2\.5|translate-y-3|translate-y-3\.5|translate-y-4|translate-y-5|translate-y-6|translate-y-7|translate-y-8|translate-y-9|translate-y-10|translate-y-11|translate-y-12|translate-y-14|translate-y-16|translate-y-20|translate-y-24|translate-y-28|translate-y-32|translate-y-36|translate-y-40|translate-y-44|translate-y-48|translate-y-52|translate-y-56|translate-y-60|translate-y-64|translate-y-72|translate-y-80|translate-y-96|translate-y-1\/2|translate-y-1\/3|translate-y-2\/3|translate-y-1\/4|translate-y-2\/4|translate-y-3\/4|translate-y-full|-translate-y-0|-translate-y-px|-translate-y-0\.5|-translate-y-1|-translate-y-1\.5|-translate-y-2|-translate-y-2\.5|-translate-y-3|-translate-y-3\.5|-translate-y-4|-translate-y-5|-translate-y-6|-translate-y-7|-translate-y-8|-translate-y-9|-translate-y-10|-translate-y-11|-translate-y-12|-translate-y-14|-translate-y-16|-translate-y-20|-translate-y-24|-translate-y-28|-translate-y-32|-translate-y-36|-translate-y-40|-translate-y-44|-translate-y-48|-translate-y-52|-translate-y-56|-translate-y-60|-translate-y-64|-translate-y-72|-translate-y-80|-translate-y-96|-translate-y-1\/2|-translate-y-1\/3|-translate-y-2\/3|-translate-y-1\/4|-translate-y-2\/4|-translate-y-3\/4|-translate-y-full|skew-x-0|skew-x-1|skew-x-2|skew-x-3|skew-x-6|skew-x-12|-skew-x-12|-skew-x-6|-skew-x-3|-skew-x-2|-skew-x-1|skew-y-0|skew-y-1|skew-y-2|skew-y-3|skew-y-6|skew-y-12|-skew-y-12|-skew-y-6|-skew-y-3|-skew-y-2|-skew-y-1)$",
95            // Interactivity
96            r"^(appearance-none|appearance-auto|cursor-auto|cursor-default|cursor-pointer|cursor-wait|cursor-text|cursor-move|cursor-help|cursor-not-allowed|pointer-events-none|pointer-events-auto|resize-none|resize-y|resize-x|resize|select-none|select-text|select-all|select-auto)$",
97            // SVG
98            r"^(fill-current|stroke-current|stroke-0|stroke-1|stroke-2)$",
99            // Accessibility
100            r"^(sr-only|not-sr-only|focus-within|focus-visible|focus|focus:outline-none|focus:outline|focus:outline-2|focus:outline-4|focus:outline-8|focus:outline-offset-0|focus:outline-offset-1|focus:outline-offset-2|focus:outline-offset-4|focus:outline-offset-8)$",
101            // Responsive prefixes
102            r"^(sm:|md:|lg:|xl:|2xl:).*$",
103            // State prefixes (removed catch-all to allow more specific patterns)
104            r"^(hover:|focus:|active:|visited:|disabled:|checked:|indeterminate:|required:|valid:|invalid:|in-range:|out-of-range:|read-only:|read-write:|optional:|placeholder-shown:|autofill:|default:|first-child:|last-child:|only-child:|first-of-type:|last-of-type:|only-of-type:|empty:|target:|root:|not:|where:|is:|has:|before:|after:|first-letter:|first-line:|selection:|marker:|placeholder:|file:|backdrop:|any-link:|link:|local-link:|scope:|current:|past:|future:|playing:|paused:|seeking:|buffering:|stalled:|muted:|volume-locked:|user-invalid:|user-valid:|modal:|picture-in-picture:|fullscreen:|resize:|scroll:|snap:|touch:|user-select:|will-change:|accent-color:|appearance:|cursor:|outline:)(bg-|text-|border-|p-|m-|w-|h-|flex|grid|block|hidden).*$",
105            // Dark mode variants
106            r"^dark:.*$",
107            r"^dark:hover:.*$",
108            r"^dark:focus:.*$",
109            r"^dark:active:.*$",
110            r"^dark:disabled:.*$",
111            r"^dark:checked:.*$",
112            r"^dark:group-hover:.*$",
113            r"^dark:group-focus:.*$",
114            // Gradient utilities
115            r"^bg-gradient-to-(r|l|t|b|tr|tl|br|bl)$",
116            r"^from-[a-zA-Z-]+-\d+$",
117            r"^via-[a-zA-Z-]+-\d+$",
118            r"^to-[a-zA-Z-]+-\d+$",
119            // Animation utilities (Enhanced with new animation types)
120            r"^animate-(none|spin|ping|pulse|bounce|fade-in|fade-out|slide-in-left|slide-in-right|slide-in-top|slide-in-bottom|zoom-in|zoom-out|wobble|shake|flip|heartbeat)$",
121            // Enhanced animation controls
122            r"^animation-iteration-count-\d+$",
123            r"^animation-play-state-(paused|running)$",
124            // State-based animations
125            r"^hover:animate-(none|spin|ping|pulse|bounce|fade-in|fade-out|slide-in-left|slide-in-right|slide-in-top|slide-in-bottom|zoom-in|zoom-out|wobble|shake|flip|heartbeat)$",
126            r"^focus:animate-(none|spin|ping|pulse|bounce|fade-in|fade-out|slide-in-left|slide-in-right|slide-in-top|slide-in-bottom|zoom-in|zoom-out|wobble|shake|flip|heartbeat)$",
127            // Transition utilities
128            r"^transition-(none|all|colors|opacity|shadow|transform)$",
129            r"^duration-(75|100|150|200|300|500|700|1000)$",
130            r"^delay-(75|100|150|200|300|500|700|1000)$",
131            r"^ease-(linear|in|out|in-out)$",
132            // Arbitrary values
133            r"^[a-zA-Z-]+-\[[^\]]+\]$",
134        ];
135
136        for pattern in patterns {
137            if let Ok(regex) = regex::Regex::new(pattern) {
138                self.allowed_patterns.push(regex);
139            }
140        }
141    }
142
143    /// Add class conflicts
144    fn add_class_conflicts(&mut self) {
145        // Display conflicts
146        let mut display_conflicts = HashSet::new();
147        display_conflicts.insert("block".to_string());
148        display_conflicts.insert("inline-block".to_string());
149        display_conflicts.insert("inline".to_string());
150        display_conflicts.insert("flex".to_string());
151        display_conflicts.insert("inline-flex".to_string());
152        display_conflicts.insert("grid".to_string());
153        display_conflicts.insert("inline-grid".to_string());
154        display_conflicts.insert("hidden".to_string());
155        self.class_conflicts
156            .insert("display".to_string(), display_conflicts);
157
158        // Position conflicts
159        let mut position_conflicts = HashSet::new();
160        position_conflicts.insert("static".to_string());
161        position_conflicts.insert("fixed".to_string());
162        position_conflicts.insert("absolute".to_string());
163        position_conflicts.insert("relative".to_string());
164        position_conflicts.insert("sticky".to_string());
165        self.class_conflicts
166            .insert("position".to_string(), position_conflicts);
167    }
168
169    /// Add required classes
170    fn add_required_classes(&mut self) {
171        // Grid requires display: grid
172        let mut grid_required = HashSet::new();
173        grid_required.insert("grid".to_string());
174        self.required_classes
175            .insert("grid-cols-".to_string(), grid_required.clone());
176        self.required_classes
177            .insert("grid-rows-".to_string(), grid_required.clone());
178        self.required_classes
179            .insert("gap-".to_string(), grid_required);
180
181        // Flex requires display: flex
182        let mut flex_required = HashSet::new();
183        flex_required.insert("flex".to_string());
184        self.required_classes
185            .insert("flex-".to_string(), flex_required.clone());
186        self.required_classes
187            .insert("items-".to_string(), flex_required.clone());
188        self.required_classes
189            .insert("justify-".to_string(), flex_required);
190    }
191
192    /// Add a custom allowed pattern
193    pub fn add_allowed_pattern(&mut self, pattern: regex::Regex) {
194        self.allowed_patterns.push(pattern);
195    }
196
197    /// Add a custom forbidden pattern
198    pub fn add_forbidden_pattern(&mut self, pattern: regex::Regex) {
199        self.forbidden_patterns.push(pattern);
200    }
201
202    /// Add a deprecated class
203    pub fn add_deprecated_class(&mut self, class: String) {
204        self.deprecated_classes.insert(class);
205    }
206
207    /// Add a class conflict
208    pub fn add_class_conflict(&mut self, group: String, class: String) {
209        self.class_conflicts.entry(group).or_default().insert(class);
210    }
211
212    /// Add a required class
213    pub fn add_required_class(&mut self, class: String, required: String) {
214        self.required_classes
215            .entry(class)
216            .or_default()
217            .insert(required);
218    }
219}
220
221/// Error reporter for validation errors
222pub struct ErrorReporter {
223    /// Whether to report errors
224    pub enabled: bool,
225    /// Error callback
226    #[allow(clippy::type_complexity)]
227    pub callback: Option<Box<dyn Fn(&ValidationError) + Send + Sync>>,
228}
229
230impl Default for ErrorReporter {
231    fn default() -> Self {
232        Self::new()
233    }
234}
235
236impl ErrorReporter {
237    /// Create a new error reporter
238    pub fn new() -> Self {
239        Self {
240            enabled: true,
241            callback: None,
242        }
243    }
244
245    /// Set error callback
246    pub fn set_callback<F>(&mut self, callback: F)
247    where
248        F: Fn(&ValidationError) + Send + Sync + 'static,
249    {
250        self.callback = Some(Box::new(callback));
251    }
252
253    /// Report an error
254    pub fn report(&self, error: &ValidationError) {
255        if self.enabled {
256            if let Some(ref callback) = self.callback {
257                callback(error);
258            }
259        }
260    }
261}
262
263/// Validates Tailwind class names at runtime
264pub struct ClassValidator {
265    #[allow(dead_code)]
266    valid_classes: HashSet<String>,
267    validation_rules: ValidationRules,
268    error_reporter: ErrorReporter,
269    custom_variant_manager: CustomVariantManager,
270}
271
272impl Default for ClassValidator {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278impl ClassValidator {
279    /// Create a new validator instance
280    pub fn new() -> Self {
281        Self {
282            valid_classes: HashSet::new(),
283            validation_rules: ValidationRules::new(),
284            error_reporter: ErrorReporter::new(),
285            custom_variant_manager: CustomVariantManager::with_defaults(),
286        }
287    }
288
289    /// Create a new validator with custom rules
290    pub fn with_rules(validation_rules: ValidationRules) -> Self {
291        Self {
292            valid_classes: HashSet::new(),
293            validation_rules,
294            error_reporter: ErrorReporter::new(),
295            custom_variant_manager: CustomVariantManager::with_defaults(),
296        }
297    }
298
299    /// Create a new validator with custom variant manager
300    pub fn with_custom_variants(custom_variant_manager: CustomVariantManager) -> Self {
301        Self {
302            valid_classes: HashSet::new(),
303            validation_rules: ValidationRules::new(),
304            error_reporter: ErrorReporter::new(),
305            custom_variant_manager,
306        }
307    }
308
309    /// Set error reporter
310    pub fn set_error_reporter(&mut self, error_reporter: ErrorReporter) {
311        self.error_reporter = error_reporter;
312    }
313
314    /// Validate a single class name
315    pub fn validate_class(&self, class_name: &str) -> std::result::Result<(), ValidationError> {
316        // Check if class is deprecated
317        if self
318            .validation_rules
319            .deprecated_classes
320            .contains(class_name)
321        {
322            let error = ValidationError::DeprecatedClass(class_name.to_string());
323            self.error_reporter.report(&error);
324            return Err(error);
325        }
326
327        // Check forbidden patterns
328        for pattern in &self.validation_rules.forbidden_patterns {
329            if pattern.is_match(class_name) {
330                let error = ValidationError::UnsupportedClass(class_name.to_string());
331                self.error_reporter.report(&error);
332                return Err(error);
333            }
334        }
335
336        // Check allowed patterns
337        let mut is_valid = false;
338        for pattern in &self.validation_rules.allowed_patterns {
339            if pattern.is_match(class_name) {
340                is_valid = true;
341                break;
342            }
343        }
344
345        if !is_valid {
346            let error = ValidationError::InvalidClass(class_name.to_string());
347            self.error_reporter.report(&error);
348            return Err(error);
349        }
350
351        Ok(())
352    }
353
354    /// Validate multiple class names
355    pub fn validate_classes(&self, classes: &[String]) -> std::result::Result<(), ValidationError> {
356        for class in classes {
357            self.validate_class(class)?;
358        }
359
360        // Check for class conflicts
361        self.check_class_conflicts(classes)?;
362
363        // Check for required classes
364        self.check_required_classes(classes)?;
365
366        Ok(())
367    }
368
369    /// Validate a custom variant (Tailwind v4.1.13 @custom-variant support)
370    pub fn validate_custom_variant(
371        &self,
372        variant: &str,
373    ) -> std::result::Result<(), ValidationError> {
374        // Use the custom variant manager to validate
375        self.custom_variant_manager
376            .validate_variant(variant)
377            .map_err(|e| ValidationError::CustomVariantValidation(e.to_string()))
378    }
379
380    /// Validate a class with custom variant
381    pub fn validate_variant_class(
382        &self,
383        variant: &str,
384        class: &str,
385    ) -> std::result::Result<(), ValidationError> {
386        // Validate the variant first
387        self.validate_custom_variant(variant)?;
388
389        // Validate the class
390        self.validate_class(class)?;
391
392        Ok(())
393    }
394
395    /// Get suggestions for a custom variant
396    pub fn get_variant_suggestions(&self, partial: &str) -> Vec<String> {
397        self.custom_variant_manager.get_suggestions(partial)
398    }
399
400    /// Register a custom variant
401    pub fn register_custom_variant(
402        &mut self,
403        variant: crate::custom_variant::CustomVariant,
404    ) -> std::result::Result<(), ValidationError> {
405        self.custom_variant_manager
406            .register(variant)
407            .map_err(|e| ValidationError::CustomVariantValidation(e.to_string()))
408    }
409
410    /// Get the custom variant manager
411    pub fn custom_variant_manager(&self) -> &CustomVariantManager {
412        &self.custom_variant_manager
413    }
414
415    /// Get mutable access to the custom variant manager
416    pub fn custom_variant_manager_mut(&mut self) -> &mut CustomVariantManager {
417        &mut self.custom_variant_manager
418    }
419
420    /// Check for class conflicts
421    fn check_class_conflicts(
422        &self,
423        classes: &[String],
424    ) -> std::result::Result<(), ValidationError> {
425        for conflicting_classes in self.validation_rules.class_conflicts.values() {
426            let mut found_classes = Vec::new();
427
428            for class in classes {
429                if conflicting_classes.contains(class) {
430                    found_classes.push(class.clone());
431                }
432            }
433
434            if found_classes.len() > 1 {
435                let error = ValidationError::ClassConflict(
436                    found_classes[0].clone(),
437                    found_classes[1].clone(),
438                );
439                self.error_reporter.report(&error);
440                return Err(error);
441            }
442        }
443
444        Ok(())
445    }
446
447    /// Check for required classes
448    fn check_required_classes(
449        &self,
450        classes: &[String],
451    ) -> std::result::Result<(), ValidationError> {
452        for class in classes {
453            for (required_prefix, required_classes) in &self.validation_rules.required_classes {
454                if class.starts_with(required_prefix) {
455                    let mut has_required = false;
456                    for required_class in required_classes {
457                        if classes.contains(required_class) {
458                            has_required = true;
459                            break;
460                        }
461                    }
462
463                    if !has_required {
464                        let error = ValidationError::InvalidClass(format!(
465                            "Class '{}' requires one of: {}",
466                            class,
467                            required_classes
468                                .iter()
469                                .map(|s| s.as_str())
470                                .collect::<Vec<_>>()
471                                .join(", ")
472                        ));
473                        self.error_reporter.report(&error);
474                        return Err(error);
475                    }
476                }
477            }
478        }
479
480        Ok(())
481    }
482
483    /// Get validation rules
484    pub fn rules(&self) -> &ValidationRules {
485        &self.validation_rules
486    }
487
488    /// Get mutable validation rules
489    pub fn rules_mut(&mut self) -> &mut ValidationRules {
490        &mut self.validation_rules
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    #[test]
499    fn test_validation_rules_creation() {
500        let rules = ValidationRules::new();
501        assert!(!rules.allowed_patterns.is_empty());
502        assert!(!rules.class_conflicts.is_empty());
503        assert!(!rules.required_classes.is_empty());
504    }
505
506    #[test]
507    fn test_class_validator_creation() {
508        let validator = ClassValidator::new();
509        assert!(!validator.rules().allowed_patterns.is_empty());
510    }
511
512    #[test]
513    fn test_validate_valid_class() {
514        let validator = ClassValidator::new();
515        assert!(validator.validate_class("bg-blue-600").is_ok());
516        assert!(validator.validate_class("text-white").is_ok());
517        assert!(validator.validate_class("px-4").is_ok());
518        assert!(validator.validate_class("py-2").is_ok());
519    }
520
521    #[test]
522    fn test_validate_arbitrary_values() {
523        let validator = ClassValidator::new();
524        // Test arbitrary values
525        assert!(validator.validate_class("w-[123px]").is_ok());
526        assert!(validator.validate_class("bg-[#ff0000]").is_ok());
527        assert!(validator.validate_class("text-[14px]").is_ok());
528        assert!(validator.validate_class("p-[1.5rem]").is_ok());
529        assert!(validator.validate_class("m-[2rem]").is_ok());
530        assert!(validator.validate_class("rounded-[8px]").is_ok());
531        assert!(validator
532            .validate_class("shadow-[0_4px_6px_rgba(0,0,0,0.1)]")
533            .is_ok());
534        assert!(validator.validate_class("opacity-[0.5]").is_ok());
535        assert!(validator.validate_class("z-[999]").is_ok());
536        assert!(validator.validate_class("top-[10px]").is_ok());
537        assert!(validator.validate_class("right-[20px]").is_ok());
538        assert!(validator.validate_class("bottom-[30px]").is_ok());
539        assert!(validator.validate_class("left-[40px]").is_ok());
540    }
541
542    #[test]
543    fn test_validate_dark_mode_variants() {
544        let validator = ClassValidator::new();
545        // Test dark mode variants
546        assert!(validator.validate_class("dark:bg-gray-800").is_ok());
547        assert!(validator.validate_class("dark:text-white").is_ok());
548        assert!(validator.validate_class("dark:border-gray-700").is_ok());
549        assert!(validator.validate_class("dark:hover:bg-gray-700").is_ok());
550        assert!(validator.validate_class("dark:focus:bg-gray-600").is_ok());
551        assert!(validator.validate_class("dark:active:bg-gray-500").is_ok());
552        assert!(validator
553            .validate_class("dark:disabled:bg-gray-400")
554            .is_ok());
555        assert!(validator.validate_class("dark:checked:bg-gray-300").is_ok());
556        assert!(validator
557            .validate_class("dark:group-hover:bg-gray-200")
558            .is_ok());
559        assert!(validator
560            .validate_class("dark:group-focus:bg-gray-100")
561            .is_ok());
562    }
563
564    #[test]
565    fn test_validate_gradient_variants() {
566        let validator = ClassValidator::new();
567        // Test gradient variants
568        assert!(validator.validate_class("bg-gradient-to-r").is_ok());
569        assert!(validator.validate_class("bg-gradient-to-l").is_ok());
570        assert!(validator.validate_class("bg-gradient-to-t").is_ok());
571        assert!(validator.validate_class("bg-gradient-to-b").is_ok());
572        assert!(validator.validate_class("bg-gradient-to-tr").is_ok());
573        assert!(validator.validate_class("bg-gradient-to-tl").is_ok());
574        assert!(validator.validate_class("bg-gradient-to-br").is_ok());
575        assert!(validator.validate_class("bg-gradient-to-bl").is_ok());
576        assert!(validator.validate_class("from-blue-500").is_ok());
577        assert!(validator.validate_class("via-purple-500").is_ok());
578        assert!(validator.validate_class("to-pink-500").is_ok());
579    }
580
581    #[test]
582    fn test_validate_fractional_spacing() {
583        let validator = ClassValidator::new();
584        // Test fractional spacing values
585        assert!(validator.validate_class("p-0.5").is_ok());
586        assert!(validator.validate_class("p-1.5").is_ok());
587        assert!(validator.validate_class("p-2.5").is_ok());
588        assert!(validator.validate_class("p-3.5").is_ok());
589        assert!(validator.validate_class("m-0.5").is_ok());
590        assert!(validator.validate_class("m-1.5").is_ok());
591        assert!(validator.validate_class("m-2.5").is_ok());
592        assert!(validator.validate_class("m-3.5").is_ok());
593        assert!(validator.validate_class("px-0.5").is_ok());
594        assert!(validator.validate_class("py-1.5").is_ok());
595        assert!(validator.validate_class("pt-2.5").is_ok());
596        assert!(validator.validate_class("pr-3.5").is_ok());
597        assert!(validator.validate_class("pb-0.5").is_ok());
598        assert!(validator.validate_class("pl-1.5").is_ok());
599        assert!(validator.validate_class("mx-2.5").is_ok());
600        assert!(validator.validate_class("my-3.5").is_ok());
601        assert!(validator.validate_class("mt-0.5").is_ok());
602        assert!(validator.validate_class("mr-1.5").is_ok());
603        assert!(validator.validate_class("mb-2.5").is_ok());
604        assert!(validator.validate_class("ml-3.5").is_ok());
605    }
606
607    #[test]
608    fn test_validate_animation_system() {
609        let validator = ClassValidator::new();
610        // Test animation classes
611        assert!(validator.validate_class("animate-none").is_ok());
612        assert!(validator.validate_class("animate-spin").is_ok());
613        assert!(validator.validate_class("animate-ping").is_ok());
614        assert!(validator.validate_class("animate-pulse").is_ok());
615        assert!(validator.validate_class("animate-bounce").is_ok());
616
617        // Test transition classes
618        assert!(validator.validate_class("transition-none").is_ok());
619        assert!(validator.validate_class("transition-all").is_ok());
620        assert!(validator.validate_class("transition-colors").is_ok());
621        assert!(validator.validate_class("transition-opacity").is_ok());
622        assert!(validator.validate_class("transition-shadow").is_ok());
623        assert!(validator.validate_class("transition-transform").is_ok());
624
625        // Test transition duration classes
626        assert!(validator.validate_class("duration-75").is_ok());
627        assert!(validator.validate_class("duration-100").is_ok());
628        assert!(validator.validate_class("duration-150").is_ok());
629        assert!(validator.validate_class("duration-200").is_ok());
630        assert!(validator.validate_class("duration-300").is_ok());
631        assert!(validator.validate_class("duration-500").is_ok());
632        assert!(validator.validate_class("duration-700").is_ok());
633        assert!(validator.validate_class("duration-1000").is_ok());
634
635        // Test transition delay classes
636        assert!(validator.validate_class("delay-75").is_ok());
637        assert!(validator.validate_class("delay-100").is_ok());
638        assert!(validator.validate_class("delay-150").is_ok());
639        assert!(validator.validate_class("delay-200").is_ok());
640        assert!(validator.validate_class("delay-300").is_ok());
641        assert!(validator.validate_class("delay-500").is_ok());
642        assert!(validator.validate_class("delay-700").is_ok());
643        assert!(validator.validate_class("delay-1000").is_ok());
644
645        // Test transition timing function classes
646        assert!(validator.validate_class("ease-linear").is_ok());
647        assert!(validator.validate_class("ease-in").is_ok());
648        assert!(validator.validate_class("ease-out").is_ok());
649        assert!(validator.validate_class("ease-in-out").is_ok());
650    }
651
652    #[test]
653    fn test_validate_invalid_class() {
654        let validator = ClassValidator::new();
655        assert!(validator.validate_class("invalid-class").is_err());
656        assert!(validator.validate_class("bg-invalid-color").is_err());
657    }
658
659    #[test]
660    fn test_validate_deprecated_class() {
661        let mut validator = ClassValidator::new();
662        validator
663            .rules_mut()
664            .add_deprecated_class("deprecated-class".to_string());
665
666        assert!(validator.validate_class("deprecated-class").is_err());
667    }
668
669    #[test]
670    fn test_validate_class_conflicts() {
671        let validator = ClassValidator::new();
672        let classes = vec!["block".to_string(), "flex".to_string()];
673
674        assert!(validator.validate_classes(&classes).is_err());
675    }
676
677    #[test]
678    fn test_validate_required_classes() {
679        let validator = ClassValidator::new();
680        let classes = vec!["grid-cols-2".to_string()];
681
682        // This should fail because grid-cols-2 requires display: grid
683        assert!(validator.validate_classes(&classes).is_err());
684    }
685
686    #[test]
687    fn test_validate_multiple_classes() {
688        let validator = ClassValidator::new();
689        let classes = vec![
690            "bg-blue-600".to_string(),
691            "text-white".to_string(),
692            "px-4".to_string(),
693            "py-2".to_string(),
694        ];
695
696        assert!(validator.validate_classes(&classes).is_ok());
697    }
698
699    #[test]
700    fn test_error_reporter() {
701        let mut reporter = ErrorReporter::new();
702        let error_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
703        let error_count_clone = error_count.clone();
704
705        reporter.set_callback(move |_error| {
706            error_count_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
707        });
708
709        let error = ValidationError::InvalidClass("test".to_string());
710        reporter.report(&error);
711
712        assert_eq!(error_count.load(std::sync::atomic::Ordering::Relaxed), 1);
713    }
714
715    #[test]
716    fn test_validation_error_display() {
717        let error = ValidationError::InvalidClass("test".to_string());
718        assert_eq!(format!("{}", error), "Invalid class name: test");
719
720        let error = ValidationError::ClassConflict("class1".to_string(), "class2".to_string());
721        assert_eq!(
722            format!("{}", error),
723            "Class conflict: class1 conflicts with class2"
724        );
725
726        let error = ValidationError::DeprecatedClass("deprecated".to_string());
727        assert_eq!(format!("{}", error), "Deprecated class: deprecated");
728
729        let error = ValidationError::UnsupportedClass("unsupported".to_string());
730        assert_eq!(format!("{}", error), "Unsupported class: unsupported");
731    }
732
733    #[test]
734    fn test_validate_enhanced_animations() {
735        let validator = ClassValidator::new();
736
737        // Valid basic animations
738        assert!(validator.validate_class("animate-spin").is_ok());
739        assert!(validator.validate_class("animate-bounce").is_ok());
740        assert!(validator.validate_class("animate-pulse").is_ok());
741
742        // Valid extended animations
743        assert!(validator.validate_class("animate-fade-in").is_ok());
744        assert!(validator.validate_class("animate-fade-out").is_ok());
745        assert!(validator.validate_class("animate-slide-in-left").is_ok());
746        assert!(validator.validate_class("animate-slide-in-right").is_ok());
747        assert!(validator.validate_class("animate-slide-in-top").is_ok());
748        assert!(validator.validate_class("animate-slide-in-bottom").is_ok());
749        assert!(validator.validate_class("animate-zoom-in").is_ok());
750        assert!(validator.validate_class("animate-zoom-out").is_ok());
751        assert!(validator.validate_class("animate-wobble").is_ok());
752        assert!(validator.validate_class("animate-shake").is_ok());
753        assert!(validator.validate_class("animate-flip").is_ok());
754        assert!(validator.validate_class("animate-heartbeat").is_ok());
755
756        // Valid animation controls
757        assert!(validator
758            .validate_class("animation-iteration-count-1")
759            .is_ok());
760        assert!(validator
761            .validate_class("animation-iteration-count-3")
762            .is_ok());
763        assert!(validator
764            .validate_class("animation-play-state-paused")
765            .is_ok());
766        assert!(validator
767            .validate_class("animation-play-state-running")
768            .is_ok());
769
770        // Valid state-based animations
771        assert!(validator.validate_class("hover:animate-bounce").is_ok());
772        assert!(validator.validate_class("focus:animate-pulse").is_ok());
773        assert!(validator.validate_class("hover:animate-fade-in").is_ok());
774        assert!(validator
775            .validate_class("focus:animate-slide-in-left")
776            .is_ok());
777
778        // Invalid animation classes
779        assert!(validator.validate_class("animate-invalid").is_err());
780        assert!(validator
781            .validate_class("animate-unknown-animation")
782            .is_err());
783        assert!(validator.validate_class("hover:animate-invalid").is_err());
784    }
785
786    #[test]
787    fn test_validate_enhanced_spacing() {
788        let validator = ClassValidator::new();
789
790        // Valid enhanced spacing with fractional values
791        assert!(validator.validate_class("p-0.5").is_ok());
792        assert!(validator.validate_class("p-1.5").is_ok());
793        assert!(validator.validate_class("p-2.5").is_ok());
794        assert!(validator.validate_class("p-3.5").is_ok());
795        assert!(validator.validate_class("m-0.5").is_ok());
796        assert!(validator.validate_class("m-1.5").is_ok());
797
798        // Valid enhanced spacing with new directions
799        assert!(validator.validate_class("ps-4").is_ok());
800        assert!(validator.validate_class("pe-4").is_ok());
801        assert!(validator.validate_class("ms-4").is_ok());
802        assert!(validator.validate_class("me-4").is_ok());
803
804        // Valid negative margins
805        assert!(validator.validate_class("-m-4").is_ok());
806        assert!(validator.validate_class("-mt-2").is_ok());
807        assert!(validator.validate_class("-mx-3").is_ok());
808
809        // Valid space and divide utilities
810        assert!(validator.validate_class("space-x-4").is_ok());
811        assert!(validator.validate_class("space-y-2").is_ok());
812        assert!(validator.validate_class("space-x-reverse").is_ok());
813        assert!(validator.validate_class("space-y-reverse").is_ok());
814        assert!(validator.validate_class("divide-x-2").is_ok());
815        assert!(validator.validate_class("divide-y-4").is_ok());
816        assert!(validator.validate_class("divide-x-reverse").is_ok());
817        assert!(validator.validate_class("divide-y-reverse").is_ok());
818
819        // Valid fractional space utilities
820        assert!(validator.validate_class("space-x-0.5").is_ok());
821        assert!(validator.validate_class("space-y-1.5").is_ok());
822        assert!(validator.validate_class("divide-x-0.5").is_ok());
823        assert!(validator.validate_class("divide-y-2.5").is_ok());
824    }
825
826    #[test]
827    fn test_validate_comprehensive_modern_features() {
828        let validator = ClassValidator::new();
829
830        // Test comprehensive validation of all new features
831        let modern_classes = vec![
832            // Enhanced animations
833            "animate-fade-in",
834            "animate-slide-in-right",
835            "animate-zoom-out",
836            "animate-heartbeat",
837            "hover:animate-wobble",
838            "focus:animate-shake",
839            "animation-iteration-count-2",
840            "animation-play-state-paused",
841            // Enhanced spacing
842            "p-1.5",
843            "m-2.5",
844            "ps-4",
845            "me-6",
846            "-mx-8",
847            "space-x-0.5",
848            "divide-y-3.5",
849            "space-x-reverse",
850            "divide-y-reverse",
851            // Dark mode variants
852            "dark:bg-gray-800",
853            "dark:text-white",
854            "dark:hover:bg-gray-700",
855            "dark:focus:text-gray-100",
856            // Gradients
857            "bg-gradient-to-r",
858            "from-blue-500",
859            "via-purple-500",
860            "to-pink-500",
861            // Arbitrary values
862            "w-[123px]",
863            "bg-[#ff0000]",
864            "text-[14px]",
865            "p-[1.5rem]",
866        ];
867
868        for class in modern_classes {
869            assert!(
870                validator.validate_class(class).is_ok(),
871                "Failed to validate modern class: {}",
872                class
873            );
874        }
875    }
876}