1use serde::{Deserialize, Serialize};
58use std::borrow::Cow;
59
60const DEFAULT_RING_COLOR: &str = "rgb(59 130 246 / 0.5)";
61const DEFAULT_BORDER_COLOR: &str = "currentColor";
62const DEFAULT_PLACEHOLDER_COLOR: &str = "#9ca3af";
63const DEFAULT_FONT_FAMILY_SANS: &str = r#"ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji""#;
64const DEFAULT_FONT_FAMILY_MONO: &str = r#"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace"#;
65
66const DEFAULT_PREFLIGHT: &str = concat!(
67 r#"*, ::before, ::after {
70 box-sizing: border-box;
71 border-width: 0;
72 border-style: solid;
73 border-color: theme('borderColor');
74}
75
76::before, ::after {
77 --en-content: '';
78}
79
80"#,
81 r#"html {
86 line-height: 1.5;
87 -webkit-text-size-adjust: 100%;
88 -moz-tab-size: 4;
89 tab-size: 4;
90 font-family: theme('fontFamily.sans');
91}
92
93"#,
94 r#"body {
97 margin: 0;
98 line-height: inherit;
99}
100
101"#,
102 r#"hr {
106 height: 0;
107 color: inherit;
108 border-top-width: 1px;
109}
110
111"#,
112 r#"abbr:where([title]) {
114 text-decoration: underline dotted;
115}
116
117"#,
118 r#"h1, h2, h3, h4, h5, h6 {
120 font-size: inherit;
121 font-weight: inherit;
122}
123
124"#,
125 r#"a {
127 color: inherit;
128 text-decoration: inherit;
129}
130
131"#,
132 r#"b, strong {
134 font-weight: bolder;
135}
136
137"#,
138 r#"code, kbd, samp, pre {
141 font-family: theme('fontFamily.mono');
142 font-size: 1em;
143}
144
145"#,
146 r#"small {
148 font-size: 80%;
149}
150
151"#,
152 r#"sub, sup {
154 font-size: 75%;
155 line-height: 0;
156 position: relative;
157 vertical-align: baseline;
158}
159
160sub {
161 bottom: -0.25em;
162}
163
164sup {
165 top: -0.5em;
166}
167
168"#,
169 r#"table {
173 text-indent: 0;
174 border-color: inherit;
175 border-collapse: collapse;
176}
177
178"#,
179 r#"button, input, optgroup, select, textarea {
183 font-family: inherit;
184 font-size: 100%;
185 font-weight: inherit;
186 line-height: inherit;
187 color: inherit;
188 margin: 0;
189 padding: 0;
190}
191
192"#,
193 r#"button, select {
195 text-transform: none;
196}
197
198"#,
199 r#"button, [type='button'], [type='reset'], [type='submit'] {
202 -webkit-appearance: button;
203 background-color: transparent;
204 background-image: none;
205}
206
207"#,
208 r#":-moz-focusring {
210 outline: auto;
211}
212
213"#,
214 r#":-moz-ui-invalid {
216 box-shadow: none;
217}
218
219"#,
220 r#"progress {
222 vertical-align: baseline;
223}
224
225"#,
226 r#"::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
228 height: auto;
229}
230
231"#,
232 r#"[type='search'] {
235 -webkit-appearance: textfield;
236 outline-offset: -2px;
237}
238
239"#,
240 r#"::-webkit-search-decoration {
242 -webkit-appearance: none;
243}
244
245"#,
246 r#"::-webkit-file-upload-button {
249 -webkit-appearance: button;
250 font: inherit;
251}
252
253"#,
254 r#"summary {
256 display: list-item;
257}
258
259"#,
260 r#"blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {
262 margin: 0;
263}
264
265fieldset {
266 margin: 0;
267 padding: 0;
268}
269
270legend {
271 padding: 0;
272}
273
274ol, ul, menu {
275 list-style: none;
276 margin: 0;
277 padding: 0;
278}
279
280"#,
281 r#"textarea {
283 resize: vertical;
284}
285
286"#,
287 r#"input::placeholder, textarea::placeholder {
290 opacity: 1;
291 color: theme('placeholderColor');
292}
293
294"#,
295 r#"button, [role="button"] {
297 cursor: pointer;
298}
299
300"#,
301 r#":disabled {
303 cursor: default;
304}
305
306"#,
307 r#"img, svg, video, canvas, audio, iframe, embed, object {
311 display: block;
312 vertical-align: middle;
313}
314
315"#,
316 r#"img, video {
318 max-width: 100%;
319 height: auto;
320}
321
322"#,
323 r#"*, ::before, ::after {
325 --en-border-spacing-x: 0;
326 --en-border-spacing-y: 0;
327 --en-translate-x: 0;
328 --en-translate-y: 0;
329 --en-rotate: 0;
330 --en-skew-x: 0;
331 --en-skew-y: 0;
332 --en-scale-x: 1;
333 --en-scale-y: 1;
334 --en-pan-x: ;
335 --en-pan-y: ;
336 --en-pinch-zoom: ;
337 --en-scroll-snap-strictness: proximity;
338 --en-ordinal: ;
339 --en-slashed-zero: ;
340 --en-numeric-figure: ;
341 --en-numeric-spacing: ;
342 --en-numeric-fraction: ;
343 --en-ring-inset: ;
344 --en-ring-offset-width: 0px;
345 --en-ring-offset-color: #fff;
346 --en-ring-color: theme('ringColor');
347 --en-ring-offset-shadow: 0 0 #0000;
348 --en-ring-shadow: 0 0 #0000;
349 --en-shadow: 0 0 #0000;
350 --en-shadow-colored: 0 0 #0000;
351 --en-blur: ;
352 --en-brightness: ;
353 --en-contrast: ;
354 --en-grayscale: ;
355 --en-hue-rotate: ;
356 --en-invert: ;
357 --en-saturate: ;
358 --en-sepia: ;
359 --en-drop-shadow: ;
360 --en-backdrop-blur: ;
361 --en-backdrop-brightness: ;
362 --en-backdrop-contrast: ;
363 --en-backdrop-grayscale: ;
364 --en-backdrop-hue-rotate: ;
365 --en-backdrop-invert: ;
366 --en-backdrop-opacity: ;
367 --en-backdrop-saturate: ;
368 --en-backdrop-sepia: ;
369}
370
371::-webkit-backdrop {
372 --en-border-spacing-x: 0;
373 --en-border-spacing-y: 0;
374 --en-translate-x: 0;
375 --en-translate-y: 0;
376 --en-rotate: 0;
377 --en-skew-x: 0;
378 --en-skew-y: 0;
379 --en-scale-x: 1;
380 --en-scale-y: 1;
381 --en-pan-x: ;
382 --en-pan-y: ;
383 --en-pinch-zoom: ;
384 --en-scroll-snap-strictness: proximity;
385 --en-ordinal: ;
386 --en-slashed-zero: ;
387 --en-numeric-figure: ;
388 --en-numeric-spacing: ;
389 --en-numeric-fraction: ;
390 --en-ring-inset: ;
391 --en-ring-offset-width: 0px;
392 --en-ring-offset-color: #fff;
393 --en-ring-color: theme('ringColor');
394 --en-ring-offset-shadow: 0 0 #0000;
395 --en-ring-shadow: 0 0 #0000;
396 --en-shadow: 0 0 #0000;
397 --en-shadow-colored: 0 0 #0000;
398 --en-blur: ;
399 --en-brightness: ;
400 --en-contrast: ;
401 --en-grayscale: ;
402 --en-hue-rotate: ;
403 --en-invert: ;
404 --en-saturate: ;
405 --en-sepia: ;
406 --en-drop-shadow: ;
407 --en-backdrop-blur: ;
408 --en-backdrop-brightness: ;
409 --en-backdrop-contrast: ;
410 --en-backdrop-grayscale: ;
411 --en-backdrop-hue-rotate: ;
412 --en-backdrop-invert: ;
413 --en-backdrop-opacity: ;
414 --en-backdrop-saturate: ;
415 --en-backdrop-sepia: ;
416}
417
418::backdrop {
419 --en-border-spacing-x: 0;
420 --en-border-spacing-y: 0;
421 --en-translate-x: 0;
422 --en-translate-y: 0;
423 --en-rotate: 0;
424 --en-skew-x: 0;
425 --en-skew-y: 0;
426 --en-scale-x: 1;
427 --en-scale-y: 1;
428 --en-pan-x: ;
429 --en-pan-y: ;
430 --en-pinch-zoom: ;
431 --en-scroll-snap-strictness: proximity;
432 --en-ordinal: ;
433 --en-slashed-zero: ;
434 --en-numeric-figure: ;
435 --en-numeric-spacing: ;
436 --en-numeric-fraction: ;
437 --en-ring-inset: ;
438 --en-ring-offset-width: 0px;
439 --en-ring-offset-color: #fff;
440 --en-ring-color: theme('ringColor');
441 --en-ring-offset-shadow: 0 0 #0000;
442 --en-ring-shadow: 0 0 #0000;
443 --en-shadow: 0 0 #0000;
444 --en-shadow-colored: 0 0 #0000;
445 --en-blur: ;
446 --en-brightness: ;
447 --en-contrast: ;
448 --en-grayscale: ;
449 --en-hue-rotate: ;
450 --en-invert: ;
451 --en-saturate: ;
452 --en-sepia: ;
453 --en-drop-shadow: ;
454 --en-backdrop-blur: ;
455 --en-backdrop-brightness: ;
456 --en-backdrop-contrast: ;
457 --en-backdrop-grayscale: ;
458 --en-backdrop-hue-rotate: ;
459 --en-backdrop-invert: ;
460 --en-backdrop-opacity: ;
461 --en-backdrop-saturate: ;
462 --en-backdrop-sepia: ;
463}"#
464);
465
466#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
470#[serde(rename_all = "lowercase")]
471#[serde(tag = "type", content = "css")]
472pub enum Preflight {
473 None,
475
476 Custom(Cow<'static, str>),
478
479 Full {
481 ring_color: Option<Cow<'static, str>>,
483
484 border_color: Option<Cow<'static, str>>,
486
487 placeholder_color: Option<Cow<'static, str>>,
489
490 font_family_sans: Option<Cow<'static, str>>,
492
493 font_family_mono: Option<Cow<'static, str>>,
495 },
496}
497
498impl Preflight {
499 pub fn new_none() -> Self {
501 Self::None
502 }
503
504 pub fn new_custom<T: Into<Cow<'static, str>>>(css: T) -> Self {
506 Self::Custom(css.into())
507 }
508
509 pub fn new_full() -> Self {
511 Self::Full {
512 ring_color: None,
513 border_color: None,
514 placeholder_color: None,
515 font_family_sans: None,
516 font_family_mono: None,
517 }
518 }
519
520 #[must_use]
524 pub fn ring_color<T: Into<Cow<'static, str>>>(mut self, new_ring_color: T) -> Self {
525 if let Self::Full {
526 ref mut ring_color, ..
527 } = self
528 {
529 *ring_color = Some(new_ring_color.into());
530 }
531
532 self
533 }
534
535 #[must_use]
539 pub fn border_color<T: Into<Cow<'static, str>>>(mut self, new_border_color: T) -> Self {
540 if let Self::Full {
541 ref mut border_color,
542 ..
543 } = self
544 {
545 *border_color = Some(new_border_color.into());
546 }
547
548 self
549 }
550
551 #[must_use]
555 pub fn placeholder_color<T: Into<Cow<'static, str>>>(
556 mut self,
557 new_placeholder_color: T,
558 ) -> Self {
559 if let Self::Full {
560 ref mut placeholder_color,
561 ..
562 } = self
563 {
564 *placeholder_color = Some(new_placeholder_color.into());
565 }
566
567 self
568 }
569
570 #[must_use]
574 pub fn font_family_sans<T: Into<Cow<'static, str>>>(mut self, new_font_family_sans: T) -> Self {
575 if let Self::Full {
576 ref mut font_family_sans,
577 ..
578 } = self
579 {
580 *font_family_sans = Some(new_font_family_sans.into());
581 }
582
583 self
584 }
585
586 #[must_use]
590 pub fn font_family_mono<T: Into<Cow<'static, str>>>(mut self, new_font_family_mono: T) -> Self {
591 if let Self::Full {
592 ref mut font_family_mono,
593 ..
594 } = self
595 {
596 *font_family_mono = Some(new_font_family_mono.into());
597 }
598
599 self
600 }
601
602 pub(crate) fn build(&self) -> Cow<'static, str> {
603 match self {
604 Self::None => Cow::from(""),
605 Self::Custom(css) => css.clone(),
606 Self::Full {
607 ring_color,
608 border_color,
609 font_family_sans,
610 font_family_mono,
611 placeholder_color,
612 } => Cow::from(
613 DEFAULT_PREFLIGHT
614 .replace(
615 "theme('ringColor')",
616 ring_color
617 .as_ref()
618 .unwrap_or(&Cow::Borrowed(DEFAULT_RING_COLOR)),
619 )
620 .replace(
621 "theme('borderColor')",
622 border_color
623 .as_ref()
624 .unwrap_or(&Cow::Borrowed(DEFAULT_BORDER_COLOR)),
625 )
626 .replace(
627 "theme('placeholderColor')",
628 placeholder_color
629 .as_ref()
630 .unwrap_or(&Cow::Borrowed(DEFAULT_PLACEHOLDER_COLOR)),
631 )
632 .replace(
633 "theme('fontFamily.sans')",
634 font_family_sans
635 .as_ref()
636 .unwrap_or(&Cow::Borrowed(DEFAULT_FONT_FAMILY_SANS)),
637 )
638 .replace(
639 "theme('fontFamily.mono')",
640 font_family_mono
641 .as_ref()
642 .unwrap_or(&Cow::Borrowed(DEFAULT_FONT_FAMILY_MONO)),
643 ),
644 ),
645 }
646 }
647}
648
649impl Default for Preflight {
650 fn default() -> Self {
651 Self::Full {
652 ring_color: None,
653 border_color: None,
654 placeholder_color: None,
655 font_family_sans: None,
656 font_family_mono: None,
657 }
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664 use crate::{generate, Config};
665
666 use pretty_assertions::assert_eq;
667
668 #[test]
669 #[allow(clippy::too_many_lines)]
670 fn full_preflight() {
671 let preflight = Preflight::new_full()
672 .ring_color("#f00")
673 .border_color("#0f0")
674 .placeholder_color("#00f")
675 .font_family_sans("sans-serif")
676 .font_family_mono("monospace");
677 let config = Config {
678 preflight,
679 ..Default::default()
680 };
681
682 let generated = generate(["w-full"], &config);
683
684 assert_eq!(
685 generated,
686 String::from(
687 r#"*, ::before, ::after {
688 box-sizing: border-box;
689 border-width: 0;
690 border-style: solid;
691 border-color: #0f0;
692}
693
694::before, ::after {
695 --en-content: '';
696}
697
698html {
699 line-height: 1.5;
700 -webkit-text-size-adjust: 100%;
701 -moz-tab-size: 4;
702 tab-size: 4;
703 font-family: sans-serif;
704}
705
706body {
707 margin: 0;
708 line-height: inherit;
709}
710
711hr {
712 height: 0;
713 color: inherit;
714 border-top-width: 1px;
715}
716
717abbr:where([title]) {
718 text-decoration: underline dotted;
719}
720
721h1, h2, h3, h4, h5, h6 {
722 font-size: inherit;
723 font-weight: inherit;
724}
725
726a {
727 color: inherit;
728 text-decoration: inherit;
729}
730
731b, strong {
732 font-weight: bolder;
733}
734
735code, kbd, samp, pre {
736 font-family: monospace;
737 font-size: 1em;
738}
739
740small {
741 font-size: 80%;
742}
743
744sub, sup {
745 font-size: 75%;
746 line-height: 0;
747 position: relative;
748 vertical-align: baseline;
749}
750
751sub {
752 bottom: -0.25em;
753}
754
755sup {
756 top: -0.5em;
757}
758
759table {
760 text-indent: 0;
761 border-color: inherit;
762 border-collapse: collapse;
763}
764
765button, input, optgroup, select, textarea {
766 font-family: inherit;
767 font-size: 100%;
768 font-weight: inherit;
769 line-height: inherit;
770 color: inherit;
771 margin: 0;
772 padding: 0;
773}
774
775button, select {
776 text-transform: none;
777}
778
779button, [type='button'], [type='reset'], [type='submit'] {
780 -webkit-appearance: button;
781 background-color: transparent;
782 background-image: none;
783}
784
785:-moz-focusring {
786 outline: auto;
787}
788
789:-moz-ui-invalid {
790 box-shadow: none;
791}
792
793progress {
794 vertical-align: baseline;
795}
796
797::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
798 height: auto;
799}
800
801[type='search'] {
802 -webkit-appearance: textfield;
803 outline-offset: -2px;
804}
805
806::-webkit-search-decoration {
807 -webkit-appearance: none;
808}
809
810::-webkit-file-upload-button {
811 -webkit-appearance: button;
812 font: inherit;
813}
814
815summary {
816 display: list-item;
817}
818
819blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {
820 margin: 0;
821}
822
823fieldset {
824 margin: 0;
825 padding: 0;
826}
827
828legend {
829 padding: 0;
830}
831
832ol, ul, menu {
833 list-style: none;
834 margin: 0;
835 padding: 0;
836}
837
838textarea {
839 resize: vertical;
840}
841
842input::placeholder, textarea::placeholder {
843 opacity: 1;
844 color: #00f;
845}
846
847button, [role="button"] {
848 cursor: pointer;
849}
850
851:disabled {
852 cursor: default;
853}
854
855img, svg, video, canvas, audio, iframe, embed, object {
856 display: block;
857 vertical-align: middle;
858}
859
860img, video {
861 max-width: 100%;
862 height: auto;
863}
864
865*, ::before, ::after {
866 --en-border-spacing-x: 0;
867 --en-border-spacing-y: 0;
868 --en-translate-x: 0;
869 --en-translate-y: 0;
870 --en-rotate: 0;
871 --en-skew-x: 0;
872 --en-skew-y: 0;
873 --en-scale-x: 1;
874 --en-scale-y: 1;
875 --en-pan-x: ;
876 --en-pan-y: ;
877 --en-pinch-zoom: ;
878 --en-scroll-snap-strictness: proximity;
879 --en-ordinal: ;
880 --en-slashed-zero: ;
881 --en-numeric-figure: ;
882 --en-numeric-spacing: ;
883 --en-numeric-fraction: ;
884 --en-ring-inset: ;
885 --en-ring-offset-width: 0px;
886 --en-ring-offset-color: #fff;
887 --en-ring-color: #f00;
888 --en-ring-offset-shadow: 0 0 #0000;
889 --en-ring-shadow: 0 0 #0000;
890 --en-shadow: 0 0 #0000;
891 --en-shadow-colored: 0 0 #0000;
892 --en-blur: ;
893 --en-brightness: ;
894 --en-contrast: ;
895 --en-grayscale: ;
896 --en-hue-rotate: ;
897 --en-invert: ;
898 --en-saturate: ;
899 --en-sepia: ;
900 --en-drop-shadow: ;
901 --en-backdrop-blur: ;
902 --en-backdrop-brightness: ;
903 --en-backdrop-contrast: ;
904 --en-backdrop-grayscale: ;
905 --en-backdrop-hue-rotate: ;
906 --en-backdrop-invert: ;
907 --en-backdrop-opacity: ;
908 --en-backdrop-saturate: ;
909 --en-backdrop-sepia: ;
910}
911
912::-webkit-backdrop {
913 --en-border-spacing-x: 0;
914 --en-border-spacing-y: 0;
915 --en-translate-x: 0;
916 --en-translate-y: 0;
917 --en-rotate: 0;
918 --en-skew-x: 0;
919 --en-skew-y: 0;
920 --en-scale-x: 1;
921 --en-scale-y: 1;
922 --en-pan-x: ;
923 --en-pan-y: ;
924 --en-pinch-zoom: ;
925 --en-scroll-snap-strictness: proximity;
926 --en-ordinal: ;
927 --en-slashed-zero: ;
928 --en-numeric-figure: ;
929 --en-numeric-spacing: ;
930 --en-numeric-fraction: ;
931 --en-ring-inset: ;
932 --en-ring-offset-width: 0px;
933 --en-ring-offset-color: #fff;
934 --en-ring-color: #f00;
935 --en-ring-offset-shadow: 0 0 #0000;
936 --en-ring-shadow: 0 0 #0000;
937 --en-shadow: 0 0 #0000;
938 --en-shadow-colored: 0 0 #0000;
939 --en-blur: ;
940 --en-brightness: ;
941 --en-contrast: ;
942 --en-grayscale: ;
943 --en-hue-rotate: ;
944 --en-invert: ;
945 --en-saturate: ;
946 --en-sepia: ;
947 --en-drop-shadow: ;
948 --en-backdrop-blur: ;
949 --en-backdrop-brightness: ;
950 --en-backdrop-contrast: ;
951 --en-backdrop-grayscale: ;
952 --en-backdrop-hue-rotate: ;
953 --en-backdrop-invert: ;
954 --en-backdrop-opacity: ;
955 --en-backdrop-saturate: ;
956 --en-backdrop-sepia: ;
957}
958
959::backdrop {
960 --en-border-spacing-x: 0;
961 --en-border-spacing-y: 0;
962 --en-translate-x: 0;
963 --en-translate-y: 0;
964 --en-rotate: 0;
965 --en-skew-x: 0;
966 --en-skew-y: 0;
967 --en-scale-x: 1;
968 --en-scale-y: 1;
969 --en-pan-x: ;
970 --en-pan-y: ;
971 --en-pinch-zoom: ;
972 --en-scroll-snap-strictness: proximity;
973 --en-ordinal: ;
974 --en-slashed-zero: ;
975 --en-numeric-figure: ;
976 --en-numeric-spacing: ;
977 --en-numeric-fraction: ;
978 --en-ring-inset: ;
979 --en-ring-offset-width: 0px;
980 --en-ring-offset-color: #fff;
981 --en-ring-color: #f00;
982 --en-ring-offset-shadow: 0 0 #0000;
983 --en-ring-shadow: 0 0 #0000;
984 --en-shadow: 0 0 #0000;
985 --en-shadow-colored: 0 0 #0000;
986 --en-blur: ;
987 --en-brightness: ;
988 --en-contrast: ;
989 --en-grayscale: ;
990 --en-hue-rotate: ;
991 --en-invert: ;
992 --en-saturate: ;
993 --en-sepia: ;
994 --en-drop-shadow: ;
995 --en-backdrop-blur: ;
996 --en-backdrop-brightness: ;
997 --en-backdrop-contrast: ;
998 --en-backdrop-grayscale: ;
999 --en-backdrop-hue-rotate: ;
1000 --en-backdrop-invert: ;
1001 --en-backdrop-opacity: ;
1002 --en-backdrop-saturate: ;
1003 --en-backdrop-sepia: ;
1004}
1005
1006.w-full {
1007 width: 100%;
1008}"#
1009 )
1010 );
1011 }
1012
1013 #[test]
1014 fn custom_preflight() {
1015 let preflight = Preflight::new_custom(
1016 "html, body {
1017 width: 100%;
1018 height: 100%;
1019 padding: 0;
1020 margin: 0;
1021 overflow-x: hidden;
1022}",
1023 );
1024 let config = Config {
1025 preflight,
1026 ..Default::default()
1027 };
1028
1029 let generated = generate(["w-full"], &config);
1030
1031 assert_eq!(
1032 generated,
1033 "html, body {
1034 width: 100%;
1035 height: 100%;
1036 padding: 0;
1037 margin: 0;
1038 overflow-x: hidden;
1039}
1040
1041.w-full {
1042 width: 100%;
1043}"
1044 );
1045 }
1046
1047 #[test]
1048 fn no_preflight() {
1049 let config = Config {
1050 preflight: Preflight::new_none(),
1051 ..Default::default()
1052 };
1053
1054 let generated = generate(["w-full"], &config);
1055
1056 assert_eq!(
1057 generated,
1058 ".w-full {
1059 width: 100%;
1060}"
1061 );
1062 }
1063}