1use std::collections::{BTreeMap, BTreeSet};
33use std::fmt::Write;
34use std::sync::Arc;
35
36use beck_diag::{Diagnostic, Diagnostics, Span};
37
38use crate::check::{Def, Program};
39use crate::core::{children, Const, Core, CoreKind, Prim};
40
41const DEPTH: usize = 8;
48
49#[derive(Clone, Debug, Default)]
51pub struct Styles {
52 pub classes: BTreeSet<Arc<str>>,
55 pub dynamic: Vec<Dynamic>,
57 pub named: Vec<Named>,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct Named {
69 pub class: Arc<str>,
70 pub in_def: Arc<str>,
73 pub span: Span,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct Dynamic {
79 pub in_def: Arc<str>,
81 pub span: Span,
82 pub because: Because,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub enum Because {
92 Concatenated,
94 FromData,
97 NotFollowed,
100}
101
102impl Because {
103 pub fn because(&self) -> &'static str {
105 match self {
106 Because::Concatenated => {
107 "it is built with `+`, so the name exists only while the page is being rendered. A \
108 list of alternatives is the shape that can be styled: `class=[\"btn\", \"primary\" \
109 if hot else \"plain\"]`"
110 }
111 Because::FromData => {
112 "it is computed rather than named — a field, a lookup, a call to a primitive — so \
113 the names it can take are in the data rather than in the program"
114 }
115 Because::NotFollowed => {
116 "it is behind a shape this analysis does not enter — a call through a value, a \
117 `let`-bound name, or a chain of definitions deeper than it follows"
118 }
119 }
120 }
121}
122
123pub fn classes(program: &Program) -> Styles {
125 let mut out = Styles::default();
126 for (name, def) in &program.defs {
127 sites(&def.body, name, &program.defs, &mut out);
128 }
129 out
130}
131
132fn sites(c: &Core, in_def: &Arc<str>, defs: &BTreeMap<Arc<str>, Def>, out: &mut Styles) {
134 if let CoreKind::Prim {
135 op: Prim::HtmlAttr,
136 args,
137 } = &c.kind
138 {
139 if args.len() == 2 && is_class(&args[0]) {
140 let mut seen = BTreeSet::new();
141 if let Err(because) = tokens(&args[1], defs, &mut seen, DEPTH, in_def, out) {
142 out.dynamic.push(Dynamic {
143 in_def: in_def.clone(),
144 span: args[1].span,
145 because,
146 });
147 }
148 }
149 }
150 for child in children(c) {
151 sites(child, in_def, defs, out);
152 }
153}
154
155fn is_class(c: &Core) -> bool {
157 matches!(&c.kind, CoreKind::Const(Const::Str(s)) if &**s == "class")
158}
159
160fn tokens(
162 c: &Core,
163 defs: &BTreeMap<Arc<str>, Def>,
164 seen: &mut BTreeSet<Arc<str>>,
165 depth: usize,
166 in_def: &Arc<str>,
167 out: &mut Styles,
168) -> Result<(), Because> {
169 if depth == 0 {
170 return Err(Because::NotFollowed);
171 }
172 match &c.kind {
173 CoreKind::Const(Const::Str(s)) => {
176 for token in s.split_whitespace() {
177 let class: Arc<str> = Arc::from(token);
178 out.classes.insert(class.clone());
179 out.named.push(Named {
180 class,
181 in_def: in_def.clone(),
182 span: c.span,
183 });
184 }
185 Ok(())
186 }
187 CoreKind::Prim {
189 op: Prim::StrJoin,
190 args,
191 } if args.len() == 2 => tokens(&args[0], defs, seen, depth - 1, in_def, out),
192 CoreKind::ListLit(items) => items
193 .iter()
194 .try_for_each(|i| tokens(i, defs, seen, depth - 1, in_def, out)),
195 CoreKind::If { then, alt, .. } => {
198 tokens(then, defs, seen, depth - 1, in_def, out)?;
199 tokens(alt, defs, seen, depth - 1, in_def, out)
200 }
201 CoreKind::Match { arms, .. } => arms
202 .iter()
203 .try_for_each(|a| tokens(&a.body, defs, seen, depth - 1, in_def, out)),
204 CoreKind::Global(name) => follow(name, defs, seen, depth, out),
207 CoreKind::App { func, .. } => match &func.kind {
208 CoreKind::Global(name) => follow(name, defs, seen, depth, out),
209 CoreKind::Lam { body, .. } => tokens(body, defs, seen, depth - 1, in_def, out),
210 _ => Err(Because::NotFollowed),
211 },
212 CoreKind::Prim {
213 op: Prim::Add,
214 args,
215 } if args.len() == 2 => {
216 let _ = args;
220 Err(Because::Concatenated)
221 }
222 CoreKind::Var(_) | CoreKind::Field { .. } | CoreKind::Prim { .. } => Err(Because::FromData),
226 _ => Err(Because::NotFollowed),
227 }
228}
229
230fn follow(
232 name: &Arc<str>,
233 defs: &BTreeMap<Arc<str>, Def>,
234 seen: &mut BTreeSet<Arc<str>>,
235 depth: usize,
236 out: &mut Styles,
237) -> Result<(), Because> {
238 if !seen.insert(name.clone()) {
239 return Err(Because::NotFollowed);
240 }
241 let Some(def) = defs.get(name) else {
242 return Err(Because::NotFollowed);
243 };
244 let body = match &def.body.kind {
246 CoreKind::Lam { body, .. } => body,
247 _ => &def.body,
248 };
249 let answer = tokens(body, defs, seen, depth - 1, name, out);
252 seen.remove(name);
253 answer
254}
255
256pub fn check_classes(program: &Program, diags: &mut Diagnostics) {
290 for site in &classes(program).named {
291 if is_utility(&site.class) {
292 continue;
293 }
294 let allowed = usize::from(site.class.len() >= 8) + 1;
295 let Some((_, near)) = nearest_utility(&site.class).filter(|(d, _)| *d <= allowed) else {
296 continue;
297 };
298 diags.push(
299 Diagnostic::warning(
300 "B0222",
301 format!(
302 "`{}` is not a utility, and is one slip from one",
303 site.class
304 ),
305 site.span,
306 )
307 .with_primary_label("no rule is emitted for this class")
308 .with_note(
309 "a class this compiler does not know is a class of your own and is left alone — \
310 `beck explain style` lists which of a page's are which. This one is close enough \
311 to a utility to be worth asking about",
312 )
313 .with_fix(format!("did you mean `{near}`?")),
314 );
315 }
316}
317
318pub fn nearest_utility(name: &str) -> Option<(usize, &'static str)> {
328 let mut best: Option<((usize, usize), &'static str)> = None;
329 for candidate in CLOSED.iter() {
330 let d = distance(name, candidate);
331 let rank = (d, name.len().abs_diff(candidate.len()));
335 if best.is_none_or(|(b, _)| rank < b) {
336 best = Some((rank, candidate));
337 }
338 }
339 best.map(|((d, _), name)| (d, name))
340}
341
342fn distance(a: &str, b: &str) -> usize {
348 let b: Vec<char> = b.chars().collect();
349 let mut row: Vec<usize> = (0..=b.len()).collect();
350 for (i, x) in a.chars().enumerate() {
351 let mut corner = row[0];
352 row[0] = i + 1;
353 for (j, y) in b.iter().enumerate() {
354 let next = (row[j] + 1)
355 .min(row[j + 1] + 1)
356 .min(corner + usize::from(x != *y));
357 corner = row[j + 1];
358 row[j + 1] = next;
359 }
360 }
361 row[b.len()]
362}
363
364static CLOSED: std::sync::LazyLock<Vec<String>> =
366 std::sync::LazyLock::new(|| enumerate().0.into_iter().collect());
367
368#[derive(Clone, Debug, PartialEq, Eq)]
379pub struct Rule {
380 pub at: Vec<&'static str>,
382 pub selector: String,
384 pub decls: Vec<(&'static str, String)>,
386}
387
388impl Rule {
389 fn tokens(&self, out: &mut BTreeSet<&'static str>) {
391 for (_, value) in &self.decls {
392 let mut rest = value.as_str();
393 while let Some(at) = rest.find("var(--") {
394 rest = &rest[at + 4..];
395 let end = rest
396 .find(|c: char| !c.is_ascii_alphanumeric() && c != '-')
397 .unwrap_or(rest.len());
398 if let Some((name, _)) = THEME.iter().find(|(n, _)| *n == &rest[..end]) {
399 out.insert(name);
400 }
401 rest = &rest[end..];
402 }
403 }
404 }
405}
406
407pub fn is_utility(name: &str) -> bool {
414 rule(name).is_some()
415}
416
417pub fn rule(name: &str) -> Option<Rule> {
439 let mut parts: Vec<&str> = name.split(':').collect();
440 let base = parts.pop()?;
441 let mut at = Vec::new();
442 let mut pseudo = String::new();
443 for v in &parts {
444 let (media, suffix) = variant(v)?;
445 at.extend(media);
446 pseudo.push_str(suffix);
447 }
448 let (decls, shape) = base_rule(base)?;
449 let class = format!(".{}{pseudo}", escape(name));
450 Some(Rule {
451 at,
452 selector: match shape {
453 Shape::Between => format!(":where({class} > :not(:last-child))"),
455 Shape::Element => class,
456 },
457 decls,
458 })
459}
460
461#[derive(Clone, Copy, Debug, PartialEq, Eq)]
463enum Shape {
464 Element,
466 Between,
469}
470
471fn variant(v: &str) -> Option<(Option<&'static str>, &'static str)> {
476 Some(match v {
477 "hover" => (Some("@media (hover: hover)"), ":hover"),
478 "focus" => (None, ":focus"),
479 "focus-visible" => (None, ":focus-visible"),
480 "focus-within" => (None, ":focus-within"),
481 "active" => (None, ":active"),
482 "visited" => (None, ":visited"),
483 "disabled" => (None, ":disabled"),
484 "checked" => (None, ":checked"),
485 "first" => (None, ":first-child"),
486 "last" => (None, ":last-child"),
487 "odd" => (None, ":nth-child(odd)"),
488 "even" => (None, ":nth-child(even)"),
489 "empty" => (None, ":empty"),
490 "dark" => (Some("@media (prefers-color-scheme: dark)"), ""),
491 "motion-safe" => (Some("@media (prefers-reduced-motion: no-preference)"), ""),
492 "motion-reduce" => (Some("@media (prefers-reduced-motion: reduce)"), ""),
493 "print" => (Some("@media print"), ""),
494 "sm" => (Some("@media (width >= 40rem)"), ""),
495 "md" => (Some("@media (width >= 48rem)"), ""),
496 "lg" => (Some("@media (width >= 64rem)"), ""),
497 "xl" => (Some("@media (width >= 80rem)"), ""),
498 "2xl" => (Some("@media (width >= 96rem)"), ""),
499 _ => return None,
500 })
501}
502
503fn escape(name: &str) -> String {
510 let mut out = String::with_capacity(name.len() + 4);
511 for (i, c) in name.chars().enumerate() {
512 match c {
513 '0'..='9' if i == 0 => out.push_str(&format!("\\3{c} ")),
514 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => out.push(c),
515 _ if !c.is_ascii() => out.push(c),
516 _ => {
517 out.push('\\');
518 out.push(c);
519 }
520 }
521 }
522 out
523}
524
525fn base_rule(base: &str) -> Option<(Vec<(&'static str, String)>, Shape)> {
527 let one = |property: &'static str, value: &str| {
528 Some((vec![(property, value.to_string())], Shape::Element))
529 };
530 if let Some(decls) = WORDS.iter().find(|(n, _)| *n == base) {
531 return Some((
532 decls.1.iter().map(|(p, v)| (*p, v.to_string())).collect(),
533 Shape::Element,
534 ));
535 }
536 if let Some(rest) = base.strip_prefix("text-") {
537 if TEXT_SIZES.contains(&rest) {
538 return Some((
539 vec![
540 ("font-size", format!("var(--text-{rest})")),
541 (
542 "line-height",
543 format!("var(--tw-leading, var(--text-{rest}--line-height))"),
544 ),
545 ],
546 Shape::Element,
547 ));
548 }
549 return colour(rest).and_then(|v| one("color", &v));
550 }
551 if let Some(rest) = base.strip_prefix("font-") {
552 if WEIGHTS.contains(&rest) {
553 return Some((
554 vec![
555 ("--tw-font-weight", format!("var(--font-weight-{rest})")),
556 ("font-weight", format!("var(--font-weight-{rest})")),
557 ],
558 Shape::Element,
559 ));
560 }
561 if ["sans", "serif", "mono"].contains(&rest) {
562 return one("font-family", &format!("var(--font-{rest})"));
563 }
564 return None;
565 }
566 if let Some(rest) = base.strip_prefix("rounded-") {
567 return match rest {
568 "none" => one("border-radius", "0"),
569 "full" => one("border-radius", "calc(infinity * 1px)"),
570 "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" => {
571 one("border-radius", &format!("var(--radius-{rest})"))
572 }
573 _ => None,
574 };
575 }
576 if let Some(rest) = base.strip_prefix("border-") {
577 if ["0", "2", "4", "8"].contains(&rest) {
578 return Some((
579 vec![
580 ("border-style", "var(--tw-border-style)".to_string()),
581 ("border-width", format!("{rest}px")),
582 ],
583 Shape::Element,
584 ));
585 }
586 return colour(rest).and_then(|v| one("border-color", &v));
587 }
588 if let Some(rest) = base.strip_prefix("items-") {
589 return match rest {
590 "start" => one("align-items", "flex-start"),
591 "center" => one("align-items", "center"),
592 "end" => one("align-items", "flex-end"),
593 "baseline" => one("align-items", "baseline"),
594 "stretch" => one("align-items", "stretch"),
595 _ => None,
596 };
597 }
598 if let Some(rest) = base.strip_prefix("justify-") {
599 return match rest {
600 "start" => one("justify-content", "flex-start"),
601 "center" => one("justify-content", "center"),
602 "end" => one("justify-content", "flex-end"),
603 "between" => one("justify-content", "space-between"),
604 "around" => one("justify-content", "space-around"),
605 "evenly" => one("justify-content", "space-evenly"),
606 "stretch" => one("justify-content", "stretch"),
607 _ => None,
608 };
609 }
610 if let Some(rest) = base.strip_prefix("flex-") {
611 return match rest {
612 "row" => one("flex-direction", "row"),
613 "row-reverse" => one("flex-direction", "row-reverse"),
614 "col" => one("flex-direction", "column"),
615 "col-reverse" => one("flex-direction", "column-reverse"),
616 "wrap" => one("flex-wrap", "wrap"),
617 "nowrap" => one("flex-wrap", "nowrap"),
618 "wrap-reverse" => one("flex-wrap", "wrap-reverse"),
619 "1" => one("flex", "1"),
620 "auto" => one("flex", "auto"),
621 "initial" => one("flex", "0 auto"),
622 "none" => one("flex", "none"),
623 _ => None,
624 };
625 }
626 if let Some(rest) = base.strip_prefix("overflow-") {
627 return match rest {
628 "auto" | "hidden" | "clip" | "visible" | "scroll" => one("overflow", rest),
629 _ => None,
630 };
631 }
632 for (family, property) in COLOURED {
633 if let Some(rest) = base.strip_prefix(family) {
634 return colour(rest).and_then(|v| one(property, &v));
635 }
636 }
637 for (family, properties, screen) in SIZED {
638 if let Some(rest) = base.strip_prefix(family) {
639 let value = match rest {
640 "full" => "100%".to_string(),
641 "min" => "min-content".to_string(),
642 "max" => "max-content".to_string(),
643 "fit" => "fit-content".to_string(),
644 "auto" if !family.starts_with("max-") => "auto".to_string(),
645 "screen" if *screen => match family.contains('h') {
646 true => "100vh".to_string(),
647 false => "100vw".to_string(),
648 },
649 _ => spacing(rest)?,
650 };
651 return Some((
652 properties.iter().map(|p| (*p, value.clone())).collect(),
653 Shape::Element,
654 ));
655 }
656 }
657 for (family, kind) in SPACED {
658 let Some(rest) = base.strip_prefix(family) else {
659 continue;
660 };
661 let Some(rest) = rest.strip_prefix('-') else {
662 continue;
663 };
664 let value = match rest {
665 "auto" => match kind {
666 Spaced::Padding { .. } | Spaced::Between { .. } => return None,
667 Spaced::Margin { property } => return one(property, "auto"),
668 },
669 _ => spacing(rest)?,
670 };
671 return Some(match kind {
672 Spaced::Padding { property } | Spaced::Margin { property } => {
673 (vec![(*property, value)], Shape::Element)
674 }
675 Spaced::Between {
679 reverse,
680 start,
681 end,
682 } => (
683 match value == "0px" {
684 true => vec![
687 (reverse, "0".to_string()),
688 (start, "0".to_string()),
689 (end, "0".to_string()),
690 ],
691 false => vec![
692 (reverse, "0".to_string()),
693 (start, format!("calc({value} * var({reverse}))")),
694 (end, format!("calc({value} * calc(1 - var({reverse})))")),
695 ],
696 },
697 Shape::Between,
698 ),
699 });
700 }
701 None
702}
703
704fn spacing(rest: &str) -> Option<String> {
710 if rest == "px" {
711 return Some("1px".to_string());
712 }
713 if !number(rest) {
714 return None;
715 }
716 Some(match rest.parse::<f64>().ok()? {
717 0.0 => "0px".to_string(),
718 1.0 => "var(--spacing)".to_string(),
719 _ => format!("calc(var(--spacing) * {rest})"),
720 })
721}
722
723const WORDS: &[(&str, &[(&str, &str)])] = &[
725 ("flex", &[("display", "flex")]),
726 ("inline-flex", &[("display", "inline-flex")]),
727 ("grid", &[("display", "grid")]),
728 ("inline-grid", &[("display", "inline-grid")]),
729 ("block", &[("display", "block")]),
730 ("inline-block", &[("display", "inline-block")]),
731 ("inline", &[("display", "inline")]),
732 ("hidden", &[("display", "none")]),
733 ("contents", &[("display", "contents")]),
734 ("flow-root", &[("display", "flow-root")]),
735 ("table", &[("display", "table")]),
736 ("static", &[("position", "static")]),
737 ("relative", &[("position", "relative")]),
738 ("absolute", &[("position", "absolute")]),
739 ("fixed", &[("position", "fixed")]),
740 ("sticky", &[("position", "sticky")]),
741 ("italic", &[("font-style", "italic")]),
742 ("not-italic", &[("font-style", "normal")]),
743 ("underline", &[("text-decoration-line", "underline")]),
744 ("overline", &[("text-decoration-line", "overline")]),
745 ("line-through", &[("text-decoration-line", "line-through")]),
746 ("no-underline", &[("text-decoration-line", "none")]),
747 ("uppercase", &[("text-transform", "uppercase")]),
748 ("lowercase", &[("text-transform", "lowercase")]),
749 ("capitalize", &[("text-transform", "capitalize")]),
750 ("normal-case", &[("text-transform", "none")]),
751 (
752 "truncate",
753 &[
754 ("overflow", "hidden"),
755 ("text-overflow", "ellipsis"),
756 ("white-space", "nowrap"),
757 ],
758 ),
759 (
760 "border",
761 &[
762 ("border-style", "var(--tw-border-style)"),
763 ("border-width", "1px"),
764 ],
765 ),
766 ("rounded", &[("border-radius", "0.25rem")]),
767 (
768 "sr-only",
769 &[
770 ("position", "absolute"),
771 ("width", "1px"),
772 ("height", "1px"),
773 ("padding", "0"),
774 ("margin", "-1px"),
775 ("overflow", "hidden"),
776 ("clip-path", "inset(50%)"),
777 ("white-space", "nowrap"),
778 ("border-width", "0"),
779 ],
780 ),
781 (
782 "outline-none",
783 &[("--tw-outline-style", "none"), ("outline-style", "none")],
784 ),
785];
786
787const COLOURED: &[(&str, &str)] = &[
789 ("bg-", "background-color"),
790 ("fill-", "fill"),
791 ("stroke-", "stroke"),
792 ("ring-", "--tw-ring-color"),
793 ("outline-", "outline-color"),
794 ("decoration-", "text-decoration-color"),
795];
796
797const SIZED: &[(&str, &[&str], bool)] = &[
802 ("size-", &["width", "height"], false),
803 ("min-w-", &["min-width"], true),
804 ("min-h-", &["min-height"], true),
805 ("max-w-", &["max-width"], true),
806 ("max-h-", &["max-height"], true),
807 ("w-", &["width"], true),
808 ("h-", &["height"], true),
809];
810
811enum Spaced {
813 Padding {
814 property: &'static str,
815 },
816 Margin {
817 property: &'static str,
818 },
819 Between {
820 reverse: &'static str,
821 start: &'static str,
822 end: &'static str,
823 },
824}
825
826const SPACED: &[(&str, Spaced)] = &[
830 (
831 "gap-x",
832 Spaced::Padding {
833 property: "column-gap",
834 },
835 ),
836 (
837 "gap-y",
838 Spaced::Padding {
839 property: "row-gap",
840 },
841 ),
842 (
843 "space-x",
844 Spaced::Between {
845 reverse: "--tw-space-x-reverse",
846 start: "margin-inline-start",
847 end: "margin-inline-end",
848 },
849 ),
850 (
851 "space-y",
852 Spaced::Between {
853 reverse: "--tw-space-y-reverse",
854 start: "margin-block-start",
855 end: "margin-block-end",
856 },
857 ),
858 (
859 "px",
860 Spaced::Padding {
861 property: "padding-inline",
862 },
863 ),
864 (
865 "py",
866 Spaced::Padding {
867 property: "padding-block",
868 },
869 ),
870 (
871 "pt",
872 Spaced::Padding {
873 property: "padding-top",
874 },
875 ),
876 (
877 "pr",
878 Spaced::Padding {
879 property: "padding-right",
880 },
881 ),
882 (
883 "pb",
884 Spaced::Padding {
885 property: "padding-bottom",
886 },
887 ),
888 (
889 "pl",
890 Spaced::Padding {
891 property: "padding-left",
892 },
893 ),
894 (
895 "ps",
896 Spaced::Padding {
897 property: "padding-inline-start",
898 },
899 ),
900 (
901 "pe",
902 Spaced::Padding {
903 property: "padding-inline-end",
904 },
905 ),
906 (
907 "mx",
908 Spaced::Margin {
909 property: "margin-inline",
910 },
911 ),
912 (
913 "my",
914 Spaced::Margin {
915 property: "margin-block",
916 },
917 ),
918 (
919 "mt",
920 Spaced::Margin {
921 property: "margin-top",
922 },
923 ),
924 (
925 "mr",
926 Spaced::Margin {
927 property: "margin-right",
928 },
929 ),
930 (
931 "mb",
932 Spaced::Margin {
933 property: "margin-bottom",
934 },
935 ),
936 (
937 "ml",
938 Spaced::Margin {
939 property: "margin-left",
940 },
941 ),
942 (
943 "ms",
944 Spaced::Margin {
945 property: "margin-inline-start",
946 },
947 ),
948 (
949 "me",
950 Spaced::Margin {
951 property: "margin-inline-end",
952 },
953 ),
954 ("gap", Spaced::Padding { property: "gap" }),
955 ("inset", Spaced::Margin { property: "inset" }),
956 ("top", Spaced::Margin { property: "top" }),
957 ("right", Spaced::Margin { property: "right" }),
958 ("bottom", Spaced::Margin { property: "bottom" }),
959 ("left", Spaced::Margin { property: "left" }),
960 (
961 "p",
962 Spaced::Padding {
963 property: "padding",
964 },
965 ),
966 ("m", Spaced::Margin { property: "margin" }),
967];
968
969const TEXT_SIZES: &[&str] = &[
970 "xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl",
971];
972
973const WEIGHTS: &[&str] = &[
974 "thin",
975 "extralight",
976 "light",
977 "normal",
978 "medium",
979 "semibold",
980 "bold",
981 "extrabold",
982 "black",
983];
984
985const PALETTE: &[&str] = &[
987 "slate", "gray", "zinc", "neutral", "stone", "red", "orange", "amber", "yellow", "lime",
988 "green", "emerald", "teal", "cyan", "sky", "blue", "indigo", "violet", "purple", "fuchsia",
989 "pink", "rose",
990];
991
992const SHADES: &[&str] = &[
993 "50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950",
994];
995
996fn colour(rest: &str) -> Option<String> {
1002 match rest {
1003 "transparent" => return Some("transparent".to_string()),
1004 "current" => return Some("currentcolor".to_string()),
1005 "inherit" => return Some("inherit".to_string()),
1006 "white" | "black" => return Some(format!("var(--color-{rest})")),
1007 _ => {}
1008 }
1009 let (name, shade) = rest.rsplit_once('-')?;
1010 match PALETTE.contains(&name) && SHADES.contains(&shade) {
1011 true => Some(format!("var(--color-{rest})")),
1012 false => None,
1013 }
1014}
1015
1016fn number(rest: &str) -> bool {
1019 !rest.is_empty()
1020 && rest.chars().all(|c| c.is_ascii_digit() || c == '.')
1021 && rest.chars().filter(|c| *c == '.').count() <= 1
1022 && rest.chars().next().is_some_and(|c| c.is_ascii_digit())
1023 && rest.chars().last().is_some_and(|c| c.is_ascii_digit())
1024}
1025
1026pub fn enumerate() -> (Vec<String>, Vec<&'static str>) {
1034 let mut names: Vec<String> = Vec::new();
1035 let mut add = |name: String| {
1036 if rule(&name).is_some() {
1037 names.push(name);
1038 }
1039 };
1040 for (word, _) in WORDS {
1041 add(word.to_string());
1042 }
1043 let colours: Vec<String> = ["white", "black", "transparent", "current", "inherit"]
1044 .iter()
1045 .map(|k| k.to_string())
1046 .chain(
1047 PALETTE
1048 .iter()
1049 .flat_map(|p| SHADES.iter().map(move |s| format!("{p}-{s}"))),
1050 )
1051 .collect();
1052 for family in COLOURED.iter().map(|(f, _)| *f).chain(["text-", "border-"]) {
1053 for colour in &colours {
1054 add(format!("{family}{colour}"));
1055 }
1056 }
1057 for size in TEXT_SIZES {
1058 add(format!("text-{size}"));
1059 }
1060 for weight in WEIGHTS.iter().chain(&["sans", "serif", "mono"]) {
1061 add(format!("font-{weight}"));
1062 }
1063 for radius in [
1064 "none", "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "full",
1065 ] {
1066 add(format!("rounded-{radius}"));
1067 }
1068 for width in ["0", "2", "4", "8"] {
1069 add(format!("border-{width}"));
1070 }
1071 for (family, values) in [
1072 (
1073 "items-",
1074 &["start", "center", "end", "baseline", "stretch"][..],
1075 ),
1076 (
1077 "justify-",
1078 &[
1079 "start", "center", "end", "between", "around", "evenly", "stretch",
1080 ][..],
1081 ),
1082 (
1083 "flex-",
1084 &[
1085 "row",
1086 "row-reverse",
1087 "col",
1088 "col-reverse",
1089 "wrap",
1090 "nowrap",
1091 "wrap-reverse",
1092 "1",
1093 "auto",
1094 "initial",
1095 "none",
1096 ][..],
1097 ),
1098 (
1099 "overflow-",
1100 &["auto", "hidden", "clip", "visible", "scroll"][..],
1101 ),
1102 ] {
1103 for value in values {
1104 add(format!("{family}{value}"));
1105 }
1106 }
1107 for (family, _, _) in SIZED {
1114 for value in ["full", "auto", "screen", "min", "max", "fit", "px"] {
1115 add(format!("{family}{value}"));
1116 }
1117 for step in SCALE {
1118 add(format!("{family}{step}"));
1119 }
1120 }
1121 for (family, _) in SPACED {
1122 for value in ["px", "auto"] {
1123 add(format!("{family}-{value}"));
1124 }
1125 for step in SCALE {
1126 add(format!("{family}-{step}"));
1127 }
1128 }
1129 (names, VARIANTS.to_vec())
1130}
1131
1132const SCALE: &[&str] = &[
1138 "0", "0.5", "1", "1.5", "2", "2.5", "3", "3.5", "4", "5", "6", "7", "8", "9", "10", "11", "12",
1139 "14", "16", "20", "24", "28", "32", "36", "40", "44", "48", "52", "56", "60", "64", "72", "80",
1140 "96",
1141];
1142
1143const VARIANTS: &[&str] = &[
1145 "hover",
1146 "focus",
1147 "focus-visible",
1148 "focus-within",
1149 "active",
1150 "visited",
1151 "disabled",
1152 "checked",
1153 "first",
1154 "last",
1155 "odd",
1156 "even",
1157 "empty",
1158 "dark",
1159 "motion-safe",
1160 "motion-reduce",
1161 "print",
1162 "sm",
1163 "md",
1164 "lg",
1165 "xl",
1166 "2xl",
1167];
1168
1169const SUPPORTS: &str = "((-webkit-hyphens: none) and (not (margin-trim: inline))) or \
1172 ((-moz-orient: inline) and (not (color:rgb(from red r g b))))";
1173
1174pub fn theme(token: &str) -> Option<&'static str> {
1176 THEME.iter().find(|(n, _)| *n == token).map(|(_, v)| *v)
1177}
1178
1179pub fn theme_tokens() -> &'static [(&'static str, &'static str)] {
1181 THEME
1182}
1183
1184pub fn properties() -> &'static [(&'static str, &'static str)] {
1186 PROPERTIES
1187}
1188
1189pub fn supports() -> &'static str {
1191 SUPPORTS
1192}
1193
1194const THEME: &[(&str, &str)] = &[
1207 ("--color-amber-100", "oklch(96.2% 0.059 95.617)"),
1208 ("--color-amber-200", "oklch(92.4% 0.12 95.746)"),
1209 ("--color-amber-300", "oklch(87.9% 0.169 91.605)"),
1210 ("--color-amber-400", "oklch(82.8% 0.189 84.429)"),
1211 ("--color-amber-50", "oklch(98.7% 0.022 95.277)"),
1212 ("--color-amber-500", "oklch(76.9% 0.188 70.08)"),
1213 ("--color-amber-600", "oklch(66.6% 0.179 58.318)"),
1214 ("--color-amber-700", "oklch(55.5% 0.163 48.998)"),
1215 ("--color-amber-800", "oklch(47.3% 0.137 46.201)"),
1216 ("--color-amber-900", "oklch(41.4% 0.112 45.904)"),
1217 ("--color-amber-950", "oklch(27.9% 0.077 45.635)"),
1218 ("--color-black", "#000"),
1219 ("--color-blue-100", "oklch(93.2% 0.032 255.585)"),
1220 ("--color-blue-200", "oklch(88.2% 0.059 254.128)"),
1221 ("--color-blue-300", "oklch(80.9% 0.105 251.813)"),
1222 ("--color-blue-400", "oklch(70.7% 0.165 254.624)"),
1223 ("--color-blue-50", "oklch(97% 0.014 254.604)"),
1224 ("--color-blue-500", "oklch(62.3% 0.214 259.815)"),
1225 ("--color-blue-600", "oklch(54.6% 0.245 262.881)"),
1226 ("--color-blue-700", "oklch(48.8% 0.243 264.376)"),
1227 ("--color-blue-800", "oklch(42.4% 0.199 265.638)"),
1228 ("--color-blue-900", "oklch(37.9% 0.146 265.522)"),
1229 ("--color-blue-950", "oklch(28.2% 0.091 267.935)"),
1230 ("--color-cyan-100", "oklch(95.6% 0.045 203.388)"),
1231 ("--color-cyan-200", "oklch(91.7% 0.08 205.041)"),
1232 ("--color-cyan-300", "oklch(86.5% 0.127 207.078)"),
1233 ("--color-cyan-400", "oklch(78.9% 0.154 211.53)"),
1234 ("--color-cyan-50", "oklch(98.4% 0.019 200.873)"),
1235 ("--color-cyan-500", "oklch(71.5% 0.143 215.221)"),
1236 ("--color-cyan-600", "oklch(60.9% 0.126 221.723)"),
1237 ("--color-cyan-700", "oklch(52% 0.105 223.128)"),
1238 ("--color-cyan-800", "oklch(45% 0.085 224.283)"),
1239 ("--color-cyan-900", "oklch(39.8% 0.07 227.392)"),
1240 ("--color-cyan-950", "oklch(30.2% 0.056 229.695)"),
1241 ("--color-emerald-100", "oklch(95% 0.052 163.051)"),
1242 ("--color-emerald-200", "oklch(90.5% 0.093 164.15)"),
1243 ("--color-emerald-300", "oklch(84.5% 0.143 164.978)"),
1244 ("--color-emerald-400", "oklch(76.5% 0.177 163.223)"),
1245 ("--color-emerald-50", "oklch(97.9% 0.021 166.113)"),
1246 ("--color-emerald-500", "oklch(69.6% 0.17 162.48)"),
1247 ("--color-emerald-600", "oklch(59.6% 0.145 163.225)"),
1248 ("--color-emerald-700", "oklch(50.8% 0.118 165.612)"),
1249 ("--color-emerald-800", "oklch(43.2% 0.095 166.913)"),
1250 ("--color-emerald-900", "oklch(37.8% 0.077 168.94)"),
1251 ("--color-emerald-950", "oklch(26.2% 0.051 172.552)"),
1252 ("--color-fuchsia-100", "oklch(95.2% 0.037 318.852)"),
1253 ("--color-fuchsia-200", "oklch(90.3% 0.076 319.62)"),
1254 ("--color-fuchsia-300", "oklch(83.3% 0.145 321.434)"),
1255 ("--color-fuchsia-400", "oklch(74% 0.238 322.16)"),
1256 ("--color-fuchsia-50", "oklch(97.7% 0.017 320.058)"),
1257 ("--color-fuchsia-500", "oklch(66.7% 0.295 322.15)"),
1258 ("--color-fuchsia-600", "oklch(59.1% 0.293 322.896)"),
1259 ("--color-fuchsia-700", "oklch(51.8% 0.253 323.949)"),
1260 ("--color-fuchsia-800", "oklch(45.2% 0.211 324.591)"),
1261 ("--color-fuchsia-900", "oklch(40.1% 0.17 325.612)"),
1262 ("--color-fuchsia-950", "oklch(29.3% 0.136 325.661)"),
1263 ("--color-gray-100", "oklch(96.7% 0.003 264.542)"),
1264 ("--color-gray-200", "oklch(92.8% 0.006 264.531)"),
1265 ("--color-gray-300", "oklch(87.2% 0.01 258.338)"),
1266 ("--color-gray-400", "oklch(70.7% 0.022 261.325)"),
1267 ("--color-gray-50", "oklch(98.5% 0.002 247.839)"),
1268 ("--color-gray-500", "oklch(55.1% 0.027 264.364)"),
1269 ("--color-gray-600", "oklch(44.6% 0.03 256.802)"),
1270 ("--color-gray-700", "oklch(37.3% 0.034 259.733)"),
1271 ("--color-gray-800", "oklch(27.8% 0.033 256.848)"),
1272 ("--color-gray-900", "oklch(21% 0.034 264.665)"),
1273 ("--color-gray-950", "oklch(13% 0.028 261.692)"),
1274 ("--color-green-100", "oklch(96.2% 0.044 156.743)"),
1275 ("--color-green-200", "oklch(92.5% 0.084 155.995)"),
1276 ("--color-green-300", "oklch(87.1% 0.15 154.449)"),
1277 ("--color-green-400", "oklch(79.2% 0.209 151.711)"),
1278 ("--color-green-50", "oklch(98.2% 0.018 155.826)"),
1279 ("--color-green-500", "oklch(72.3% 0.219 149.579)"),
1280 ("--color-green-600", "oklch(62.7% 0.194 149.214)"),
1281 ("--color-green-700", "oklch(52.7% 0.154 150.069)"),
1282 ("--color-green-800", "oklch(44.8% 0.119 151.328)"),
1283 ("--color-green-900", "oklch(39.3% 0.095 152.535)"),
1284 ("--color-green-950", "oklch(26.6% 0.065 152.934)"),
1285 ("--color-indigo-100", "oklch(93% 0.034 272.788)"),
1286 ("--color-indigo-200", "oklch(87% 0.065 274.039)"),
1287 ("--color-indigo-300", "oklch(78.5% 0.115 274.713)"),
1288 ("--color-indigo-400", "oklch(67.3% 0.182 276.935)"),
1289 ("--color-indigo-50", "oklch(96.2% 0.018 272.314)"),
1290 ("--color-indigo-500", "oklch(58.5% 0.233 277.117)"),
1291 ("--color-indigo-600", "oklch(51.1% 0.262 276.966)"),
1292 ("--color-indigo-700", "oklch(45.7% 0.24 277.023)"),
1293 ("--color-indigo-800", "oklch(39.8% 0.195 277.366)"),
1294 ("--color-indigo-900", "oklch(35.9% 0.144 278.697)"),
1295 ("--color-indigo-950", "oklch(25.7% 0.09 281.288)"),
1296 ("--color-lime-100", "oklch(96.7% 0.067 122.328)"),
1297 ("--color-lime-200", "oklch(93.8% 0.127 124.321)"),
1298 ("--color-lime-300", "oklch(89.7% 0.196 126.665)"),
1299 ("--color-lime-400", "oklch(84.1% 0.238 128.85)"),
1300 ("--color-lime-50", "oklch(98.6% 0.031 120.757)"),
1301 ("--color-lime-500", "oklch(76.8% 0.233 130.85)"),
1302 ("--color-lime-600", "oklch(64.8% 0.2 131.684)"),
1303 ("--color-lime-700", "oklch(53.2% 0.157 131.589)"),
1304 ("--color-lime-800", "oklch(45.3% 0.124 130.933)"),
1305 ("--color-lime-900", "oklch(40.5% 0.101 131.063)"),
1306 ("--color-lime-950", "oklch(27.4% 0.072 132.109)"),
1307 ("--color-neutral-100", "oklch(97% 0 none)"),
1308 ("--color-neutral-200", "oklch(92.2% 0 none)"),
1309 ("--color-neutral-300", "oklch(87% 0 none)"),
1310 ("--color-neutral-400", "oklch(70.8% 0 none)"),
1311 ("--color-neutral-50", "oklch(98.5% 0 none)"),
1312 ("--color-neutral-500", "oklch(55.6% 0 none)"),
1313 ("--color-neutral-600", "oklch(43.9% 0 none)"),
1314 ("--color-neutral-700", "oklch(37.1% 0 none)"),
1315 ("--color-neutral-800", "oklch(26.9% 0 none)"),
1316 ("--color-neutral-900", "oklch(20.5% 0 none)"),
1317 ("--color-neutral-950", "oklch(14.5% 0 none)"),
1318 ("--color-orange-100", "oklch(95.4% 0.038 75.164)"),
1319 ("--color-orange-200", "oklch(90.1% 0.076 70.697)"),
1320 ("--color-orange-300", "oklch(83.7% 0.128 66.29)"),
1321 ("--color-orange-400", "oklch(75% 0.183 55.934)"),
1322 ("--color-orange-50", "oklch(98% 0.016 73.684)"),
1323 ("--color-orange-500", "oklch(70.5% 0.213 47.604)"),
1324 ("--color-orange-600", "oklch(64.6% 0.222 41.116)"),
1325 ("--color-orange-700", "oklch(55.3% 0.195 38.402)"),
1326 ("--color-orange-800", "oklch(47% 0.157 37.304)"),
1327 ("--color-orange-900", "oklch(40.8% 0.123 38.172)"),
1328 ("--color-orange-950", "oklch(26.6% 0.079 36.259)"),
1329 ("--color-pink-100", "oklch(94.8% 0.028 342.258)"),
1330 ("--color-pink-200", "oklch(89.9% 0.061 343.231)"),
1331 ("--color-pink-300", "oklch(82.3% 0.12 346.018)"),
1332 ("--color-pink-400", "oklch(71.8% 0.202 349.761)"),
1333 ("--color-pink-50", "oklch(97.1% 0.014 343.198)"),
1334 ("--color-pink-500", "oklch(65.6% 0.241 354.308)"),
1335 ("--color-pink-600", "oklch(59.2% 0.249 0.584)"),
1336 ("--color-pink-700", "oklch(52.5% 0.223 3.958)"),
1337 ("--color-pink-800", "oklch(45.9% 0.187 3.815)"),
1338 ("--color-pink-900", "oklch(40.8% 0.153 2.432)"),
1339 ("--color-pink-950", "oklch(28.4% 0.109 3.907)"),
1340 ("--color-purple-100", "oklch(94.6% 0.033 307.174)"),
1341 ("--color-purple-200", "oklch(90.2% 0.063 306.703)"),
1342 ("--color-purple-300", "oklch(82.7% 0.119 306.383)"),
1343 ("--color-purple-400", "oklch(71.4% 0.203 305.504)"),
1344 ("--color-purple-50", "oklch(97.7% 0.014 308.299)"),
1345 ("--color-purple-500", "oklch(62.7% 0.265 303.9)"),
1346 ("--color-purple-600", "oklch(55.8% 0.288 302.321)"),
1347 ("--color-purple-700", "oklch(49.6% 0.265 301.924)"),
1348 ("--color-purple-800", "oklch(43.8% 0.218 303.724)"),
1349 ("--color-purple-900", "oklch(38.1% 0.176 304.987)"),
1350 ("--color-purple-950", "oklch(29.1% 0.149 302.717)"),
1351 ("--color-red-100", "oklch(93.6% 0.032 17.717)"),
1352 ("--color-red-200", "oklch(88.5% 0.062 18.334)"),
1353 ("--color-red-300", "oklch(80.8% 0.114 19.571)"),
1354 ("--color-red-400", "oklch(70.4% 0.191 22.216)"),
1355 ("--color-red-50", "oklch(97.1% 0.013 17.38)"),
1356 ("--color-red-500", "oklch(63.7% 0.237 25.331)"),
1357 ("--color-red-600", "oklch(57.7% 0.245 27.325)"),
1358 ("--color-red-700", "oklch(50.5% 0.213 27.518)"),
1359 ("--color-red-800", "oklch(44.4% 0.177 26.899)"),
1360 ("--color-red-900", "oklch(39.6% 0.141 25.723)"),
1361 ("--color-red-950", "oklch(25.8% 0.092 26.042)"),
1362 ("--color-rose-100", "oklch(94.1% 0.03 12.58)"),
1363 ("--color-rose-200", "oklch(89.2% 0.058 10.001)"),
1364 ("--color-rose-300", "oklch(81% 0.117 11.638)"),
1365 ("--color-rose-400", "oklch(71.2% 0.194 13.428)"),
1366 ("--color-rose-50", "oklch(96.9% 0.015 12.422)"),
1367 ("--color-rose-500", "oklch(64.5% 0.246 16.439)"),
1368 ("--color-rose-600", "oklch(58.6% 0.253 17.585)"),
1369 ("--color-rose-700", "oklch(51.4% 0.222 16.935)"),
1370 ("--color-rose-800", "oklch(45.5% 0.188 13.697)"),
1371 ("--color-rose-900", "oklch(41% 0.159 10.272)"),
1372 ("--color-rose-950", "oklch(27.1% 0.105 12.094)"),
1373 ("--color-sky-100", "oklch(95.1% 0.026 236.824)"),
1374 ("--color-sky-200", "oklch(90.1% 0.058 230.902)"),
1375 ("--color-sky-300", "oklch(82.8% 0.111 230.318)"),
1376 ("--color-sky-400", "oklch(74.6% 0.16 232.661)"),
1377 ("--color-sky-50", "oklch(97.7% 0.013 236.62)"),
1378 ("--color-sky-500", "oklch(68.5% 0.169 237.323)"),
1379 ("--color-sky-600", "oklch(58.8% 0.158 241.966)"),
1380 ("--color-sky-700", "oklch(50% 0.134 242.749)"),
1381 ("--color-sky-800", "oklch(44.3% 0.11 240.79)"),
1382 ("--color-sky-900", "oklch(39.1% 0.09 240.876)"),
1383 ("--color-sky-950", "oklch(29.3% 0.066 243.157)"),
1384 ("--color-slate-100", "oklch(96.8% 0.007 247.896)"),
1385 ("--color-slate-200", "oklch(92.9% 0.013 255.508)"),
1386 ("--color-slate-300", "oklch(86.9% 0.022 252.894)"),
1387 ("--color-slate-400", "oklch(70.4% 0.04 256.788)"),
1388 ("--color-slate-50", "oklch(98.4% 0.003 247.858)"),
1389 ("--color-slate-500", "oklch(55.4% 0.046 257.417)"),
1390 ("--color-slate-600", "oklch(44.6% 0.043 257.281)"),
1391 ("--color-slate-700", "oklch(37.2% 0.044 257.287)"),
1392 ("--color-slate-800", "oklch(27.9% 0.041 260.031)"),
1393 ("--color-slate-900", "oklch(20.8% 0.042 265.755)"),
1394 ("--color-slate-950", "oklch(12.9% 0.042 264.695)"),
1395 ("--color-stone-100", "oklch(97% 0.001 106.424)"),
1396 ("--color-stone-200", "oklch(92.3% 0.003 48.717)"),
1397 ("--color-stone-300", "oklch(86.9% 0.005 56.366)"),
1398 ("--color-stone-400", "oklch(70.9% 0.01 56.259)"),
1399 ("--color-stone-50", "oklch(98.5% 0.001 106.423)"),
1400 ("--color-stone-500", "oklch(55.3% 0.013 58.071)"),
1401 ("--color-stone-600", "oklch(44.4% 0.011 73.639)"),
1402 ("--color-stone-700", "oklch(37.4% 0.01 67.558)"),
1403 ("--color-stone-800", "oklch(26.8% 0.007 34.298)"),
1404 ("--color-stone-900", "oklch(21.6% 0.006 56.043)"),
1405 ("--color-stone-950", "oklch(14.7% 0.004 49.25)"),
1406 ("--color-teal-100", "oklch(95.3% 0.051 180.801)"),
1407 ("--color-teal-200", "oklch(91% 0.096 180.426)"),
1408 ("--color-teal-300", "oklch(85.5% 0.138 181.071)"),
1409 ("--color-teal-400", "oklch(77.7% 0.152 181.912)"),
1410 ("--color-teal-50", "oklch(98.4% 0.014 180.72)"),
1411 ("--color-teal-500", "oklch(70.4% 0.14 182.503)"),
1412 ("--color-teal-600", "oklch(60% 0.118 184.704)"),
1413 ("--color-teal-700", "oklch(51.1% 0.096 186.391)"),
1414 ("--color-teal-800", "oklch(43.7% 0.078 188.216)"),
1415 ("--color-teal-900", "oklch(38.6% 0.063 188.416)"),
1416 ("--color-teal-950", "oklch(27.7% 0.046 192.524)"),
1417 ("--color-violet-100", "oklch(94.3% 0.029 294.588)"),
1418 ("--color-violet-200", "oklch(89.4% 0.057 293.283)"),
1419 ("--color-violet-300", "oklch(81.1% 0.111 293.571)"),
1420 ("--color-violet-400", "oklch(70.2% 0.183 293.541)"),
1421 ("--color-violet-50", "oklch(96.9% 0.016 293.756)"),
1422 ("--color-violet-500", "oklch(60.6% 0.25 292.717)"),
1423 ("--color-violet-600", "oklch(54.1% 0.281 293.009)"),
1424 ("--color-violet-700", "oklch(49.1% 0.27 292.581)"),
1425 ("--color-violet-800", "oklch(43.2% 0.232 292.759)"),
1426 ("--color-violet-900", "oklch(38% 0.189 293.745)"),
1427 ("--color-violet-950", "oklch(28.3% 0.141 291.089)"),
1428 ("--color-white", "#fff"),
1429 ("--color-yellow-100", "oklch(97.3% 0.071 103.193)"),
1430 ("--color-yellow-200", "oklch(94.5% 0.129 101.54)"),
1431 ("--color-yellow-300", "oklch(90.5% 0.182 98.111)"),
1432 ("--color-yellow-400", "oklch(85.2% 0.199 91.936)"),
1433 ("--color-yellow-50", "oklch(98.7% 0.026 102.212)"),
1434 ("--color-yellow-500", "oklch(79.5% 0.184 86.047)"),
1435 ("--color-yellow-600", "oklch(68.1% 0.162 75.834)"),
1436 ("--color-yellow-700", "oklch(55.4% 0.135 66.442)"),
1437 ("--color-yellow-800", "oklch(47.6% 0.114 61.907)"),
1438 ("--color-yellow-900", "oklch(42.1% 0.095 57.708)"),
1439 ("--color-yellow-950", "oklch(28.6% 0.066 53.813)"),
1440 ("--color-zinc-100", "oklch(96.7% 0.001 286.375)"),
1441 ("--color-zinc-200", "oklch(92% 0.004 286.32)"),
1442 ("--color-zinc-300", "oklch(87.1% 0.006 286.286)"),
1443 ("--color-zinc-400", "oklch(70.5% 0.015 286.067)"),
1444 ("--color-zinc-50", "oklch(98.5% 0 none)"),
1445 ("--color-zinc-500", "oklch(55.2% 0.016 285.938)"),
1446 ("--color-zinc-600", "oklch(44.2% 0.017 285.786)"),
1447 ("--color-zinc-700", "oklch(37% 0.013 285.805)"),
1448 ("--color-zinc-800", "oklch(27.4% 0.006 286.033)"),
1449 ("--color-zinc-900", "oklch(21% 0.006 285.885)"),
1450 ("--color-zinc-950", "oklch(14.1% 0.005 285.823)"),
1451 ("--default-font-family", "var(--font-sans)"),
1452 ("--default-mono-font-family", "var(--font-mono)"),
1453 ("--font-mono", "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace"),
1454 ("--font-sans", "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\""),
1455 ("--font-serif", "ui-serif, Georgia, Cambria, \"Times New Roman\", Times, serif"),
1456 ("--font-weight-black", "900"),
1457 ("--font-weight-bold", "700"),
1458 ("--font-weight-extrabold", "800"),
1459 ("--font-weight-extralight", "200"),
1460 ("--font-weight-light", "300"),
1461 ("--font-weight-medium", "500"),
1462 ("--font-weight-normal", "400"),
1463 ("--font-weight-semibold", "600"),
1464 ("--font-weight-thin", "100"),
1465 ("--radius-2xl", "1rem"),
1466 ("--radius-3xl", "1.5rem"),
1467 ("--radius-4xl", "2rem"),
1468 ("--radius-lg", "0.5rem"),
1469 ("--radius-md", "0.375rem"),
1470 ("--radius-sm", "0.25rem"),
1471 ("--radius-xl", "0.75rem"),
1472 ("--radius-xs", "0.125rem"),
1473 ("--spacing", "0.25rem"),
1474 ("--text-2xl", "1.5rem"),
1475 ("--text-2xl--line-height", "calc(2 / 1.5)"),
1476 ("--text-3xl", "1.875rem"),
1477 ("--text-3xl--line-height", "calc(2.25 / 1.875)"),
1478 ("--text-4xl", "2.25rem"),
1479 ("--text-4xl--line-height", "calc(2.5 / 2.25)"),
1480 ("--text-5xl", "3rem"),
1481 ("--text-5xl--line-height", "1"),
1482 ("--text-6xl", "3.75rem"),
1483 ("--text-6xl--line-height", "1"),
1484 ("--text-7xl", "4.5rem"),
1485 ("--text-7xl--line-height", "1"),
1486 ("--text-8xl", "6rem"),
1487 ("--text-8xl--line-height", "1"),
1488 ("--text-9xl", "8rem"),
1489 ("--text-9xl--line-height", "1"),
1490 ("--text-base", "1rem"),
1491 ("--text-base--line-height", "calc(1.5 / 1)"),
1492 ("--text-lg", "1.125rem"),
1493 ("--text-lg--line-height", "calc(1.75 / 1.125)"),
1494 ("--text-sm", "0.875rem"),
1495 ("--text-sm--line-height", "calc(1.25 / 0.875)"),
1496 ("--text-xl", "1.25rem"),
1497 ("--text-xl--line-height", "calc(1.75 / 1.25)"),
1498 ("--text-xs", "0.75rem"),
1499 ("--text-xs--line-height", "calc(1 / 0.75)"),
1500];
1501
1502pub fn stylesheet(styles: &Styles) -> String {
1530 let mut rules: Vec<(Arc<str>, Rule)> = styles
1531 .classes
1532 .iter()
1533 .filter_map(|c| rule(c).map(|r| (c.clone(), r)))
1534 .collect();
1535 rules.sort_by(|(a, x), (b, y)| x.at.len().cmp(&y.at.len()).then_with(|| a.cmp(b)));
1537
1538 let mut tokens: BTreeSet<&'static str> = PREFLIGHT_TOKENS.iter().copied().collect();
1539 let mut internals: BTreeSet<&'static str> = BTreeSet::new();
1540 for (_, r) in &rules {
1541 r.tokens(&mut tokens);
1542 for (property, value) in &r.decls {
1543 for (name, _) in PROPERTIES {
1544 if property == name || value.contains(name) {
1545 internals.insert(name);
1546 }
1547 }
1548 }
1549 }
1550
1551 let mut out = String::with_capacity(2048);
1552 out.push_str("/* Written by `beck build` — one rule per class this program's pages can\n");
1553 out.push_str(" carry, and nothing else. docs/104 §104.4. */\n");
1554 out.push_str(PREFLIGHT);
1555 if !tokens.is_empty() {
1556 out.push_str(":root{");
1557 for token in &tokens {
1558 let value = THEME
1559 .iter()
1560 .find(|(n, _)| n == token)
1561 .map(|(_, v)| *v)
1562 .unwrap_or("");
1563 let _ = write!(out, "{token}:{value};");
1564 }
1565 out.push_str("}\n");
1566 }
1567 for (name, body) in PROPERTIES.iter().filter(|(n, _)| internals.contains(n)) {
1568 let _ = writeln!(out, "@property {name}{{{body}}}");
1569 }
1570 let initial: Vec<&(&str, &str)> = PROPERTIES
1575 .iter()
1576 .filter(|(n, body)| internals.contains(n) && body.contains("initial-value"))
1577 .collect();
1578 if !initial.is_empty() {
1579 let _ = write!(out, "@supports {SUPPORTS}{{*,::before,::after,::backdrop{{");
1580 for (name, body) in initial {
1581 let value = body
1582 .rsplit_once("initial-value:")
1583 .map_or("", |(_, v)| v.trim());
1584 let _ = write!(out, "{name}:{value};");
1585 }
1586 out.push_str("}}\n");
1587 }
1588 for (_, rule) in &rules {
1589 for at in &rule.at {
1590 let _ = write!(out, "{at}{{");
1591 }
1592 let _ = write!(out, "{}{{", rule.selector);
1593 for (property, value) in &rule.decls {
1594 let _ = write!(out, "{property}:{value};");
1595 }
1596 out.push('}');
1597 out.push_str(&"}".repeat(rule.at.len()));
1598 out.push('\n');
1599 }
1600 out
1601}
1602
1603const PREFLIGHT: &str = concat!(
1611 "*,::before,::after{box-sizing:border-box;margin:0;padding:0;border:0 solid}\n",
1612 "html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:var(--default-font-family)}\n",
1613 "ul,ol{list-style:none}\n",
1614 "a{color:inherit;text-decoration:inherit}\n",
1615 "button,input,select,textarea{font:inherit;color:inherit;background:transparent}\n",
1616 "button{cursor:pointer}\n",
1617 "img,svg,video,canvas{display:block;max-width:100%}\n",
1618 "h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}\n",
1619 "table{border-collapse:collapse}\n",
1620);
1621
1622const PREFLIGHT_TOKENS: &[&str] = &["--default-font-family", "--font-sans"];
1624
1625const PROPERTIES: &[(&str, &str)] = &[
1627 (
1628 "--tw-border-style",
1629 "syntax:\"*\";inherits:false;initial-value:solid",
1630 ),
1631 ("--tw-font-weight", "syntax:\"*\";inherits:false"),
1632 (
1633 "--tw-space-x-reverse",
1634 "syntax:\"*\";inherits:false;initial-value:0",
1635 ),
1636 (
1637 "--tw-space-y-reverse",
1638 "syntax:\"*\";inherits:false;initial-value:0",
1639 ),
1640];