1use serde::{Deserialize, Serialize};
73use std::borrow::Cow;
74
75const DEFAULT_FONT_FEATURE_SETTINGS: &str = "normal";
76const DEFAULT_FONT_VARIATION_SETTINGS: &str = "normal";
77const DEFAULT_FONT_FAMILY_SANS: &str = "ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'";
78const DEFAULT_FONT_FAMILY_MONO: &str = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
79
80const DEFAULT_PREFLIGHT: &str = concat!(
81 r#"*, ::after, ::before, ::backdrop, ::file-selector-button {
87 box-sizing: border-box;
88 margin: 0;
89 padding: 0;
90 border: 0 solid;
91}
92
93::before, ::after {
94 --en-content: '';
95}
96
97"#,
98 r#"html, :host {
108 line-height: 1.5;
109 -webkit-text-size-adjust: 100%;
110 tab-size: 4;
111 font-family: theme('fontFamily.sans');
112 font-feature-settings: theme('fontFeatureSettings.sans');
113 font-variation-settings: theme('fontVariationSettings.sans');
114 -webkit-tap-highlight-color: transparent;
115}
116
117"#,
118 r#"hr {
124 height: 0;
125 color: inherit;
126 border-top-width: 1px;
127}
128
129"#,
130 r#"abbr:where([title]) {
132 -webkit-text-decoration: underline dotted;
133 text-decoration: underline dotted;
134}
135
136"#,
137 r#"h1, h2, h3, h4, h5, h6 {
139 font-size: inherit;
140 font-weight: inherit;
141}
142
143"#,
144 r#"a {
146 color: inherit;
147 -webkit-text-decoration: inherit;
148 text-decoration: inherit;
149}
150
151"#,
152 r#"b, strong {
154 font-weight: bolder;
155}
156
157"#,
158 r#"code, kbd, samp, pre {
165 font-family: theme('fontFamily.mono');
166 font-feature-settings: theme('fontFeatureSettings.mono');
167 font-variation-settings: theme('fontVariationSettings.mono');
168 font-size: 1em;
169}
170
171"#,
172 r#"small {
174 font-size: 80%;
175}
176
177"#,
178 r#"sub, sup {
180 font-size: 75%;
181 line-height: 0;
182 position: relative;
183 vertical-align: baseline;
184}
185
186"#,
187 r#"sub {
188 bottom: -0.25em;
189}
190
191"#,
192 r#"sup {
193 top: -0.5em;
194}
195
196"#,
197 r#"table {
203 text-indent: 0;
204 border-color: inherit;
205 border-collapse: collapse;
206}
207
208"#,
209 r#":-moz-focusring {
211 outline: auto;
212}
213
214"#,
215 r#"progress {
217 vertical-align: baseline;
218}
219
220"#,
221 r#"summary {
223 display: list-item;
224}
225
226"#,
227 r#"ol, ul, menu {
229 list-style: none;
230}
231
232"#,
233 r#"img, svg, video, canvas, audio, iframe, embed, object {
239 display: block;
240 vertical-align: middle;
241}
242
243"#,
244 r#"img, video {
246 max-width: 100%;
247 height: auto;
248}
249
250"#,
251 r#"button, input, select, optgroup, textarea, ::file-selector-button {
258 font: inherit;
259 font-feature-settings: inherit;
260 font-variation-settings: inherit;
261 letter-spacing: inherit;
262 color: inherit;
263 border-radius: 0;
264 background-color: transparent;
265 opacity: 1;
266}
267
268"#,
269 r#":where(select:is([multiple], [size])) optgroup {
271 font-weight: bolder;
272}
273
274"#,
275 r#":where(select:is([multiple], [size])) optgroup option {
277 padding-inline-start: 20px;
278}
279
280"#,
281 r#"::file-selector-button {
283 margin-inline-end: 4px;
284}
285
286"#,
287 r#"::placeholder {
289 opacity: 1;
290}
291
292"#,
293 r#"@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or (contain-intrinsic-size: 1px) /* Safari 17+ */ {
298 ::placeholder {
299 color: color-mix(in oklab, currentcolor 50%, transparent);
300 }
301}
302
303"#,
304 r#"textarea {
306 resize: vertical;
307}
308
309"#,
310 r#"::-webkit-search-decoration {
312 -webkit-appearance: none;
313}
314
315"#,
316 r#"::-webkit-date-and-time-value {
321 min-height: 1lh;
322 text-align: inherit;
323}
324
325"#,
326 r#"::-webkit-datetime-edit {
328 display: inline-flex;
329}
330
331"#,
332 r#"::-webkit-datetime-edit-fields-wrapper {
334 padding: 0;
335}
336
337"#,
338 r#"::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
339 padding-block: 0;
340}
341
342"#,
343 r#":-moz-ui-invalid {
345 box-shadow: none;
346}
347
348"#,
349 r#"button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
351 appearance: button;
352}
353
354"#,
355 r#"::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
357 height: auto;
358}
359
360"#,
361 r#"[hidden]:where(:not([hidden='until-found'])) {
363 display: none !important;
364}
365
366"#,
367 r#"*, ::before, ::after, ::backdrop, ::-webkit-backdrop {
369 --en-border-spacing-x: 0;
370 --en-border-spacing-y: 0;
371 --en-translate-x: 0;
372 --en-translate-y: 0;
373 --en-translate-z: 0;
374 --en-rotate-x: 0;
375 --en-rotate-y: 0;
376 --en-rotate-z: 0;
377 --en-skew-x: 0;
378 --en-skew-y: 0;
379 --en-scale-x: 1;
380 --en-scale-y: 1;
381 --en-scale-z: 1;
382 --en-pan-x: ;
383 --en-pan-y: ;
384 --en-pinch-zoom: ;
385 --en-scroll-snap-strictness: proximity;
386 --en-ordinal: ;
387 --en-slashed-zero: ;
388 --en-numeric-figure: ;
389 --en-numeric-spacing: ;
390 --en-numeric-fraction: ;
391 --en-ring-inset: ;
392 --en-ring-offset-width: 0px;
393 --en-ring-offset-color: #fff;
394 --en-ring-color: currentColor;
395 --en-ring-offset-shadow: 0 0 #0000;
396 --en-ring-shadow: 0 0 #0000;
397 --en-shadow: 0 0 #0000;
398 --en-shadow-colored: 0 0 #0000;
399 --en-blur: ;
400 --en-brightness: ;
401 --en-contrast: ;
402 --en-grayscale: ;
403 --en-hue-rotate: ;
404 --en-invert: ;
405 --en-saturate: ;
406 --en-sepia: ;
407 --en-drop-shadow: ;
408 --en-backdrop-blur: ;
409 --en-backdrop-brightness: ;
410 --en-backdrop-contrast: ;
411 --en-backdrop-grayscale: ;
412 --en-backdrop-hue-rotate: ;
413 --en-backdrop-invert: ;
414 --en-backdrop-opacity: ;
415 --en-backdrop-saturate: ;
416 --en-backdrop-sepia: ;
417}"#
418);
419
420#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
443#[serde(rename_all = "lowercase")]
444pub enum Preflight {
445 None,
447
448 Custom(Cow<'static, str>),
450
451 Full {
453 font_feature_settings_sans: Option<Cow<'static, str>>,
455
456 font_variation_settings_sans: Option<Cow<'static, str>>,
458
459 font_feature_settings_mono: Option<Cow<'static, str>>,
461
462 font_variation_settings_mono: Option<Cow<'static, str>>,
464
465 font_family_sans: Option<Cow<'static, str>>,
467
468 font_family_mono: Option<Cow<'static, str>>,
470 },
471}
472
473impl Preflight {
474 pub fn new_none() -> Self {
476 Self::None
477 }
478
479 pub fn new_custom<T: Into<Cow<'static, str>>>(css: T) -> Self {
481 Self::Custom(css.into())
482 }
483
484 pub fn new_full() -> Self {
486 Self::Full {
487 font_feature_settings_sans: None,
488 font_variation_settings_sans: None,
489 font_feature_settings_mono: None,
490 font_variation_settings_mono: None,
491 font_family_sans: None,
492 font_family_mono: None,
493 }
494 }
495
496 #[must_use]
500 pub fn font_feature_settings_sans<T: Into<Cow<'static, str>>>(
501 mut self,
502 new_font_feature_settings: T,
503 ) -> Self {
504 if let Self::Full {
505 ref mut font_feature_settings_sans,
506 ..
507 } = self
508 {
509 *font_feature_settings_sans = Some(new_font_feature_settings.into());
510 }
511
512 self
513 }
514
515 #[must_use]
519 pub fn font_variation_settings_sans<T: Into<Cow<'static, str>>>(
520 mut self,
521 new_font_variation_settings: T,
522 ) -> Self {
523 if let Self::Full {
524 ref mut font_variation_settings_sans,
525 ..
526 } = self
527 {
528 *font_variation_settings_sans = Some(new_font_variation_settings.into());
529 }
530
531 self
532 }
533
534 #[must_use]
538 pub fn font_feature_settings_mono<T: Into<Cow<'static, str>>>(
539 mut self,
540 new_font_feature_settings: T,
541 ) -> Self {
542 if let Self::Full {
543 ref mut font_feature_settings_mono,
544 ..
545 } = self
546 {
547 *font_feature_settings_mono = Some(new_font_feature_settings.into());
548 }
549
550 self
551 }
552
553 #[must_use]
557 pub fn font_variation_settings_mono<T: Into<Cow<'static, str>>>(
558 mut self,
559 new_font_variation_settings: T,
560 ) -> Self {
561 if let Self::Full {
562 ref mut font_variation_settings_mono,
563 ..
564 } = self
565 {
566 *font_variation_settings_mono = Some(new_font_variation_settings.into());
567 }
568
569 self
570 }
571
572 #[must_use]
576 pub fn font_family_sans<T: Into<Cow<'static, str>>>(mut self, new_font_family_sans: T) -> Self {
577 if let Self::Full {
578 ref mut font_family_sans,
579 ..
580 } = self
581 {
582 *font_family_sans = Some(new_font_family_sans.into());
583 }
584
585 self
586 }
587
588 #[must_use]
592 pub fn font_family_mono<T: Into<Cow<'static, str>>>(mut self, new_font_family_mono: T) -> Self {
593 if let Self::Full {
594 ref mut font_family_mono,
595 ..
596 } = self
597 {
598 *font_family_mono = Some(new_font_family_mono.into());
599 }
600
601 self
602 }
603
604 pub(crate) fn build(&self) -> Cow<'static, str> {
605 match self {
606 Self::None => Cow::from(""),
607 Self::Custom(css) => css.clone(),
608 Self::Full {
609 font_feature_settings_sans,
610 font_variation_settings_sans,
611 font_feature_settings_mono,
612 font_variation_settings_mono,
613 font_family_sans,
614 font_family_mono,
615 } => Cow::from(
616 DEFAULT_PREFLIGHT
617 .replace(
618 "theme('fontFeatureSettings.sans')",
619 font_feature_settings_sans
620 .as_ref()
621 .unwrap_or(&Cow::Borrowed(DEFAULT_FONT_FEATURE_SETTINGS)),
622 )
623 .replace(
624 "theme('fontVariationSettings.sans')",
625 font_variation_settings_sans
626 .as_ref()
627 .unwrap_or(&Cow::Borrowed(DEFAULT_FONT_VARIATION_SETTINGS)),
628 )
629 .replace(
630 "theme('fontFeatureSettings.mono')",
631 font_feature_settings_mono
632 .as_ref()
633 .unwrap_or(&Cow::Borrowed(DEFAULT_FONT_FEATURE_SETTINGS)),
634 )
635 .replace(
636 "theme('fontVariationSettings.mono')",
637 font_variation_settings_mono
638 .as_ref()
639 .unwrap_or(&Cow::Borrowed(DEFAULT_FONT_VARIATION_SETTINGS)),
640 )
641 .replace(
642 "theme('fontFamily.sans')",
643 font_family_sans
644 .as_ref()
645 .unwrap_or(&Cow::Borrowed(DEFAULT_FONT_FAMILY_SANS)),
646 )
647 .replace(
648 "theme('fontFamily.mono')",
649 font_family_mono
650 .as_ref()
651 .unwrap_or(&Cow::Borrowed(DEFAULT_FONT_FAMILY_MONO)),
652 ),
653 ),
654 }
655 }
656}
657
658impl Default for Preflight {
659 fn default() -> Self {
660 Self::Full {
661 font_feature_settings_sans: None,
662 font_variation_settings_sans: None,
663 font_feature_settings_mono: None,
664 font_variation_settings_mono: None,
665 font_family_sans: None,
666 font_family_mono: None,
667 }
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use crate::{generate, Config};
675
676 use pretty_assertions::assert_eq;
677
678 #[test]
679 #[allow(clippy::too_many_lines)]
680 fn full_preflight() {
681 let preflight = Preflight::new_full()
682 .font_feature_settings_sans("tnum")
683 .font_variation_settings_sans("'xhgt' 0.7")
684 .font_feature_settings_mono("'liga' 0")
685 .font_variation_settings_mono("'whgt' 850")
686 .font_family_sans("sans-serif")
687 .font_family_mono("monospace");
688 let config = Config {
689 preflight,
690 ..Default::default()
691 };
692
693 let generated = generate(["w-full"], &config);
694
695 assert_eq!(
696 generated,
697 String::from(
698 r#"*, ::after, ::before, ::backdrop, ::file-selector-button {
699 box-sizing: border-box;
700 margin: 0;
701 padding: 0;
702 border: 0 solid;
703}
704
705::before, ::after {
706 --en-content: '';
707}
708
709html, :host {
710 line-height: 1.5;
711 -webkit-text-size-adjust: 100%;
712 tab-size: 4;
713 font-family: sans-serif;
714 font-feature-settings: tnum;
715 font-variation-settings: 'xhgt' 0.7;
716 -webkit-tap-highlight-color: transparent;
717}
718
719hr {
720 height: 0;
721 color: inherit;
722 border-top-width: 1px;
723}
724
725abbr:where([title]) {
726 -webkit-text-decoration: underline dotted;
727 text-decoration: underline dotted;
728}
729
730h1, h2, h3, h4, h5, h6 {
731 font-size: inherit;
732 font-weight: inherit;
733}
734
735a {
736 color: inherit;
737 -webkit-text-decoration: inherit;
738 text-decoration: inherit;
739}
740
741b, strong {
742 font-weight: bolder;
743}
744
745code, kbd, samp, pre {
746 font-family: monospace;
747 font-feature-settings: 'liga' 0;
748 font-variation-settings: 'whgt' 850;
749 font-size: 1em;
750}
751
752small {
753 font-size: 80%;
754}
755
756sub, sup {
757 font-size: 75%;
758 line-height: 0;
759 position: relative;
760 vertical-align: baseline;
761}
762
763sub {
764 bottom: -0.25em;
765}
766
767sup {
768 top: -0.5em;
769}
770
771table {
772 text-indent: 0;
773 border-color: inherit;
774 border-collapse: collapse;
775}
776
777:-moz-focusring {
778 outline: auto;
779}
780
781progress {
782 vertical-align: baseline;
783}
784
785summary {
786 display: list-item;
787}
788
789ol, ul, menu {
790 list-style: none;
791}
792
793img, svg, video, canvas, audio, iframe, embed, object {
794 display: block;
795 vertical-align: middle;
796}
797
798img, video {
799 max-width: 100%;
800 height: auto;
801}
802
803button, input, select, optgroup, textarea, ::file-selector-button {
804 font: inherit;
805 font-feature-settings: inherit;
806 font-variation-settings: inherit;
807 letter-spacing: inherit;
808 color: inherit;
809 border-radius: 0;
810 background-color: transparent;
811 opacity: 1;
812}
813
814:where(select:is([multiple], [size])) optgroup {
815 font-weight: bolder;
816}
817
818:where(select:is([multiple], [size])) optgroup option {
819 padding-inline-start: 20px;
820}
821
822::file-selector-button {
823 margin-inline-end: 4px;
824}
825
826::placeholder {
827 opacity: 1;
828}
829
830@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or (contain-intrinsic-size: 1px) /* Safari 17+ */ {
831 ::placeholder {
832 color: color-mix(in oklab, currentcolor 50%, transparent);
833 }
834}
835
836textarea {
837 resize: vertical;
838}
839
840::-webkit-search-decoration {
841 -webkit-appearance: none;
842}
843
844::-webkit-date-and-time-value {
845 min-height: 1lh;
846 text-align: inherit;
847}
848
849::-webkit-datetime-edit {
850 display: inline-flex;
851}
852
853::-webkit-datetime-edit-fields-wrapper {
854 padding: 0;
855}
856
857::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
858 padding-block: 0;
859}
860
861:-moz-ui-invalid {
862 box-shadow: none;
863}
864
865button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
866 appearance: button;
867}
868
869::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
870 height: auto;
871}
872
873[hidden]:where(:not([hidden='until-found'])) {
874 display: none !important;
875}
876
877*, ::before, ::after, ::backdrop, ::-webkit-backdrop {
878 --en-border-spacing-x: 0;
879 --en-border-spacing-y: 0;
880 --en-translate-x: 0;
881 --en-translate-y: 0;
882 --en-translate-z: 0;
883 --en-rotate-x: 0;
884 --en-rotate-y: 0;
885 --en-rotate-z: 0;
886 --en-skew-x: 0;
887 --en-skew-y: 0;
888 --en-scale-x: 1;
889 --en-scale-y: 1;
890 --en-scale-z: 1;
891 --en-pan-x: ;
892 --en-pan-y: ;
893 --en-pinch-zoom: ;
894 --en-scroll-snap-strictness: proximity;
895 --en-ordinal: ;
896 --en-slashed-zero: ;
897 --en-numeric-figure: ;
898 --en-numeric-spacing: ;
899 --en-numeric-fraction: ;
900 --en-ring-inset: ;
901 --en-ring-offset-width: 0px;
902 --en-ring-offset-color: #fff;
903 --en-ring-color: currentColor;
904 --en-ring-offset-shadow: 0 0 #0000;
905 --en-ring-shadow: 0 0 #0000;
906 --en-shadow: 0 0 #0000;
907 --en-shadow-colored: 0 0 #0000;
908 --en-blur: ;
909 --en-brightness: ;
910 --en-contrast: ;
911 --en-grayscale: ;
912 --en-hue-rotate: ;
913 --en-invert: ;
914 --en-saturate: ;
915 --en-sepia: ;
916 --en-drop-shadow: ;
917 --en-backdrop-blur: ;
918 --en-backdrop-brightness: ;
919 --en-backdrop-contrast: ;
920 --en-backdrop-grayscale: ;
921 --en-backdrop-hue-rotate: ;
922 --en-backdrop-invert: ;
923 --en-backdrop-opacity: ;
924 --en-backdrop-saturate: ;
925 --en-backdrop-sepia: ;
926}
927
928.w-full {
929 width: 100%;
930}"#
931 )
932 );
933 }
934
935 #[test]
936 fn custom_preflight() {
937 let preflight = Preflight::new_custom(
938 "html, body {
939 width: 100%;
940 height: 100%;
941 padding: 0;
942 margin: 0;
943 overflow-x: hidden;
944}",
945 );
946 let config = Config {
947 preflight,
948 ..Default::default()
949 };
950
951 let generated = generate(["w-full"], &config);
952
953 assert_eq!(
954 generated,
955 "html, body {
956 width: 100%;
957 height: 100%;
958 padding: 0;
959 margin: 0;
960 overflow-x: hidden;
961}
962
963.w-full {
964 width: 100%;
965}"
966 );
967 }
968
969 #[test]
970 fn no_preflight() {
971 let config = Config {
972 preflight: Preflight::new_none(),
973 ..Default::default()
974 };
975
976 let generated = generate(["w-full"], &config);
977
978 assert_eq!(
979 generated,
980 ".w-full {
981 width: 100%;
982}"
983 );
984 }
985}