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(
49 "Custom variant name cannot be empty",
50 ));
51 }
52
53 if name.starts_with('-')
55 || name.starts_with('_')
56 || name.ends_with('-')
57 || name.ends_with('_')
58 {
59 return Err(TailwindError::validation(format!(
60 "Custom variant '{}' cannot start or end with '-' or '_'",
61 name
62 )));
63 }
64
65 if name.starts_with("@-") {
67 return Err(TailwindError::validation(format!(
68 "Custom variant '{}' cannot start with '@-'",
69 name
70 )));
71 }
72
73 Ok(())
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub struct CustomVariant {
80 pub variant_type: CustomVariantType,
82 pub name: String,
84 pub value: Option<String>,
86 pub enabled: bool,
88 pub config: HashMap<String, serde_json::Value>,
90}
91
92impl CustomVariant {
93 pub fn new(
95 variant_type: CustomVariantType,
96 name: String,
97 value: Option<String>,
98 ) -> Result<Self> {
99 CustomVariantType::validate_name(&name)?;
101
102 Ok(Self {
103 variant_type,
104 name,
105 value,
106 enabled: true,
107 config: HashMap::new(),
108 })
109 }
110
111 pub fn aria(name: String, value: Option<String>) -> Result<Self> {
113 Self::new(CustomVariantType::Aria, name, value)
114 }
115
116 pub fn data(name: String, value: Option<String>) -> Result<Self> {
118 Self::new(CustomVariantType::Data, name, value)
119 }
120
121 pub fn supports(name: String, value: Option<String>) -> Result<Self> {
123 Self::new(CustomVariantType::Supports, name, value)
124 }
125
126 pub fn container(name: String, value: Option<String>) -> Result<Self> {
128 Self::new(CustomVariantType::Container, name, value)
129 }
130
131 pub fn media(name: String, value: Option<String>) -> Result<Self> {
133 Self::new(CustomVariantType::Media, name, value)
134 }
135
136 pub fn custom(name: String, value: Option<String>) -> Result<Self> {
138 Self::new(CustomVariantType::Custom(name.clone()), name, value)
139 }
140
141 pub fn to_variant_string(&self) -> String {
143 let prefix = self.variant_type.prefix();
144 let base = format!("{}{}", prefix, self.name);
145
146 if let Some(value) = &self.value {
147 format!("{}={}", base, value)
148 } else {
149 base
150 }
151 }
152
153 pub fn to_css_selector(&self) -> String {
155 match &self.variant_type {
156 CustomVariantType::Aria => {
157 if let Some(value) = &self.value {
158 format!("[aria-{}={}]", self.name, value)
159 } else {
160 format!("[aria-{}]", self.name)
161 }
162 }
163 CustomVariantType::Data => {
164 if let Some(value) = &self.value {
165 format!("[data-{}={}]", self.name, value)
166 } else {
167 format!("[data-{}]", self.name)
168 }
169 }
170 CustomVariantType::Supports => {
171 format!("@supports ({})", self.name)
172 }
173 CustomVariantType::Container => {
174 format!("@container {}", self.name)
175 }
176 CustomVariantType::Media => {
177 format!("@media {}", self.name)
178 }
179 CustomVariantType::Custom(name) => {
180 name.clone()
182 }
183 }
184 }
185
186 pub fn enable(&mut self) {
188 self.enabled = true;
189 }
190
191 pub fn disable(&mut self) {
193 self.enabled = false;
194 }
195
196 pub fn set_config(&mut self, key: String, value: serde_json::Value) {
198 self.config.insert(key, value);
199 }
200
201 pub fn get_config(&self, key: &str) -> Option<&serde_json::Value> {
203 self.config.get(key)
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209pub struct CustomVariantManager {
210 variants: HashMap<String, CustomVariant>,
212 known_values: HashMap<String, HashSet<String>>,
214}
215
216impl CustomVariantManager {
217 pub fn new() -> Self {
219 Self {
220 variants: HashMap::new(),
221 known_values: HashMap::new(),
222 }
223 }
224
225 pub fn register(&mut self, variant: CustomVariant) -> Result<()> {
227 let key = variant.to_variant_string();
228
229 if self.variants.contains_key(&key) {
231 return Err(TailwindError::validation(format!(
232 "Custom variant '{}' is already registered",
233 key
234 )));
235 }
236
237 self.variants.insert(key, variant);
238 Ok(())
239 }
240
241 pub fn get(&self, key: &str) -> Option<&CustomVariant> {
243 self.variants.get(key)
244 }
245
246 pub fn get_all(&self) -> &HashMap<String, CustomVariant> {
248 &self.variants
249 }
250
251 pub fn get_by_type(&self, variant_type: &CustomVariantType) -> Vec<&CustomVariant> {
253 self.variants
254 .values()
255 .filter(|v| &v.variant_type == variant_type)
256 .collect()
257 }
258
259 pub fn remove(&mut self, key: &str) -> Option<CustomVariant> {
261 self.variants.remove(key)
262 }
263
264 pub fn contains(&self, key: &str) -> bool {
266 self.variants.contains_key(key)
267 }
268
269 pub fn add_known_values(&mut self, variant_key: String, values: HashSet<String>) {
271 self.known_values.insert(variant_key, values);
272 }
273
274 pub fn get_known_values(&self, variant_key: &str) -> Option<&HashSet<String>> {
276 self.known_values.get(variant_key)
277 }
278
279 pub fn get_suggestions(&self, partial: &str) -> Vec<String> {
281 let mut suggestions = Vec::new();
282
283 for key in self.variants.keys() {
285 if key.starts_with(partial) {
286 suggestions.push(key.clone());
287 }
288 }
289
290 for (variant_key, values) in &self.known_values {
292 if variant_key.starts_with(partial) {
293 for value in values {
294 suggestions.push(format!("{}={}", variant_key, value));
295 }
296 }
297 }
298
299 suggestions.sort();
300 suggestions.dedup();
301 suggestions
302 }
303
304 pub fn validate_variant(&self, variant: &str) -> Result<()> {
306 if self.variants.contains_key(variant) {
308 return Ok(());
309 }
310
311 if variant.starts_with("aria-")
313 || variant.starts_with("data-")
314 || variant.starts_with("supports-")
315 || variant.starts_with("container-")
316 || variant.starts_with("media-")
317 {
318 return Ok(());
319 }
320
321 if variant.starts_with("@-") {
323 return Err(TailwindError::validation(format!(
324 "Variant '{}' cannot start with '@-'",
325 variant
326 )));
327 }
328
329 if variant.starts_with('-')
330 || variant.starts_with('_')
331 || variant.ends_with('-')
332 || variant.ends_with('_')
333 {
334 return Err(TailwindError::validation(format!(
335 "Variant '{}' cannot start or end with '-' or '_'",
336 variant
337 )));
338 }
339
340 Ok(())
341 }
342
343 pub fn with_defaults() -> Self {
345 let mut manager = Self::new();
346
347 let aria_variants = vec![
349 ("checked", None),
350 ("disabled", None),
351 ("expanded", None),
352 ("hidden", None),
353 ("pressed", None),
354 ("required", None),
355 ("selected", None),
356 ];
357
358 for (name, value) in aria_variants {
359 if let Ok(variant) = CustomVariant::aria(name.to_string(), value) {
360 let _ = manager.register(variant);
361 }
362 }
363
364 let data_variants = vec![
366 ("theme", Some("dark".to_string())),
367 ("theme", Some("light".to_string())),
368 ("state", Some("loading".to_string())),
369 ("state", Some("error".to_string())),
370 ];
371
372 for (name, value) in data_variants {
373 if let Ok(variant) = CustomVariant::data(name.to_string(), value) {
374 let _ = manager.register(variant);
375 }
376 }
377
378 let supports_variants = vec![("backdrop-filter", None), ("grid", None), ("flexbox", None)];
380
381 for (name, value) in supports_variants {
382 if let Ok(variant) = CustomVariant::supports(name.to_string(), value) {
383 let _ = manager.register(variant);
384 }
385 }
386
387 manager
388 }
389}
390
391impl Default for CustomVariantManager {
392 fn default() -> Self {
393 Self::with_defaults()
394 }
395}
396
397impl fmt::Display for CustomVariant {
398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399 write!(f, "{}", self.to_variant_string())
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_custom_variant_creation() {
409 let variant = CustomVariant::aria("checked".to_string(), None).unwrap();
410 assert_eq!(variant.to_variant_string(), "aria-checked");
411 assert_eq!(variant.to_css_selector(), "[aria-checked]");
412 }
413
414 #[test]
415 fn test_custom_variant_with_value() {
416 let variant = CustomVariant::data("theme".to_string(), Some("dark".to_string())).unwrap();
417 assert_eq!(variant.to_variant_string(), "data-theme=dark");
418 assert_eq!(variant.to_css_selector(), "[data-theme=dark]");
419 }
420
421 #[test]
422 fn test_custom_variant_validation() {
423 assert!(CustomVariantType::validate_name("valid-name").is_ok());
425 assert!(CustomVariantType::validate_name("valid_name").is_ok());
426 assert!(CustomVariantType::validate_name("validname").is_ok());
427
428 assert!(CustomVariantType::validate_name("-invalid").is_err());
430 assert!(CustomVariantType::validate_name("invalid-").is_err());
431 assert!(CustomVariantType::validate_name("_invalid").is_err());
432 assert!(CustomVariantType::validate_name("invalid_").is_err());
433 }
434
435 #[test]
436 fn test_custom_variant_manager() {
437 let mut manager = CustomVariantManager::new();
438
439 let variant = CustomVariant::aria("checked".to_string(), None).unwrap();
440 manager.register(variant).unwrap();
441
442 assert!(manager.contains("aria-checked"));
443 assert!(manager.get("aria-checked").is_some());
444 }
445
446 #[test]
447 fn test_custom_variant_suggestions() {
448 let manager = CustomVariantManager::with_defaults();
449
450 let suggestions = manager.get_suggestions("aria-");
451 assert!(!suggestions.is_empty());
452 assert!(suggestions.contains(&"aria-checked".to_string()));
453 }
454
455 #[test]
456 fn test_custom_variant_validation_in_manager() {
457 let manager = CustomVariantManager::with_defaults();
458
459 assert!(manager.validate_variant("aria-checked").is_ok());
461 assert!(manager.validate_variant("data-theme=dark").is_ok());
462
463 assert!(manager.validate_variant("@-invalid").is_err());
465 assert!(manager.validate_variant("-invalid").is_err());
466 assert!(manager.validate_variant("invalid-").is_err());
467 }
468}