1use crate::error::{Result, TailwindError};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::fmt;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum CustomVariantType {
14 Aria,
16 Data,
18 Supports,
20 Container,
22 Media,
24 Custom(String),
26}
27
28impl CustomVariantType {
29 pub fn prefix(&self) -> &'static str {
31 match self {
32 CustomVariantType::Aria => "aria-",
33 CustomVariantType::Data => "data-",
34 CustomVariantType::Supports => "supports-",
35 CustomVariantType::Container => "container-",
36 CustomVariantType::Media => "media-",
37 CustomVariantType::Custom(_name) => {
38 ""
41 }
42 }
43 }
44
45 pub fn validate_name(name: &str) -> Result<()> {
47 if name.is_empty() {
48 return Err(TailwindError::validation("Custom variant name cannot be empty"));
49 }
50
51 if name.starts_with('-') || name.starts_with('_') ||
53 name.ends_with('-') || name.ends_with('_') {
54 return Err(TailwindError::validation(
55 format!("Custom variant '{}' cannot start or end with '-' or '_'", name)
56 ));
57 }
58
59 if name.starts_with("@-") {
61 return Err(TailwindError::validation(
62 format!("Custom variant '{}' cannot start with '@-'", name)
63 ));
64 }
65
66 Ok(())
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct CustomVariant {
73 pub variant_type: CustomVariantType,
75 pub name: String,
77 pub value: Option<String>,
79 pub enabled: bool,
81 pub config: HashMap<String, serde_json::Value>,
83}
84
85impl CustomVariant {
86 pub fn new(variant_type: CustomVariantType, name: String, value: Option<String>) -> Result<Self> {
88 CustomVariantType::validate_name(&name)?;
90
91 Ok(Self {
92 variant_type,
93 name,
94 value,
95 enabled: true,
96 config: HashMap::new(),
97 })
98 }
99
100 pub fn aria(name: String, value: Option<String>) -> Result<Self> {
102 Self::new(CustomVariantType::Aria, name, value)
103 }
104
105 pub fn data(name: String, value: Option<String>) -> Result<Self> {
107 Self::new(CustomVariantType::Data, name, value)
108 }
109
110 pub fn supports(name: String, value: Option<String>) -> Result<Self> {
112 Self::new(CustomVariantType::Supports, name, value)
113 }
114
115 pub fn container(name: String, value: Option<String>) -> Result<Self> {
117 Self::new(CustomVariantType::Container, name, value)
118 }
119
120 pub fn media(name: String, value: Option<String>) -> Result<Self> {
122 Self::new(CustomVariantType::Media, name, value)
123 }
124
125 pub fn custom(name: String, value: Option<String>) -> Result<Self> {
127 Self::new(CustomVariantType::Custom(name.clone()), name, value)
128 }
129
130 pub fn to_variant_string(&self) -> String {
132 let prefix = self.variant_type.prefix();
133 let base = format!("{}{}", prefix, self.name);
134
135 if let Some(value) = &self.value {
136 format!("{}={}", base, value)
137 } else {
138 base
139 }
140 }
141
142 pub fn to_css_selector(&self) -> String {
144 match &self.variant_type {
145 CustomVariantType::Aria => {
146 if let Some(value) = &self.value {
147 format!("[aria-{}={}]", self.name, value)
148 } else {
149 format!("[aria-{}]", self.name)
150 }
151 }
152 CustomVariantType::Data => {
153 if let Some(value) = &self.value {
154 format!("[data-{}={}]", self.name, value)
155 } else {
156 format!("[data-{}]", self.name)
157 }
158 }
159 CustomVariantType::Supports => {
160 format!("@supports ({})", self.name)
161 }
162 CustomVariantType::Container => {
163 format!("@container {}", self.name)
164 }
165 CustomVariantType::Media => {
166 format!("@media {}", self.name)
167 }
168 CustomVariantType::Custom(name) => {
169 name.clone()
171 }
172 }
173 }
174
175 pub fn enable(&mut self) {
177 self.enabled = true;
178 }
179
180 pub fn disable(&mut self) {
182 self.enabled = false;
183 }
184
185 pub fn set_config(&mut self, key: String, value: serde_json::Value) {
187 self.config.insert(key, value);
188 }
189
190 pub fn get_config(&self, key: &str) -> Option<&serde_json::Value> {
192 self.config.get(key)
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198pub struct CustomVariantManager {
199 variants: HashMap<String, CustomVariant>,
201 known_values: HashMap<String, HashSet<String>>,
203}
204
205impl CustomVariantManager {
206 pub fn new() -> Self {
208 Self {
209 variants: HashMap::new(),
210 known_values: HashMap::new(),
211 }
212 }
213
214 pub fn register(&mut self, variant: CustomVariant) -> Result<()> {
216 let key = variant.to_variant_string();
217
218 if self.variants.contains_key(&key) {
220 return Err(TailwindError::validation(
221 format!("Custom variant '{}' is already registered", key)
222 ));
223 }
224
225 self.variants.insert(key, variant);
226 Ok(())
227 }
228
229 pub fn get(&self, key: &str) -> Option<&CustomVariant> {
231 self.variants.get(key)
232 }
233
234 pub fn get_all(&self) -> &HashMap<String, CustomVariant> {
236 &self.variants
237 }
238
239 pub fn get_by_type(&self, variant_type: &CustomVariantType) -> Vec<&CustomVariant> {
241 self.variants
242 .values()
243 .filter(|v| &v.variant_type == variant_type)
244 .collect()
245 }
246
247 pub fn remove(&mut self, key: &str) -> Option<CustomVariant> {
249 self.variants.remove(key)
250 }
251
252 pub fn contains(&self, key: &str) -> bool {
254 self.variants.contains_key(key)
255 }
256
257 pub fn add_known_values(&mut self, variant_key: String, values: HashSet<String>) {
259 self.known_values.insert(variant_key, values);
260 }
261
262 pub fn get_known_values(&self, variant_key: &str) -> Option<&HashSet<String>> {
264 self.known_values.get(variant_key)
265 }
266
267 pub fn get_suggestions(&self, partial: &str) -> Vec<String> {
269 let mut suggestions = Vec::new();
270
271 for key in self.variants.keys() {
273 if key.starts_with(partial) {
274 suggestions.push(key.clone());
275 }
276 }
277
278 for (variant_key, values) in &self.known_values {
280 if variant_key.starts_with(partial) {
281 for value in values {
282 suggestions.push(format!("{}={}", variant_key, value));
283 }
284 }
285 }
286
287 suggestions.sort();
288 suggestions.dedup();
289 suggestions
290 }
291
292 pub fn validate_variant(&self, variant: &str) -> Result<()> {
294 if self.variants.contains_key(variant) {
296 return Ok(());
297 }
298
299 if variant.starts_with("aria-") ||
301 variant.starts_with("data-") ||
302 variant.starts_with("supports-") ||
303 variant.starts_with("container-") ||
304 variant.starts_with("media-") {
305 return Ok(());
306 }
307
308 if variant.starts_with("@-") {
310 return Err(TailwindError::validation(
311 format!("Variant '{}' cannot start with '@-'", variant)
312 ));
313 }
314
315 if variant.starts_with('-') || variant.starts_with('_') ||
316 variant.ends_with('-') || variant.ends_with('_') {
317 return Err(TailwindError::validation(
318 format!("Variant '{}' cannot start or end with '-' or '_'", variant)
319 ));
320 }
321
322 Ok(())
323 }
324
325 pub fn with_defaults() -> Self {
327 let mut manager = Self::new();
328
329 let aria_variants = vec![
331 ("checked", None),
332 ("disabled", None),
333 ("expanded", None),
334 ("hidden", None),
335 ("pressed", None),
336 ("required", None),
337 ("selected", None),
338 ];
339
340 for (name, value) in aria_variants {
341 if let Ok(variant) = CustomVariant::aria(name.to_string(), value) {
342 let _ = manager.register(variant);
343 }
344 }
345
346 let data_variants = vec![
348 ("theme", Some("dark".to_string())),
349 ("theme", Some("light".to_string())),
350 ("state", Some("loading".to_string())),
351 ("state", Some("error".to_string())),
352 ];
353
354 for (name, value) in data_variants {
355 if let Ok(variant) = CustomVariant::data(name.to_string(), value) {
356 let _ = manager.register(variant);
357 }
358 }
359
360 let supports_variants = vec![
362 ("backdrop-filter", None),
363 ("grid", None),
364 ("flexbox", None),
365 ];
366
367 for (name, value) in supports_variants {
368 if let Ok(variant) = CustomVariant::supports(name.to_string(), value) {
369 let _ = manager.register(variant);
370 }
371 }
372
373 manager
374 }
375}
376
377impl Default for CustomVariantManager {
378 fn default() -> Self {
379 Self::with_defaults()
380 }
381}
382
383impl fmt::Display for CustomVariant {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 write!(f, "{}", self.to_variant_string())
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn test_custom_variant_creation() {
395 let variant = CustomVariant::aria("checked".to_string(), None).unwrap();
396 assert_eq!(variant.to_variant_string(), "aria-checked");
397 assert_eq!(variant.to_css_selector(), "[aria-checked]");
398 }
399
400 #[test]
401 fn test_custom_variant_with_value() {
402 let variant = CustomVariant::data("theme".to_string(), Some("dark".to_string())).unwrap();
403 assert_eq!(variant.to_variant_string(), "data-theme=dark");
404 assert_eq!(variant.to_css_selector(), "[data-theme=dark]");
405 }
406
407 #[test]
408 fn test_custom_variant_validation() {
409 assert!(CustomVariantType::validate_name("valid-name").is_ok());
411 assert!(CustomVariantType::validate_name("valid_name").is_ok());
412 assert!(CustomVariantType::validate_name("validname").is_ok());
413
414 assert!(CustomVariantType::validate_name("-invalid").is_err());
416 assert!(CustomVariantType::validate_name("invalid-").is_err());
417 assert!(CustomVariantType::validate_name("_invalid").is_err());
418 assert!(CustomVariantType::validate_name("invalid_").is_err());
419 }
420
421 #[test]
422 fn test_custom_variant_manager() {
423 let mut manager = CustomVariantManager::new();
424
425 let variant = CustomVariant::aria("checked".to_string(), None).unwrap();
426 manager.register(variant).unwrap();
427
428 assert!(manager.contains("aria-checked"));
429 assert!(manager.get("aria-checked").is_some());
430 }
431
432 #[test]
433 fn test_custom_variant_suggestions() {
434 let mut manager = CustomVariantManager::with_defaults();
435
436 let suggestions = manager.get_suggestions("aria-");
437 assert!(!suggestions.is_empty());
438 assert!(suggestions.contains(&"aria-checked".to_string()));
439 }
440
441 #[test]
442 fn test_custom_variant_validation_in_manager() {
443 let manager = CustomVariantManager::with_defaults();
444
445 assert!(manager.validate_variant("aria-checked").is_ok());
447 assert!(manager.validate_variant("data-theme=dark").is_ok());
448
449 assert!(manager.validate_variant("@-invalid").is_err());
451 assert!(manager.validate_variant("-invalid").is_err());
452 assert!(manager.validate_variant("invalid-").is_err());
453 }
454}