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