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