12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508(** CSS rule cascade sort order
Provides the [indexed_rule] type and [compare_indexed_rules], the comparison
function used to sort assembled CSS rules into Tailwind v4 cascade order. *)moduleCss=Cascade.Css(* ======================================================================== *)(* Types *)(* ======================================================================== *)(** Classification of CSS selectors for ordering purposes *)typeselector_kind=|Simple(** Plain class selector like .foo *)|Pseudo_element(** Selector with pseudo-element like .before:absolute::before *)|Complexof{has_focus:bool;has_focus_within:bool;has_focus_visible:bool;has_group:bool;(** group-* without :has() like group-focus *)has_peer:bool;(** peer-* without :has() like peer-checked *)has_group_has:bool;(** group-* with :has() like group-has-[:checked] *)has_peer_has:bool;(** peer-* with :has() like peer-has-[:checked] *)has_standalone_has:bool;has_aria:bool;}(** Selector with combinators, pseudo-classes, etc. *)(* One component of a rule's variant sort key: the slot the token sorts in, plus
what separates two tokens that share it. A breakpoint token carries the width
it names, so [sm] and [md] no longer collapse onto one key; a group-/ peer-
token carries the state it wraps, so [group-focus] and [group-has] keep their
order; data and arbitrary tokens carry the value that distinguishes groups
inside their slot. *)(* Tailwind orders container variants exactly like breakpoints, and every
container condition it emits is a width range wrapped in an optional
container name and an optional negation. Project one onto the media query it
is equivalent to, so the breakpoint ordering applies unchanged: [@max-sm] is
a negated lower bound, which is the [not all and (...)] shape media already
classifies as an upper bound. The name plays no part -- Tailwind interleaves
a named container with the unnamed ones at its width. *)letreccontainer_media_projection(c:Css.Container.t)=matchcwith|Css.Container.Named(_,inner)->container_media_projectioninner|Css.Container.Feature_queryq->Someq|Css.Container.Notinner->(matchcontainer_media_projectioninnerwith|Some(Css.Media.Condcond)->Some(Css.Media.Type{prefix=SomeCss.Media.Not;type_=Css.Media.All;trailing=Somecond;})|_->None)(* The parser keeps a bare width bound in its compact form, which is the
[(min-width: V)] feature spelled short. *)|Css.Container.Min_width_remrem->Some(Css.Media.feature"min-width"(Css.Media.Length(Css.Remrem)))|Css.Container.Min_width_pxpx->Some(Css.Media.feature"min-width"(Css.Media.Length(Css.Px(float_of_intpx))))|Css.Container.And_|Css.Container.Or_|Css.Container.Style_|Css.Container.Scroll_state_->None(* What Tailwind sorts a container variant on. It reads the value off the class
rather than the width that value resolves to, and keys it by the unit -- or,
when the value is a call, by the name before the parenthesis. A size off the
[--container] scale is resolved through the theme before the key is taken,
and that scale is rem throughout. *)typecontainer_value={name:string;(* The value's unit, or the name of the call it is. *)call:bool;(* The value is a function call. *)text:string;(* The value as the class spells it. *)upper:bool;(* A [@max-*] bound, which no lower bound ties with. *)width:Css.Media.keyoption;(* The bound as a breakpoint key, so two bounds of one kind order by the
width they resolve to. *)}(* Tailwind strips every run of digits and dots to reach the unit, so a sign
stays behind with it. *)letunit_of_container_valuetext=letbuf=Buffer.create(String.lengthtext)inString.iter(func->ifnot((c>='0'&&c<='9')||c='.')thenBuffer.add_charbufc)text;Buffer.contentsbuf(* Read the container value out of one modifier token: [@min-[64rem]] and
[@[theme(--breakpoint-lg)]] carry it in the bracket, [@lg] and [@min-lg] name
a size on the [--container] scale. A [/name] tail aims the query at a named
container and says nothing about the width. *)letcontainer_value_of_tokentoken=letn=String.lengthtokeninifn<2||token.[0]<>'@'thenNoneelseletwidth=Option.bind(Modifiers.container_query_of_tokentoken)(funq->Option.mapCss.Media.sort_key(container_media_projection(Containers.container_query_to_conditionq)))inletbody=String.subtoken1(n-1)inletupper=String.starts_with~prefix:"max-"bodyinletbody=ifupper||String.starts_with~prefix:"min-"bodythenString.subbody4(String.lengthbody-4)elsebodyinifString.lengthbody>1&&body.[0]='['thenmatchString.rindex_optbody']'with|Someiwheni>1->(lettext=String.subbody1(i-1)inmatchString.index_opttext'('with|Somej->Some{name=String.subtext0j;call=true;text;upper;width}|None->Some{name=unit_of_container_valuetext;call=false;text;upper;width;})|_->NoneelseSome{name="rem";call=false;text=body;upper;width}letcontainer_value_of_prefixprefix=List.find_mapcontainer_value_of_token(Parse.split_on_colonprefix)(* Tailwind registers the [@max-*] variants as one group ahead of the group
holding [@*] and [@min-*], so every upper bound precedes every lower bound
whatever value either names, and the value orders bounds of one kind only. A
call keys on its name, which the resolved length cannot carry; every other
pair orders by the width. Two spellings of one width are left to the
caller. *)letcompare_container_boundsv1v2=ifv1.upper<>v2.upperthenSome(Bool.comparev2.upperv1.upper)elseifv1.call||v2.callthenletc=String.comparev1.namev2.nameinSome(ifc<>0thencelseString.comparev1.textv2.text)elseifv1.name=v2.name&&v1.text=v2.textthenNoneelsematch(v1.width,v2.width)with|Somek1,Somek2->Some(Css.Media.compare_keysk1k2)|_->Nonetypevariant_component={slot:int;breakpoint:Css.Media.keyoption;reverse_breakpoint:bool;wrapped:intlist;value_key:stringoption;container:container_valueoption;}(** Relationship between two rules being compared *)typerule_relationship=|Same_utilityofstring(** Both rules from same base utility *)|Different_utilities(** Rules from different utilities *)typeindexed_rule={index:int;rule_type:[`Regular|`MediaofCss.Media.t|`ContainerofCss.Container.t|`Starting|`SupportsofCss.Supports.t];selector:Css.Selector.t;selector_str:string;selector_kind:selector_kind;has_modifier_colon:bool;props:Css.declarationlist;declared:bool;(* The rule came in as finished CSS from a project's own [@utility]. *)declaration_count:int;(* How many declarations the rule emits, those of its nested rules
included. Tailwind breaks a tie between two rules that write the same
property by this, widest first, so the narrower one wins the cascade.
Read only when one side is [declared]: the built-ins of a family are
already separated by their suborders, and tw writes vendor-prefixed
spellings Tailwind leaves to its optimizer, so counting them against
each other would move rules the corpus pins. *)order:int*int;nested:Css.statementlist;base_class:stringoption;merge_key:stringoption;variant_order:int;variant_key:string*int*int;(* Precomputed (variant prefix, effective inner order, collapsed data
depth) - see [variant_sort_key]. Read by [compare_variant_ordered]. *)variant_orders:variant_componentlist;(* The rule's variant order keys sorted descending - see
[variant_order_list]. Compared lexicographically by
[compare_variant_ordered] so a stacked variant sorts into the group of
its highest-order component, after that group's base rules, and two
stacks with the same variant multiset (e.g. group:hover and
hover:group) get identical keys. *)base_class_key:string;(* The rule's base class ("" when it has none), read by
[compare_by_base_class] as the lexicographic sort key. *)media_key:Css.Media.keyoption;(* Precomputed sort key of the rule's own media condition (the [`Media]
case of [rule_type]); [None] otherwise. Lets media comparisons use
[Css.Media.compare_keys] instead of re-serializing the query. *)nested_media_key:Css.Media.keyoption;(* Precomputed sort key of a single nested media condition. *)responsive_media_key:Css.Media.keyoption;(* Precomputed sort key of the rule's breakpoint condition, taken from
whichever nesting level carries it - see [responsive_media_key]. *)}(** An indexed CSS rule ready for sorting. [index] preserves source order;
[order] is the [(priority, suborder)] pair from the utility definition;
[variant_order] places modifier-prefixed rules after their base
counterparts. *)(* ======================================================================== *)(* Debug *)(* ======================================================================== *)(* The comparator's tracing, driven by [TW_DEBUG_SORT]. Nothing called the
setter this used to expose, so the flag was always false and every trace
below it was unreachable in any build - the diagnostic that sort work most
wants was dead. An environment variable makes it usable without a recompile
and without an API nobody calls. *)letdebug_compare=Sys.getenv_opt"TW_DEBUG_SORT"<>Noneletdebug_compare_enabled()=debug_compare(* ======================================================================== *)(* Selector Classification *)(* ======================================================================== *)letis_simple_class_selectorsel=matchselwithCss.Selector.Class_->true|_->false(** Compare complex selector kinds. Returns ordering value for sorting. At equal
priority levels, the order is: simple/complex < pseudo-element < group <
group-has < peer < peer-has < focus-within < focus-visible < has < aria *)letcomplex_selector_order=function|Complex{has_aria=true;_}->60|Complex{has_standalone_has=true;_}->50|Complex{has_focus_visible=true;_}->40|Complex{has_focus_within=true;_}->30|Complex{has_peer_has=true;_}->21|Complex{has_peer=true;_}->20|Complex{has_group_has=true;_}->11|Complex{has_group=true;_}->10|Pseudo_element->5(* After simple/complex but before late modifiers *)|Simple->0|Complex_->0(** Determine the relationship between two rules *)letrule_relationshipr1r2=match(r1.base_class,r2.base_class)with|Somebc1,Somebc2whenbc1=bc2->Same_utilitybc1|_->Different_utilities(** Traverse a selector tree like [Css.Selector.any] but skip [Not] children.
This prevents :not(:focus) from being classified as a focus modifier, which
would break ordering for not-* variant rules. *)letrecany_outside_notp=function|Css.Selector.Not_->false|Css.Selector.Compoundxs->List.exists(any_outside_notp)xs|Css.Selector.Combined(a,_,b)->any_outside_notpa||any_outside_notpb|Css.Selector.Relative(_,b)->any_outside_notpb|Css.Selector.Listxs->List.exists(any_outside_notp)xs|Css.Selector.Isxs|Css.Selector.Wherexs|Css.Selector.Hasxs->List.exists(any_outside_notp)xs|s->ps(** Classify a selector into Simple or Complex with focus/has analysis. For List
selectors (merged selectors like `.a, .b`), classify based on the first
element to preserve sort order. *)letclassify_selectorsel=letsel_to_classify=matchCss.Selector.as_listselwith|Some(first::_)->first|Some[]->sel|None->selinifis_simple_class_selectorsel_to_classifythenSimpleelseifCss.Selector.has_pseudo_elementsel_to_classifythenPseudo_elementelselethas_has_pseudos=any_outside_not(functionCss.Selector.Has_->true|_->false)sinlethas_aria_attrs=any_outside_not(function|Css.Selector.Attribute(_,Css.Selector.Aria_,_,_)->true|_->false)sinlethas_group=Css.Selector.has_group_markersel_to_classifyinlethas_peer=Css.Selector.has_peer_markersel_to_classifyinlethas_has=has_has_pseudosel_to_classifyinComplex{has_focus=any_outside_not(functionCss.Selector.Focus->true|_->false)sel_to_classify;has_focus_within=any_outside_not(functionCss.Selector.Focus_within->true|_->false)sel_to_classify;has_focus_visible=any_outside_not(functionCss.Selector.Focus_visible->true|_->false)sel_to_classify;has_group=has_group&¬has_has;has_peer=has_peer&¬has_has;has_group_has=has_group&&has_has;has_peer_has=has_peer&&has_has;has_standalone_has=has_has&&(nothas_group)&¬has_peer;has_aria=has_aria_attrsel_to_classify;}(** Get sort key for preference media conditions. Tailwind order: reduced-motion
(no-preference, reduce) < contrast (more, less) *)letpreference_condition_ordercond=Css.Media.preference_ordercond(** Count modifier colons in a selector's first class name. Used to determine
modifier stacking depth for hover media interleaving. *)letselector_modifier_depthsel=matchCss.Selector.first_classselwith|Somecls->String.fold_left(funaccc->ifc=':'thenacc+1elseacc)0cls|None->0(* Shares [any_outside_not]'s traversal: the hand-rolled one missed [Has] and
[Relative], so :hover inside a :has() did not count. Both want the same
thing, a :hover that is not under a :not(). *)(** Check if a selector contains :hover pseudo-class at any depth (used to
detect compound variants like group-hocus that combine hover+focus). *)letselector_has_hoversel=any_outside_not(functionCss.Selector.Hover->true|_->false)sel(* Determine sort group for rule types. Regular and Media are grouped together
to preserve utility grouping - media queries appear immediately after their
base utility rule. *)letrule_type_order=function|`Regular->0|`Media_->0(* Same as Regular to keep grouped *)|`Supports_->0(* Same as Regular to keep grouped with base rule *)|`Container_->1|`Starting->2(* Extract media sort key using Css.Media.kind and group_order. Returns (group,
subkey) where subkey is rem value for responsive conditions. *)letextract_media_sort_key=function|`Mediacond->Css.Media.group_order(Css.Media.kindcond)|_->(0,0.)(* Precompute the sort keys for a rule's own and nested media conditions, so the
comparators use [Css.Media.compare_keys] (cheap) instead of re-serializing
the query on every comparison. See [media_key]/[nested_media_key]. *)letmedia_sort_keysrule_typenested=letmedia_key=matchrule_typewith|`Mediac->Some(Css.Media.sort_keyc)|`Containerc->Option.mapCss.Media.sort_key(container_media_projectionc)|_->Noneinletnested_media_key=matchnestedwith|[n]->(matchCss.as_medianwith|Some(c,_)->Some(Css.Media.sort_keyc)|None->None)|_->Nonein(media_key,nested_media_key)(* The breakpoint a rule sorts under, wherever its variant stack puts it:
sm:hover: writes the breakpoint outside the hover query and hover:sm: writes
it inside, and Tailwind groups both under that breakpoint. *)letresponsive_media_keyrule_typenested=letof_condc=matchCss.Media.kindcwith|Css.Media.Responsive_|Css.Media.Responsive_max_->Some(Css.Media.sort_keyc)|_->Noneinmatchrule_typewith|`MediacwhenOption.is_some(of_condc)->of_condc|_->(matchnestedwith|[n]->(matchCss.as_medianwithSome(c,_)->of_condc|None->None)|_->None)(* ======================================================================== *)(* Priority Comparison *)(* ======================================================================== *)(** Compare two simple selectors by suborder, then alphabetically, then index.
Index fallback is critical for utilities like prose that emit multiple rules
with the same selector - preserves original order. *)letcompare_simple_selectorssel_str1sel_str2s1s2i1i2=letsub_cmp=Int.compares1s2inifsub_cmp<>0thensub_cmpelseletsel_cmp=String.comparesel_str1sel_str2inifsel_cmp<>0thensel_cmpelseInt.comparei1i2(** Compare two complex selectors by kind, then selector string (for aria), then
suborder, then index. Order: focus < group-has < peer-has < has < aria. For
aria selectors, sort by attribute name before property to match Tailwind. *)letcompare_complex_selectorssel_str1sel_str2kind1kind2s1s2i1i2=letk1=complex_selector_orderkind1andk2=complex_selector_orderkind2inifk1<>k2thenInt.comparek1k2elseifmatchkind1withComplex{has_aria=true;_}->true|_->falsethen(* Both are aria selectors - compare by selector string (aria attribute)
before suborder (property shade) to match Tailwind v4 behavior *)letsel_cmp=String.comparesel_str1sel_str2inifsel_cmp<>0thensel_cmpelseletsub_cmp=Int.compares1s2inifsub_cmp<>0thensub_cmpelseInt.comparei1i2else(* Other complex selectors - use suborder first *)letsub_cmp=Int.compares1s2inifsub_cmp<>0thensub_cmpelseletsel_cmp=String.comparesel_str1sel_str2inifsel_cmp<>0thensel_cmpelseInt.comparei1i2(** Compare rules by priority, then suborder, then by selector kind. Uses
type-directed dispatch based on selector classification. At the same
priority/suborder, cross-kind comparisons preserve source order (index) to
match tailwindcss output exactly. *)letcompare_by_priority_suborder_alphakind1kind2sel_str1sel_str2(p1,s1)(p2,s2)i1i2=letprio_cmp=Int.comparep1p2inifprio_cmp<>0thenprio_cmpelseletsub_cmp=Int.compares1s2inifsub_cmp<>0thensub_cmpelsematch(kind1,kind2)with|Simple,Simple->compare_simple_selectorssel_str1sel_str2s1s2i1i2|Pseudo_element,Pseudo_element->compare_simple_selectorssel_str1sel_str2s1s2i1i2|Pseudo_element,Simple->Int.comparei1i2|Pseudo_element,Complex_->(* Prose rules need pseudo-elements after complex selectors *)Int.comparei1i2|Simple,Pseudo_element->Int.comparei1i2|Simple,Complex_->Int.comparei1i2|Complex_,Pseudo_element->Int.comparei1i2|Complex_,Simple->Int.comparei1i2|Complex_,Complex_->compare_complex_selectorssel_str1sel_str2kind1kind2s1s2i1i2(* ======================================================================== *)(* Media Query Comparison *)(* ======================================================================== *)(* The two groups below are compared on something other than their key, so they
need naming. Taken from [Css.Media.group_order] rather than written out, so
they follow it if it moves. *)letresponsive_group=fst(Css.Media.group_order(Css.Media.Responsive(0,0.)))letaccessibility_preference_group=fst(Css.Media.group_orderCss.Media.Preference_accessibility)(* The rank the variant table gives a breakpoint prefix. Read back from the
table for the same reason as the two groups above. *)letresponsive_variant_order=Modifiers.variant_order_of_prefix"sm"letnegation_variant_order=Modifiers.variant_order_of_prefix"not-hover"(** Compare two media conditions within the same group *)letcompare_media_conditionsgroup1sub1sub2cond1cond2key1key2=ifgroup1=responsive_groupthenFloat.comparesub1sub2elseifgroup1=accessibility_preference_groupthenmatch(cond1,cond2)with|Somec1,Somec2->Int.compare(preference_condition_orderc1)(preference_condition_orderc2)|_->0elsematch(key1,key2)with|Somek1,Somek2->Css.Media.compare_keysk1k2|_->0(* The [order] field is a [(priority, suborder)] pair; comparing it with the
polymorphic [compare] boxes both ints on every call, and the comparator runs
once per rule pair. *)letcompare_order(p1,s1)(p2,s2)=letprio_cmp=Int.comparep1p2inifprio_cmp<>0thenprio_cmpelseInt.compares1s2(* For hover media, separate rules by modifier depth so that single-modifier
hover rules (group-hover:flex) form a separate block from stacked hover rules
(group-focus:group-hover:flex) *)letcompare_hover_depthcond1cond2sel1sel2=match(cond1,cond2)with|Somec1,Somec2whenCss.Media.kindc1=Css.Media.Hover&&Css.Media.kindc2=Css.Media.Hover->Int.compare(selector_modifier_depthsel1)(selector_modifier_depthsel2)|_->0(* Compare by nested media condition when both have nested media. This sorts
stacked min/max variants like min-sm:max-xl vs min-sm:max-lg by the inner
media condition. *)letcompare_nested_media_condnk1nk2=match(nk1,nk2)with|Somek1,Somek2->Css.Media.compare_keysk1k2|_->0letcompare_same_media_group(r1:indexed_rule)(r2:indexed_rule)cond1cond2=letdepth_cmp=compare_hover_depthcond1cond2r1.selectorr2.selectorinifdepth_cmp<>0thendepth_cmpelseletnested_media_cmp=compare_nested_media_condr1.nested_media_keyr2.nested_media_keyinifnested_media_cmp<>0thennested_media_cmpelseletsame_utility=match(r1.base_class,r2.base_class)with|Someb1,Someb2->String.equalb1b2|_->falseinifsame_utilitythenletorder_cmp=compare_orderr1.orderr2.orderiniforder_cmp<>0thenorder_cmpelseInt.comparer1.indexr2.indexelsecompare_by_priority_suborder_alphar1.selector_kindr2.selector_kindr1.selector_strr2.selector_strr1.orderr2.orderr1.indexr2.index(* The nested blocks that are variants. A nested [@media] or [@container] is
one, the [@media (hover: hover)] of [sm:hover:], and sorts after the plain
rules of its group. A nested [@supports] is not: it is the colour twin an
opacity utility carries beside its fallback, which Tailwind keeps together,
so the rule sorts where a plain rule of that utility does. *)letnested_variants(r:indexed_rule)=(* The container utility carries its breakpoint rules beside it, under a
variant as at the top level, so a class with one modifier nesting a
breakpoint is that utility's own output and not a stacked variant. *)letsingle_modifier=letprefix,_,_=r.variant_keyinmatchParse.split_on_colonprefixwith[_]->true|_->falseinletown_breakpointstmt=single_modifier&&matchCss.as_mediastmtwith|Some(c,_)->(matchCss.Media.kindcwith|Css.Media.Responsive_->true|_->false)|None->falseinList.filter(funstmt->(Option.is_some(Css.as_mediastmt)||Option.is_some(Css.as_containerstmt))&¬(own_breakpointstmt))r.nestedletcompare_media_rules(r1:indexed_rule)(r2:indexed_rule)=letsame_utility=match(r1.base_class,r2.base_class)with|Someb1,Someb2->String.equalb1b2|_->falseinifsame_utilitythen(* A progressive-enhancement pair under a media variant is emitted as two
media outputs for the same class: the fallback has declarations and the
enhancement carries a nested [@supports]. Preserve their source order,
just as the regular-vs-media comparator below does for one utility. *)Int.comparer1.indexr2.indexelseletnested_cmp=Bool.compare(nested_variantsr1<>[])(nested_variantsr2<>[])inifnested_cmp<>0thennested_cmpelseletgroup1,sub1=extract_media_sort_keyr1.rule_typeinletgroup2,sub2=extract_media_sort_keyr2.rule_typeinletkey_cmp=Int.comparegroup1group2inifkey_cmp<>0thenkey_cmpelseletcond1=matchr1.rule_typewith`Mediac->Somec|_->Noneinletcond2=matchr2.rule_typewith`Mediac->Somec|_->Noneinletcond_cmp=compare_media_conditionsgroup1sub1sub2cond1cond2r1.media_keyr2.media_keyinifcond_cmp<>0thencond_cmpelsecompare_same_media_groupr1r2cond1cond2(* ======================================================================== *)(* Regular vs Media Comparison *)(* ======================================================================== *)(* For the same base utility, preserve original order (index) to keep media
rules adjacent to their related state rules. *)letcompare_same_utility_regular_mediar1r2=Int.comparer1.indexr2.index(** Check if a selector has special modifiers that should come at the end.
Includes :has() variants (group-has, peer-has, has) and focus-within/
focus-visible variants that have modifier prefixes.
Note: Regular focus: with modifier prefix is NOT a late modifier. Only
focus-within: and focus-visible: are late modifiers.
IMPORTANT: Only selectors with modifier colons (like `.focus-within\:ring`)
are late modifiers. Native pseudo-classes like `.form-radio:focus` are not.
*)letis_late_modifiersel_kindhas_modifier_colon=matchsel_kindwith|Complex{has_group_has=true;_}->true|Complex{has_peer_has=true;_}->true|Complex{has_focus_within=true;_}->has_modifier_colon|Complex{has_focus_visible=true;_}->has_modifier_colon|Complex{has_standalone_has=true;_}->true|_->false(** Check if a selector is a state modifier rule. These include :active,
:disabled, [aria-*], and :has() selectors with modifier colons. *)letis_state_modifier_rulesel_kindhas_modifier_colonsel_str=ifnothas_modifier_colonthenfalseelsematchsel_kindwith|Complex{has_aria=true;_}->true|Complex{has_standalone_has=true;_}->true|Complex_->String.ends_with~suffix:":active"sel_str||String.ends_with~suffix:":disabled"sel_str|_->false(** Check if a selector is a focus: modifier rule (has :focus pseudo-class and
modifier colon). These rules come AFTER hover:hover media but BEFORE other
modifier-prefixed media like motion-safe:/motion-reduce:/contrast-more:. *)letis_focus_modifier_rulesel_kindhas_modifier_colon=has_modifier_colon&&matchsel_kindwithComplex{has_focus=true;_}->true|_->false(** Compare by (priority, suborder), defaulting to [regular_first] (-1). *)letcompare_by_order_regular_first(p1,s1)(p2,s2)=letprio_cmp=Int.comparep1p2inifprio_cmp<>0thenprio_cmpelseletsub_cmp=Int.compares1s2inifsub_cmp<>0thensub_cmpelse-1(** Compare Regular vs Media rules from different utilities. Uses selector
classification to determine ordering. *)lettry_hover_media_interleave(r1:indexed_rule)(r2:indexed_rule)media_type=matchmedia_typewith|SomeCss.Media.Hoverwhenis_focus_modifier_ruler1.selector_kindr1.has_modifier_colon->letd1=selector_modifier_depthr1.selectorinletd2=selector_modifier_depthr2.selectorinlethas_hov=selector_has_hoverr1.selectorinifd2>d1&¬has_hovthenSome(-1)elseSome1|_->Noneletcompare_different_utility_regular_media(r1:indexed_rule)(r2:indexed_rule)media_type=matchtry_hover_media_interleaver1r2media_typewith|Somec->c|None->(ifis_focus_modifier_ruler1.selector_kindr1.has_modifier_colonthen1elseletis_modifier_prefixed_media=r2.has_modifier_colon&&matchmedia_typewith|Some(Css.Media.Preference_appearance|Css.Media.Preference_accessibility)->true|_->falseinifis_modifier_prefixed_mediathenifis_late_modifierr1.selector_kindr1.has_modifier_colonthen1else-1elseifnotr2.has_modifier_colonthenletprio_cmp=Int.compare(fstr1.order)(fstr2.order)inifprio_cmp<>0thenprio_cmpelsematchmedia_typewith|Some(Css.Media.Responsive_|Css.Media.Hover)->-1|_->compare_by_order_regular_firstr1.orderr2.orderelsematchmedia_typewith|Some(Css.Media.Hover|Css.Media.Responsive_)->-1|_->compare_by_order_regular_firstr1.orderr2.order)(** Compare Regular vs Media rules using rule relationship dispatch. *)letcompare_regular_vs_mediar1r2=matchrule_relationshipr1r2with|Same_utility_->compare_same_utility_regular_mediar1r2|Different_utilities->letmedia_type=matchr2.rule_typewith|`Mediam->Some(Css.Media.kindm)|_->Noneincompare_different_utility_regular_mediar1r2media_type(* ======================================================================== *)(* Regular Rule Comparison *)(* ======================================================================== *)(** Compare pseudo-element vs non-pseudo-element selectors. Simple selectors
ALWAYS come before Pseudo_element selectors within the same priority group.
*)letcompare_pseudo_elementskind1kind2_sel1_sel2=match(kind1,kind2)with|Simple,Pseudo_element->Some(-1)|Pseudo_element,Simple->Some1|Pseudo_element,Pseudo_element->None|_,_->None(** Compare rules by order tuple then index. Used for same-utility regular
rules, starting style rules, and as a generic tiebreaker. *)letcompare_by_order_then_indexr1r2=letorder_cmp=compare_orderr1.orderr2.orderiniforder_cmp<>0thenorder_cmpelseInt.comparer1.indexr2.indexletcompare_same_utility_regular=compare_by_order_then_indexletcompare_base_class_optionbc1bc2=match(bc1,bc2)with|Somebc1,Somebc2->String.comparebc1bc2|Some_,None->-1|None,Some_->1|None,None->0letcompare_by_priority_indexr1r2=letp1,s1=r1.orderandp2,s2=r2.orderinletprio_cmp=Int.comparep1p2inifprio_cmp<>0thenprio_cmpelseletsub_cmp=Int.compares1s2inifsub_cmp<>0thensub_cmpelseletbc_cmp=compare_base_class_optionr1.base_classr2.base_classinifbc_cmp<>0thenbc_cmpelseletidx_cmp=Int.comparer1.indexr2.indexinifidx_cmp<>0thenidx_cmpelseString.comparer1.selector_strr2.selector_strletis_digitc=c>='0'&&c<='9'(* Natural sort comparison: treats consecutive digit sequences as integers.
E.g., "2.5" < "2.25" because 5 < 25 when compared as numbers. This matches
Tailwind v4's selector ordering for opacity modifiers like /2.5 vs /2.25. *)letnatural_extract_numbersi=letrecgojacc=ifj>=String.lengths||not(is_digits.[j])then(acc,j)elsego(j+1)((acc*10)+Char.codes.[j]-Char.code'0')ingoi0(* Skip CSS escape backslash before '#': compare \# as #. Only unescape \# —
other escapes like \/ need the backslash for correct opacity modifier
ordering. *)letnatural_skip_hash_escapesilen=ifi<len&&s.[i]='\\'&&i+1<len&&s.[i+1]='#'theni+1elseiletboundary_comparei1len1i2len2=ifi1>=len1&&i2>=len2then`Equalelseifi1>=len1then`Lesselseifi2>=len2then`Greaterelse`Continueletnatural_compares1s2=letlen1=String.lengths1andlen2=String.lengths2inletreccompare_ati1i2=matchboundary_comparei1len1i2len2with|`Equal->0|`Less->-1|`Greater->1|`Continue->leti1=natural_skip_hash_escapes1i1len1inleti2=natural_skip_hash_escapes2i2len2incompare_at_charsi1i2andcompare_at_charsi1i2=matchboundary_comparei1len1i2len2with|`Equal->0|`Less->-1|`Greater->1|`Continue->letc1=s1.[i1]andc2=s2.[i2]inifis_digitc1&&is_digitc2thenletn1,end1=natural_extract_numbers1i1inletn2,end2=natural_extract_numbers2i2inletnum_cmp=Int.comparen1n2inifnum_cmp<>0thennum_cmpelsecompare_atend1end2elseletchar_cmp=Char.comparec1c2inifchar_cmp<>0thenchar_cmpelsecompare_at(i1+1)(i2+1)incompare_at00(* Tailwind orders the values of dynamic candidates by the raw candidate
spelling. This naturally interleaves digit-led theme names with numeric
values (2, 2xl, 10), and puts the [(--var)] shorthand before both. The
handler suborders still separate the property families themselves. *)letcandidate_value_familybase_class=let_,base=Modifiers.of_stringbase_classinList.find_opt(funprefix->letn=String.lengthprefixinString.lengthbase>n&&String.starts_with~prefix:(prefix^"-")base)["pbs";"pbe";"px";"py";"ps";"pe";"pt";"pr";"pb";"pl";"p";"min-inline";"max-inline";"min-block";"max-block";"min-w";"max-w";"min-h";"max-h";"inline";"block";"size";"basis";"w";"h";]letcompare_candidate_valuesr1r2=iffstr1.order<>fstr2.orderthenNoneelsematch(candidate_value_familyr1.base_class_key,candidate_value_familyr2.base_class_key)with|Somef1,Somef2whenString.equalf1f2->Some(natural_comparer1.base_class_keyr2.base_class_key)|_->Noneletcompare_late_modifiersr1r2kind1kind2=letk1=complex_selector_orderkind1andk2=complex_selector_orderkind2inifk1<>k2thenInt.comparek1k2elsecompare_by_priority_indexr1r2(** Check if a selector kind is a focus-visible late modifier *)letis_focus_visible_late_modifierkindhas_modifier_colon=is_late_modifierkindhas_modifier_colon&&matchkindwith|Complex{has_focus_visible=true;_}->true|_->false(** Compare focus-visible and state modifier ordering. Returns [Some cmp] if at
least one rule is a focus-visible or state modifier, [None] otherwise. *)letcompare_focus_visible_stater1r2kind1kind2=letfv1=is_focus_visible_late_modifierkind1r1.has_modifier_coloninletfv2=is_focus_visible_late_modifierkind2r2.has_modifier_coloninlets1=is_state_modifier_rulekind1r1.has_modifier_colonr1.selector_strinlets2=is_state_modifier_rulekind2r2.has_modifier_colonr2.selector_striniffv1&&s2thenSome(-1)elseifs1&&fv2thenSome1elseiffv1&&(notfv2)&¬s2thenSome1elseiffv2&&(notfv1)&¬s1thenSome(-1)elseiffv1&&fv2thenSome(compare_by_priority_indexr1r2)elseifs1&¬s2thenSome1elseifs2&¬s1thenSome(-1)elseifs1&&s2thenSome(compare_by_priority_indexr1r2)elseNone(** Compare focus modifier ordering. Returns [Some cmp] if at least one rule is
a focus modifier, [None] otherwise. *)letcompare_focus_modifier_orderingr1r2kind1kind2=letf1=is_focus_modifier_rulekind1r1.has_modifier_coloninletf2=is_focus_modifier_rulekind2r2.has_modifier_coloniniff1&¬f2thenSome1elseiff2&¬f1thenSome(-1)elseiff1&&f2thenSome(compare_by_priority_indexr1r2)elseNone(* A project's [@utility] borrows the slot of the property it writes, so it
lands among the built-ins of that family and the two orders decide which
wins. Tailwind puts the rule carrying more declarations first: [select-none]
writes the prefixed spelling of [user-select] as well as the plain one, so it
comes before a declared utility writing [user-select] alone whatever that
utility is called. *)letcompare_declared_widthr1r2=ifnot(r1.declared||r2.declared)then0elseInt.comparer2.declaration_countr1.declaration_count(** Compare by priority, suborder, late modifiers, then natural selector sort.
Used as the final comparison when focus-visible/state/focus modifiers don't
apply. *)letcompare_by_prio_sub_later1r2kind1kind2=letp1,_=r1.orderandp2,_=r2.orderinletprio_cmp=Int.comparep1p2inifprio_cmp<>0thenprio_cmpelselet_,s1=r1.orderand_,s2=r2.orderinletsub_cmp=Int.compares1s2inifsub_cmp<>0thensub_cmpelseletlate1=is_late_modifierkind1r1.has_modifier_coloninletlate2=is_late_modifierkind2r2.has_modifier_coloniniflate1&¬late2then1elseiflate2&¬late1then-1elseiflate1&&late2thencompare_late_modifiersr1r2kind1kind2else(* Two utilities share a slot when they are named for the same property,
which is where a project's own [@utility] lands. The wider rule goes
first - [select-none] writes the prefixed spelling of [user-select]
as well as the plain one - and only rules of equal width fall through
to the candidate name. *)letwidth_cmp=compare_declared_widthr1r2inifwidth_cmp<>0thenwidth_cmpelsenatural_comparer1.base_class_keyr2.base_class_keyletcompare_cross_utility_regularr1r2=letp1,s1=r1.orderandp2,s2=r2.orderinletkind1=r1.selector_kindinletkind2=r2.selector_kindinifdebug_comparethen(letsel1=r1.selector_strinletsel2=r2.selector_strinletkind_str=function|Simple->"Simple"|Pseudo_element->"Pseudo_element"|Complex_->"Complex"inprerr_string(String.concat""["compare_cross_prio: ";sel1;" (";string_of_intp1;",";string_of_ints1;") vs ";sel2;" (";string_of_intp2;",";string_of_ints2;")\n";]);prerr_string(String.concat""["compare_cross_kind: ";sel1;" (";kind_strkind1;") vs ";sel2;" (";kind_strkind2;")\n";]));matchcompare_candidate_valuesr1r2with|Somecmpwhencmp<>0->cmp|Some_|None->(letsame_order=p1=p2&&s1=s2inmatchifsame_orderthencompare_pseudo_elementskind1kind2r1.selectorr2.selectorelseNonewith|Somecmp->cmp|None->(matchcompare_focus_visible_stater1r2kind1kind2with|Somecmp->cmp|None->(matchcompare_focus_modifier_orderingr1r2kind1kind2with|Somecmp->cmp|None->compare_by_prio_sub_later1r2kind1kind2)))(** Compare two Regular rules using rule relationship dispatch. *)letcompare_regular_rulesr1r2=letrel=rule_relationshipr1r2inifdebug_comparethenprerr_string(String.concat""["compare_regular: ";r1.selector_str;" vs ";r2.selector_str;" -> ";(matchrelwith|Same_utilitybc->"Same:"^bc|Different_utilities->"Different");"\n";]);matchrelwith|Same_utility_->compare_same_utility_regularr1r2|Different_utilities->compare_cross_utility_regularr1r2letcompare_starting_rules=compare_by_order_then_index(* ======================================================================== *)(* Main Rule Comparison *)(* ======================================================================== *)letsupports_suffixs=ifString.starts_with~prefix:"supports-"sthenSome(String.subs9(String.lengths-9))elseifString.starts_with~prefix:"not-supports-"sthenSome(String.subs13(String.lengths-13))elseNone(* Sort key for supports modifier variants: named before bracket. Negating a
supports variant changes its condition, not its position within this
group. *)letsupports_sort_keybc=matchOption.bindbcsupports_suffixwith|Someafter->ifString.lengthafter>0&&after.[0]='['then(1,after)else(0,after)|None->(0,"")(* A [supports-*] variant rule, whose @supports condition is the variant itself.
A colour utility's progressive-enhancement @supports carries the colour's own
base class and must not be ordered by this key. *)letis_modifier_supportsbc=matchbcwithSomes->Option.is_some(supports_suffixs)|None->false(* Compare supports modifier rules by sort key *)letcompare_supports_by_keyr1r2=letg1,k1=supports_sort_keyr1.base_classinletg2,k2=supports_sort_keyr2.base_classinletgrp_cmp=Int.compareg1g2inifgrp_cmp<>0thengrp_cmpelseletkey_cmp=natural_comparek1k2inifkey_cmp<>0thenkey_cmpelseInt.comparer1.indexr2.index(* Compare by order tuple, then selector, then index *)letcompare_by_order_then_selectorr1r2=letorder_cmp=compare_orderr1.orderr2.orderiniforder_cmp<>0thenorder_cmpelseletsel_cmp=natural_comparer1.selector_strr2.selector_strinifsel_cmp<>0thensel_cmpelseInt.comparer1.indexr2.index(* Compare nested media conditions *)letcompare_nested_mediar1r2=match(nested_variantsr1,nested_variantsr2)with|[],[]->0|[],_->-1|_,[]->1|[_],[_]->(match(r1.nested_media_key,r2.nested_media_key)with|Somek1,Somek2->Css.Media.compare_keysk1k2|_->0)|_->0(* Repeating an element variant, or stacking predicates from the same data slot,
changes the selector without adding another sort slot. Keep the selector's
tokens intact; in the sort key retain only the innermost token of each
adjacent run. Named and arbitrary data variants occupy separate slots. *)letcollapse_repeated_variant_slotsmodifiers=letis_element=function"*"|"**"->true|_->falseinletis_datamodifier=String.starts_with~prefix:"data-"modifierinletsame_collapsible_slotouterinner=(is_elementouter&&String.equalouterinner)||is_dataouter&&is_datainner&&Modifiers.variant_order_of_prefixouter=Modifiers.variant_order_of_prefixinnerinletrecloopdata_depthacc=function|outer::(inner::_asrest)whensame_collapsible_slotouterinner->letdata_depth=ifis_dataouterthendata_depth+1elsedata_depthinloopdata_depthaccrest|modifier::rest->loopdata_depth(modifier::acc)rest|[]->(List.revacc,data_depth)inloop0[]modifiers(* Extract the modifier prefix from a base_class, e.g. "hover:p-4" -> "hover".
Split with the modifier parser, not on the last ':': an arbitrary value can
hold one, and [hover:bg-[color:var(--x)]] split naively yields the prefix
[hover:bg-[color]. *)letvariant_prefix_and_data_depth=function|Somes->letmodifiers,_=Modifiers.of_stringsinletmodifiers,data_depth=collapse_repeated_variant_slotsmodifiersin(String.concat":"modifiers,data_depth)|None->("",0)(* Compute variant order for a modifier prefix, stripping group-/peer-
wrappers *)letstrip_group_peer_vop=ifString.starts_with~prefix:"group-"pthenModifiers.variant_order_of_prefix(String.subp6(String.lengthp-6))elseifString.starts_with~prefix:"peer-"pthenModifiers.variant_order_of_prefix(String.subp5(String.lengthp-5))elseModifiers.variant_order_of_prefixp(* The outermost container token of each rule, compared as its variant component
is. *)letcompare_container_valuesr1r2p1p2=match(r1.rule_type,r2.rule_type)with|`Container_,`Container_->(match(container_value_of_prefixp1,container_value_of_prefixp2)with|Somev1,Somev2whenv1.name=v2.name&&v1.text=v2.text->(* [@lg] and [@min-lg] name one width and merge into one block, so
whichever is written last wins the cascade. The length key cannot
separate them - it is the same length - and leaving them to the
comparators below decides it by an order that is not Tailwind's.
Tailwind orders the two spellings by name, so [@lg] precedes
[@min-lg] and an element carrying both renders the same. [@max-lg]
names the same width and the other side of it, which the length key
does separate, so it is not this pair. *)Some(String.comparep1p2)|Somev1,Somev2->compare_container_boundsv1v2|_->None)|_->None(* Compute the inner variant order for a compound prefix like "hover:focus" *)letinner_voprefix=matchParse.split_on_colonprefixwith|[]->0|[_]->ifString.starts_with~prefix:"group-"prefixthenModifiers.variant_order_of_prefix(String.subprefix6(String.lengthprefix-6))elseifString.starts_with~prefix:"peer-"prefixthenModifiers.variant_order_of_prefix(String.subprefix5(String.lengthprefix-5))else0|outer::_::_asparts->ifString.starts_with~prefix:"group-"outer||String.starts_with~prefix:"peer-"outerthenList.fold_left(funaccp->maxacc(strip_group_peer_vop))0parts+1elseModifiers.variant_order_of_prefix(String.concat":"(List.tlparts))(* Effective inner variant order: prefer prefix-derived, fall back to nested
media *)leteffective_ivo_ofnestedprefix=letivo=inner_voprefixinifivo>0thenivoelsematchnestedwith|[n]->(matchCss.as_medianwith|Some(cond,_)->Modifiers.variant_order_of_media_condcond|None->0)|_->0(* The variant prefix and effective inner order are pure functions of a rule's
base class and nested statements, but [compare_variant_ordered] needs them on
every comparison. Precompute them once per rule (see [add_index]) so the hot
sort comparator only reads the result. *)letvariant_sort_keybase_classnested=letprefix,data_depth=variant_prefix_and_data_depthbase_classin(prefix,effective_ivo_ofnestedprefix,data_depth)(* Two components in the same slot are separated by the breakpoint first: a rule
whose highest-order variant is a breakpoint groups under that breakpoint, so
first:sm:m-2 stays beside sm:bg-top instead of falling past md:block. A slot
with no width on one side leaves that to the tie-breakers below, as the
wrapped state and then the rest of the stack. *)letcompare_variant_componentsab=letslot_cmp=Int.comparea.slotb.slotinifslot_cmp<>0thenslot_cmpelseletbp_cmp=match(a.breakpoint,b.breakpoint)with|Somek1,Somek2->letc=Css.Media.compare_keysk1k2inifa.reverse_breakpoint&&b.reverse_breakpointthen-celsec|Some_,None|None,Some_|None,None->0inifbp_cmp<>0thenbp_cmpelseletcontainer_cmp=match(a.container,b.container)with|Somev1,Somev2->Option.value~default:0(compare_container_boundsv1v2)|_->0inifcontainer_cmp<>0thencontainer_cmpelseletwrapped_cmp=List.compareInt.comparea.wrappedb.wrappedinifwrapped_cmp<>0thenwrapped_cmpelseOption.compareString.comparea.value_keyb.value_key(* Tailwind compares arbitrary variants by the selector they denote, after
decoding bracket-space underscores. A bare compound is implicitly anchored on
the candidate, while a selector with [&], a relative selector, or an at-rule
already carries its own context. *)letarbitrary_variant_selector_keytoken=ifnot(Parse.is_bracket_valuetoken)thenNoneelseletselector=token|>Parse.bracket_inner|>Parse.decode_underscores|>String.triminifselector=""thenNoneelseletfirst=selector.[0]iniffirst<>'>'&&first<>'+'&&first<>'~'&&first<>'@'&¬(String.containsselector'&')thenSome("&:is("^selector^")")elseSomeselectorletrecvariant_value_keytoken=matcharbitrary_variant_selector_keytokenwith|Some_askey->key|NonewhenString.starts_with~prefix:"data-"token->Sometoken|None->Option.bind(Modifiers.variant_inner_tokentoken)variant_value_key(* One modifier token's sort key. The slot alone leaves every breakpoint on one
key and every group-/peer- spelling on another, so the component carries what
separates two tokens inside a slot: the width for a breakpoint, read off the
media query the rule renders as, and the wrapped state for group-/peer-, so
group-focus and group-has keep their focus-before-has order, and the
predicate spelling for data variants, so a lower-order compound component
does not push [data-focus:has-checked] past every other data predicate. *)lettoken_order_key?theme~breakpointtoken=letslot=Modifiers.variant_order_of_prefix?themetokeninletwrapped=Modifiers.variant_inner_order_path?themetokeninletreverse_breakpoint=slot=negation_variant_order&&matchModifiers.variant_inner_tokentokenwith|Someinner->Modifiers.variant_order_of_prefix?themeinner=responsive_variant_order|None->falseinletbreakpoint=ifslot=responsive_variant_order||reverse_breakpointthenbreakpointelseNoneinletvalue_key=variant_value_keytokeninletcontainer=container_value_of_tokentokenin{slot;breakpoint;reverse_breakpoint;wrapped;value_key;container}(* The variant order keys of a class's modifier stack, sorted descending.
Tailwind sorts a candidate by this list compared lexicographically ascending,
so a stacked variant sorts into the group of its highest-order component and
after that group's base rules, and two stacks with the same variant multiset
(group:hover vs hover:group) get identical keys. Falls back to the scalar
[variant_order] for selector-derived variants (before:/after:) that carry no
order-bearing prefix in the base class. *)letvariant_order_list?themebase_classvariant_orderbreakpoint=letfrom_bc=matchbase_classwith|None->[]|Somebc->letmodifiers,_=Modifiers.of_stringbcinletmodifiers,_=collapse_repeated_variant_slotsmodifiersinList.filter_map(funm->letkey=token_order_key?theme~breakpointminifkey.slot>0thenSomekeyelseNone)modifiers|>List.sort(funab->compare_variant_componentsba)inmatchfrom_bcwith|[]whenvariant_order>0->[{slot=variant_order;breakpoint=None;reverse_breakpoint=false;wrapped=[];value_key=None;container=None;};]|l->l(* Compare two descending variant-order-key lists lexicographically, ascending
on the first differing key, with a shorter (prefix) list sorting first so
base rules precede the compounds built on them. *)letreccompare_variant_order_listsl1l2=match(l1,l2)with|[],[]->0|[],_->-1|_,[]->1|a::r1,b::r2->letc=compare_variant_componentsabinifc<>0thencelsecompare_variant_order_listsr1r2(** Classify bracket content: pseudo-class brackets ([:checked]) sort before
combinator/ampersand brackets ([&>img], [+img], etc.). *)letbracket_content_keyp=matchString.index_optp'['with|Someiwheni+1<String.lengthp->letfirst_char=p.[i+1]iniffirst_char=':'then0(* pseudo-class *)else1(* combinator/ampersand/other *)|_->1(** Compare two bracket-containing variant prefixes: by bracket content type
(pseudo-class before combinator), then by raw name. *)letcompare_both_bracket_prefixesp1p2=letbk_cmp=Int.compare(bracket_content_keyp1)(bracket_content_keyp2)inifbk_cmp<>0thenbk_cmpelseString.comparep1p2(** Compare variant prefixes for bracket ordering. Named variants (has-checked)
sort before bracket variants (has-[:checked]) within the same variant group.
Element-variant permutations with identical component keys tie here so their
utilities can interleave in [compare_variant_tail]. *)letcompare_bracket_prefixesp1_prefixp2_prefix=lethas_bracketp=String.lengthp>0&&String.containsp'['inlethas_element_variantp=String.containsp'*'inletb1=has_bracketp1_prefixandb2=has_bracketp2_prefixinifb1&¬b2then1elseifb2&¬b1then-1elseifb1&&b2thencompare_both_bracket_prefixesp1_prefixp2_prefixelseifhas_element_variantp1_prefix&&has_element_variantp2_prefixthen0elseString.comparep1_prefixp2_prefix(* Compare rules when both have variant_order > 0 *)letnested_orderr=matchnested_variantsrwith|[]->0(* non-nested: middle *)|[stmt]->(match(r.rule_type,Css.as_mediastmt)with|`Mediac,Some(nested,_)whenCss.Media.kindc=Css.Media.Hover&&Css.Media.kindnested=Css.Media.Hover->2(* doubly-nested hover: after everything *)|_,Some(nested,_)whenCss.Media.kindnested=Css.Media.Hover->-1(* single hover nested: first *)|_->1(* other nested: last *))|_->1(* multiple nested: last *)(* Last resort for two rules that share a variant group, a breakpoint and a
prefix: the utility's own priority, then the selector. *)letcompare_variant_tailr1r2=matchcompare_candidate_valuesr1r2with|Somecmpwhencmp<>0->cmp|Some_|None->(letp1,s1=r1.orderandp2,s2=r2.orderinletprio_cmp=Int.comparep1p2inifprio_cmp<>0thenprio_cmpelseletsub_cmp=Int.compares1s2inifsub_cmp<>0thensub_cmpelsematch(r1.selector_kind,r2.selector_kind)with|Simple,Simple->(* Same priority/suborder simple rules (e.g. two arbitrary bg
colors) break ties by selector like the regular layer, matching
Tailwind's alphabetical order. *)natural_comparer1.selector_strr2.selector_str|_->(* Complex rules (prose's descendant selectors) keep base class +
source order so a component stays one block. Arbitrary values
in a variant, e.g. hover:from-[rgba(5,...)] vs
hover:from-[rgba(14,...)], share a prefix and differ only in
the numeric part, so order those numerically like Tailwind.
Identical base classes (prose's :where rules all key on
"prose") tie at 0 and fall back to source order, unchanged. *)letclass_cmp=natural_comparer1.base_class_keyr2.base_class_keyinifclass_cmp<>0thenclass_cmpelseInt.comparer1.indexr2.index)letcompare_variant_orderedr1r2=letsame_utility=match(r1.base_class,r2.base_class)with|Someb1,Someb2->String.equalb1b2|_->falseinifsame_utilitythenInt.comparer1.indexr2.indexelsematch(r1.rule_type,r2.rule_type)with|`Supports_,`Supports_whenr1.variant_order=r2.variant_order&&is_modifier_supportsr1.base_class&&is_modifier_supportsr2.base_class->compare_supports_by_keyr1r2|_->letlist_cmp=compare_variant_order_listsr1.variant_ordersr2.variant_ordersiniflist_cmp<>0thenlist_cmpelseletp1_prefix,_,data_depth1=r1.variant_keyinletp2_prefix,_,data_depth2=r2.variant_keyin(* The descending variant-order lists tie (same variant multiset), so
hover:sm: and sm:hover: arrive here indistinguishable. The query a
rule writes on the outside decides between them, hover before sm
and sm before md; a nested breakpoint or hover, the prefix and the
utility's own priority order what is left. *)letmedia_cmp=matchcompare_container_valuesr1r2p1_prefixp2_prefixwith|Somec->c|None->(match(r1.media_key,r2.media_key)with|Somek1,Somek2->Css.Media.compare_keysk1k2|_->0)inifmedia_cmp<>0thenmedia_cmpelseletnested_cmp=Int.compare(nested_orderr1)(nested_orderr2)inifnested_cmp<>0thennested_cmpelseletnested_media_cmp=compare_nested_mediar1r2inifnested_media_cmp<>0thennested_media_cmpelse(* Two container variants at the same width are already fully
ordered: what remains is the utility's own priority, so the
prefix must not step in and group @sm/main away from @sm. *)letprefix_cmp=match(r1.rule_type,r2.rule_type)with|`Container_,`Container_->0|_->compare_bracket_prefixesp1_prefixp2_prefixinifprefix_cmp<>0thenprefix_cmpelseletdata_depth_cmp=Int.comparedata_depth1data_depth2inifdata_depth_cmp<>0thendata_depth_cmpelsecompare_variant_tailr1r2(* Compare two Supports rules *)letcompare_supports_rulesr1r2=letm1=is_modifier_supportsr1.base_classinletm2=is_modifier_supportsr2.base_classinifm1&&m2thencompare_supports_by_keyr1r2elsecompare_by_order_then_selectorr1r2(** Compare indexed rules for sorting. Uses type-directed dispatch based on
rule_type. This is the main entry point for sorting assembled CSS rules into
Tailwind v4 cascade order. *)letcompare_indexed_rulesr1r2=(ifdebug_comparethenletrule_type_str=function|`Regular->"R"|`Media_->"M"|`Container_->"C"|`Starting->"S"|`Supports_->"U"inprerr_string(String.concat""["compare_indexed: ";r1.selector_str;" vs ";r2.selector_str;" (types: ";rule_type_strr1.rule_type;"/";rule_type_strr2.rule_type;")\n";]));ifr1.variant_order>0&&r2.variant_order>0thencompare_variant_orderedr1r2elseifr1.variant_order>0then1elseifr2.variant_order>0then-1elselettype_cmp=Int.compare(rule_type_orderr1.rule_type)(rule_type_orderr2.rule_type)iniftype_cmp<>0thentype_cmpelsematch(r1.rule_type,r2.rule_type)with|`Regular,`Regular->compare_regular_rulesr1r2|`Media_,`Media_->compare_media_rulesr1r2|`Regular,`Media_->compare_regular_vs_mediar1r2|`Media_,`Regular->-compare_regular_vs_mediar2r1|`Starting,`Starting->compare_starting_rulesr1r2|`Container_,`Container_->Int.comparer1.indexr2.index|`Supports_,`Supports_->compare_supports_rulesr1r2|`Regular,`Supports_|`Supports_,`Regular->compare_by_order_then_selectorr1r2|`Supports_,`Media_|`Media_,`Supports_->compare_by_order_then_indexr1r2|_,_->Int.comparer1.indexr2.index