1use 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#[derive(Debug)]
25pub struct ContextCanHandle<'a, 'b, 'c> {
26 pub config: &'a Config,
28
29 pub modifier: &'b Modifier<'c>,
31}
32
33#[derive(Debug)]
37pub struct ContextHandle<'a, 'b, 'c, 'd, 'e> {
38 pub config: &'a Config,
40
41 pub modifier: &'b Modifier<'c>,
43
44 pub buffer: &'d mut Buffer,
46
47 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 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 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 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 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
783pub 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
825pub 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#[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 let mut base_class = String::with_capacity(1 + selector.full.len());
872 base_class.push('.');
873
874 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 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 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 let content_start = buffer.len();
908
909 buffer.indent();
911 rule_content_fn(context);
912
913 let ContextHandle {
914 buffer, selector, ..
915 } = context;
916
917 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 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
1002pub 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 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()); 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-['Hello_world!']",
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-['hello-']",
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-['Times_New_Roman',Helvetica,serif]",
1708 "font-[Roboto,'Open_Sans',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-['Hello_world!']",
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}