tailwind_rs_core/utilities/
css_nesting.rs

1//! CSS Nesting utilities for tailwind-rs
2//!
3//! This module provides utilities for CSS nesting features.
4//! It includes support for nested selectors, nested media queries, and nested pseudo-classes.
5
6use crate::classes::ClassBuilder;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// CSS nesting selector types
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum NestingSelector {
13    /// Direct child selector (>)
14    DirectChild,
15    /// Descendant selector (space)
16    Descendant,
17    /// Adjacent sibling selector (+)
18    AdjacentSibling,
19    /// General sibling selector (~)
20    GeneralSibling,
21    /// Custom selector
22    Custom(String),
23}
24
25impl fmt::Display for NestingSelector {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            NestingSelector::DirectChild => write!(f, ">"),
29            NestingSelector::Descendant => write!(f, " "),
30            NestingSelector::AdjacentSibling => write!(f, "+"),
31            NestingSelector::GeneralSibling => write!(f, "~"),
32            NestingSelector::Custom(selector) => write!(f, "{}", selector),
33        }
34    }
35}
36
37impl NestingSelector {
38    /// Get the CSS class name for this nesting selector
39    pub fn to_class_name(&self) -> String {
40        match self {
41            NestingSelector::DirectChild => "nest-child".to_string(),
42            NestingSelector::Descendant => "nest-descendant".to_string(),
43            NestingSelector::AdjacentSibling => "nest-adjacent".to_string(),
44            NestingSelector::GeneralSibling => "nest-sibling".to_string(),
45            NestingSelector::Custom(selector) => format!("nest-{}", selector.replace(" ", "-").replace(">", "child").replace("+", "adjacent").replace("~", "sibling")),
46        }
47    }
48
49    /// Get the CSS value for this nesting selector
50    pub fn to_css_value(&self) -> String {
51        self.to_string()
52    }
53}
54
55/// CSS nesting pseudo-class types
56#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
57pub enum NestingPseudoClass {
58    /// Hover pseudo-class
59    Hover,
60    /// Focus pseudo-class
61    Focus,
62    /// Active pseudo-class
63    Active,
64    /// Visited pseudo-class
65    Visited,
66    /// Link pseudo-class
67    Link,
68    /// First child pseudo-class
69    FirstChild,
70    /// Last child pseudo-class
71    LastChild,
72    /// Nth child pseudo-class
73    NthChild(String),
74    /// Custom pseudo-class
75    Custom(String),
76}
77
78impl fmt::Display for NestingPseudoClass {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            NestingPseudoClass::Hover => write!(f, ":hover"),
82            NestingPseudoClass::Focus => write!(f, ":focus"),
83            NestingPseudoClass::Active => write!(f, ":active"),
84            NestingPseudoClass::Visited => write!(f, ":visited"),
85            NestingPseudoClass::Link => write!(f, ":link"),
86            NestingPseudoClass::FirstChild => write!(f, ":first-child"),
87            NestingPseudoClass::LastChild => write!(f, ":last-child"),
88            NestingPseudoClass::NthChild(n) => write!(f, ":nth-child({})", n),
89            NestingPseudoClass::Custom(pseudo) => write!(f, ":{}", pseudo),
90        }
91    }
92}
93
94impl NestingPseudoClass {
95    /// Get the CSS class name for this nesting pseudo-class
96    pub fn to_class_name(&self) -> String {
97        match self {
98            NestingPseudoClass::Hover => "nest-hover".to_string(),
99            NestingPseudoClass::Focus => "nest-focus".to_string(),
100            NestingPseudoClass::Active => "nest-active".to_string(),
101            NestingPseudoClass::Visited => "nest-visited".to_string(),
102            NestingPseudoClass::Link => "nest-link".to_string(),
103            NestingPseudoClass::FirstChild => "nest-first-child".to_string(),
104            NestingPseudoClass::LastChild => "nest-last-child".to_string(),
105            NestingPseudoClass::NthChild(n) => format!("nest-nth-child-{}", n.replace("n", "n").replace(" ", "-")),
106            NestingPseudoClass::Custom(pseudo) => format!("nest-{}", pseudo),
107        }
108    }
109
110    /// Get the CSS value for this nesting pseudo-class
111    pub fn to_css_value(&self) -> String {
112        self.to_string()
113    }
114}
115
116/// CSS nesting media query types
117#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
118pub enum NestingMediaQuery {
119    /// Small screen media query
120    Small,
121    /// Medium screen media query
122    Medium,
123    /// Large screen media query
124    Large,
125    /// Extra large screen media query
126    ExtraLarge,
127    /// Dark mode media query
128    Dark,
129    /// Light mode media query
130    Light,
131    /// Print media query
132    Print,
133    /// Screen media query
134    Screen,
135    /// Custom media query
136    Custom(String),
137}
138
139impl fmt::Display for NestingMediaQuery {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        match self {
142            NestingMediaQuery::Small => write!(f, "(min-width: 640px)"),
143            NestingMediaQuery::Medium => write!(f, "(min-width: 768px)"),
144            NestingMediaQuery::Large => write!(f, "(min-width: 1024px)"),
145            NestingMediaQuery::ExtraLarge => write!(f, "(min-width: 1280px)"),
146            NestingMediaQuery::Dark => write!(f, "(prefers-color-scheme: dark)"),
147            NestingMediaQuery::Light => write!(f, "(prefers-color-scheme: light)"),
148            NestingMediaQuery::Print => write!(f, "print"),
149            NestingMediaQuery::Screen => write!(f, "screen"),
150            NestingMediaQuery::Custom(query) => write!(f, "{}", query),
151        }
152    }
153}
154
155impl NestingMediaQuery {
156    /// Get the CSS class name for this nesting media query
157    pub fn to_class_name(&self) -> String {
158        match self {
159            NestingMediaQuery::Small => "nest-sm".to_string(),
160            NestingMediaQuery::Medium => "nest-md".to_string(),
161            NestingMediaQuery::Large => "nest-lg".to_string(),
162            NestingMediaQuery::ExtraLarge => "nest-xl".to_string(),
163            NestingMediaQuery::Dark => "nest-dark".to_string(),
164            NestingMediaQuery::Light => "nest-light".to_string(),
165            NestingMediaQuery::Print => "nest-print".to_string(),
166            NestingMediaQuery::Screen => "nest-screen".to_string(),
167            NestingMediaQuery::Custom(query) => format!("nest-{}", query.replace("(", "").replace(")", "").replace(" ", "-").replace(":", "-").replace("--", "-")),
168        }
169    }
170
171    /// Get the CSS value for this nesting media query
172    pub fn to_css_value(&self) -> String {
173        self.to_string()
174    }
175}
176
177/// Trait for adding CSS nesting to ClassBuilder
178pub trait CssNestingUtilities {
179    /// Set nesting selector
180    fn nesting_selector(self, selector: NestingSelector) -> Self;
181    /// Set nesting pseudo-class
182    fn nesting_pseudo_class(self, pseudo_class: NestingPseudoClass) -> Self;
183    /// Set nesting media query
184    fn nesting_media_query(self, media_query: NestingMediaQuery) -> Self;
185    /// Set nested class with selector
186    fn nested_class(self, selector: NestingSelector, class: &str) -> Self;
187    /// Set nested class with pseudo-class
188    fn nested_pseudo_class(self, pseudo_class: NestingPseudoClass, class: &str) -> Self;
189    /// Set nested class with media query
190    fn nested_media_query(self, media_query: NestingMediaQuery, class: &str) -> Self;
191}
192
193impl CssNestingUtilities for ClassBuilder {
194    fn nesting_selector(self, selector: NestingSelector) -> Self {
195        self.class(&selector.to_class_name())
196    }
197
198    fn nesting_pseudo_class(self, pseudo_class: NestingPseudoClass) -> Self {
199        self.class(&pseudo_class.to_class_name())
200    }
201
202    fn nesting_media_query(self, media_query: NestingMediaQuery) -> Self {
203        self.class(&media_query.to_class_name())
204    }
205
206    fn nested_class(self, selector: NestingSelector, class: &str) -> Self {
207        let nested_class = format!("{}-{}", selector.to_class_name(), class);
208        self.class(&nested_class)
209    }
210
211    fn nested_pseudo_class(self, pseudo_class: NestingPseudoClass, class: &str) -> Self {
212        let nested_class = format!("{}-{}", pseudo_class.to_class_name(), class);
213        self.class(&nested_class)
214    }
215
216    fn nested_media_query(self, media_query: NestingMediaQuery, class: &str) -> Self {
217        let nested_class = format!("{}-{}", media_query.to_class_name(), class);
218        self.class(&nested_class)
219    }
220}
221
222/// Convenience methods for common nesting patterns
223pub trait CssNestingConvenience {
224    /// Set nested hover class
225    fn nested_hover(self, class: &str) -> Self;
226    /// Set nested focus class
227    fn nested_focus(self, class: &str) -> Self;
228    /// Set nested active class
229    fn nested_active(self, class: &str) -> Self;
230    /// Set nested first child class
231    fn nested_first_child(self, class: &str) -> Self;
232    /// Set nested last child class
233    fn nested_last_child(self, class: &str) -> Self;
234    /// Set nested small screen class
235    fn nested_sm(self, class: &str) -> Self;
236    /// Set nested medium screen class
237    fn nested_md(self, class: &str) -> Self;
238    /// Set nested large screen class
239    fn nested_lg(self, class: &str) -> Self;
240    /// Set nested dark mode class
241    fn nested_dark(self, class: &str) -> Self;
242    /// Set nested light mode class
243    fn nested_light(self, class: &str) -> Self;
244}
245
246impl CssNestingConvenience for ClassBuilder {
247    fn nested_hover(self, class: &str) -> Self {
248        self.nested_pseudo_class(NestingPseudoClass::Hover, class)
249    }
250
251    fn nested_focus(self, class: &str) -> Self {
252        self.nested_pseudo_class(NestingPseudoClass::Focus, class)
253    }
254
255    fn nested_active(self, class: &str) -> Self {
256        self.nested_pseudo_class(NestingPseudoClass::Active, class)
257    }
258
259    fn nested_first_child(self, class: &str) -> Self {
260        self.nested_pseudo_class(NestingPseudoClass::FirstChild, class)
261    }
262
263    fn nested_last_child(self, class: &str) -> Self {
264        self.nested_pseudo_class(NestingPseudoClass::LastChild, class)
265    }
266
267    fn nested_sm(self, class: &str) -> Self {
268        self.nested_media_query(NestingMediaQuery::Small, class)
269    }
270
271    fn nested_md(self, class: &str) -> Self {
272        self.nested_media_query(NestingMediaQuery::Medium, class)
273    }
274
275    fn nested_lg(self, class: &str) -> Self {
276        self.nested_media_query(NestingMediaQuery::Large, class)
277    }
278
279    fn nested_dark(self, class: &str) -> Self {
280        self.nested_media_query(NestingMediaQuery::Dark, class)
281    }
282
283    fn nested_light(self, class: &str) -> Self {
284        self.nested_media_query(NestingMediaQuery::Light, class)
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::classes::ClassBuilder;
292
293    #[test]
294    fn test_nesting_selector_enum_values() {
295        assert_eq!(NestingSelector::DirectChild.to_string(), ">");
296        assert_eq!(NestingSelector::Descendant.to_string(), " ");
297        assert_eq!(NestingSelector::AdjacentSibling.to_string(), "+");
298        assert_eq!(NestingSelector::GeneralSibling.to_string(), "~");
299        assert_eq!(NestingSelector::Custom("div".to_string()).to_string(), "div");
300    }
301
302    #[test]
303    fn test_nesting_selector_class_names() {
304        assert_eq!(NestingSelector::DirectChild.to_class_name(), "nest-child");
305        assert_eq!(NestingSelector::Descendant.to_class_name(), "nest-descendant");
306        assert_eq!(NestingSelector::AdjacentSibling.to_class_name(), "nest-adjacent");
307        assert_eq!(NestingSelector::GeneralSibling.to_class_name(), "nest-sibling");
308        assert_eq!(NestingSelector::Custom("div".to_string()).to_class_name(), "nest-div");
309    }
310
311    #[test]
312    fn test_nesting_pseudo_class_enum_values() {
313        assert_eq!(NestingPseudoClass::Hover.to_string(), ":hover");
314        assert_eq!(NestingPseudoClass::Focus.to_string(), ":focus");
315        assert_eq!(NestingPseudoClass::Active.to_string(), ":active");
316        assert_eq!(NestingPseudoClass::FirstChild.to_string(), ":first-child");
317        assert_eq!(NestingPseudoClass::NthChild("2n".to_string()).to_string(), ":nth-child(2n)");
318        assert_eq!(NestingPseudoClass::Custom("custom".to_string()).to_string(), ":custom");
319    }
320
321    #[test]
322    fn test_nesting_pseudo_class_class_names() {
323        assert_eq!(NestingPseudoClass::Hover.to_class_name(), "nest-hover");
324        assert_eq!(NestingPseudoClass::Focus.to_class_name(), "nest-focus");
325        assert_eq!(NestingPseudoClass::Active.to_class_name(), "nest-active");
326        assert_eq!(NestingPseudoClass::FirstChild.to_class_name(), "nest-first-child");
327        assert_eq!(NestingPseudoClass::NthChild("2n".to_string()).to_class_name(), "nest-nth-child-2n");
328        assert_eq!(NestingPseudoClass::Custom("custom".to_string()).to_class_name(), "nest-custom");
329    }
330
331    #[test]
332    fn test_nesting_media_query_enum_values() {
333        assert_eq!(NestingMediaQuery::Small.to_string(), "(min-width: 640px)");
334        assert_eq!(NestingMediaQuery::Medium.to_string(), "(min-width: 768px)");
335        assert_eq!(NestingMediaQuery::Large.to_string(), "(min-width: 1024px)");
336        assert_eq!(NestingMediaQuery::Dark.to_string(), "(prefers-color-scheme: dark)");
337        assert_eq!(NestingMediaQuery::Print.to_string(), "print");
338        assert_eq!(NestingMediaQuery::Custom("(max-width: 600px)".to_string()).to_string(), "(max-width: 600px)");
339    }
340
341    #[test]
342    fn test_nesting_media_query_class_names() {
343        assert_eq!(NestingMediaQuery::Small.to_class_name(), "nest-sm");
344        assert_eq!(NestingMediaQuery::Medium.to_class_name(), "nest-md");
345        assert_eq!(NestingMediaQuery::Large.to_class_name(), "nest-lg");
346        assert_eq!(NestingMediaQuery::Dark.to_class_name(), "nest-dark");
347        assert_eq!(NestingMediaQuery::Print.to_class_name(), "nest-print");
348        assert_eq!(NestingMediaQuery::Custom("(max-width: 600px)".to_string()).to_class_name(), "nest-max-width-600px");
349    }
350
351    #[test]
352    fn test_css_nesting_utilities() {
353        let classes = ClassBuilder::new()
354            .nesting_selector(NestingSelector::DirectChild)
355            .nesting_pseudo_class(NestingPseudoClass::Hover)
356            .nesting_media_query(NestingMediaQuery::Small)
357            .nested_class(NestingSelector::Descendant, "text-blue-500")
358            .nested_pseudo_class(NestingPseudoClass::Focus, "text-red-500")
359            .nested_media_query(NestingMediaQuery::Medium, "text-green-500");
360
361        let result = classes.build();
362        assert!(result.classes.contains("nest-child"));
363        assert!(result.classes.contains("nest-hover"));
364        assert!(result.classes.contains("nest-sm"));
365        assert!(result.classes.contains("nest-descendant-text-blue-500"));
366        assert!(result.classes.contains("nest-focus-text-red-500"));
367        assert!(result.classes.contains("nest-md-text-green-500"));
368    }
369
370    #[test]
371    fn test_css_nesting_convenience() {
372        let classes = ClassBuilder::new()
373            .nested_hover("text-blue-500")
374            .nested_focus("text-red-500")
375            .nested_active("text-green-500")
376            .nested_first_child("text-yellow-500")
377            .nested_last_child("text-purple-500")
378            .nested_sm("text-pink-500")
379            .nested_md("text-indigo-500")
380            .nested_lg("text-cyan-500")
381            .nested_dark("text-gray-500")
382            .nested_light("text-white");
383
384        let result = classes.build();
385        assert!(result.classes.contains("nest-hover-text-blue-500"));
386        assert!(result.classes.contains("nest-focus-text-red-500"));
387        assert!(result.classes.contains("nest-active-text-green-500"));
388        assert!(result.classes.contains("nest-first-child-text-yellow-500"));
389        assert!(result.classes.contains("nest-last-child-text-purple-500"));
390        assert!(result.classes.contains("nest-sm-text-pink-500"));
391        assert!(result.classes.contains("nest-md-text-indigo-500"));
392        assert!(result.classes.contains("nest-lg-text-cyan-500"));
393        assert!(result.classes.contains("nest-dark-text-gray-500"));
394        assert!(result.classes.contains("nest-light-text-white"));
395    }
396
397    #[test]
398    fn test_css_nesting_serialization() {
399        let selector = NestingSelector::DirectChild;
400        let serialized = serde_json::to_string(&selector).unwrap();
401        let deserialized: NestingSelector = serde_json::from_str(&serialized).unwrap();
402        assert_eq!(selector, deserialized);
403
404        let pseudo_class = NestingPseudoClass::Hover;
405        let serialized = serde_json::to_string(&pseudo_class).unwrap();
406        let deserialized: NestingPseudoClass = serde_json::from_str(&serialized).unwrap();
407        assert_eq!(pseudo_class, deserialized);
408
409        let media_query = NestingMediaQuery::Small;
410        let serialized = serde_json::to_string(&media_query).unwrap();
411        let deserialized: NestingMediaQuery = serde_json::from_str(&serialized).unwrap();
412        assert_eq!(media_query, deserialized);
413    }
414
415    #[test]
416    fn test_css_nesting_comprehensive_usage() {
417        let classes = ClassBuilder::new()
418            .nesting_selector(NestingSelector::DirectChild)
419            .nesting_pseudo_class(NestingPseudoClass::Hover)
420            .nesting_media_query(NestingMediaQuery::Small)
421            .nested_class(NestingSelector::Descendant, "text-blue-500")
422            .nested_pseudo_class(NestingPseudoClass::Focus, "text-red-500")
423            .nested_media_query(NestingMediaQuery::Medium, "text-green-500")
424            .nested_hover("text-yellow-500")
425            .nested_focus("text-purple-500")
426            .nested_active("text-pink-500")
427            .nested_first_child("text-indigo-500")
428            .nested_last_child("text-cyan-500")
429            .nested_sm("text-gray-500")
430            .nested_md("text-white")
431            .nested_lg("text-black")
432            .nested_dark("text-gray-100")
433            .nested_light("text-gray-900");
434
435        let result = classes.build();
436        assert!(result.classes.contains("nest-child"));
437        assert!(result.classes.contains("nest-hover"));
438        assert!(result.classes.contains("nest-sm"));
439        assert!(result.classes.contains("nest-descendant-text-blue-500"));
440        assert!(result.classes.contains("nest-focus-text-red-500"));
441        assert!(result.classes.contains("nest-md-text-green-500"));
442        assert!(result.classes.contains("nest-hover-text-yellow-500"));
443        assert!(result.classes.contains("nest-focus-text-purple-500"));
444        assert!(result.classes.contains("nest-active-text-pink-500"));
445        assert!(result.classes.contains("nest-first-child-text-indigo-500"));
446        assert!(result.classes.contains("nest-last-child-text-cyan-500"));
447        assert!(result.classes.contains("nest-sm-text-gray-500"));
448        assert!(result.classes.contains("nest-md-text-white"));
449        assert!(result.classes.contains("nest-lg-text-black"));
450        assert!(result.classes.contains("nest-dark-text-gray-100"));
451        assert!(result.classes.contains("nest-light-text-gray-900"));
452    }
453}