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