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