encre_css/
generator.rs

1//! Define the main [`generate`] function used to scan content and to generate CSS styles.
2use crate::{
3    config::{Config, MaxShortcutDepth},
4    preflight::Preflight,
5    selector::{parse, Modifier, Selector, Variant},
6    utils::buffer::Buffer,
7};
8
9use std::{borrow::Cow, collections::BTreeSet};
10
11/// The context used in the [`Plugin::can_handle`] method.
12///
13/// [`Plugin::can_handle`]: crate::plugins::Plugin::can_handle
14#[derive(Debug)]
15pub struct ContextCanHandle<'a, 'b, 'c> {
16    /// The generator's configuration.
17    pub config: &'a Config,
18
19    /// The modifier which will be checked.
20    pub modifier: &'b Modifier<'c>,
21}
22
23/// The context used in the [`Plugin::handle`] method.
24///
25/// [`Plugin::handle`]: crate::plugins::Plugin::handle
26#[derive(Debug)]
27pub struct ContextHandle<'a, 'b, 'c, 'd, 'e> {
28    /// The generator's configuration.
29    pub config: &'a Config,
30
31    /// The modifier which will have its CSS generated.
32    pub modifier: &'b Modifier<'c>,
33
34    /// The buffer containing the whole generated CSS.
35    pub buffer: &'d mut Buffer,
36
37    // Private fields used in `generate_class` and `generate_at_rules`
38    selector: &'e Selector<'e>,
39}
40
41/// Generate the needed CSS at-rules (e.g @media).
42///
43/// Note: The inner class (e.g. .foo-bar) is not handled by this function, see [`generate_wrapper`].
44///
45/// The second argument, a closure, is called to generate the CSS content of the rule.
46///
47/// # Errors
48///
49/// Returns [`fmt::Error`] indicating whether writing to the buffer succeeded.
50///
51/// [`fmt::Error`]: std::fmt::Error
52pub fn generate_at_rules<T: FnOnce(&mut ContextHandle)>(
53    context: &mut ContextHandle,
54    rule_content_fn: T,
55) {
56    let ContextHandle {
57        buffer, selector, ..
58    } = context;
59
60    if !selector.variants.is_empty() {
61        selector.variants.iter().for_each(|variant| {
62            if variant.template.starts_with('@') {
63                buffer.line(format_args!("{} {{", variant.template));
64                buffer.indent();
65            }
66        });
67    }
68
69    rule_content_fn(context);
70
71    let ContextHandle { buffer, .. } = context;
72    while !buffer.is_unindented() {
73        buffer.unindent();
74
75        if buffer.is_unindented() {
76            buffer.raw("}");
77        } else {
78            buffer.line("}");
79        }
80    }
81}
82
83/// Generate a CSS rule with a class.
84///
85/// Note: At-rules (e.g. @media) are not handled by this function, see [`generate_wrapper`].
86///
87/// The second argument, a closure, is called to generate the CSS content of the rule.
88/// The third argument is used to add a custom string just after the class (e.g. `> *`).
89///
90/// # Errors
91///
92/// Returns [`fmt::Error`] indicating whether writing to the buffer succeeded.
93///
94/// [`fmt::Error`]: std::fmt::Error
95#[allow(clippy::too_many_lines)]
96pub fn generate_class<T: FnOnce(&mut ContextHandle)>(
97    context: &mut ContextHandle,
98    rule_content_fn: T,
99    custom_after_class: &str,
100) {
101    let ContextHandle {
102        buffer, selector, ..
103    } = context;
104
105    // Write the class
106    let mut base_class = String::with_capacity(1 + selector.full.len());
107    base_class.push('.');
108
109    selector.full.chars().enumerate().for_each(|(i, ch)| {
110        if !ch.is_alphanumeric() && ch != '-' && ch != '_' {
111            base_class.push('\\');
112            base_class.push(ch);
113        } else if i == 0 && ch.is_numeric() {
114            // CSS classes must not start with a number, we need to escape it
115            base_class.push_str("\\3");
116            base_class.push(ch);
117        } else {
118            base_class.push(ch);
119        }
120    });
121
122    if !selector.variants.is_empty() {
123        // Variants are applied from right to left
124        // (https://tailwindcss.com/docs/upgrade-guide#variant-stacking-order),
125        // so no need to reverse the variants
126        selector.variants.iter().for_each(|variant| {
127            if !variant.template.starts_with('@') {
128                base_class = variant.template.replace('&', &base_class);
129            }
130        });
131    }
132    buffer.line(format_args!("{base_class}{custom_after_class} {{"));
133
134    // Store the index of the start of the class content (useful when the `important` flag is present)
135    let content_start = buffer.len();
136
137    // Rule content
138    buffer.indent();
139    rule_content_fn(context);
140
141    let ContextHandle {
142        buffer, selector, ..
143    } = context;
144
145    // If the rule is selecting the `::before` or `::after` pseudo elements, we need to generate a
146    // default `content` property
147    if selector
148        .variants
149        .iter()
150        .any(|variant| ["&::before", "&::after"].contains(&&*variant.template))
151    {
152        buffer.line("content: var(--en-content);");
153    }
154
155    // If the `important` flag is present we need to replace all `;\n` or `;\r\n`
156    // to ` !important;\n` or ` !important;\r\n`
157    if selector.is_important {
158        let mut extra_index = 0;
159        let positions = buffer[content_start..]
160            .match_indices('\n')
161            .map(|i| i.0)
162            .collect::<Vec<usize>>();
163
164        for index in positions {
165            if index - 1 == 0 {
166                continue;
167            }
168
169            let index = content_start + extra_index + index;
170            let index = if &buffer[index - 1..index] == "\r" {
171                index - 1
172            } else {
173                index
174            };
175            let replace_with = " !important;";
176            buffer.replace_range(index - 1..index, replace_with);
177            extra_index += replace_with.len() - 1;
178        }
179    }
180
181    buffer.unindent();
182    if buffer.is_unindented() {
183        buffer.raw("}");
184    } else {
185        buffer.line("}");
186    }
187}
188
189/// Generate the complete CSS wrapper needed for a single rule.
190///
191/// This function is a combination of the [`generate_at_rules`] and [`generate_class`] functions.
192///
193/// The second argument, a closure, is called to generate the CSS content of the rule.
194///
195/// # Errors
196///
197/// Returns [`fmt::Error`] indicating whether writing to the buffer succeeded.
198///
199/// [`fmt::Error`]: std::fmt::Error
200pub fn generate_wrapper<T: FnOnce(&mut ContextHandle)>(
201    context: &mut ContextHandle,
202    rule_content_fn: T,
203) {
204    generate_at_rules(context, |context| {
205        generate_class(context, rule_content_fn, "");
206    });
207}
208
209fn resolve_selector<'a>(
210    selector: &'a str,
211    full_class: Option<&'a str>,
212    selectors: &mut BTreeSet<Selector<'a>>,
213    config: &'a Config,
214    config_derived_variants: &[(Cow<'static, str>, Variant<'static>)],
215    depth: MaxShortcutDepth,
216) {
217    if depth.get() == 0 {
218        return;
219    }
220
221    if let Some(expanded) = config.shortcuts.get(selector) {
222        expanded.split(' ').for_each(|shortcut_target| {
223            resolve_selector(
224                shortcut_target,
225                full_class.or(Some(selector)),
226                selectors,
227                config,
228                config_derived_variants,
229                MaxShortcutDepth::new(depth.get() - 1),
230            );
231        });
232    } else {
233        selectors.extend(
234            parse(selector, None, full_class, config, config_derived_variants)
235                .into_iter()
236                .filter_map(Result::ok),
237        );
238    }
239}
240
241/// Generate the CSS styles needed based on the given sources.
242///
243/// Each source will be scanned in order to extract atomic classes, then CSS will be generated for
244/// each class found.
245///
246/// By default, it splits the source by spaces, double quotes, single quotes, backticks and new
247/// lines, while ignoring the content inside arbitrary values/variants and variant groups.
248///
249/// This function also removes duplicated selectors and sorts the generated CSS classes based on
250/// the order in which they were defined to avoid conflicts.
251pub fn generate<'a>(sources: impl IntoIterator<Item = &'a str>, config: &Config) -> String {
252    let config_derived_variants = config.get_derived_variants();
253    let mut selectors = BTreeSet::new();
254
255    // Add selectors from the safelist
256    for safe_selector in config.safelist.iter() {
257        if let Some(expanded) = config.shortcuts.get(&**safe_selector) {
258            expanded.split(' ').for_each(|shortcut_target| {
259                selectors.extend(
260                    parse(
261                        shortcut_target,
262                        None,
263                        Some(safe_selector),
264                        config,
265                        &config_derived_variants,
266                    )
267                    .into_iter()
268                    .filter_map(Result::ok),
269                );
270            });
271        } else {
272            selectors.extend(
273                parse(safe_selector, None, None, config, &config_derived_variants)
274                    .into_iter()
275                    .filter_map(Result::ok),
276            );
277        }
278    }
279
280    for source in sources {
281        let new_selectors = config.scanner.scan(source);
282
283        for selector in new_selectors {
284            resolve_selector(
285                selector,
286                None,
287                &mut selectors,
288                config,
289                &config_derived_variants,
290                config.max_shortcut_depth,
291            );
292        }
293    }
294
295    let preflight = config.preflight.build();
296    let mut buffer = Buffer::with_capacity(10 * selectors.len()); // TODO: More accurate value
297    buffer.raw(&preflight);
298
299    for selector in selectors {
300        if buffer.len() != preflight.len() || config.preflight != Preflight::None {
301            buffer.raw("\n\n");
302        }
303
304        let mut context = ContextHandle {
305            config,
306            modifier: &selector.modifier,
307            buffer: &mut buffer,
308            selector: &selector,
309        };
310
311        if selector.plugin.needs_wrapping() {
312            generate_wrapper(&mut context, |context| selector.plugin.handle(context));
313        } else {
314            selector.plugin.handle(&mut context);
315        }
316    }
317
318    buffer.into_inner()
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use crate::{config::DarkMode, utils::testing::base_config};
325
326    use pretty_assertions::assert_eq;
327
328    #[test]
329    fn not_parsing_too_loosely() {
330        let generated = generate(["flex-test-[]"], &base_config());
331        assert!(generated.is_empty());
332    }
333
334    #[test]
335    fn divide_and_space_between_special_class() {
336        let generated = generate(
337            [
338                "hover:space-x-1",
339                "space-x-2",
340                "[&:has(.class)_>_*]:space-y-3",
341                "divide-red-100",
342                "divide-dashed",
343                "divide-x-[11px]",
344                "xl:[&_>_*]:divide-y-2",
345            ],
346            &base_config(),
347        );
348
349        assert_eq!(
350            generated,
351            String::from(
352                r".space-x-2 > :not(:last-child) {
353  --en-space-x-reverse: 0;
354  margin-inline-start: calc(0.5rem * var(--en-space-x-reverse));
355  margin-inline-end: calc(0.5rem * calc(1 - var(--en-space-x-reverse)));
356}
357
358.divide-x-\[11px\] > :not([hidden]) ~ :not([hidden]) {
359  --en-divide-x-reverse: 0;
360  border-inline-start-width: calc(11px * var(--en-divide-x-reverse));
361  border-inline-end-width: calc(11px * calc(1 - var(--en-divide-x-reverse)));
362}
363
364.divide-dashed > :not([hidden]) ~ :not([hidden]) {
365  border-style: dashed;
366}
367
368.divide-red-100 > :not([hidden]) ~ :not([hidden]) {
369  border-color: oklch(93.6% .032 17.717);
370}
371
372.hover\:space-x-1:hover > :not(:last-child) {
373  --en-space-x-reverse: 0;
374  margin-inline-start: calc(0.25rem * var(--en-space-x-reverse));
375  margin-inline-end: calc(0.25rem * calc(1 - var(--en-space-x-reverse)));
376}
377
378@media (width >= 80rem) {
379  .xl\:\[\&_\>_\*\]\:divide-y-2 > * > :not([hidden]) ~ :not([hidden]) {
380    --en-divide-y-reverse: 0;
381    border-block-start-width: calc(2px * var(--en-divide-y-reverse));
382    border-block-end-width: calc(2px * calc(1 - var(--en-divide-y-reverse)));
383  }
384}
385
386.\[\&\:has\(\.class\)_\>_\*\]\:space-y-3:has(.class) > * > :not(:last-child) {
387  --en-space-y-reverse: 0;
388  margin-block-start: calc(0.75rem * var(--en-space-y-reverse));
389  margin-block-end: calc(0.75rem * calc(1 - var(--en-space-y-reverse)));
390}"
391            )
392        );
393    }
394
395    #[test]
396    fn negative_values() {
397        let generated = generate(
398            [
399                "-top-2",
400                "-z-2",
401                "-order-2",
402                "-mb8",
403                "-translate-x-52",
404                "-rotate-90",
405                "-skew-x-2",
406                "-scale-50",
407                "-scroll-mt-2",
408                "-space-x-2",
409                "-indent-2",
410                "-hue-rotate-60",
411                "hover:-hue-rotate-60",
412                "-backdrop-hue-rotate-90",
413            ],
414            &base_config(),
415        );
416
417        assert_eq!(
418            generated,
419            String::from(
420                r".-top-2 {
421  top: -0.5rem;
422}
423
424.-z-2 {
425  z-index: -2;
426}
427
428.-order-2 {
429  order: -2;
430}
431
432.-mb8 {
433  margin-bottom: -2rem;
434}
435
436.-translate-x-52 {
437  --en-translate-x: -13rem;
438  transform: translate3d(var(--en-translate-x), var(--en-translate-y), var(--en-translate-z)) rotateX(var(--en-rotate-x)) rotateY(var(--en-rotate-y)) rotateZ(var(--en-rotate-z)) skewX(var(--en-skew-x)) skewY(var(--en-skew-y)) scale3d(var(--en-scale-x), var(--en-scale-y), var(--en-scale-z));
439}
440
441.-rotate-90 {
442  --en-rotate-x: -90deg;
443  --en-rotate-y: -90deg;
444  transform: translate3d(var(--en-translate-x), var(--en-translate-y), var(--en-translate-z)) rotateX(var(--en-rotate-x)) rotateY(var(--en-rotate-y)) rotateZ(var(--en-rotate-z)) skewX(var(--en-skew-x)) skewY(var(--en-skew-y)) scale3d(var(--en-scale-x), var(--en-scale-y), var(--en-scale-z));
445}
446
447.-skew-x-2 {
448  --en-skew-x: -2deg;
449  transform: translate3d(var(--en-translate-x), var(--en-translate-y), var(--en-translate-z)) rotateX(var(--en-rotate-x)) rotateY(var(--en-rotate-y)) rotateZ(var(--en-rotate-z)) skewX(var(--en-skew-x)) skewY(var(--en-skew-y)) scale3d(var(--en-scale-x), var(--en-scale-y), var(--en-scale-z));
450}
451
452.-scale-50 {
453  --en-scale-x: -0.5;
454  --en-scale-y: -0.5;
455  transform: translate3d(var(--en-translate-x), var(--en-translate-y), var(--en-translate-z)) rotateX(var(--en-rotate-x)) rotateY(var(--en-rotate-y)) rotateZ(var(--en-rotate-z)) skewX(var(--en-skew-x)) skewY(var(--en-skew-y)) scale3d(var(--en-scale-x), var(--en-scale-y), var(--en-scale-z));
456}
457
458.-scroll-mt-2 {
459  scroll-margin-top: -0.5rem;
460}
461
462.-space-x-2 > :not(:last-child) {
463  --en-space-x-reverse: 0;
464  margin-inline-start: calc(-0.5rem * var(--en-space-x-reverse));
465  margin-inline-end: calc(-0.5rem * calc(1 - var(--en-space-x-reverse)));
466}
467
468.-indent-2 {
469  text-indent: -0.5rem;
470}
471
472.-hue-rotate-60 {
473  --en-hue-rotate: hue-rotate(-60deg);
474  filter: var(--en-blur) var(--en-brightness) var(--en-contrast) var(--en-grayscale) var(--en-hue-rotate) var(--en-invert) var(--en-saturate) var(--en-sepia) var(--en-drop-shadow);
475}
476
477.-backdrop-hue-rotate-90 {
478  --en-backdrop-hue-rotate: hue-rotate(-90deg);
479  -webkit-backdrop-filter: var(--en-backdrop-blur) var(--en-backdrop-brightness) var(--en-backdrop-contrast) var(--en-backdrop-grayscale) var(--en-backdrop-hue-rotate) var(--en-backdrop-invert) var(--en-backdrop-opacity) var(--en-backdrop-saturate) var(--en-backdrop-sepia);
480  backdrop-filter: var(--en-backdrop-blur) var(--en-backdrop-brightness) var(--en-backdrop-contrast) var(--en-backdrop-grayscale) var(--en-backdrop-hue-rotate) var(--en-backdrop-invert) var(--en-backdrop-opacity) var(--en-backdrop-saturate) var(--en-backdrop-sepia);
481}
482
483.hover\:-hue-rotate-60:hover {
484  --en-hue-rotate: hue-rotate(-60deg);
485  filter: var(--en-blur) var(--en-brightness) var(--en-contrast) var(--en-grayscale) var(--en-hue-rotate) var(--en-invert) var(--en-saturate) var(--en-sepia) var(--en-drop-shadow);
486}"
487            )
488        );
489    }
490
491    #[test]
492    fn gen_css_for_simple_selector() {
493        let generated = generate(["text-current"], &base_config());
494
495        assert_eq!(
496            generated,
497            String::from(
498                ".text-current {
499  color: currentColor;
500}"
501            )
502        );
503    }
504
505    #[test]
506    fn gen_css_with_important_flag() {
507        let generated = generate(
508            [
509                "!w-full",
510                "!-mb-8",
511                "!shadow-sm",
512                "!-hue-rotate-60",
513                "focus:!w-2",
514                "focus:!-mb-2",
515            ],
516            &base_config(),
517        );
518
519        assert_eq!(
520            generated,
521            String::from(
522                r".\!-mb-8 {
523  margin-bottom: -2rem !important;
524}
525
526.\!w-full {
527  width: 100% !important;
528}
529
530.\!shadow-sm {
531  --en-shadow: 0 1px 3px 0 var(--en-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--en-shadow-color, rgb(0 0 0 / 0.1)) !important;
532  box-shadow: var(--en-inset-shadow, 0 0 #0000), var(--en-inset-ring-shadow, 0 0 #0000), var(--en-ring-offset-shadow, 0 0 #0000), var(--en-ring-shadow, 0 0 #0000), var(--en-shadow) !important;
533}
534
535.\!-hue-rotate-60 {
536  --en-hue-rotate: hue-rotate(-60deg) !important;
537  filter: var(--en-blur) var(--en-brightness) var(--en-contrast) var(--en-grayscale) var(--en-hue-rotate) var(--en-invert) var(--en-saturate) var(--en-sepia) var(--en-drop-shadow) !important;
538}
539
540.focus\:\!-mb-2:focus {
541  margin-bottom: -0.5rem !important;
542}
543
544.focus\:\!w-2:focus {
545  width: 0.5rem !important;
546}",
547            )
548        );
549    }
550
551    #[test]
552    fn gen_css_for_selector_needing_custom_css() {
553        let generated = generate(["animate-pulse", "animate-pulse"], &base_config());
554
555        assert_eq!(
556            generated,
557            String::from(
558                "@-webkit-keyframes pulse {
559  50% {
560    opacity: .5;
561  }
562}
563
564@keyframes pulse {
565  0%, 100% {
566    opacity: 1;
567  }
568  50% {
569    opacity: .5;
570  }
571}
572
573.animate-pulse {
574  -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
575  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
576}"
577            )
578        );
579    }
580
581    #[test]
582    fn gen_css_for_arbitrary_value() {
583        let generated = generate(
584            [
585                "w[12px]",
586                "bg-[red]",
587                "bg-[url(../img/image_with_underscores.png)]",
588                "mt-[calc(100%-10px)]",
589                "2xl:pb-[calc((100%/2)-10px+2rem)]",
590            ],
591            &base_config(),
592        );
593
594        assert_eq!(
595            generated,
596            String::from(
597                r".mt-\[calc\(100\%-10px\)\] {
598  margin-top: calc(100% - 10px);
599}
600
601.w\[12px\] {
602  width: 12px;
603}
604
605.bg-\[red\] {
606  background-color: red;
607}
608
609.bg-\[url\(\.\.\/img\/image_with_underscores\.png\)\] {
610  background-image: url(../img/image_with_underscores.png);
611}
612
613@media (width >= 96rem) {
614  .\32xl\:pb-\[calc\(\(100\%\/2\)-10px\+2rem\)\] {
615    padding-bottom: calc((100% / 2) - 10px + 2rem);
616  }
617}"
618            )
619        );
620    }
621
622    #[test]
623    fn gen_css_for_arbitrary_value_with_hint() {
624        let generated = generate(["bg-[color:red]", "hover:bg-[color:red]"], &base_config());
625
626        assert_eq!(
627            generated,
628            String::from(
629                r".bg-\[color\:red\] {
630  background-color: red;
631}
632
633.hover\:bg-\[color\:red\]:hover {
634  background-color: red;
635}"
636            )
637        );
638    }
639
640    #[test]
641    fn gen_css_for_selector_with_simple_variant() {
642        let generated = generate(["focus:w-full"], &base_config());
643
644        assert_eq!(
645            generated,
646            String::from(
647                r".focus\:w-full:focus {
648  width: 100%;
649}"
650            )
651        );
652    }
653
654    #[test]
655    fn gen_selector_css_variants_test() {
656        let generated = generate([
657            "sm:hover:bg-red-400",
658            "focus:hover:bg-red-600",
659            "active:rtl:bg-red-800",
660            "md:focus:selection:bg-blue-100",
661            "rtl:active:focus:lg:underline",
662            "print:ltr:xl:hover:focus:active:text-yellow-300",
663            "2xl:motion-safe:landscape:focus-within:visited:first:odd:checked:open:rtl:bg-purple-100",
664            "hover:file:bg-pink-600",
665            "file:hover:bg-pink-600",
666            "sm:before:target:content-[&#39;Hello_world!&#39;]",
667            "marker:selection:hover:bg-green-200",
668            "group-hover:bg-green-300",
669            "group-focus:bg-green-400",
670            "peer-invalid:bg-red-500",
671            "peer-not-invalid:bg-green-500",
672        ], &base_config());
673
674        assert_eq!(
675            generated,
676            String::from(
677                r#".marker\:selection\:hover\:bg-green-200 *::marker, .marker\:selection\:hover\:bg-green-200::marker *::selection, .marker\:selection\:hover\:bg-green-200 *::marker, .marker\:selection\:hover\:bg-green-200::marker::selection:hover {
678  background-color: oklch(92.5% .084 155.995);
679}
680
681.file\:hover\:bg-pink-600::file-selector-button, .file\:hover\:bg-pink-600::-webkit-file-upload-button:hover {
682  background-color: oklch(59.2% .249 .584);
683}
684
685.hover\:file\:bg-pink-600:hover::file-selector-button, .hover\:file\:bg-pink-600:hover::-webkit-file-upload-button {
686  background-color: oklch(59.2% .249 .584);
687}
688
689.focus\:hover\:bg-red-600:focus:hover {
690  background-color: oklch(57.7% .245 27.325);
691}
692
693[dir="rtl"] .active\:rtl\:bg-red-800:active {
694  background-color: oklch(44.4% .177 26.899);
695}
696
697@media (width >= 64rem) {
698  [dir="rtl"] .rtl\:active\:focus\:lg\:underline:active:focus {
699    -webkit-text-decoration-line: underline;
700    text-decoration-line: underline;
701  }
702}
703
704@media print {
705  @media (width >= 80rem) {
706    [dir="ltr"] .print\:ltr\:xl\:hover\:focus\:active\:text-yellow-300:hover:focus:active {
707      color: oklch(90.5% .182 98.111);
708    }
709  }
710}
711
712@media (width >= 40rem) {
713  .sm\:before\:target\:content-\[\&\#39\;Hello_world\!\&\#39\;\]::before:target {
714    --en-content: 'Hello world!';
715    content: var(--en-content);
716  }
717}
718
719@media (width >= 40rem) {
720  .sm\:hover\:bg-red-400:hover {
721    background-color: oklch(70.4% .191 22.216);
722  }
723}
724
725@media (width >= 48rem) {
726  .md\:focus\:selection\:bg-blue-100:focus *::selection, .md\:focus\:selection\:bg-blue-100:focus::selection {
727    background-color: oklch(93.2% .032 255.585);
728  }
729}
730
731@media (width >= 96rem) {
732  @media (prefers-reduced-motion: no-preference) {
733    @media (orientation: landscape) {
734      [dir="rtl"] .\32xl\:motion-safe\:landscape\:focus-within\:visited\:first\:odd\:checked\:open\:rtl\:bg-purple-100:focus-within:visited:first-child:nth-child(odd):checked[open] {
735        background-color: oklch(94.6% .033 307.174);
736      }
737    }
738  }
739}
740
741.group:hover .group-hover\:bg-green-300 {
742  background-color: oklch(87.1% .15 154.449);
743}
744
745.group:focus .group-focus\:bg-green-400 {
746  background-color: oklch(79.2% .209 151.711);
747}
748
749.peer:not(:invalid) ~ .peer-not-invalid\:bg-green-500 {
750  background-color: oklch(72.3% .219 149.579);
751}
752
753.peer:invalid ~ .peer-invalid\:bg-red-500 {
754  background-color: oklch(63.7% .237 25.331);
755}"#
756            )
757        );
758    }
759
760    #[test]
761    fn gen_css_for_duplicated_selectors() {
762        let generated = generate(["bg-red-500 bg-red-500", "bg-red-500"], &base_config());
763
764        assert_eq!(
765            generated,
766            String::from(
767                ".bg-red-500 {
768  background-color: oklch(63.7% .237 25.331);
769}"
770            )
771        );
772    }
773
774    #[test]
775    fn gen_css_for_selector_with_arbitrary_property() {
776        let generated = generate(["hover:[mask-type:luminance]"], &base_config());
777
778        assert_eq!(
779            generated,
780            String::from(
781                r".hover\:\[mask-type\:luminance\]:hover {
782  mask-type: luminance;
783}"
784            )
785        );
786    }
787
788    #[test]
789    fn gen_css_for_selector_with_arbitrary_variant() {
790        let generated = generate(
791            [
792                "[&_>_*]:before:content-[&#39;hello-&#39;]",
793                "[&:has(.active)]:bg-blue-500",
794                "[@supports_(display:grid)]:grid",
795                "[@supports_not_(display:grid)]:float-right",
796            ],
797            &base_config(),
798        );
799
800        assert_eq!(
801            generated,
802            String::from(
803                r"@supports not (display:grid) {
804  .\[\@supports_not_\(display\:grid\)\]\:float-right {
805    float: right;
806  }
807}
808
809@supports (display:grid) {
810  .\[\@supports_\(display\:grid\)\]\:grid {
811    display: grid;
812  }
813}
814
815.\[\&\:has\(\.active\)\]\:bg-blue-500:has(.active) {
816  background-color: oklch(62.3% .214 259.815);
817}
818
819.\[\&_\>_\*\]\:before\:content-\[\&\#39\;hello-\&\#39\;\] > *::before {
820  --en-content: 'hello-';
821  content: var(--en-content);
822}"
823            )
824        );
825    }
826
827    #[test]
828    fn gen_css_for_variant_group() {
829        let generated = generate(
830            ["xl:(focus:(outline,outline-red-200),dark:(bg-black,text-white))"],
831            &base_config(),
832        );
833
834        assert_eq!(
835            generated,
836            String::from(
837                r"@media (width >= 80rem) {
838  .xl\:\(focus\:\(outline\,outline-red-200\)\,dark\:\(bg-black\,text-white\)\):focus {
839    outline-color: oklch(88.5% .062 18.334);
840  }
841}
842
843@media (width >= 80rem) {
844  .xl\:\(focus\:\(outline\,outline-red-200\)\,dark\:\(bg-black\,text-white\)\):focus {
845    outline-style: solid;
846  }
847}
848
849@media (prefers-color-scheme: dark) {
850  @media (width >= 80rem) {
851    .xl\:\(focus\:\(outline\,outline-red-200\)\,dark\:\(bg-black\,text-white\)\) {
852      color: #fff;
853    }
854  }
855}
856
857@media (prefers-color-scheme: dark) {
858  @media (width >= 80rem) {
859    .xl\:\(focus\:\(outline\,outline-red-200\)\,dark\:\(bg-black\,text-white\)\) {
860      background-color: #000;
861    }
862  }
863}"
864            )
865        );
866    }
867
868    #[test]
869    fn default_modifier_values_for_rounded() {
870        let generated = generate([
871            "rounded-tr-sm rounded-tr-md rounded-sm rounded-md rounded-t-sm rounded-bl-xl border-x border border-4 border-t-2",
872        ], &base_config());
873
874        assert_eq!(
875            generated,
876            String::from(
877                ".rounded-md {
878  border-radius: 0.375rem;
879}
880
881.rounded-sm {
882  border-radius: 0.25rem;
883}
884
885.rounded-t-sm {
886  border-top-left-radius: 0.25rem;
887  border-top-right-radius: 0.25rem;
888}
889
890.rounded-tr-md {
891  border-top-right-radius: 0.375rem;
892}
893
894.rounded-tr-sm {
895  border-top-right-radius: 0.25rem;
896}
897
898.rounded-bl-xl {
899  border-bottom-left-radius: 0.75rem;
900}
901
902.border {
903  border-width: 1px;
904}
905
906.border-4 {
907  border-width: 4px;
908}
909
910.border-x {
911  border-inline-width: 1px;
912}
913
914.border-t-2 {
915  border-top-width: 2px;
916}"
917            )
918        );
919    }
920
921    #[test]
922    fn gen_css_for_font_with_spaces() {
923        let generated = generate(
924            [
925                "font-[&#39;Times_New_Roman&#39;,Helvetica,serif]",
926                "font-[Roboto,&#39;Open_Sans&#39;,sans-serif]",
927            ],
928            &base_config(),
929        );
930
931        assert_eq!(
932            generated,
933            String::from(
934                r".font-\[\&\#39\;Times_New_Roman\&\#39\;\,Helvetica\,serif\] {
935  font-family: 'Times New Roman',Helvetica,serif;
936}
937
938.font-\[Roboto\,\&\#39\;Open_Sans\&\#39\;\,sans-serif\] {
939  font-family: Roboto,'Open Sans',sans-serif;
940}"
941            )
942        );
943    }
944
945    #[test]
946    fn gen_css_for_container() {
947        let generated = generate(["container"], &base_config());
948
949        assert_eq!(
950            generated,
951            String::from(
952                ".container {
953  width: 100%;
954}
955
956@media (width >= 40rem) {
957  .container {
958    max-width: 40rem;
959  }
960}
961
962@media (width >= 48rem) {
963  .container {
964    max-width: 48rem;
965  }
966}
967
968@media (width >= 64rem) {
969  .container {
970    max-width: 64rem;
971  }
972}
973
974@media (width >= 80rem) {
975  .container {
976    max-width: 80rem;
977  }
978}
979
980@media (width >= 96rem) {
981  .container {
982    max-width: 96rem;
983  }
984}"
985            )
986        );
987
988        let generated = generate(["md:container", "md:mx-auto"], &base_config());
989
990        assert_eq!(
991            generated,
992            String::from(
993                r"@media (width >= 48rem) {
994  .md\:mx-auto {
995    margin-inline: auto;
996  }
997}
998
999@media (width >= 48rem) {
1000  .md\:container {
1001    width: 100%;
1002  }
1003}
1004
1005@media (width >= 48rem) {
1006  @media (width >= 40rem) {
1007    .md\:container {
1008      max-width: 40rem;
1009    }
1010  }
1011
1012  @media (width >= 48rem) {
1013    .md\:container {
1014      max-width: 48rem;
1015    }
1016  }
1017
1018  @media (width >= 64rem) {
1019    .md\:container {
1020      max-width: 64rem;
1021    }
1022  }
1023
1024  @media (width >= 80rem) {
1025    .md\:container {
1026      max-width: 80rem;
1027    }
1028  }
1029
1030  @media (width >= 96rem) {
1031    .md\:container {
1032      max-width: 96rem;
1033    }
1034  }
1035}"
1036            )
1037        );
1038    }
1039
1040    #[test]
1041    fn gen_css_for_selector_with_before_after_variant() {
1042        let generated = generate(
1043            [
1044                "before:bg-red-500",
1045                "before:content-[&#39;Hello_world!&#39;]",
1046                "after:rounded-full",
1047                "after:content-[counter(foo)]",
1048            ],
1049            &base_config(),
1050        );
1051
1052        assert_eq!(
1053            generated,
1054            String::from(
1055                r".before\:content-\[\&\#39\;Hello_world\!\&\#39\;\]::before {
1056  --en-content: 'Hello world!';
1057  content: var(--en-content);
1058}
1059
1060.before\:bg-red-500::before {
1061  background-color: oklch(63.7% .237 25.331);
1062  content: var(--en-content);
1063}
1064
1065.after\:content-\[counter\(foo\)\]::after {
1066  --en-content: counter(foo);
1067  content: var(--en-content);
1068}
1069
1070.after\:rounded-full::after {
1071  border-radius: 9999px;
1072  content: var(--en-content);
1073}"
1074            )
1075        );
1076    }
1077
1078    #[test]
1079    fn gen_css_for_selector_with_dark_variant() {
1080        let generated = generate(["dark:mt-px"], &base_config());
1081
1082        assert_eq!(
1083            generated,
1084            String::from(
1085                r"@media (prefers-color-scheme: dark) {
1086  .dark\:mt-px {
1087    margin-top: 1px;
1088  }
1089}"
1090            )
1091        );
1092
1093        let mut config = base_config();
1094        config.theme.dark_mode = DarkMode::new_class(".dark");
1095
1096        let generated = generate(["dark:mt-px"], &config);
1097
1098        assert_eq!(
1099            generated,
1100            String::from(
1101                r".dark .dark\:mt-px {
1102  margin-top: 1px;
1103}"
1104            )
1105        );
1106    }
1107
1108    #[test]
1109    fn variant_ordering() {
1110        let generated = generate(["*:first:text-green-400"], &base_config());
1111
1112        assert_eq!(
1113            generated,
1114            String::from(
1115                r".\*\:first\:text-green-400 > *:first-child {
1116  color: oklch(79.2% .209 151.711);
1117}"
1118            )
1119        );
1120
1121        let mut config = base_config();
1122        config.theme.dark_mode = DarkMode::new_class(".dark");
1123
1124        let generated = generate(["dark:mt-px"], &config);
1125
1126        assert_eq!(
1127            generated,
1128            String::from(
1129                r".dark .dark\:mt-px {
1130  margin-top: 1px;
1131}"
1132            )
1133        );
1134    }
1135
1136    #[test]
1137    fn named_group_and_peer() {
1138        let generated = generate(
1139            ["group-checked/item:block peer-checked/item:block peer-not-checked/item:block"],
1140            &base_config(),
1141        );
1142
1143        assert_eq!(
1144            generated,
1145            String::from(
1146                r".group\/item:checked .group-checked\/item\:block {
1147  display: block;
1148}
1149
1150.peer\/item:not(:checked) ~ .peer-not-checked\/item\:block {
1151  display: block;
1152}
1153
1154.peer\/item:checked ~ .peer-checked\/item\:block {
1155  display: block;
1156}"
1157            )
1158        );
1159    }
1160
1161    #[test]
1162    fn prefixed_variants() {
1163        let generated = generate(
1164            ["supports-[display:flex]:flex nth-of-type-[span]:text-red-500 data-[active]:block"],
1165            &base_config(),
1166        );
1167
1168        assert_eq!(
1169            generated,
1170            String::from(
1171                r".data-\[active\]\:block[data-active] {
1172  display: block;
1173}
1174
1175.nth-of-type-\[span\]\:text-red-500:nth-of-type(span) {
1176  color: oklch(63.7% .237 25.331);
1177}
1178
1179@supports (display:flex) {
1180  .supports-\[display\:flex\]\:flex {
1181    display: flex;
1182  }
1183}"
1184            )
1185        );
1186    }
1187
1188    #[test]
1189    fn arbitrary_values_test() {
1190        use std::fs;
1191
1192        let file_content = fs::read_to_string("tests/fixtures/arbitrary-values.html").unwrap();
1193        let _generated = generate([file_content.as_str()], &base_config());
1194    }
1195}