Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Page
Library
Module
Module type
Parameter
Class
Class type
Source
sizing.ml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075(** Sizing utilities for width and height *) module Css = Cascade.Css module Handler = struct let class_float = Pp.float open Style open Css (** The property a sizing family sets. [Width_and_height] is [size-*], which sets both. *) type prop = | Width | Height | Min_width | Min_height | Max_width | Max_height | Width_and_height | Inline_size | Min_inline_size | Max_inline_size | Block_size | Min_block_size | Max_block_size (** The value a sizing class carries. [Keyword] and [Themed] come from the family's own table, so they carry their class spelling and their suborder with them; a [Themed] value also carries the theme-variable declaration and the [var()] reference the utility emits. *) type value = | Keyword of { spelling : string; length : length; order : int } | Themed of { spelling : string; decl : declaration; value : length; order : int; } | Spacing of float | Fraction of string | Arbitrary of string * length | Arbitrary_raw of string * string (** a bracket no length reader took: the family names its longhands, so the value is forwarded verbatim - Tailwind's token-stream contract *) | Theme_named of string (** a size the project named in its own [@theme] *) type t = | Sized of prop * value | Aspect_auto | Aspect_square | Aspect_video | Aspect_ratio of float * float (* aspect-4/3, aspect-8.5/11 *) | Aspect_arbitrary of string * string (* aspect-[16/9]: the bracket as written, and the value it declares *) | Aspect_theme of string (* ratio named by a project [--aspect-*] token *) let name = "sizing" (* Tailwind spacing order helper: matches canonical spacing scale order. n is the rem value, so we convert to class units (multiply by 4). *) let spacing_suborder n = let class_units = n *. 4. in let integer = int_of_float (floor class_units) in let frac = class_units -. float_of_int integer in (* Fractional part order: .0, .5, .25, .75 (not numeric!) *) let frac_order = if frac = 0.0 then 0 else if abs_float (frac -. 0.5) < 0.01 then 1 else if abs_float (frac -. 0.25) < 0.01 then 2 else if abs_float (frac -. 0.75) < 0.01 then 3 else 4 (* fallback for other values *) in (integer * 4) + frac_order (* Tailwind interleaves spacing values and fractions within a sizing family by the integer part of their magnitude (spacing value or fraction numerator): e.g. w-0.5, w-1, w-1.5, w-1/2, w-1/3, w-2, w-2/3, w-3/4. Spacing sorts by value, fractions of numerator n come just after the spacing values with that integer part, ordered by denominator. Both stay well under the per-family arbitrary offset (5_000_000). *) let spacing_value_order n = spacing_suborder n * 100 (* A spacing value is stored as rem (class number * 0.25). A fraction n/m has numerator [n], whose class number is [n], so [spacing_suborder (n *. 0.25)] puts it on the spacing scale. [+ 4] steps to the next class boundary (spacing_suborder for an integer class k is 4k); [- 1] pulls every n/m into the last slot before that boundary, after every floor-n spacing value. The natural candidate tiebreak then orders the unbounded denominators. *) let fraction_value_order f = match Parse.fraction f with | Some (n, _) -> ((spacing_suborder (float_of_int n *. 0.25) + 4) * 100) - 1 | None -> 490000 (* Within a family: spacing and fractions interleaved by magnitude, then an arbitrary value, then the keywords. *) let arbitrary_off = 5_000_000 let keyword_off = 6_000_000 (* Family bases are 10M apart so the interleaved spacing/fraction range (< 5M) and the arbitrary/keyword offsets never overflow into the next family. *) let aspect_base = -10_000_000 (* Within the dimension families, size-* sorts before h/w/max/min. *) let size_base = 0 let h_base = 10_000_000 let max_h_base = 20_000_000 let min_h_base = 30_000_000 let w_base = 40_000_000 let max_w_base = 50_000_000 let min_w_base = 60_000_000 (* Tailwind registers the logical sizing utilities after every other one, so they sort last rather than beside w-* and h-*; [logical_priority] carries that. Their bases run in the alphabetical order Tailwind falls back to. *) let logical = 80_000_000 let block_base = logical let inline_base = 90_000_000 let max_block_base = 100_000_000 let max_inline_base = 110_000_000 let min_block_base = 120_000_000 let min_inline_base = 130_000_000 let logical_priority = 38 (* A Tailwind sizing fraction [n/m] resolves to [n/m * 100%]: any numerator over any positive denominator, read as a percentage rather than off a fixed scale. *) let fraction_pct = Parse.fraction_pct (* Container size theme variables - ordered from smallest to largest *) let container_3xs = Var.theme Css.Length "container-3xs" ~order:(5, 0) let container_2xs = Var.theme Css.Length "container-2xs" ~order:(5, 1) let container_xs = Var.theme Css.Length "container-xs" ~order:(5, 2) let container_sm = Var.theme Css.Length "container-sm" ~order:(5, 3) let container_md = Var.theme Css.Length "container-md" ~order:(5, 4) let container_lg = Var.theme Css.Length "container-lg" ~order:(5, 5) let container_xl = Var.theme Css.Length "container-xl" ~order:(5, 6) let container_2xl = Var.theme Css.Length "container-2xl" ~order:(5, 7) let container_3xl = Var.theme Css.Length "container-3xl" ~order:(5, 8) let container_4xl = Var.theme Css.Length "container-4xl" ~order:(5, 9) let container_5xl = Var.theme Css.Length "container-5xl" ~order:(5, 10) let container_6xl = Var.theme Css.Length "container-6xl" ~order:(5, 11) let container_7xl = Var.theme Css.Length "container-7xl" ~order:(5, 12) (* The container scale doubles as the named width scale in v4. This is the only place its names, theme variables, defaults and order live. *) let container_scale = [ ("3xs", container_3xs, (Rem 16.0 : length)); ("2xs", container_2xs, Rem 18.0); ("xs", container_xs, Rem 20.0); ("sm", container_sm, Rem 24.0); ("md", container_md, Rem 28.0); ("lg", container_lg, Rem 32.0); ("xl", container_xl, Rem 36.0); ("2xl", container_2xl, Rem 42.0); ("3xl", container_3xl, Rem 48.0); ("4xl", container_4xl, Rem 56.0); ("5xl", container_5xl, Rem 64.0); ("6xl", container_6xl, Rem 72.0); ("7xl", container_7xl, Rem 80.0); ] let container_binding name = List.find_map (fun (n, var, default) -> if n = name then Some (var, default) else None) container_scale (* Publish the scale through the theme-token registry, the way rule.ml publishes the breakpoints, so [max-w-[theme(--container-md)]] resolves and [theme(static)] emits it. *) let () = List.iter (fun (_, var, default) -> Theme.register_default var default) container_scale (* Breakpoint theme vars, referenced by the (v3) max-w-screen-* utilities. Negative suborders keep them before --container-* in the theme layer, as Tailwind emits them. *) let breakpoint_sm = Var.theme Css.Length "breakpoint-sm" ~order:(5, -5) let breakpoint_md = Var.theme Css.Length "breakpoint-md" ~order:(5, -4) let breakpoint_lg = Var.theme Css.Length "breakpoint-lg" ~order:(5, -3) let breakpoint_xl = Var.theme Css.Length "breakpoint-xl" ~order:(5, -2) let breakpoint_2xl = Var.theme Css.Length "breakpoint-2xl" ~order:(5, -1) (* Table entries: [kw] resolves to a literal length, [themed] to a theme variable carrying its default. *) let kw spelling length order = Keyword { spelling; length; order } let themed spelling var default order = let decl, ref_ = Var.binding var default in Themed { spelling; decl; value = Var ref_; order } (* The whole container scale, in scale order, from suborder offset [off]. *) let container_entries off = List.mapi (fun i (spelling, var, default) -> themed spelling var default (off + i)) container_scale (* The container scale in a bespoke suborder, given as name/offset pairs. *) let container_entries_by offs = List.filter_map (fun (spelling, var, default) -> Option.map (fun off -> themed spelling var default off) (List.assoc_opt spelling offs)) container_scale type family = { prefix : string; (** class prefix, e.g. ["min-w"] *) css_name : string; (** property name, as error messages spell it *) decls : (length -> declaration) list; properties : string list; (** the longhands [decls] writes, for a value no typed setter can hold *) base : int; (** suborder base for the family *) spacing_off : int; (** extra offset in front of the spacing band *) entries : value list; (** the keywords this family admits *) } let width_family = { prefix = "w"; css_name = "width"; decls = [ width ]; properties = [ "width" ]; base = w_base; spacing_off = 0; entries = [ kw "auto" Auto (keyword_off + 0); kw "dvw" (Dvw 100.) (keyword_off + 1); kw "dvh" (Dvh 100.) (keyword_off + 1); kw "lvh" (Lvh 100.) (keyword_off + 1); kw "svh" (Svh 100.) (keyword_off + 1); kw "fit" Fit_content (keyword_off + 2); kw "full" (Pct 100.0) (keyword_off + 3); kw "lvw" (Lvw 100.) (keyword_off + 4); kw "max" Max_content (keyword_off + 5); kw "min" Min_content (keyword_off + 6); kw "px" (Px 1.0) (keyword_off + 7); kw "screen" (Vw 100.0) (keyword_off + 8); kw "svw" (Svw 100.) (keyword_off + 9); ] @ container_entries (keyword_off + 10); } let height_family = { prefix = "h"; css_name = "height"; decls = [ height ]; properties = [ "height" ]; base = h_base; spacing_off = 0; entries = [ kw "auto" Auto (keyword_off + 0); kw "dvh" (Dvh 100.) (keyword_off + 1); kw "fit" Fit_content (keyword_off + 2); kw "full" (Pct 100.0) (keyword_off + 3); kw "lh" (Lh 1.) (keyword_off + 4); kw "lvh" (Lvh 100.) (keyword_off + 5); kw "max" Max_content (keyword_off + 6); kw "min" Min_content (keyword_off + 7); kw "px" (Px 1.0) (keyword_off + 8); kw "screen" (Vh 100.0) (keyword_off + 9); kw "svh" (Svh 100.) (keyword_off + 10); kw "dvw" (Dvw 100.) (keyword_off + 10); kw "lvw" (Lvw 100.) (keyword_off + 10); kw "svw" (Svw 100.) (keyword_off + 10); ]; } let min_width_family = { prefix = "min-w"; css_name = "min-width"; decls = [ min_width ]; properties = [ "min-width" ]; base = min_w_base; spacing_off = 0; entries = [ kw "0" (Px 0.) 0; kw "auto" Auto (keyword_off + 0); kw "fit" Fit_content (keyword_off + 1); kw "full" (Pct 100.0) (keyword_off + 2); kw "dvh" (Dvh 100.) (keyword_off + 2); kw "dvw" (Dvw 100.) (keyword_off + 2); kw "lvh" (Lvh 100.) (keyword_off + 2); kw "lvw" (Lvw 100.) (keyword_off + 2); kw "svh" (Svh 100.) (keyword_off + 2); kw "svw" (Svw 100.) (keyword_off + 2); kw "px" (Px 1.0) (keyword_off + 2); kw "screen" (Vw 100.0) (keyword_off + 2); kw "max" Max_content (keyword_off + 3); kw "min" Min_content (keyword_off + 4); ] @ container_entries (keyword_off + 5); } let min_height_family = { prefix = "min-h"; css_name = "min-height"; decls = [ min_height ]; properties = [ "min-height" ]; base = min_h_base; spacing_off = 0; entries = [ kw "0" (Px 0.) 0; kw "auto" Auto (keyword_off + 0); kw "dvh" (Dvh 100.) (keyword_off + 1); kw "fit" Fit_content (keyword_off + 2); kw "full" (Pct 100.0) (keyword_off + 3); kw "lh" (Lh 1.) (keyword_off + 4); kw "lvh" (Lvh 100.) (keyword_off + 5); kw "max" Max_content (keyword_off + 6); kw "min" Min_content (keyword_off + 7); kw "screen" (Vh 100.0) (keyword_off + 8); kw "svh" (Svh 100.) (keyword_off + 9); kw "px" (Px 1.0) (keyword_off + 9); kw "dvw" (Dvw 100.) (keyword_off + 9); kw "lvw" (Lvw 100.) (keyword_off + 9); kw "svw" (Svw 100.) (keyword_off + 9); ]; } (* Tailwind orders max-width in three bands: the container scale sizes (2xl..7xl) by number, then the numeric spacing values, then an arbitrary value, then the letter-prefixed keywords alphabetically. Only this family shifts its spacing band ([spacing_off]); its fractions stay put. *) let max_width_family = { prefix = "max-w"; css_name = "max-width"; decls = [ max_width ]; properties = [ "max-width" ]; base = max_w_base; spacing_off = 1_000_000; entries = [ kw "dvh" (Dvh 100.) 0; kw "dvw" (Dvw 100.) 0; kw "lvh" (Lvh 100.) 0; kw "lvw" (Lvw 100.) 0; kw "svh" (Svh 100.) 0; kw "svw" (Svw 100.) 0; kw "px" (Px 1.0) 0; kw "screen" (Vw 100.0) 0; kw "fit" Fit_content (keyword_off + 0); kw "full" (Pct 100.0) (keyword_off + 1); kw "max" Max_content (keyword_off + 3); kw "min" Min_content (keyword_off + 5); kw "none" None (keyword_off + 6); kw "prose" (Ch 65.0) (keyword_off + 7); themed "screen-2xl" breakpoint_2xl (Rem 96.) (keyword_off + 8); themed "screen-lg" breakpoint_lg (Rem 64.) (keyword_off + 9); themed "screen-md" breakpoint_md (Rem 48.) (keyword_off + 10); themed "screen-sm" breakpoint_sm (Rem 40.) (keyword_off + 11); themed "screen-xl" breakpoint_xl (Rem 80.) (keyword_off + 12); ] @ container_entries_by [ ("2xs", 0); ("3xs", 0); ("2xl", 0); ("3xl", 1); ("4xl", 2); ("5xl", 3); ("6xl", 4); ("7xl", 5); ("lg", keyword_off + 2); ("md", keyword_off + 4); ("sm", keyword_off + 13); ("xl", keyword_off + 14); ("xs", keyword_off + 15); ]; } let max_height_family = { prefix = "max-h"; css_name = "max-height"; decls = [ max_height ]; properties = [ "max-height" ]; base = max_h_base; spacing_off = 0; entries = [ kw "dvh" (Dvh 100.) (keyword_off + 0); kw "fit" Fit_content (keyword_off + 1); kw "full" (Pct 100.0) (keyword_off + 2); kw "lh" (Lh 1.) (keyword_off + 3); kw "lvh" (Lvh 100.) (keyword_off + 4); kw "max" Max_content (keyword_off + 5); kw "min" Min_content (keyword_off + 6); kw "none" None (keyword_off + 7); kw "screen" (Vh 100.0) (keyword_off + 8); kw "svh" (Svh 100.) (keyword_off + 9); kw "px" (Px 1.0) (keyword_off + 9); kw "dvw" (Dvw 100.) (keyword_off + 9); kw "lvw" (Lvw 100.) (keyword_off + 9); kw "svw" (Svw 100.) (keyword_off + 9); ]; } let size_family = { prefix = "size"; css_name = "size"; decls = [ width; height ]; properties = [ "width"; "height" ]; base = size_base; spacing_off = 0; entries = [ kw "auto" Auto (keyword_off + 0); kw "fit" Fit_content (keyword_off + 1); kw "full" (Pct 100.0) (keyword_off + 2); kw "dvh" (Dvh 100.) (keyword_off + 2); kw "dvw" (Dvw 100.) (keyword_off + 2); kw "lvh" (Lvh 100.) (keyword_off + 2); kw "lvw" (Lvw 100.) (keyword_off + 2); kw "svh" (Svh 100.) (keyword_off + 2); kw "svw" (Svw 100.) (keyword_off + 2); kw "px" (Px 1.0) (keyword_off + 2); kw "max" Max_content (keyword_off + 3); kw "min" Min_content (keyword_off + 4); ]; } let inline_family = { prefix = "inline"; css_name = "inline-size"; decls = [ inline_size ]; properties = [ "inline-size" ]; base = inline_base; spacing_off = 0; entries = [ kw "auto" Auto (keyword_off + 0); kw "dvw" (Dvw 100.) (keyword_off + 1); kw "fit" Fit_content (keyword_off + 2); kw "full" (Pct 100.) (keyword_off + 3); kw "lvw" (Lvw 100.) (keyword_off + 4); kw "max" Max_content (keyword_off + 5); kw "min" Min_content (keyword_off + 6); kw "screen" (Vw 100.) (keyword_off + 7); kw "svw" (Svw 100.) (keyword_off + 8); kw "px" (Px 1.) (keyword_off + 20); ] @ container_entries (keyword_off + 9); } let min_inline_family = { prefix = "min-inline"; css_name = "min-inline-size"; decls = [ min_inline_size ]; properties = [ "min-inline-size" ]; base = min_inline_base; spacing_off = 0; entries = [ kw "auto" Auto (keyword_off + 0); kw "fit" Fit_content (keyword_off + 1); kw "full" (Pct 100.) (keyword_off + 2); kw "screen" (Vw 100.0) (keyword_off + 2); kw "px" (Px 1.0) (keyword_off + 2); kw "svw" (Svw 100.) (keyword_off + 2); kw "lvw" (Lvw 100.) (keyword_off + 2); kw "dvw" (Dvw 100.) (keyword_off + 2); kw "max" Max_content (keyword_off + 3); kw "min" Min_content (keyword_off + 4); ] @ container_entries (keyword_off + 5); } let max_inline_family = { prefix = "max-inline"; css_name = "max-inline-size"; decls = [ max_inline_size ]; properties = [ "max-inline-size" ]; base = max_inline_base; spacing_off = 0; entries = [ kw "fit" Fit_content (keyword_off + 0); kw "full" (Pct 100.) (keyword_off + 1); kw "min" Min_content (keyword_off + 1); kw "screen" (Vw 100.0) (keyword_off + 1); kw "px" (Px 1.0) (keyword_off + 1); kw "svw" (Svw 100.) (keyword_off + 1); kw "lvw" (Lvw 100.) (keyword_off + 1); kw "dvw" (Dvw 100.) (keyword_off + 1); kw "max" Max_content (keyword_off + 2); kw "none" None (keyword_off + 3); ] @ container_entries (keyword_off + 4); } let block_family = { prefix = "block"; css_name = "block-size"; decls = [ block_size ]; properties = [ "block-size" ]; base = block_base; spacing_off = 0; entries = [ kw "auto" Auto (keyword_off + 0); kw "dvh" (Dvh 100.) (keyword_off + 1); kw "fit" Fit_content (keyword_off + 2); kw "full" (Pct 100.) (keyword_off + 3); kw "lh" (Lh 1.) (keyword_off + 4); kw "lvh" (Lvh 100.) (keyword_off + 5); kw "max" Max_content (keyword_off + 6); kw "min" Min_content (keyword_off + 7); kw "screen" (Vh 100.) (keyword_off + 8); kw "svh" (Svh 100.) (keyword_off + 9); kw "px" (Px 1.) (keyword_off + 20); ]; } let min_block_family = { prefix = "min-block"; css_name = "min-block-size"; decls = [ min_block_size ]; properties = [ "min-block-size" ]; base = min_block_base; spacing_off = 0; entries = [ kw "auto" Auto (keyword_off + 0); kw "dvh" (Dvh 100.) (keyword_off + 1); kw "fit" Fit_content (keyword_off + 2); kw "full" (Pct 100.) (keyword_off + 3); kw "px" (Px 1.0) (keyword_off + 3); kw "lh" (Lh 1.) (keyword_off + 4); kw "lvh" (Lvh 100.) (keyword_off + 5); kw "max" Max_content (keyword_off + 6); kw "min" Min_content (keyword_off + 7); kw "screen" (Vh 100.) (keyword_off + 8); kw "svh" (Svh 100.) (keyword_off + 9); ]; } let max_block_family = { prefix = "max-block"; css_name = "max-block-size"; decls = [ max_block_size ]; properties = [ "max-block-size" ]; base = max_block_base; spacing_off = 0; entries = [ kw "dvh" (Dvh 100.) (keyword_off + 0); kw "fit" Fit_content (keyword_off + 1); kw "full" (Pct 100.) (keyword_off + 2); kw "px" (Px 1.0) (keyword_off + 2); kw "lh" (Lh 1.) (keyword_off + 3); kw "lvh" (Lvh 100.) (keyword_off + 4); kw "max" Max_content (keyword_off + 5); kw "min" Min_content (keyword_off + 6); kw "none" None (keyword_off + 7); kw "screen" (Vh 100.) (keyword_off + 8); kw "svh" (Svh 100.) (keyword_off + 9); ]; } let family = function | Width -> width_family | Height -> height_family | Min_width -> min_width_family | Min_height -> min_height_family | Max_width -> max_width_family | Max_height -> max_height_family | Width_and_height -> size_family | Inline_size -> inline_family | Min_inline_size -> min_inline_family | Max_inline_size -> max_inline_family | Block_size -> block_family | Min_block_size -> min_block_family | Max_block_size -> max_block_family (* The part of the class name that follows the family prefix. *) let class_suffix = function | Keyword k -> k.spelling | Themed t -> t.spelling | Spacing n -> class_float (n *. 4.) | Fraction f -> f | Arbitrary (raw, _) | Arbitrary_raw (raw, _) -> "[" ^ raw ^ "]" | Theme_named spelling -> spelling let value_order f = function | Keyword k -> k.order | Themed t -> t.order | Spacing n -> f.spacing_off + spacing_value_order n | Fraction s -> fraction_value_order s | Arbitrary _ | Arbitrary_raw _ -> arbitrary_off (* A project-named size has no slot in the built-in scale, so it sorts with the other project-supplied values and ties are broken by class name. *) | Theme_named _ -> arbitrary_off let lookup f spelling = List.find_opt (fun v -> class_suffix v = spelling) f.entries (* [keyword prop name] is family [prop]'s entry for keyword [name]; every caller names one the family's table lists. *) let keyword prop name = let f = family prop in match lookup f name with | Some v -> Sized (prop, v) | None -> invalid_arg ("sizing: no such utility: " ^ f.prefix ^ "-" ^ name) let aspect_auto' = style [ Css.aspect_ratio Auto ] (* Theme variables for aspect ratios *) (* Tailwind's [@theme] declares [--aspect-video] after the perspective scale (8, 10-16) and before the [--default-*] tokens (8, 30+). *) let aspect_video_var = Var.theme Css.Aspect_ratio "aspect-video" ~order:(8, 20) (* aspect-square inlines the 1/1 ratio in v4 (no --aspect-square theme token), unlike aspect-video which references the --aspect-video token. *) let aspect_square' = style [ Css.aspect_ratio (Ratio (1., 1.)) ] let aspect_video_ratio : Css.aspect_ratio = Ratio (16., 9.) (* Published the same way, so [aspect-[theme(--aspect-video)]] resolves. *) let () = Theme.register_default aspect_video_var aspect_video_ratio let aspect_video' = let decl, r = Var.binding aspect_video_var aspect_video_ratio in style (decl :: [ Css.aspect_ratio (Var r) ]) let aspect_ratio' w h = style [ Css.aspect_ratio (Ratio (w, h)) ] (* A [--aspect-*] token the project declared names a ratio the built-in scale has no slot for; the utility references it and the theme layer carries the project's own spelling. *) let aspect_named_cache = Domain_cache.v 8 let aspect_named_var name = Domain_cache.or_add aspect_named_cache name (fun () -> Var.theme Css.Aspect_ratio ("aspect-" ^ name) ~order:(8, 21)) let is_theme_aspect theme n = Scheme.theme_value (Some theme) ("aspect-" ^ n) <> None let parse_theme_ratio raw = match String.split_on_char '/' raw with | [ w; h ] -> ( match ( float_of_string_opt (String.trim w), float_of_string_opt (String.trim h) ) with | Some w, Some h -> Some (Css.Ratio (w, h) : Css.aspect_ratio) | _ -> None) | [ n ] -> Option.map (fun f : Css.aspect_ratio -> Css.Ratio (f, 1.)) (float_of_string_opt (String.trim n)) | _ -> None let aspect_theme' theme name = match Scheme.theme_value (Some theme) ("aspect-" ^ name) with | None -> style [] | Some raw -> ( match parse_theme_ratio raw with | None -> style [] | Some ratio -> let decl, r = Var.binding (aspect_named_var name) ratio in style [ decl; Css.aspect_ratio (Var r) ]) (* v4 resolves a named size through the namespaces its family lists before the container scale: [w-sm] reads [--width-sm], then [--spacing-sm], and only then the [--container-sm] default. *) let named_namespaces = function | Width -> [ "width"; "spacing" ] | Min_width -> [ "min-width"; "spacing" ] | Max_width -> [ "max-width"; "spacing"; "container" ] | Inline_size | Min_inline_size | Max_inline_size -> [ "spacing" ] | _ -> [] let named_override theme prop spelling : Style.t option = let f = family prop in List.find_map (fun namespace -> let token = namespace ^ "-" ^ spelling in Option.map (fun value -> let decl = Css.custom_property ~layer:"theme" ("--" ^ token) value in let length : length = Var (Var.theme_ref token) in style (decl :: List.map (fun decl -> decl length) f.decls)) (Scheme.theme_value (Some theme) token)) (named_namespaces prop) let is_theme_named theme prop spelling = List.exists (fun namespace -> Scheme.theme_value (Some theme) (namespace ^ "-" ^ spelling) <> None) (named_namespaces prop) let sized_style theme prop v = let f = family prop in let set len = style (List.map (fun decl -> decl len) f.decls) in let set_arbitrary raw len = let theme_decl : Css.declaration option = let bare_name = Parse.extract_var_name raw in if String.equal bare_name raw then Option.None else Scheme.token theme bare_name |> Option.map (fun value -> Css.custom_property ~layer:"theme" ("--" ^ bare_name) value) in style (Option.to_list theme_decl @ List.map (fun decl -> decl len) f.decls) in match v with | Keyword k -> set k.length | Arbitrary (raw, len) -> set_arbitrary raw len | Arbitrary_raw (_, value) -> style (List.filter_map (fun property -> Parse.opaque_declaration property value) f.properties) | Fraction s -> ( match (Parse.fraction s, fraction_pct s) with | Some (n, m), Some _ -> set (Parse.fraction_length n m) | _ -> failwith ("Unknown " ^ f.css_name ^ " fraction: " ^ s)) | Spacing n -> (* [n] is in rem; the class number is [n * 4] because --spacing is 0.25rem. calc(var(--spacing) * n) keeps v4 compatibility. *) let decl, len = Theme.spacing_calc_float ~theme (n *. 4.) in style (decl :: List.map (fun d -> d len) f.decls) | Themed t -> ( match named_override theme prop t.spelling with | Some s -> s | None -> style (t.decl :: List.map (fun d -> d t.value) f.decls)) | Theme_named spelling -> ( match named_override theme prop spelling with | Some s -> s | None -> style []) let to_style theme = function | Sized (prop, v) -> sized_style theme prop v | Aspect_auto -> aspect_auto' | Aspect_square -> aspect_square' | Aspect_video -> aspect_video' | Aspect_ratio (w, h) -> aspect_ratio' w h | Aspect_arbitrary (_, value) -> style (Option.to_list (Parse.opaque_declaration "aspect-ratio" value)) | Aspect_theme name -> aspect_theme' theme name let err_not_utility = Error (`Msg "Not a sizing utility") let err_invalid_value name value = Error (`Msg ("Invalid " ^ name ^ " value: " ^ value)) (* A [/] inside a bracket belongs to the value ([w-[calc(2px/2)]]), not to a fraction. *) let is_bracket v = String.starts_with ~prefix:"[" v let parse_arbitrary s : (string * Css.length) option = (* Parse bracket values: [4px], [1rem], [calc(100vh-4rem)], etc. Uses Css.parse_length for full CSS length parsing including calc(). Returns (raw_inner, parsed_length) where raw_inner is used for the CSS class name selector (preserving original formatting). *) if Parse.is_bracket_value s then let inner = Parse.bracket_inner s in (* A data-type hint chooses the longhand and says nothing about the value. A sizing family writes one longhand, so every hint lands here and the length reader is handed what follows it; [inner] keeps the hint because the class name has to. *) match Parse.value_after_hint inner with | None -> None | Some value -> Option.map (fun l -> (inner, l)) (Parse.arbitrary_length value) else None (* A spacing step, and a ratio part, is a non-negative multiple of 0.25 ([isValidSpacingMultiplier]), so [w-1.75] and [8.5/11] are utilities while [w-1.7] and [1.23/4.56] are not. *) let is_quarter_multiple f = f >= 0. && Float.rem f 0.25 = 0. (* One parser for all thirteen sizing families: the family's own keyword table, then the fraction, bracket and spacing tails they share. *) let parse_sized ~theme prop v = let f = family prop in match lookup f v with | Some value -> Ok (Sized (prop, value)) | None -> ( if String.contains v '/' && not (is_bracket v) then if fraction_pct v <> None then Ok (Sized (prop, Fraction v)) else err_invalid_value (f.css_name ^ " fraction") v else if is_bracket v then match parse_arbitrary v with | Some (raw, len) -> Ok (Sized (prop, Arbitrary (raw, len))) | None when not (Parse.is_bracket_value v) -> (* [w-[10px][20px]] is two brackets glued together, which is one value to neither reader nor to Tailwind. *) err_invalid_value f.css_name v | None -> ( (* The family names its longhands, so a bracket no length reader took still reaches them. *) let inner = Parse.bracket_inner v in match Parse.arbitrary_declaration_value inner with | Some raw -> Ok (Sized (prop, Arbitrary_raw (inner, raw))) | None -> err_invalid_value f.css_name v) else match Parse.decimal_float v with | Some n when is_quarter_multiple n && Theme.has_spacing_step ~theme n -> Ok (Sized (prop, Spacing (n *. 0.25))) | _ -> (* A size the project named in its own [@theme], under one of the namespaces this family reads. *) if is_theme_named theme prop v then Ok (Sized (prop, Theme_named v)) else err_invalid_value f.css_name v) let parse_max_w_screen s = match lookup max_width_family ("screen-" ^ s) with | Some value -> Ok (Sized (Max_width, value)) | None -> err_invalid_value "max-width screen size" s (* A bare [aspect-4/3] is a class suffix, so each part is a plain decimal and a quarter multiple, the way a spacing step is. The bracket spelling shares none of that: it is a token stream, parsed below. *) let parse_aspect_ratio s = match String.split_on_char '/' s with | [ w; h ] -> ( match (Parse.decimal_float w, Parse.decimal_float h) with | Some w, Some h when is_quarter_multiple w && is_quarter_multiple h -> Ok (Aspect_ratio (w, h)) | _ -> err_not_utility) | _ -> err_not_utility let of_class theme class_name = let parse_sized prop v = parse_sized ~theme prop v in match Parse.split_class class_name with | [ "w"; value ] -> parse_sized Width value | [ "h"; value ] -> parse_sized Height value | [ "min"; "w"; value ] -> parse_sized Min_width value | [ "min"; "h"; value ] -> parse_sized Min_height value | [ "max"; "w"; value ] -> parse_sized Max_width value | [ "max"; "w"; "screen"; size ] -> parse_max_w_screen size | [ "max"; "h"; value ] -> parse_sized Max_height value | [ "size"; value ] -> parse_sized Width_and_height value | [ "inline"; value ] -> parse_sized Inline_size value | [ "min"; "inline"; value ] -> parse_sized Min_inline_size value | [ "max"; "inline"; value ] -> parse_sized Max_inline_size value | [ "block"; value ] -> parse_sized Block_size value | [ "min"; "block"; value ] -> parse_sized Min_block_size value | [ "max"; "block"; value ] -> parse_sized Max_block_size value | [ "aspect"; "auto" ] -> Ok Aspect_auto | [ "aspect"; "square" ] -> Ok Aspect_square | [ "aspect"; "video" ] -> Ok Aspect_video (* Tailwind reads nothing inside an [aspect-[...]] bracket: it is one token-stream site, so [aspect-[foo]] and [aspect-[1.23/4.56]] both reach [aspect-ratio] as written. Only text that could end the declaration or swallow the rest of the rule names no utility. *) | [ "aspect"; value ] when Parse.is_bracket_value value -> ( let inner = Parse.bracket_inner value in match Parse.arbitrary_declaration_value inner with | Some declared -> Ok (Aspect_arbitrary (inner, declared)) | None -> err_not_utility) | [ "aspect"; value ] when is_theme_aspect theme value -> Ok (Aspect_theme value) | [ "aspect"; value ] -> parse_aspect_ratio value | _ -> err_not_utility let suborder = function | Sized (prop, v) -> let f = family prop in f.base + value_order f v (* Every aspect candidate writes the same property. One slot lets the natural class-name tie-break order numeric ratios, then brackets and keywords without large numerators spilling past the tail. *) | Aspect_ratio _ | Aspect_arbitrary _ | Aspect_auto | Aspect_square | Aspect_video | Aspect_theme _ -> aspect_base (** Priority 6: sizing utilities (w-*, h-*, max-w-*, ...) come before flex-1/flex-col in Tailwind's order. The logical ones are registered after every other utility, so they sort last instead of beside w-* and h-*. *) let priority u = if suborder u >= logical then logical_priority else 6 let to_class = function | Sized (prop, v) -> (family prop).prefix ^ "-" ^ class_suffix v | Aspect_auto -> "aspect-auto" | Aspect_square -> "aspect-square" | Aspect_video -> "aspect-video" | Aspect_theme name -> "aspect-" ^ name | Aspect_ratio (w, h) -> "aspect-" ^ class_float w ^ "/" ^ class_float h | Aspect_arbitrary (raw, _) -> "aspect-[" ^ raw ^ "]" let examples = [ keyword Height "auto"; keyword Max_height "none"; keyword Min_height "auto"; keyword Width "auto"; keyword Max_width "none"; keyword Min_width "auto"; Aspect_auto; keyword Block_size "auto"; keyword Max_block_size "none"; keyword Min_block_size "auto"; keyword Inline_size "auto"; keyword Max_inline_size "none"; keyword Min_inline_size "auto"; ] end open Handler module Utility_factory = Utility.Make (Handler) (** Register the sizing utility handlers *) (** Public API returning Utility.t *) let utility = Utility_factory.v let () = () (* Ensure utility is defined before usage below *) (* [n] is in rem units; the spacing scale steps by 0.25rem. *) let spacing prop n = Sized (prop, Spacing n) let fraction prop f = Sized (prop, Fraction f) let scaled prop n = utility (spacing prop (float_of_int n *. 0.25)) (* The half-step sibling of [scaled]: same spacing-scale value, a fractional class number (e.g. [0.5]) instead of an integer one. *) let scaled' prop n = utility (spacing prop (n *. 0.25)) (* Top-level wrappers returning Utility.t, following the Utility.Handler pattern *) (* Width *) let w n = scaled Width n let w' n = scaled' Width n let w_auto = utility (keyword Width "auto") let w_full = utility (keyword Width "full") let w_screen = utility (keyword Width "screen") let w_min = utility (keyword Width "min") let w_max = utility (keyword Width "max") let w_fit = utility (keyword Width "fit") let w_1_2 = utility (fraction Width "1/2") let w_1_3 = utility (fraction Width "1/3") let w_2_3 = utility (fraction Width "2/3") let w_1_4 = utility (fraction Width "1/4") let w_3_4 = utility (fraction Width "3/4") let w_1_5 = utility (fraction Width "1/5") let w_2_5 = utility (fraction Width "2/5") let w_3_5 = utility (fraction Width "3/5") let w_4_5 = utility (fraction Width "4/5") (* Height *) let h n = scaled Height n let h' n = scaled' Height n let h_auto = utility (keyword Height "auto") let h_full = utility (keyword Height "full") let h_screen = utility (keyword Height "screen") let h_min = utility (keyword Height "min") let h_max = utility (keyword Height "max") let h_fit = utility (keyword Height "fit") let h_1_2 = utility (fraction Height "1/2") let h_1_3 = utility (fraction Height "1/3") let h_2_3 = utility (fraction Height "2/3") let h_1_4 = utility (fraction Height "1/4") let h_3_4 = utility (fraction Height "3/4") let h_1_5 = utility (fraction Height "1/5") let h_2_5 = utility (fraction Height "2/5") let h_3_5 = utility (fraction Height "3/5") let h_4_5 = utility (fraction Height "4/5") (* Min width *) let min_w n = scaled Min_width n let min_w' n = scaled' Min_width n let min_w_0 = utility (keyword Min_width "0") let min_w_full = utility (keyword Min_width "full") let min_w_min = utility (keyword Min_width "min") let min_w_max = utility (keyword Min_width "max") let min_w_fit = utility (keyword Min_width "fit") (* Max width *) let max_w n = scaled Max_width n let max_w' n = scaled' Max_width n let max_w_none = utility (keyword Max_width "none") let max_w_xs = utility (keyword Max_width "xs") let max_w_sm = utility (keyword Max_width "sm") let max_w_md = utility (keyword Max_width "md") let max_w_lg = utility (keyword Max_width "lg") let max_w_xl = utility (keyword Max_width "xl") let max_w_2xl = utility (keyword Max_width "2xl") let max_w_3xl = utility (keyword Max_width "3xl") let max_w_4xl = utility (keyword Max_width "4xl") let max_w_5xl = utility (keyword Max_width "5xl") let max_w_6xl = utility (keyword Max_width "6xl") let max_w_7xl = utility (keyword Max_width "7xl") let max_w_full = utility (keyword Max_width "full") let max_w_min = utility (keyword Max_width "min") let max_w_max = utility (keyword Max_width "max") let max_w_fit = utility (keyword Max_width "fit") let max_w_prose = utility (keyword Max_width "prose") let max_w_screen_sm = utility (keyword Max_width "screen-sm") let max_w_screen_md = utility (keyword Max_width "screen-md") let max_w_screen_lg = utility (keyword Max_width "screen-lg") let max_w_screen_xl = utility (keyword Max_width "screen-xl") let max_w_screen_2xl = utility (keyword Max_width "screen-2xl") (* Min height *) let min_h n = scaled Min_height n let min_h' n = scaled' Min_height n let min_h_0 = utility (keyword Min_height "0") let min_h_full = utility (keyword Min_height "full") let min_h_screen = utility (keyword Min_height "screen") let min_h_min = utility (keyword Min_height "min") let min_h_max = utility (keyword Min_height "max") let min_h_fit = utility (keyword Min_height "fit") (* Max height *) let max_h n = scaled Max_height n let max_h' n = scaled' Max_height n let max_h_none = utility (keyword Max_height "none") let max_h_full = utility (keyword Max_height "full") let max_h_screen = utility (keyword Max_height "screen") let max_h_min = utility (keyword Max_height "min") let max_h_max = utility (keyword Max_height "max") let max_h_fit = utility (keyword Max_height "fit") (* Size (width and height combined) *) let size n = scaled Width_and_height n let size' n = scaled' Width_and_height n let size_auto = utility (keyword Width_and_height "auto") let size_full = utility (keyword Width_and_height "full") let size_min = utility (keyword Width_and_height "min") let size_max = utility (keyword Width_and_height "max") let size_fit = utility (keyword Width_and_height "fit") (* Aspect ratio *) let aspect_auto = utility Aspect_auto let aspect_square = utility Aspect_square let aspect_video = utility Aspect_video let aspect_ratio w h = utility (Aspect_ratio (float_of_int w, float_of_int h)) (* Order exposure for this module *) let order (u : Utility.base) = if String.equal (Utility.name_of_base u) Handler.name then Some (Utility.order u) else None (* Export container theme variables for use by other modules (e.g., Columns) *) let container_binding = Handler.container_binding let container_3xs = Handler.container_3xs let container_2xs = Handler.container_2xs let container_xs = Handler.container_xs let container_sm = Handler.container_sm let container_md = Handler.container_md let container_lg = Handler.container_lg let container_xl = Handler.container_xl let container_2xl = Handler.container_2xl let container_3xl = Handler.container_3xl let container_4xl = Handler.container_4xl let container_5xl = Handler.container_5xl let container_6xl = Handler.container_6xl let container_7xl = Handler.container_7xl