Skip to main content

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    plugins::{
5        Arbitrary, Color, CustomPlugin, DynamicPropertyName, ExtraSlash, Functional,
6        ListProperties, ListValues, Number, Plugin, PropertyName, Spacing, StaticPropertyName,
7    },
8    preflight::Preflight,
9    selector::{
10        Modifier, Selector, Variant, parse,
11        trie::{Trie, build_trie},
12    },
13    utils::{buffer::Buffer, color, shadow, spacing},
14};
15
16use std::{
17    borrow::Cow,
18    collections::{BTreeSet, HashMap},
19};
20
21/// The context used in the [`Functional`] plugin kind `can_handle` field.
22///
23/// [`Functional`]: crate::plugins::Functional
24#[derive(Debug)]
25pub struct ContextCanHandle<'a, 'b, 'c> {
26    /// The generator's configuration.
27    pub config: &'a Config,
28
29    /// The modifier which will be checked.
30    pub modifier: &'b Modifier<'c>,
31}
32
33/// The context used in the [`Functional`] plugin kind `handle` field.
34///
35/// [`Functional`]: crate::plugins::Functional
36#[derive(Debug)]
37pub struct ContextHandle<'a, 'b, 'c, 'd, 'e> {
38    /// The generator's configuration.
39    pub config: &'a Config,
40
41    /// The modifier which will have its CSS generated.
42    pub modifier: &'b Modifier<'c>,
43
44    /// The buffer containing the whole generated CSS.
45    pub buffer: &'d mut Buffer,
46
47    // Private fields used in `generate_class` and `generate_at_rules`
48    selector: &'e Selector<'e>,
49}
50
51fn push_css_lines(prop: &StaticPropertyName, value: &str, context: &mut ContextHandle) {
52    match prop {
53        PropertyName::SingleProp(prop) => {
54            context.buffer.line(format_args!("{prop}: {value};"));
55        }
56        PropertyName::MultipleProps(props) => {
57            for prop in *props {
58                context.buffer.line(format_args!("{prop}: {value};"));
59            }
60        }
61    }
62}
63
64fn dynamic_push_css_lines(prop: &DynamicPropertyName, value: &str, context: &mut ContextHandle) {
65    match prop {
66        DynamicPropertyName::SingleProp(prop) => {
67            context.buffer.line(format_args!("{prop}: {value};"));
68        }
69        DynamicPropertyName::MultipleProps(props) => {
70            for prop in props {
71                context.buffer.line(format_args!("{prop}: {value};"));
72            }
73        }
74    }
75}
76
77fn push_css_lines_with_templating(
78    prop: &StaticPropertyName,
79    template: &Option<StaticPropertyName>,
80    value: &str,
81    slash_value: Option<&str>,
82    context: &mut ContextHandle,
83) {
84    let transform_template = |template: &str| {
85        if let Some(slash_value) = slash_value {
86            template.replace("{}", value).replace("{/}", slash_value)
87        } else {
88            template.replace("{}", value)
89        }
90    };
91
92    match prop {
93        PropertyName::SingleProp(prop) => {
94            let value = if let Some(template) = template {
95                let PropertyName::SingleProp(template) = template else {
96                    unreachable!(
97                        "this variant is asserted when finding the plugin in find_plugin.rs"
98                    );
99                };
100                Cow::Owned(transform_template(template))
101            } else {
102                Cow::Borrowed(value)
103            };
104
105            context.buffer.line(format_args!("{prop}: {value};"));
106        }
107        PropertyName::MultipleProps(props) => {
108            if let Some(template) = template {
109                let PropertyName::MultipleProps(template) = template else {
110                    unreachable!(
111                        "this variant is asserted when finding the plugin in find_plugin.rs"
112                    );
113                };
114                for (i, prop) in props.iter().enumerate() {
115                    // The length of plugin.template_multiple is asserted to be the
116                    // same as the length of the list of property names when finding the plugin in
117                    // find_plugin.rs
118                    let value = transform_template(template[i]);
119                    context.buffer.line(format_args!("{prop}: {value};"));
120                }
121            } else {
122                for prop in *props {
123                    context.buffer.line(format_args!("{prop}: {value};"));
124                }
125            }
126        }
127    }
128}
129
130fn dynamic_push_css_lines_with_templating(
131    prop: &DynamicPropertyName,
132    template: &Option<DynamicPropertyName>,
133    value: &str,
134    slash_value: Option<&str>,
135    context: &mut ContextHandle,
136) {
137    let transform_template = |template: &str| {
138        if let Some(slash_value) = slash_value {
139            template.replace("{}", value).replace("{/}", slash_value)
140        } else {
141            template.replace("{}", value)
142        }
143    };
144
145    match prop {
146        DynamicPropertyName::SingleProp(prop) => {
147            let value = if let Some(template) = &template {
148                let PropertyName::SingleProp(template) = template else {
149                    unreachable!(
150                        "this variant is asserted when finding the plugin in find_plugin.rs"
151                    );
152                };
153                Cow::Owned(transform_template(template))
154            } else {
155                Cow::Borrowed(value)
156            };
157
158            context.buffer.line(format_args!("{prop}: {value};"));
159        }
160        DynamicPropertyName::MultipleProps(props) => {
161            if let Some(template) = &template {
162                let PropertyName::MultipleProps(template) = template else {
163                    unreachable!(
164                        "this variant is asserted when finding the plugin in find_plugin.rs"
165                    );
166                };
167                for (i, prop) in props.iter().enumerate() {
168                    // The length of plugin.template_multiple is asserted to be the
169                    // same as the length of the list of property names at compile
170                    // time in the method Plugin::template_multiple
171                    let value = transform_template(&template[i]);
172                    context.buffer.line(format_args!("{prop}: {value};"));
173                }
174            } else {
175                for prop in props {
176                    context.buffer.line(format_args!("{prop}: {value};"));
177                }
178            }
179        }
180    }
181}
182
183fn add_extra_css(
184    extra_css: &phf::Map<&'static str, &'static str>,
185    context: &mut ContextHandle,
186    value: &str,
187) {
188    let Some(css) = extra_css.get(value) else {
189        return;
190    };
191
192    if !css.is_empty() {
193        context.buffer.raw(css);
194    }
195}
196
197fn dynamic_add_extra_css(
198    extra_css: &HashMap<String, String>,
199    context: &mut ContextHandle,
200    value: &str,
201) {
202    let Some(css) = extra_css.get(value) else {
203        return;
204    };
205
206    if !css.is_empty() {
207        context.buffer.raw(css);
208    }
209}
210
211fn handle(plugin: &CustomPlugin, context: &mut ContextHandle) {
212    match (&plugin, context.modifier) {
213        (
214            CustomPlugin::Static(Plugin::ListProperties(ListProperties {
215                props,
216                extra_rule_css,
217                extra_css,
218                extra_class,
219                ..
220            })),
221            Modifier::Builtin { value, .. },
222        ) => {
223            if let Some(extra_css) = extra_css {
224                add_extra_css(extra_css, context, value);
225            }
226
227            let lines = *props
228                .get(value)
229                .expect("key existence was checked in can_handle");
230
231            if !lines.is_empty() {
232                generate_at_rules(context, |context| {
233                    generate_class(
234                        context,
235                        |context| {
236                            context.buffer.lines(lines);
237
238                            if let Some(extra_rule_css) = extra_rule_css {
239                                context.buffer.lines(*extra_rule_css);
240                            }
241                        },
242                        extra_class.unwrap_or(""),
243                    );
244                });
245            }
246        }
247        (
248            CustomPlugin::Dynamic(Plugin::ListProperties(ListProperties {
249                props,
250                extra_rule_css,
251                extra_css,
252                extra_class,
253                ..
254            })),
255            Modifier::Builtin { value, .. },
256        ) => {
257            if let Some(extra_css) = extra_css {
258                dynamic_add_extra_css(extra_css, context, value);
259            }
260
261            let lines = props
262                .get(*value)
263                .expect("key existence was checked in can_handle");
264
265            if !lines.is_empty() {
266                generate_at_rules(context, |context| {
267                    generate_class(
268                        context,
269                        |context| {
270                            context.buffer.lines(lines);
271
272                            if let Some(extra_rule_css) = extra_rule_css {
273                                context.buffer.lines(extra_rule_css);
274                            }
275                        },
276                        extra_class.as_ref().map_or("", String::as_str),
277                    );
278                });
279            }
280        }
281
282        (
283            CustomPlugin::Static(Plugin::ListValues(ListValues {
284                prop,
285                values,
286                extra_slash,
287                extra_rule_css,
288                extra_css,
289                extra_class,
290                ..
291            })),
292            Modifier::Builtin { value, .. },
293        ) => {
294            let (value, template_value) = if extra_slash.is_some()
295                && let Some(index) = value.find('/')
296            {
297                let (before, after) = value.split_at(index);
298                (before, Some(&after[1..]))
299            } else {
300                (*value, extra_slash.as_ref().map(|e| e.default))
301            };
302
303            if let Some(extra_css) = extra_css {
304                add_extra_css(extra_css, context, value);
305            }
306
307            let value = *values
308                .get(value)
309                .expect("key existence was checked in can_handle");
310
311            let value = if let Some(ExtraSlash { values, .. }) = &extra_slash {
312                Cow::Owned(
313                    value.replace(
314                        "{/}",
315                        values
316                            .get(template_value.unwrap())
317                            .expect("extra_slash values are checked in can_hamdle"),
318                    ),
319                )
320            } else {
321                Cow::Borrowed(value)
322            };
323
324            generate_at_rules(context, |context| {
325                generate_class(
326                    context,
327                    |context| {
328                        push_css_lines(prop, &value, context);
329
330                        if let Some(extra_rule_css) = extra_rule_css {
331                            context.buffer.lines(*extra_rule_css);
332                        }
333                    },
334                    extra_class.unwrap_or(""),
335                );
336            });
337        }
338        (
339            CustomPlugin::Dynamic(Plugin::ListValues(ListValues {
340                prop,
341                values,
342                extra_slash,
343                extra_rule_css,
344                extra_css,
345                extra_class,
346                ..
347            })),
348            Modifier::Builtin { value, .. },
349        ) => {
350            let (value, template_value) = if extra_slash.is_some()
351                && let Some(index) = value.find('/')
352            {
353                let (before, after) = value.split_at(index);
354                (before, Some(&after[1..]))
355            } else {
356                (*value, extra_slash.as_ref().map(|e| e.default.as_str()))
357            };
358
359            if let Some(extra_css) = extra_css {
360                dynamic_add_extra_css(extra_css, context, value);
361            }
362
363            let value = values
364                .get(value)
365                .expect("key existence was checked in can_handle");
366
367            let value = if let Some(ExtraSlash { values, .. }) = &extra_slash {
368                Cow::Owned(
369                    value.replace(
370                        "{/}",
371                        values
372                            .get(template_value.unwrap())
373                            .expect("extra_slash values are checked in can_hamdle"),
374                    ),
375                )
376            } else {
377                Cow::Borrowed(value)
378            };
379
380            generate_at_rules(context, |context| {
381                generate_class(
382                    context,
383                    |context| {
384                        dynamic_push_css_lines(prop, &value, context);
385
386                        if let Some(extra_rule_css) = extra_rule_css {
387                            context.buffer.lines(extra_rule_css);
388                        }
389                    },
390                    extra_class.as_ref().map_or("", String::as_str),
391                );
392            });
393        }
394
395        (
396            CustomPlugin::Static(Plugin::Spacing(Spacing {
397                prop,
398                extra_slash,
399                extra_rule_css,
400                extra_css,
401                extra_class,
402                template,
403                ..
404            })),
405            Modifier::Builtin {
406                value, is_negative, ..
407            },
408        ) => {
409            let (value, template_value) = if extra_slash.is_some()
410                && let Some(index) = value.find('/')
411            {
412                let (before, after) = value.split_at(index);
413                (before, Some(&after[1..]))
414            } else {
415                (*value, extra_slash.as_ref().map(|e| e.default))
416            };
417
418            if let Some(extra_css) = extra_css {
419                add_extra_css(extra_css, context, value);
420            }
421
422            generate_at_rules(context, |context| {
423                generate_class(
424                    context,
425                    |context| {
426                        let value = if &*value == "auto" {
427                            Cow::Borrowed("auto")
428                        } else if &*value == "full" {
429                            if *is_negative {
430                                Cow::Borrowed("-100%")
431                            } else {
432                                Cow::Borrowed("100%")
433                            }
434                        } else {
435                            spacing::get(value, *is_negative).unwrap()
436                        };
437                        push_css_lines_with_templating(
438                            prop,
439                            template,
440                            &value,
441                            template_value,
442                            context,
443                        );
444
445                        if let Some(extra_rule_css) = extra_rule_css {
446                            context.buffer.lines(*extra_rule_css);
447                        }
448                    },
449                    extra_class.unwrap_or(""),
450                );
451            });
452        }
453        (
454            CustomPlugin::Dynamic(Plugin::Spacing(Spacing {
455                prop,
456                extra_slash,
457                extra_rule_css,
458                extra_css,
459                extra_class,
460                template,
461                ..
462            })),
463            Modifier::Builtin {
464                value, is_negative, ..
465            },
466        ) => {
467            let (value, template_value) = if extra_slash.is_some()
468                && let Some(index) = value.find('/')
469            {
470                let (before, after) = value.split_at(index);
471                (before, Some(&after[1..]))
472            } else {
473                (*value, extra_slash.as_ref().map(|e| e.default.as_str()))
474            };
475
476            if let Some(extra_css) = extra_css {
477                dynamic_add_extra_css(extra_css, context, value);
478            }
479
480            generate_at_rules(context, |context| {
481                generate_class(
482                    context,
483                    |context| {
484                        let value = if &*value == "auto" {
485                            Cow::Borrowed("auto")
486                        } else if &*value == "full" {
487                            if *is_negative {
488                                Cow::Borrowed("-100%")
489                            } else {
490                                Cow::Borrowed("100%")
491                            }
492                        } else {
493                            spacing::get(value, *is_negative).unwrap()
494                        };
495                        dynamic_push_css_lines_with_templating(
496                            prop,
497                            template,
498                            &value,
499                            template_value,
500                            context,
501                        );
502
503                        if let Some(extra_rule_css) = extra_rule_css {
504                            context.buffer.lines(extra_rule_css);
505                        }
506                    },
507                    extra_class.as_ref().map_or("", String::as_str),
508                );
509            });
510        }
511
512        (
513            CustomPlugin::Static(Plugin::Color(Color {
514                prop,
515                extra_class,
516                extra_rule_css,
517                extra_css,
518                template,
519                ..
520            })),
521            Modifier::Builtin { value, .. },
522        ) => {
523            if let Some(extra_css) = extra_css {
524                add_extra_css(extra_css, context, value);
525            }
526
527            generate_at_rules(context, |context| {
528                generate_class(
529                    context,
530                    |context| {
531                        let value = color::get(context.config, value).unwrap();
532                        push_css_lines_with_templating(prop, template, &value, None, context);
533
534                        if let Some(extra_rule_css) = extra_rule_css {
535                            context.buffer.lines(*extra_rule_css);
536                        }
537                    },
538                    extra_class.unwrap_or(""),
539                );
540            });
541        }
542        (
543            CustomPlugin::Dynamic(Plugin::Color(Color {
544                prop,
545                extra_class,
546                extra_rule_css,
547                extra_css,
548                template,
549                ..
550            })),
551            Modifier::Builtin { value, .. },
552        ) => {
553            if let Some(extra_css) = extra_css {
554                dynamic_add_extra_css(extra_css, context, value);
555            }
556
557            generate_at_rules(context, |context| {
558                generate_class(
559                    context,
560                    |context| {
561                        let value = color::get(context.config, value).unwrap();
562                        dynamic_push_css_lines_with_templating(
563                            prop, template, &*value, None, context,
564                        );
565
566                        if let Some(extra_rule_css) = extra_rule_css {
567                            context.buffer.lines(extra_rule_css);
568                        }
569                    },
570                    extra_class.as_ref().map_or("", String::as_str),
571                );
572            });
573        }
574
575        (
576            CustomPlugin::Static(Plugin::Number(Number {
577                prop,
578                divide_by,
579                extra_slash,
580                extra_rule_css,
581                extra_css,
582                extra_class,
583                template,
584                ..
585            })),
586            Modifier::Builtin { value, is_negative },
587        ) => {
588            let (value, template_value) = if extra_slash.is_some()
589                && let Some(index) = value.find('/')
590            {
591                let (before, after) = value.split_at(index);
592                (before, Some(&after[1..]))
593            } else {
594                (*value, extra_slash.as_ref().map(|e| e.default))
595            };
596
597            if let Some(extra_css) = extra_css {
598                add_extra_css(extra_css, context, value);
599            }
600
601            generate_at_rules(context, |context| {
602                generate_class(
603                    context,
604                    |context| {
605                        let coeff = if *is_negative { -1.0 } else { 1.0 };
606                        let value = if value.is_empty() {
607                            coeff.to_string()
608                        } else if value == "auto" {
609                            String::from("auto")
610                        } else {
611                            (value.parse::<usize>().unwrap() as f32 / divide_by.unwrap_or(1.0)
612                                * coeff)
613                                .to_string()
614                        };
615
616                        push_css_lines_with_templating(
617                            prop,
618                            template,
619                            &value,
620                            template_value,
621                            context,
622                        );
623
624                        if let Some(extra_rule_css) = extra_rule_css {
625                            context.buffer.lines(*extra_rule_css);
626                        }
627                    },
628                    extra_class.unwrap_or(""),
629                );
630            });
631        }
632        (
633            CustomPlugin::Dynamic(Plugin::Number(Number {
634                prop,
635                divide_by,
636                extra_slash,
637                extra_rule_css,
638                extra_css,
639                extra_class,
640                template,
641                ..
642            })),
643            Modifier::Builtin { value, is_negative },
644        ) => {
645            let (value, template_value) = if extra_slash.is_some()
646                && let Some(index) = value.find('/')
647            {
648                let (before, after) = value.split_at(index);
649                (before, Some(&after[1..]))
650            } else {
651                (*value, extra_slash.as_ref().map(|e| e.default.as_str()))
652            };
653
654            if let Some(extra_css) = extra_css {
655                dynamic_add_extra_css(extra_css, context, value);
656            }
657
658            generate_at_rules(context, |context| {
659                generate_class(
660                    context,
661                    |context| {
662                        let coeff = if *is_negative { -1.0 } else { 1.0 };
663                        let value = if value.is_empty() {
664                            coeff.to_string()
665                        } else if value == "auto" {
666                            String::from("auto")
667                        } else {
668                            (value.parse::<usize>().unwrap() as f32 / divide_by.unwrap_or(1.0)
669                                * coeff)
670                                .to_string()
671                        };
672
673                        dynamic_push_css_lines_with_templating(
674                            prop,
675                            template,
676                            &*value,
677                            template_value,
678                            context,
679                        );
680
681                        if let Some(extra_rule_css) = extra_rule_css {
682                            context.buffer.lines(extra_rule_css);
683                        }
684                    },
685                    extra_class.as_ref().map_or("", String::as_str),
686                );
687            });
688        }
689
690        (
691            CustomPlugin::Static(Plugin::Arbitrary(Arbitrary {
692                prop,
693                extra_rule_css,
694                extra_class,
695                extra_css,
696                shadow_color_replacement,
697                template,
698                ..
699            })),
700            Modifier::Arbitrary { value, .. },
701        ) => {
702            if let Some(extra_css) = extra_css {
703                add_extra_css(extra_css, context, value);
704            }
705
706            generate_at_rules(context, |context| {
707                generate_class(
708                    context,
709                    |context| {
710                        // If the shadow is malformed, just output it without modification
711                        let value = if let Some(color_replacement) = shadow_color_replacement
712                            && let Some(mut shadow) = shadow::ShadowList::parse(value)
713                        {
714                            shadow.replace_all_colors(color_replacement);
715                            &Cow::Owned(shadow.to_string())
716                        } else {
717                            value
718                        };
719
720                        push_css_lines_with_templating(prop, template, value, None, context);
721
722                        if let Some(extra_rule_css) = extra_rule_css {
723                            context.buffer.lines(*extra_rule_css);
724                        }
725                    },
726                    extra_class.unwrap_or(""),
727                );
728            });
729        }
730        (
731            CustomPlugin::Dynamic(Plugin::Arbitrary(Arbitrary {
732                prop,
733                extra_rule_css,
734                extra_css,
735                extra_class,
736                shadow_color_replacement,
737                template,
738                ..
739            })),
740            Modifier::Arbitrary { value, .. },
741        ) => {
742            if let Some(extra_css) = extra_css {
743                dynamic_add_extra_css(extra_css, context, value);
744            }
745
746            generate_at_rules(context, |context| {
747                generate_class(
748                    context,
749                    |context| {
750                        // If the shadow is malformed, just output it without modification
751                        let value = if let Some(color_replacement) = shadow_color_replacement
752                            && let Some(mut shadow) = shadow::ShadowList::parse(value)
753                        {
754                            shadow.replace_all_colors(color_replacement);
755                            &Cow::Owned(shadow.to_string())
756                        } else {
757                            value
758                        };
759
760                        dynamic_push_css_lines_with_templating(
761                            prop, template, value, None, context,
762                        );
763
764                        if let Some(extra_rule_css) = extra_rule_css {
765                            context.buffer.lines(extra_rule_css);
766                        }
767                    },
768                    extra_class.as_ref().map_or("", String::as_str),
769                );
770            });
771        }
772
773        (CustomPlugin::Static(Plugin::Functional(Functional { handle, .. })), _) => {
774            handle(context);
775        }
776        _ => unreachable!(
777            "Only plugins which can be handled are supposed to be handled. However {plugin:?} cannot handle {:?} but passed can_handle check. This is a bug in encre-css, please report it.",
778            context.modifier
779        ),
780    }
781}
782
783/// Generate the needed CSS at-rules (e.g @media).
784///
785/// Note: The inner class (e.g. .foo-bar) is not handled by this function, see [`generate_class`].
786///
787/// The second argument, a closure, is called to generate the CSS content of the rule.
788///
789/// # Errors
790///
791/// Returns [`fmt::Error`] indicating whether writing to the buffer succeeded.
792///
793/// [`fmt::Error`]: std::fmt::Error
794pub fn generate_at_rules<T: FnOnce(&mut ContextHandle)>(
795    context: &mut ContextHandle,
796    rule_content_fn: T,
797) {
798    let ContextHandle {
799        buffer, selector, ..
800    } = context;
801
802    if !selector.variants.is_empty() {
803        selector.variants.iter().for_each(|variant| {
804            if variant.template.starts_with('@') {
805                buffer.line(format_args!("{} {{", variant.template));
806                buffer.indent();
807            }
808        });
809    }
810
811    rule_content_fn(context);
812
813    let ContextHandle { buffer, .. } = context;
814    while !buffer.is_unindented() {
815        buffer.unindent();
816
817        if buffer.is_unindented() {
818            buffer.raw("}");
819        } else {
820            buffer.line("}");
821        }
822    }
823}
824
825/// Generate the complete CSS wrapper needed for a single rule.
826///
827/// This function is a combination of the [`generate_at_rules`] and [`generate_class`] functions.
828///
829/// The second argument, a closure, is called to generate the CSS content of the rule.
830///
831/// If you need to customize the generated class name (e.g adding custom pseudo-classes), you can
832/// manually call [`generate_class`] nested inside [`generate_at_rules`].
833///
834/// # Errors
835///
836/// Returns [`fmt::Error`] indicating whether writing to the buffer succeeded.
837///
838/// [`fmt::Error`]: std::fmt::Error
839pub fn generate_wrapper<T: FnOnce(&mut ContextHandle)>(
840    context: &mut ContextHandle,
841    rule_content_fn: T,
842) {
843    generate_at_rules(context, |context| {
844        generate_class(context, rule_content_fn, "");
845    });
846}
847
848/// Generate a CSS rule with a class.
849///
850/// Note: At-rules (e.g. @media) are not handled by this function, see [`generate_at_rules`].
851///
852/// The second argument, a closure, is called to generate the CSS content of the rule.
853/// The third argument is used to add a custom string just after the class (e.g. `> *`).
854///
855/// # Errors
856///
857/// Returns [`fmt::Error`] indicating whether writing to the buffer succeeded.
858///
859/// [`fmt::Error`]: std::fmt::Error
860#[allow(clippy::too_many_lines)]
861pub fn generate_class<T: FnOnce(&mut ContextHandle)>(
862    context: &mut ContextHandle,
863    rule_content_fn: T,
864    custom_after_class: &str,
865) {
866    let ContextHandle {
867        buffer, selector, ..
868    } = context;
869
870    // Write the class
871    let mut base_class = String::with_capacity(1 + selector.full.len());
872    base_class.push('.');
873
874    // The browser will automatically replace the escape codes in the classes, so we need to also
875    // replace them in the generated CSS full selector
876    let unescaped_full_selector =
877        crate::selector::parser::replace_escape_codes(Cow::Borrowed(selector.full));
878    unescaped_full_selector
879        .chars()
880        .enumerate()
881        .for_each(|(i, ch)| {
882            if !ch.is_alphanumeric() && ch != '-' && ch != '_' {
883                base_class.push('\\');
884                base_class.push(ch);
885            } else if i == 0 && ch.is_numeric() {
886                // CSS classes must not start with a number, we need to escape it
887                base_class.push_str("\\3");
888                base_class.push(ch);
889            } else {
890                base_class.push(ch);
891            }
892        });
893
894    if !selector.variants.is_empty() {
895        // Variants are applied from right to left
896        // (https://tailwindcss.com/docs/upgrade-guide#variant-stacking-order),
897        // so no need to reverse the variants
898        selector.variants.iter().for_each(|variant| {
899            if !variant.template.starts_with('@') {
900                base_class = variant.template.replace('&', &base_class);
901            }
902        });
903    }
904    buffer.line(format_args!("{base_class}{custom_after_class} {{"));
905
906    // Store the index of the start of the class content (useful when the `important` flag is present)
907    let content_start = buffer.len();
908
909    // Rule content
910    buffer.indent();
911    rule_content_fn(context);
912
913    let ContextHandle {
914        buffer, selector, ..
915    } = context;
916
917    // If the rule is selecting the `::before` or `::after` pseudo elements, we need to generate a
918    // default `content` property
919    if selector
920        .variants
921        .iter()
922        .any(|variant| ["&::before", "&::after"].contains(&&*variant.template))
923    {
924        buffer.line("content: var(--en-content);");
925    }
926
927    // If the `important` flag is present we need to replace all `;\n` or `;\r\n`
928    // to ` !important;\n` or ` !important;\r\n`
929    if selector.is_important {
930        let mut extra_index = 0;
931        let positions = buffer[content_start..]
932            .match_indices('\n')
933            .map(|i| i.0)
934            .collect::<Vec<usize>>();
935
936        for index in positions {
937            if index - 1 == 0 {
938                continue;
939            }
940
941            let index = content_start + extra_index + index;
942            let index = if &buffer[index - 1..index] == "\r" {
943                index - 1
944            } else {
945                index
946            };
947            let replace_with = " !important;";
948            buffer.replace_range(index - 1..index, replace_with);
949            extra_index += replace_with.len() - 1;
950        }
951    }
952
953    buffer.unindent();
954    if buffer.is_unindented() {
955        buffer.raw("}");
956    } else {
957        buffer.line("}");
958    }
959}
960
961fn resolve_selector<'a>(
962    selector: &'a str,
963    full_class: Option<&'a str>,
964    selectors: &mut BTreeSet<Selector<'a>>,
965    config: &'a Config,
966    config_derived_variants: &[(Cow<'static, str>, Variant<'static>)],
967    depth: MaxShortcutDepth,
968    trie: &Trie,
969) {
970    if depth.get() == 0 {
971        return;
972    }
973
974    if let Some(expanded) = config.shortcuts.get(selector) {
975        expanded.split(' ').for_each(|shortcut_target| {
976            resolve_selector(
977                shortcut_target,
978                full_class.or(Some(selector)),
979                selectors,
980                config,
981                config_derived_variants,
982                MaxShortcutDepth::new(depth.get() - 1),
983                trie,
984            );
985        });
986    } else {
987        selectors.extend(
988            parse(
989                selector,
990                None,
991                full_class,
992                config,
993                config_derived_variants,
994                trie,
995            )
996            .into_iter()
997            .filter_map(Result::ok),
998        );
999    }
1000}
1001
1002/// Generate the CSS styles needed based on the given sources.
1003///
1004/// Each source will be scanned in order to extract atomic classes, then CSS will be generated for
1005/// each class found.
1006///
1007/// By default, it splits the source by spaces, double quotes, single quotes, backticks and new
1008/// lines.
1009///
1010/// This function also removes duplicated selectors and sorts the generated CSS classes based on
1011/// the order in which they were defined to avoid conflicts.
1012pub fn generate<'a>(sources: impl IntoIterator<Item = &'a str>, config: &Config) -> String {
1013    let config_derived_variants = config.get_derived_variants();
1014    let mut selectors = BTreeSet::new();
1015    let trie = build_trie(config);
1016
1017    // Add selectors from the safelist
1018    for safe_selector in config.safelist.iter() {
1019        if let Some(expanded) = config.shortcuts.get(&**safe_selector) {
1020            expanded.split(' ').for_each(|shortcut_target| {
1021                selectors.extend(
1022                    parse(
1023                        shortcut_target,
1024                        None,
1025                        Some(safe_selector),
1026                        config,
1027                        &config_derived_variants,
1028                        &trie,
1029                    )
1030                    .into_iter()
1031                    .filter_map(Result::ok),
1032                );
1033            });
1034        } else {
1035            selectors.extend(
1036                parse(
1037                    safe_selector,
1038                    None,
1039                    None,
1040                    config,
1041                    &config_derived_variants,
1042                    &trie,
1043                )
1044                .into_iter()
1045                .filter_map(Result::ok),
1046            );
1047        }
1048    }
1049
1050    for source in sources {
1051        let new_selectors = config.scanner.scan(source);
1052
1053        for selector in new_selectors {
1054            resolve_selector(
1055                selector,
1056                None,
1057                &mut selectors,
1058                config,
1059                &config_derived_variants,
1060                config.max_shortcut_depth,
1061                &trie,
1062            );
1063        }
1064    }
1065
1066    let preflight = config.preflight.build();
1067    let mut buffer = Buffer::with_capacity(10 * selectors.len()); // TODO: More accurate value
1068    buffer.raw(&preflight);
1069
1070    for selector in selectors {
1071        if buffer.len() != preflight.len() || config.preflight != Preflight::None {
1072            buffer.raw("\n\n");
1073        }
1074
1075        let mut context = ContextHandle {
1076            config,
1077            modifier: &selector.modifier,
1078            buffer: &mut buffer,
1079            selector: &selector,
1080        };
1081
1082        handle(&selector.plugin, &mut context);
1083    }
1084
1085    buffer.into_inner()
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090    use super::*;
1091    use crate::{config::DarkMode, utils::testing::base_config};
1092
1093    use pretty_assertions::assert_eq;
1094
1095    #[test]
1096    fn not_parsing_too_loosely() {
1097        let generated = generate(["flex-test-[]", "m1", "m-1/a"], &base_config());
1098        assert!(generated.is_empty());
1099    }
1100
1101    #[test]
1102    fn divide_and_space_between_special_class() {
1103        let generated = generate(
1104            [
1105                "hover:space-x-1",
1106                "space-x-2",
1107                "[&:has(.class)_>_*]:space-y-3",
1108                "divide-red-100",
1109                "divide-dashed",
1110                "divide-x-[11px]",
1111                "xl:[&_>_*]:divide-y-2",
1112            ],
1113            &base_config(),
1114        );
1115
1116        assert_eq!(
1117            generated,
1118            String::from(
1119                r".space-x-2 > :not(:last-child) {
1120  margin-inline-start: calc(0.5rem * var(--en-space-x-reverse));
1121  margin-inline-end: calc(0.5rem * calc(1 - var(--en-space-x-reverse)));
1122  --en-space-x-reverse: 0;
1123}
1124
1125.divide-x-\[11px\] > :not([hidden]) ~ :not([hidden]) {
1126  border-inline-start-width: calc(11px * var(--en-divide-x-reverse));
1127  border-inline-end-width: calc(11px * calc(1 - var(--en-divide-x-reverse)));
1128  --en-divide-x-reverse: 0;
1129}
1130
1131.divide-dashed > :not([hidden]) ~ :not([hidden]) {
1132  border-style: dashed;
1133}
1134
1135.divide-red-100 > :not([hidden]) ~ :not([hidden]) {
1136  border-color: oklch(93.6% .032 17.717);
1137}
1138
1139.hover\:space-x-1:hover > :not(:last-child) {
1140  margin-inline-start: calc(0.25rem * var(--en-space-x-reverse));
1141  margin-inline-end: calc(0.25rem * calc(1 - var(--en-space-x-reverse)));
1142  --en-space-x-reverse: 0;
1143}
1144
1145@media (width >= 80rem) {
1146  .xl\:\[\&_\>_\*\]\:divide-y-2 > * > :not([hidden]) ~ :not([hidden]) {
1147    border-block-start-width: calc(2px * var(--en-divide-y-reverse));
1148    border-block-end-width: calc(2px * calc(1 - var(--en-divide-y-reverse)));
1149    --en-divide-y-reverse: 0;
1150  }
1151}
1152
1153.\[\&\:has\(\.class\)_\>_\*\]\:space-y-3:has(.class) > * > :not(:last-child) {
1154  margin-block-start: calc(0.75rem * var(--en-space-y-reverse));
1155  margin-block-end: calc(0.75rem * calc(1 - var(--en-space-y-reverse)));
1156  --en-space-y-reverse: 0;
1157}"
1158            )
1159        );
1160    }
1161
1162    #[test]
1163    fn negative_values() {
1164        let generated = generate(
1165            [
1166                "-top-2",
1167                "-z-2",
1168                "-order-2",
1169                "-translate-x-52",
1170                "-rotate-90",
1171                "-skew-x-2",
1172                "-scale-50",
1173                "-scroll-mt-2",
1174                "-space-x-2",
1175                "-indent-2",
1176                "-hue-rotate-60",
1177                "hover:-hue-rotate-60",
1178                "-backdrop-hue-rotate-90",
1179            ],
1180            &base_config(),
1181        );
1182
1183        assert_eq!(
1184            generated,
1185            String::from(
1186                r".-top-2 {
1187  top: -0.5rem;
1188}
1189
1190.-z-2 {
1191  z-index: -2;
1192}
1193
1194.-order-2 {
1195  order: -2;
1196}
1197
1198.-translate-x-52 {
1199  --en-translate-x: -13rem;
1200  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));
1201}
1202
1203.-rotate-90 {
1204  --en-rotate-x: -90deg;
1205  --en-rotate-y: -90deg;
1206  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));
1207}
1208
1209.-skew-x-2 {
1210  --en-skew-x: -2deg;
1211  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));
1212}
1213
1214.-scale-50 {
1215  --en-scale-x: -0.5;
1216  --en-scale-y: -0.5;
1217  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));
1218}
1219
1220.-scroll-mt-2 {
1221  scroll-margin-top: -0.5rem;
1222}
1223
1224.-space-x-2 > :not(:last-child) {
1225  margin-inline-start: calc(-0.5rem * var(--en-space-x-reverse));
1226  margin-inline-end: calc(-0.5rem * calc(1 - var(--en-space-x-reverse)));
1227  --en-space-x-reverse: 0;
1228}
1229
1230.-indent-2 {
1231  text-indent: -0.5rem;
1232}
1233
1234.-hue-rotate-60 {
1235  --en-hue-rotate: hue-rotate(-60deg);
1236  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);
1237}
1238
1239.-backdrop-hue-rotate-90 {
1240  --en-backdrop-hue-rotate: hue-rotate(-90deg);
1241  -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);
1242  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);
1243}
1244
1245.hover\:-hue-rotate-60:hover {
1246  --en-hue-rotate: hue-rotate(-60deg);
1247  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);
1248}"
1249            )
1250        );
1251    }
1252
1253    #[test]
1254    fn gen_css_for_simple_selector() {
1255        let generated = generate(["text-current"], &base_config());
1256
1257        assert_eq!(
1258            generated,
1259            String::from(
1260                ".text-current {
1261  color: currentColor;
1262}"
1263            )
1264        );
1265    }
1266
1267    #[test]
1268    fn gen_css_with_important_flag() {
1269        let generated = generate(
1270            [
1271                "!w-full",
1272                "!-mb-8",
1273                "!shadow-sm",
1274                "!-hue-rotate-60",
1275                "focus:!w-2",
1276                "focus:!-mb-2",
1277            ],
1278            &base_config(),
1279        );
1280
1281        assert_eq!(
1282            generated,
1283            String::from(
1284                r".\!-mb-8 {
1285  margin-bottom: -2rem !important;
1286}
1287
1288.\!w-full {
1289  width: 100% !important;
1290}
1291
1292.\!shadow-sm {
1293  --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;
1294  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;
1295}
1296
1297.\!-hue-rotate-60 {
1298  --en-hue-rotate: hue-rotate(-60deg) !important;
1299  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;
1300}
1301
1302.focus\:\!-mb-2:focus {
1303  margin-bottom: -0.5rem !important;
1304}
1305
1306.focus\:\!w-2:focus {
1307  width: 0.5rem !important;
1308}",
1309            )
1310        );
1311    }
1312
1313    #[test]
1314    fn gen_css_for_selector_needing_custom_css() {
1315        let generated = generate(["animate-pulse", "animate-pulse"], &base_config());
1316
1317        assert_eq!(
1318            generated,
1319            String::from(
1320                "@-webkit-keyframes pulse {
1321  50% {
1322    opacity: .5;
1323  }
1324}
1325
1326@keyframes pulse {
1327  0%, 100% {
1328    opacity: 1;
1329  }
1330  50% {
1331    opacity: .5;
1332  }
1333}
1334
1335.animate-pulse {
1336  -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1337  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1338}"
1339            )
1340        );
1341    }
1342
1343    #[test]
1344    fn gen_css_for_arbitrary_value() {
1345        let generated = generate(
1346            [
1347                "bg-[red]",
1348                "bg-[url(../img/image_with_underscores.png)]",
1349                "mt-[calc(100%-10px)]",
1350                "2xl:pb-[calc((100%/2)-10px+2rem)]",
1351            ],
1352            &base_config(),
1353        );
1354
1355        assert_eq!(
1356            generated,
1357            String::from(
1358                r".mt-\[calc\(100\%-10px\)\] {
1359  margin-top: calc(100% - 10px);
1360}
1361
1362.bg-\[red\] {
1363  background-color: red;
1364}
1365
1366.bg-\[url\(\.\.\/img\/image_with_underscores\.png\)\] {
1367  background-image: url(../img/image_with_underscores.png);
1368}
1369
1370@media (width >= 96rem) {
1371  .\32xl\:pb-\[calc\(\(100\%\/2\)-10px\+2rem\)\] {
1372    padding-bottom: calc((100% / 2) - 10px + 2rem);
1373  }
1374}"
1375            )
1376        );
1377    }
1378
1379    #[test]
1380    fn gen_css_for_arbitrary_value_with_hint() {
1381        let generated = generate(["bg-[color:red]", "hover:bg-[color:red]"], &base_config());
1382
1383        assert_eq!(
1384            generated,
1385            String::from(
1386                r".bg-\[color\:red\] {
1387  background-color: red;
1388}
1389
1390.hover\:bg-\[color\:red\]:hover {
1391  background-color: red;
1392}"
1393            )
1394        );
1395    }
1396
1397    #[test]
1398    fn gen_css_for_selector_with_simple_variant() {
1399        let generated = generate(["focus:w-full"], &base_config());
1400
1401        assert_eq!(
1402            generated,
1403            String::from(
1404                r".focus\:w-full:focus {
1405  width: 100%;
1406}"
1407            )
1408        );
1409    }
1410
1411    #[test]
1412    fn gen_selector_css_variants_test() {
1413        let generated = generate(
1414            [
1415                "sm:hover:bg-red-400",
1416                "focus:hover:bg-red-600",
1417                "active:rtl:bg-red-800",
1418                "md:focus:selection:bg-blue-100",
1419                "rtl:active:focus:lg:underline",
1420                "print:ltr:xl:hover:focus:active:text-yellow-300",
1421                "2xl:motion-safe:landscape:focus-within:visited:first:odd:checked:open:rtl:bg-purple-100",
1422                "hover:file:bg-pink-600",
1423                "file:hover:bg-pink-600",
1424                "sm:before:target:content-[&#39;Hello_world!&#39;]",
1425                "marker:selection:hover:bg-green-200",
1426                "group-hover:bg-green-300",
1427                "group-focus:bg-green-400",
1428                "peer-invalid:bg-red-500",
1429                "peer-not-invalid:bg-green-500",
1430            ],
1431            &base_config(),
1432        );
1433
1434        assert_eq!(
1435            generated,
1436            String::from(
1437                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 {
1438  background-color: oklch(92.5% .084 155.995);
1439}
1440
1441.file\:hover\:bg-pink-600::file-selector-button, .file\:hover\:bg-pink-600::-webkit-file-upload-button:hover {
1442  background-color: oklch(59.2% .249 .584);
1443}
1444
1445.hover\:file\:bg-pink-600:hover::file-selector-button, .hover\:file\:bg-pink-600:hover::-webkit-file-upload-button {
1446  background-color: oklch(59.2% .249 .584);
1447}
1448
1449.focus\:hover\:bg-red-600:focus:hover {
1450  background-color: oklch(57.7% .245 27.325);
1451}
1452
1453[dir="rtl"] .active\:rtl\:bg-red-800:active {
1454  background-color: oklch(44.4% .177 26.899);
1455}
1456
1457@media (width >= 64rem) {
1458  [dir="rtl"] .rtl\:active\:focus\:lg\:underline:active:focus {
1459    -webkit-text-decoration-line: underline;
1460    text-decoration-line: underline;
1461  }
1462}
1463
1464@media print {
1465  @media (width >= 80rem) {
1466    [dir="ltr"] .print\:ltr\:xl\:hover\:focus\:active\:text-yellow-300:hover:focus:active {
1467      color: oklch(90.5% .182 98.111);
1468    }
1469  }
1470}
1471
1472@media (width >= 40rem) {
1473  .sm\:before\:target\:content-\[\'Hello_world\!\'\]::before:target {
1474    --en-content: 'Hello world!';
1475    content: var(--en-content);
1476  }
1477}
1478
1479@media (width >= 40rem) {
1480  .sm\:hover\:bg-red-400:hover {
1481    background-color: oklch(70.4% .191 22.216);
1482  }
1483}
1484
1485@media (width >= 48rem) {
1486  .md\:focus\:selection\:bg-blue-100:focus *::selection, .md\:focus\:selection\:bg-blue-100:focus::selection {
1487    background-color: oklch(93.2% .032 255.585);
1488  }
1489}
1490
1491@media (width >= 96rem) {
1492  @media (prefers-reduced-motion: no-preference) {
1493    @media (orientation: landscape) {
1494      [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] {
1495        background-color: oklch(94.6% .033 307.174);
1496      }
1497    }
1498  }
1499}
1500
1501.group:hover .group-hover\:bg-green-300 {
1502  background-color: oklch(87.1% .15 154.449);
1503}
1504
1505.group:focus .group-focus\:bg-green-400 {
1506  background-color: oklch(79.2% .209 151.711);
1507}
1508
1509.peer:not(:invalid) ~ .peer-not-invalid\:bg-green-500 {
1510  background-color: oklch(72.3% .219 149.579);
1511}
1512
1513.peer:invalid ~ .peer-invalid\:bg-red-500 {
1514  background-color: oklch(63.7% .237 25.331);
1515}"#
1516            )
1517        );
1518    }
1519
1520    #[test]
1521    fn gen_css_for_duplicated_selectors() {
1522        let generated = generate(["bg-red-500 bg-red-500", "bg-red-500"], &base_config());
1523
1524        assert_eq!(
1525            generated,
1526            String::from(
1527                ".bg-red-500 {
1528  background-color: oklch(63.7% .237 25.331);
1529}"
1530            )
1531        );
1532    }
1533
1534    #[test]
1535    fn gen_css_for_selector_with_arbitrary_property() {
1536        let generated = generate(["hover:[mask-type:luminance]"], &base_config());
1537
1538        assert_eq!(
1539            generated,
1540            String::from(
1541                r".hover\:\[mask-type\:luminance\]:hover {
1542  mask-type: luminance;
1543}"
1544            )
1545        );
1546    }
1547
1548    #[test]
1549    fn gen_css_for_selector_with_arbitrary_variant() {
1550        let generated = generate(
1551            [
1552                "[&_>_*]:before:content-[&#39;hello-&#39;]",
1553                "[&:has(.active)]:bg-blue-500",
1554                "[@supports_(display:grid)]:grid",
1555                "[@supports_not_(display:grid)]:float-right",
1556            ],
1557            &base_config(),
1558        );
1559
1560        assert_eq!(
1561            generated,
1562            String::from(
1563                r"@supports not (display:grid) {
1564  .\[\@supports_not_\(display\:grid\)\]\:float-right {
1565    float: right;
1566  }
1567}
1568
1569@supports (display:grid) {
1570  .\[\@supports_\(display\:grid\)\]\:grid {
1571    display: grid;
1572  }
1573}
1574
1575.\[\&\:has\(\.active\)\]\:bg-blue-500:has(.active) {
1576  background-color: oklch(62.3% .214 259.815);
1577}
1578
1579.\[\&_\>_\*\]\:before\:content-\[\'hello-\'\] > *::before {
1580  --en-content: 'hello-';
1581  content: var(--en-content);
1582}"
1583            )
1584        );
1585    }
1586
1587    #[test]
1588    fn gen_css_for_variant_group() {
1589        let generated = generate(
1590            ["xl:(focus:(outline,outline-red-200),dark:(bg-black,text-white))"],
1591            &base_config(),
1592        );
1593
1594        assert_eq!(
1595            generated,
1596            String::from(
1597                r"@media (width >= 80rem) {
1598  .xl\:\(focus\:\(outline\,outline-red-200\)\,dark\:\(bg-black\,text-white\)\):focus {
1599    outline-color: oklch(88.5% .062 18.334);
1600  }
1601}
1602
1603@media (width >= 80rem) {
1604  .xl\:\(focus\:\(outline\,outline-red-200\)\,dark\:\(bg-black\,text-white\)\):focus {
1605    outline-width: 1px;
1606  }
1607}
1608
1609@media (prefers-color-scheme: dark) {
1610  @media (width >= 80rem) {
1611    .xl\:\(focus\:\(outline\,outline-red-200\)\,dark\:\(bg-black\,text-white\)\) {
1612      color: #fff;
1613    }
1614  }
1615}
1616
1617@media (prefers-color-scheme: dark) {
1618  @media (width >= 80rem) {
1619    .xl\:\(focus\:\(outline\,outline-red-200\)\,dark\:\(bg-black\,text-white\)\) {
1620      background-color: #000;
1621    }
1622  }
1623}"
1624            )
1625        );
1626
1627        let generated = generate(["(bg-blue-100,bg-blue-200,bg-blue-300)"], &base_config());
1628
1629        assert_eq!(
1630            generated,
1631            String::from(
1632                r".\(bg-blue-100\,bg-blue-200\,bg-blue-300\) {
1633  background-color: oklch(93.2% .032 255.585);
1634}
1635
1636.\(bg-blue-100\,bg-blue-200\,bg-blue-300\) {
1637  background-color: oklch(88.2% .059 254.128);
1638}
1639
1640.\(bg-blue-100\,bg-blue-200\,bg-blue-300\) {
1641  background-color: oklch(80.9% .105 251.813);
1642}"
1643            ),
1644        );
1645    }
1646
1647    #[test]
1648    fn default_modifier_values_for_rounded() {
1649        let generated = generate(
1650            [
1651                "rounded-tr-sm rounded-tr-md rounded-sm rounded-md rounded-t-sm rounded-bl-xl border-x border border-4 border-t-2",
1652            ],
1653            &base_config(),
1654        );
1655
1656        assert_eq!(
1657            generated,
1658            String::from(
1659                ".rounded-md {
1660  border-radius: 0.375rem;
1661}
1662
1663.rounded-sm {
1664  border-radius: 0.25rem;
1665}
1666
1667.rounded-t-sm {
1668  border-top-left-radius: 0.25rem;
1669  border-top-right-radius: 0.25rem;
1670}
1671
1672.rounded-tr-md {
1673  border-top-right-radius: 0.375rem;
1674}
1675
1676.rounded-tr-sm {
1677  border-top-right-radius: 0.25rem;
1678}
1679
1680.rounded-bl-xl {
1681  border-bottom-left-radius: 0.75rem;
1682}
1683
1684.border {
1685  border-width: 1px;
1686}
1687
1688.border-4 {
1689  border-width: 4px;
1690}
1691
1692.border-x {
1693  border-inline-width: 1px;
1694}
1695
1696.border-t-2 {
1697  border-top-width: 2px;
1698}"
1699            )
1700        );
1701    }
1702
1703    #[test]
1704    fn gen_css_for_font_with_spaces() {
1705        let generated = generate(
1706            [
1707                "font-[&#39;Times_New_Roman&#39;,Helvetica,serif]",
1708                "font-[Roboto,&#39;Open_Sans&#39;,sans-serif]",
1709            ],
1710            &base_config(),
1711        );
1712
1713        assert_eq!(
1714            generated,
1715            String::from(
1716                r".font-\[\'Times_New_Roman\'\,Helvetica\,serif\] {
1717  font-family: 'Times New Roman',Helvetica,serif;
1718}
1719
1720.font-\[Roboto\,\'Open_Sans\'\,sans-serif\] {
1721  font-family: Roboto,'Open Sans',sans-serif;
1722}"
1723            )
1724        );
1725    }
1726
1727    #[test]
1728    fn gen_css_for_container() {
1729        let generated = generate(["container"], &base_config());
1730
1731        assert_eq!(
1732            generated,
1733            String::from(
1734                ".container {
1735  width: 100%;
1736}
1737
1738@media (width >= 40rem) {
1739  .container {
1740    max-width: 40rem;
1741  }
1742}
1743
1744@media (width >= 48rem) {
1745  .container {
1746    max-width: 48rem;
1747  }
1748}
1749
1750@media (width >= 64rem) {
1751  .container {
1752    max-width: 64rem;
1753  }
1754}
1755
1756@media (width >= 80rem) {
1757  .container {
1758    max-width: 80rem;
1759  }
1760}
1761
1762@media (width >= 96rem) {
1763  .container {
1764    max-width: 96rem;
1765  }
1766}"
1767            )
1768        );
1769
1770        let generated = generate(["md:container", "md:mx-auto"], &base_config());
1771
1772        assert_eq!(
1773            generated,
1774            String::from(
1775                r"@media (width >= 48rem) {
1776  .md\:mx-auto {
1777    margin-inline: auto;
1778  }
1779}
1780
1781@media (width >= 48rem) {
1782  .md\:container {
1783    width: 100%;
1784  }
1785}
1786
1787@media (width >= 48rem) {
1788  @media (width >= 40rem) {
1789    .md\:container {
1790      max-width: 40rem;
1791    }
1792  }
1793
1794  @media (width >= 48rem) {
1795    .md\:container {
1796      max-width: 48rem;
1797    }
1798  }
1799
1800  @media (width >= 64rem) {
1801    .md\:container {
1802      max-width: 64rem;
1803    }
1804  }
1805
1806  @media (width >= 80rem) {
1807    .md\:container {
1808      max-width: 80rem;
1809    }
1810  }
1811
1812  @media (width >= 96rem) {
1813    .md\:container {
1814      max-width: 96rem;
1815    }
1816  }
1817}"
1818            )
1819        );
1820    }
1821
1822    #[test]
1823    fn gen_css_for_selector_with_before_after_variant() {
1824        let generated = generate(
1825            [
1826                "before:bg-red-500",
1827                "before:content-[&#39;Hello_world!&#39;]",
1828                "after:rounded-full",
1829                "after:content-[counter(foo)]",
1830            ],
1831            &base_config(),
1832        );
1833
1834        assert_eq!(
1835            generated,
1836            String::from(
1837                r".before\:content-\[\'Hello_world\!\'\]::before {
1838  --en-content: 'Hello world!';
1839  content: var(--en-content);
1840}
1841
1842.before\:bg-red-500::before {
1843  background-color: oklch(63.7% .237 25.331);
1844  content: var(--en-content);
1845}
1846
1847.after\:content-\[counter\(foo\)\]::after {
1848  --en-content: counter(foo);
1849  content: var(--en-content);
1850}
1851
1852.after\:rounded-full::after {
1853  border-radius: 9999px;
1854  content: var(--en-content);
1855}"
1856            )
1857        );
1858    }
1859
1860    #[test]
1861    fn gen_css_for_selector_with_dark_variant() {
1862        let generated = generate(["dark:mt-px"], &base_config());
1863
1864        assert_eq!(
1865            generated,
1866            String::from(
1867                r"@media (prefers-color-scheme: dark) {
1868  .dark\:mt-px {
1869    margin-top: 1px;
1870  }
1871}"
1872            )
1873        );
1874
1875        let mut config = base_config();
1876        config.theme.dark_mode = DarkMode::new_class(".dark");
1877
1878        let generated = generate(["dark:mt-px"], &config);
1879
1880        assert_eq!(
1881            generated,
1882            String::from(
1883                r".dark .dark\:mt-px {
1884  margin-top: 1px;
1885}"
1886            )
1887        );
1888    }
1889
1890    #[test]
1891    fn variant_ordering() {
1892        let generated = generate(["*:first:text-green-400"], &base_config());
1893
1894        assert_eq!(
1895            generated,
1896            String::from(
1897                r".\*\:first\:text-green-400 > *:first-child {
1898  color: oklch(79.2% .209 151.711);
1899}"
1900            )
1901        );
1902
1903        let mut config = base_config();
1904        config.theme.dark_mode = DarkMode::new_class(".dark");
1905
1906        let generated = generate(["dark:mt-px"], &config);
1907
1908        assert_eq!(
1909            generated,
1910            String::from(
1911                r".dark .dark\:mt-px {
1912  margin-top: 1px;
1913}"
1914            )
1915        );
1916    }
1917
1918    #[test]
1919    fn named_group_and_peer() {
1920        let generated = generate(
1921            [
1922                "group-checked/item:block peer-checked/item:block peer-not-checked/item:block",
1923                "peer-[:focus-within]/item:block",
1924                "peer-[:nth-of-type(3)_&]/item:block",
1925            ],
1926            &base_config(),
1927        );
1928
1929        assert_eq!(
1930            generated,
1931            String::from(
1932                r":nth-of-type(3) .peer\/item ~ .peer-\[\:nth-of-type\(3\)_\&\]\/item\:block {
1933  display: block;
1934}
1935
1936.peer\/item:focus-within ~ .peer-\[\:focus-within\]\/item\:block {
1937  display: block;
1938}
1939
1940.group\/item:checked .group-checked\/item\:block {
1941  display: block;
1942}
1943
1944.peer\/item:not(:checked) ~ .peer-not-checked\/item\:block {
1945  display: block;
1946}
1947
1948.peer\/item:checked ~ .peer-checked\/item\:block {
1949  display: block;
1950}"
1951            )
1952        );
1953    }
1954
1955    #[test]
1956    fn prefixed_variants() {
1957        let generated = generate(
1958            ["supports-[display:flex]:flex nth-of-type-[span]:text-red-500 data-[active]:block"],
1959            &base_config(),
1960        );
1961
1962        assert_eq!(
1963            generated,
1964            String::from(
1965                r".data-\[active\]\:block[data-active] {
1966  display: block;
1967}
1968
1969.nth-of-type-\[span\]\:text-red-500:nth-of-type(span) {
1970  color: oklch(63.7% .237 25.331);
1971}
1972
1973@supports (display:flex) {
1974  .supports-\[display\:flex\]\:flex {
1975    display: flex;
1976  }
1977}"
1978            )
1979        );
1980    }
1981
1982    #[test]
1983    fn layers() {
1984        let mut config = base_config();
1985        config.layers.add("1", 1);
1986        config.layers.add("2", 2);
1987        config.layers.add("3", 3);
1988        config.layers.add("4", 4);
1989
1990        let generated = generate(
1991            [
1992                "l-1:bg-red-500 l-2:bg-red-100 l-4:inset-12 l-1:(bg-blue-800,l-2:(bg-blue-700,bg-blue-600,l-3:bg-blue-500))",
1993            ],
1994            &config,
1995        );
1996
1997        assert_eq!(
1998            generated,
1999            String::from(
2000                r".l-1\:\(bg-blue-800\,l-2\:\(bg-blue-700\,bg-blue-600\,l-3\:bg-blue-500\)\) {
2001  background-color: oklch(42.4% .199 265.638);
2002}
2003
2004.l-1\:bg-red-500 {
2005  background-color: oklch(63.7% .237 25.331);
2006}
2007
2008.l-1\:\(bg-blue-800\,l-2\:\(bg-blue-700\,bg-blue-600\,l-3\:bg-blue-500\)\) {
2009  background-color: oklch(54.6% .245 262.881);
2010}
2011
2012.l-1\:\(bg-blue-800\,l-2\:\(bg-blue-700\,bg-blue-600\,l-3\:bg-blue-500\)\) {
2013  background-color: oklch(48.8% .243 264.376);
2014}
2015
2016.l-2\:bg-red-100 {
2017  background-color: oklch(93.6% .032 17.717);
2018}
2019
2020.l-1\:\(bg-blue-800\,l-2\:\(bg-blue-700\,bg-blue-600\,l-3\:bg-blue-500\)\) {
2021  background-color: oklch(62.3% .214 259.815);
2022}
2023
2024.l-4\:inset-12 {
2025  inset: 3rem;
2026}"
2027            )
2028        );
2029    }
2030
2031    #[test]
2032    fn disambiguation_works() {
2033        let config = base_config();
2034        let generated = generate(
2035            ["font-[bolder] font-[300] font-[Open_Sans] font-[generic-name:var(--font-family)] font-[number:var(--font-weight)]"],
2036            &config,
2037        );
2038        assert_eq!(generated, String::from(r".font-\[Open_Sans\] {
2039  font-family: Open Sans;
2040}
2041
2042.font-\[generic-name\:var\(--font-family\)\] {
2043  font-family: var(--font-family);
2044}
2045
2046.font-\[300\] {
2047  font-weight: 300;
2048}
2049
2050.font-\[bolder\] {
2051  font-weight: bolder;
2052}
2053
2054.font-\[number\:var\(--font-weight\)\] {
2055  font-weight: var(--font-weight);
2056}"));
2057    }
2058
2059    #[test]
2060    fn arbitrary_values_test() {
2061        use std::fs;
2062
2063        let file_content = fs::read_to_string("tests/fixtures/arbitrary-values.html").unwrap();
2064        let _generated = generate([file_content.as_str()], &base_config());
2065    }
2066}