1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761(** Tailwind variant helpers (hover, focus, dark, responsive, group/peer, etc.)
*)moduleCss=Cascade.Css(* We work with Utility.t instead of Style.t *)openStyle(** We still need Style.modifier type *)(** Helper to build class selectors *)letbuild_classprefixcls=Css.Selector.Class(prefix^cls)(** Helper functions for building class names *)lethovercls=build_class"hover:"clsletfocuscls=build_class"focus:"clsletactivecls=build_class"active:"clsletdisabledcls=build_class"disabled:"clsletaria_checkedcls=build_class"aria-checked:"clsletaria_expandedcls=build_class"aria-expanded:"clsletaria_selectedcls=build_class"aria-selected:"clsletaria_disabledcls=build_class"aria-disabled:"clsletdata_activecls=build_class"data-active:"clsletdata_inactivecls=build_class"data-inactive:"clsletfocus_withincls=build_class"focus-within:"clsletfocus_visiblecls=build_class"focus-visible:"clsletbeforecls=build_class"before:"clsletaftercls=build_class"after:"cls(** Base marker classes *)letgroup=Css.Selector.Class"group"letpeer=Css.Selector.Class"peer"(** Helper: compound selector with class prefix and pseudo-class *)letclass_pseudoprefixclspseudo=Css.Selector.compound[Css.Selector.Class(prefix^":"^cls);pseudo](** Parse an nth expression that may contain an "of <selector>" clause. Bracket
content uses underscores for spaces, e.g. "2n+1_of_.foo" → nth = 2n+1, of_ =
Some [Class "foo"]. *)letparse_nth_selectorexpr=letspaced=Parse.decode_underscoresexprinCss.Selector.read_nth_selector(Cascade.Cursor.of_stringspaced)(** Helper: breakpoint name for responsive modifiers *)letbreakpoint_namequalbp=letbase=matchbpwith|`Sm->"sm"|`Md->"md"|`Lg->"lg"|`Xl->"xl"|`Xl_2->"2xl"inmatchqualwith""->base|q->q^"-"^base(** Helper: arbitrary breakpoint class selector *)letarbitrary_breakpoint_classprefix(w:Style.arbitrary_px)cls=Css.Selector.Class(prefix^"["^w.text^"]:"^cls)(* One spelling for a class name, shared with [Style], which names the same
utilities through [Style.to_class]. *)letcompact_length=Style.compact_length(** Build class selector for an arbitrary length breakpoint *)letarbitrary_length_classprefix(l:Style.arbitrary_length)cls=Css.Selector.Class(prefix^"["^l.text^"]:"^cls)(* Substitute the resolved value into a template's [{}] placeholder. *)letcustom_variant_applytemplatevalue=lettlen=String.lengthtemplateinletrecfindi=ifi+2>tlenthenNoneelseiftemplate.[i]='{'&&template.[i+1]='}'thenSomeielsefind(i+1)inmatchfind0with|Somei->String.subtemplate0i^value^String.subtemplate(i+2)(tlen-i-2)|None->template(* Parse [is-data], [is-data-foo], or [is-data-[potato]] against the theme's own
variants, returning the (token, resolved-selector) for a [Custom_variant]. *)lettry_custom_variant(theme:Scheme.t)s=letresolvename(cv:Scheme.custom_variant)=letmakev=Custom_variant(s,custom_variant_applycv.templatev)inifs=namethenOption.mapmake(List.assoc_opt""cv.values)elseletprefix=name^"-"inifString.starts_with~prefixs&&String.lengths>String.lengthprefixthenletrest=String.subs(String.lengthprefix)(String.lengths-String.lengthprefix)inletvalue=ifString.lengthrest>=2&&rest.[0]='['&&rest.[String.lengthrest-1]=']'thenSome(String.subrest1(String.lengthrest-2))elseList.assoc_optrestcv.valuesinOption.mapmakevalueelseNoneinList.find_map(fun(name,cv)->resolvenamecv)theme.custom_variants(* Negate a container condition, cancelling double-negation and pushing through
a named container: [not (not C)] = [C]; [name (C)] -> [name (not C)]. *)letrecnegate_container(c:Css.Container.t):Css.Container.t=matchcwith|Css.Container.Notx->x|Css.Container.Named(name,x)->Css.Container.Named(name,negate_containerx)|x->Css.Container.Notx(* Parse [has-a] (declared) or [not-has-a] (negated) against the theme's
container variants, yielding a [Container_style] modifier. *)lettry_container_variant(theme:Scheme.t)s=letlookupname=List.assoc_optnametheme.container_variantsinmatchlookupswith|Somecond->Some(Container_style(s,cond))|None->ifString.starts_with~prefix:"not-"s&&String.lengths>4thenletinner=String.subs4(String.lengths-4)inOption.map(funcond->Container_style(s,negate_containercond))(lookupinner)elseNone(** Helper: direction selector (ltr/rtl) *)letdir_selectordircls=letopenCss.Selectorinletdir_sel=Dirdirinletattr_sel=attribute"dir"(Exactdir)inletdesc_sel=combineattr_selDescendantuniversalincompound[Class(dir^":"^cls);where[dir_sel;attr_sel;desc_sel]](** Helper: inert pseudo-selector *)letinert_pseudo()=letopenCss.Selectorinletinert_attr=attribute"inert"Presenceinletinert_desc=combineinert_attrDescendantuniversalinis_[inert_attr;inert_desc](** Helper: open pseudo-selector *)letopen_pseudo()=letopenCss.Selectorinis_[attribute"open"Presence;Popover_open;Open]letnest_selector~parenttemplate=letopenCss.Selectorinletsel=Css.Selector.read(Cascade.Cursor.of_stringtemplate)inifCascade.Nest.containsselthenCascade.Nest.substitute~parentselelse(* No anchor: the template compounds onto the parent. A type selector cannot
follow a class in a compound, so it goes in an [:is()]; anything else
([.line], [[data-x]], [:hover]) attaches directly. *)letinner=matchselwith|Element_|Compound(Element_::_)->is_[sel]|_->selincompound[parent;inner](** Parse arbitrary bracket content into a selector tree. In bracket content:
[_] = space, [&] = anchor (group/peer). E.g. "&_p" with group anchor ->
":where(.group) p" E.g. "&:hover" with group anchor ->
":where(.group):hover" *)letparse_arbitrary_selector_contentcontentanchor=letopenCss.Selectorinlets=Parse.decode_underscorescontentincombine(nest_selector~parent:(where[anchor])s)Descendantuniversal(** Build an anchor-based variant selector: :where(.anchor):pseudo combinator *)letanchor_pseudo_selector~anchor~combinatorclsprefixpseudo=letopenCss.Selectorinletrel=combine(compound[where[anchor];pseudo])combinatoruniversalincompound[Class(prefix^":"^cls);is_[rel]](** Build a hocus (hover + focus) variant selector for an anchor *)letanchor_hocus_selector~anchor~combinatorclslabel=letopenCss.Selectorinletrel=combine(is_[compound[where[anchor];Hover];compound[where[anchor];Focus];])combinatoruniversalincompound[Class(label^":"^cls);is_[rel]](** Build an arbitrary bracket variant selector for an anchor *)letanchor_arbitrary_selector~anchor~combinatorclssellabel=letopenCss.Selectorinletprefix=label^"-["^sel^"]"inletrel=parse_arbitrary_selector_contentselanchorinletrel=matchcombinatorwith|Css.Selector.Subsequent_sibling->((* For peer, replace outermost Descendant with Subsequent_sibling (~) *)matchrelwith|Combined(left,Descendant,(Universal_asright))->Combined(left,Subsequent_sibling,right)|other->other)|_->relincompound[Class(prefix^":"^cls);is_[rel]](** Group variant selector — :where(.group):pseudo descendant *)letgroup_selectorclsmodifier=letgp=anchor_pseudo_selector~anchor:group~combinator:Descendantclsinmatchmodifierwith|Group_hover->gp"group-hover"Hover|Group_focus->gp"group-focus"Focus|Group_first->gp"group-first"First_child|Group_last->gp"group-last"Last_child|Group_odd->gp"group-odd"(Nth_child(Odd,None))|Group_even->gp"group-even"(Nth_child(Even,None))|Group_only->gp"group-only"Only_child|Group_first_of_type->gp"group-first-of-type"First_of_type|Group_last_of_type->gp"group-last-of-type"Last_of_type|Group_only_of_type->gp"group-only-of-type"Only_of_type|Group_active->gp"group-active"Active|Group_visited->gp"group-visited"Visited|Group_disabled->gp"group-disabled"Disabled|Group_checked->gp"group-checked"Checked|Group_empty->gp"group-empty"Empty|Group_required->gp"group-required"Required|Group_valid->gp"group-valid"Valid|Group_invalid->gp"group-invalid"Invalid|Group_indeterminate->gp"group-indeterminate"Indeterminate|Group_default->gp"group-default"Default|Group_open->gp"group-open"(open_pseudo())|Group_target->gp"group-target"Target|Group_optional->gp"group-optional"Optional|Group_read_only->gp"group-read-only"Read_only|Group_read_write->gp"group-read-write"Read_write|Group_inert->gp"group-inert"(inert_pseudo())|Group_user_valid->gp"group-user-valid"User_valid|Group_user_invalid->gp"group-user-invalid"User_invalid|Group_placeholder_shown->gp"group-placeholder-shown"Placeholder_shown|Group_autofill->gp"group-autofill"Autofill|Group_in_range->gp"group-in-range"In_range|Group_out_of_range->gp"group-out-of-range"Out_of_range|Group_focus_within->gp"group-focus-within"Focus_within|Group_focus_visible->gp"group-focus-visible"Focus_visible|Group_enabled->gp"group-enabled"Enabled|Group_hocus->anchor_hocus_selector~anchor:group~combinator:Descendantcls"group-hocus"|Group_arbitrarysel->anchor_arbitrary_selector~anchor:group~combinator:Descendantclssel"group"|_->Css.Selector.Classcls(** Peer variant selector — :where(.peer):pseudo ~ *)letpeer_selectorclsmodifier=letpp=anchor_pseudo_selector~anchor:peer~combinator:Subsequent_siblingclsinmatchmodifierwith|Peer_hover->pp"peer-hover"Hover|Peer_focus->pp"peer-focus"Focus|Peer_checked->pp"peer-checked"Checked|Peer_first->pp"peer-first"First_child|Peer_last->pp"peer-last"Last_child|Peer_odd->pp"peer-odd"(Nth_child(Odd,None))|Peer_even->pp"peer-even"(Nth_child(Even,None))|Peer_only->pp"peer-only"Only_child|Peer_first_of_type->pp"peer-first-of-type"First_of_type|Peer_last_of_type->pp"peer-last-of-type"Last_of_type|Peer_only_of_type->pp"peer-only-of-type"Only_of_type|Peer_active->pp"peer-active"Active|Peer_visited->pp"peer-visited"Visited|Peer_disabled->pp"peer-disabled"Disabled|Peer_empty->pp"peer-empty"Empty|Peer_required->pp"peer-required"Required|Peer_valid->pp"peer-valid"Valid|Peer_invalid->pp"peer-invalid"Invalid|Peer_indeterminate->pp"peer-indeterminate"Indeterminate|Peer_default->pp"peer-default"Default|Peer_open->pp"peer-open"(open_pseudo())|Peer_target->pp"peer-target"Target|Peer_optional->pp"peer-optional"Optional|Peer_read_only->pp"peer-read-only"Read_only|Peer_read_write->pp"peer-read-write"Read_write|Peer_inert->pp"peer-inert"(inert_pseudo())|Peer_user_valid->pp"peer-user-valid"User_valid|Peer_user_invalid->pp"peer-user-invalid"User_invalid|Peer_placeholder_shown->pp"peer-placeholder-shown"Placeholder_shown|Peer_autofill->pp"peer-autofill"Autofill|Peer_in_range->pp"peer-in-range"In_range|Peer_out_of_range->pp"peer-out-of-range"Out_of_range|Peer_focus_within->pp"peer-focus-within"Focus_within|Peer_focus_visible->pp"peer-focus-visible"Focus_visible|Peer_enabled->pp"peer-enabled"Enabled|Peer_hocus->anchor_hocus_selector~anchor:peer~combinator:Subsequent_siblingcls"peer-hocus"|Peer_arbitrarysel->anchor_arbitrary_selector~anchor:peer~combinator:Subsequent_siblingclssel"peer"|_->Css.Selector.Classcls(** Form state modifier selector dispatch *)letform_state_selectorclsmodifier=letcp=class_pseudoinmatchmodifierwith|Checked->cp"checked"clsCss.Selector.Checked|Indeterminate->cp"indeterminate"clsCss.Selector.Indeterminate|Default->cp"default"clsCss.Selector.Default|Required->cp"required"clsCss.Selector.Required|Valid->cp"valid"clsCss.Selector.Valid|Invalid->cp"invalid"clsCss.Selector.Invalid|In_range->cp"in-range"clsCss.Selector.In_range|Out_of_range->cp"out-of-range"clsCss.Selector.Out_of_range|Placeholder_shown->cp"placeholder-shown"clsCss.Selector.Placeholder_shown|Autofill->cp"autofill"clsCss.Selector.Autofill|Read_only->cp"read-only"clsCss.Selector.Read_only|Read_write->cp"read-write"clsCss.Selector.Read_write|Optional->cp"optional"clsCss.Selector.Optional|Open->Css.Selector.compound[Css.Selector.Class("open:"^cls);Css.Selector.is_[Css.Selector.attribute"open"Presence;Popover_open;Open];]|Enabled->cp"enabled"clsCss.Selector.Enabled|Target->cp"target"clsCss.Selector.Target|Visited->cp"visited"clsCss.Selector.Visited|Inert->Css.Selector.compound[Css.Selector.Class("inert:"^cls);inert_pseudo()]|User_valid->cp"user-valid"clsCss.Selector.User_valid|User_invalid->cp"user-invalid"clsCss.Selector.User_invalid|_->group_selectorclsmodifier(** Media and responsive modifiers that prefix the class name *)letmedia_prefix_selectorclsmodifier=matchmodifierwith|Dark->Css.Selector.Class("dark:"^cls)|Motion_safe->Css.Selector.Class("motion-safe:"^cls)|Motion_reduce->Css.Selector.Class("motion-reduce:"^cls)|Contrast_more->Css.Selector.Class("contrast-more:"^cls)|Contrast_less->Css.Selector.Class("contrast-less:"^cls)|Print->Css.Selector.Class("print:"^cls)|Portrait->Css.Selector.Class("portrait:"^cls)|Landscape->Css.Selector.Class("landscape:"^cls)|Forced_colors->Css.Selector.Class("forced-colors:"^cls)|Inverted_colors->Css.Selector.Class("inverted-colors:"^cls)|Pointer_none->Css.Selector.Class("pointer-none:"^cls)|Pointer_coarse->Css.Selector.Class("pointer-coarse:"^cls)|Pointer_fine->Css.Selector.Class("pointer-fine:"^cls)|Any_pointer_none->Css.Selector.Class("any-pointer-none:"^cls)|Any_pointer_coarse->Css.Selector.Class("any-pointer-coarse:"^cls)|Any_pointer_fine->Css.Selector.Class("any-pointer-fine:"^cls)|Noscript->Css.Selector.Class("noscript:"^cls)|Responsivebp->Css.Selector.Class(breakpoint_name""bp^":"^cls)|Min_responsivebp->Css.Selector.Class(breakpoint_name"min"bp^":"^cls)|Max_responsivebp->Css.Selector.Class(breakpoint_name"max"bp^":"^cls)|Min_arbitrarypx->arbitrary_breakpoint_class"min-"pxcls|Max_arbitrarypx->arbitrary_breakpoint_class"max-"pxcls|Min_arbitrary_lengthl->arbitrary_length_class"min-"lcls|Max_arbitrary_lengthl->arbitrary_length_class"max-"lcls|Custom_responsivename->Css.Selector.Class(name^":"^cls)|Min_customname->Css.Selector.Class("min-"^name^":"^cls)|Max_customname->Css.Selector.Class("max-"^name^":"^cls)|Peer_hover|Peer_focus|Peer_checked|Peer_first|Peer_last|Peer_odd|Peer_even|Peer_only|Peer_first_of_type|Peer_last_of_type|Peer_only_of_type|Peer_active|Peer_visited|Peer_disabled|Peer_empty|Peer_required|Peer_valid|Peer_invalid|Peer_indeterminate|Peer_default|Peer_open|Peer_target|Peer_optional|Peer_read_only|Peer_read_write|Peer_inert|Peer_user_valid|Peer_user_invalid|Peer_placeholder_shown|Peer_autofill|Peer_in_range|Peer_out_of_range|Peer_focus_within|Peer_focus_visible|Peer_enabled|Peer_hocus|Peer_arbitrary_->peer_selectorclsmodifier|_->form_state_selectorclsmodifier(** Structural pseudo-class modifiers *)letstructural_selectorclsmodifier=letcp=class_pseudoinmatchmodifierwith|First->cp"first"clsCss.Selector.First_child|Last->cp"last"clsCss.Selector.Last_child|Only->cp"only"clsCss.Selector.Only_child|Odd->cp"odd"clsCss.Selector.(Nth_child(Odd,None))|Even->cp"even"clsCss.Selector.(Nth_child(Even,None))|First_of_type->cp"first-of-type"clsCss.Selector.First_of_type|Last_of_type->cp"last-of-type"clsCss.Selector.Last_of_type|Only_of_type->cp"only-of-type"clsCss.Selector.Only_of_type|Nthn->letnth,of_=parse_nth_selectorn.Style.exprinletprefix=Style.pp_nth"nth"ninCss.Selector.compound[Css.Selector.Class(prefix^":"^cls);Nth_child(nth,of_)]|Nth_lastn->letnth,of_=parse_nth_selectorn.Style.exprinletprefix=Style.pp_nth"nth-last"ninCss.Selector.compound[Css.Selector.Class(prefix^":"^cls);Nth_last_child(nth,of_)]|Nth_of_typen->letnth,of_=parse_nth_selectorn.Style.exprinletprefix=Style.pp_nth"nth-of-type"ninCss.Selector.compound[Css.Selector.Class(prefix^":"^cls);Nth_of_type(nth,of_)]|Nth_last_of_typen->letnth,of_=parse_nth_selectorn.Style.exprinletprefix=Style.pp_nth"nth-last-of-type"ninCss.Selector.compound[Css.Selector.Class(prefix^":"^cls);Nth_last_of_type(nth,of_)]|Empty->cp"empty"clsCss.Selector.Empty|_->media_prefix_selectorclsmodifier(** Aria and data attribute modifier selectors *)letaria_data_selectorclsmodifier=letopenCss.Selectorinmatchmodifierwith|Aria_checked->compound[aria_checkedcls;attribute"aria-checked"(Exact"true")]|Aria_expanded->compound[aria_expandedcls;attribute"aria-expanded"(Exact"true")]|Aria_selected->compound[aria_selectedcls;attribute"aria-selected"(Exact"true")]|Aria_disabled->compound[aria_disabledcls;attribute"aria-disabled"(Exact"true")]|Data_active->compound[data_activecls;attribute"data-active"Presence]|Data_inactive->compound[data_inactivecls;attribute"data-inactive"Presence]|Data_custom(attr,"")->compound[Class("data-"^attr^":"^cls);attribute("data-"^attr)Presence;]|Data_custom(attr,value)->compound[Class("data-["^attr^"="^value^"]:"^cls);attribute("data-"^attr)(Exactvalue);]|_->structural_selectorclsmodifier(** Pseudo-element modifier selectors *)letpseudo_element_selectorclsmodifier=letcp=class_pseudoinmatchmodifierwith|Pseudo_before->Css.Selector.compound[beforecls;Css.Selector.BeforeCss.Selector.Double]|Pseudo_after->Css.Selector.compound[aftercls;Css.Selector.AfterCss.Selector.Double]|Pseudo_marker->cp"marker"clsCss.Selector.Marker|Pseudo_selection->cp"selection"clsCss.Selector.Selection|Pseudo_placeholder->cp"placeholder"clsCss.Selector.Placeholder|Pseudo_backdrop->cp"backdrop"clsCss.Selector.Backdrop|Pseudo_file->cp"file"clsCss.Selector.File_selector_button|Pseudo_first_letter->cp"first-letter"cls(Css.Selector.First_letterCss.Selector.Double)|Pseudo_first_line->cp"first-line"cls(Css.Selector.First_lineCss.Selector.Double)|Pseudo_details_content->cp"details-content"clsCss.Selector.Details_content|_->aria_data_selectorclsmodifier(** Map prose element variant names to their CSS element selectors *)letprose_element_selectorsname=letopenCss.Selectorinmatchnamewith|"headings"->[Element(None,"h1");Element(None,"h2");Element(None,"h3");Element(None,"h4");Element(None,"h5");Element(None,"h6");Element(None,"th");]|"lead"->[Attribute(None,Regular"class",Whitespace_list"lead",None)]|s->[Element(None,s)](** Build the inner prose element selector for a prose element variant. Creates
":where(ELTS):not(:where(not-prose, not-prose descendant))" which is
combined as a descendant of the outer class selector. *)letnot_prose_exclusion_selector()=letopenCss.Selectorinletnot_prose_class=attribute"class"(Whitespace_list"not-prose")inletnot_prose_descendant=combinenot_prose_classDescendantuniversalinletnot_prose_where=where[not_prose_class;not_prose_descendant]innot[not_prose_where]letprose_element_inner_selectorname=letopenCss.Selectorincompound[where(prose_element_selectorsname);not_prose_exclusion_selector()](** Generate CSS selector for a modifier and base class *)letto_selector(modifier:modifier)cls=letopenCss.Selectorinmatchmodifierwith|Hover->compound[hovercls;Hover]|Focus->compound[focuscls;Focus]|Active->compound[activecls;Active]|Disabled->compound[disabledcls;Disabled]|Focus_within->compound[focus_withincls;Focus_within]|Focus_visible->compound[focus_visiblecls;Focus_visible](* Child/descendant selectors *)|Children->letchild_sel=combine(Class("*:"^cls))Childuniversalinis_[child_sel]|Descendants->letdesc_sel=combine(Class("**:"^cls))Descendantuniversalinis_[desc_sel]|Ltr->dir_selector"ltr"cls|Rtl->dir_selector"rtl"cls(* Hocus/Device_hocus — compound :hover, :focus *)|Hocus->compound[Class("hocus:"^cls);is_[Hover;Focus]]|Device_hocus->compound[Class("device-hocus:"^cls);is_[Hover;Focus]](* Prose element variants — the class, then the elements it targets inside it.
Returning the class alone loses the descendant part wherever the selector
is built from here rather than in the routing, which is every rule that is
already a query: [prose-p:md:text-lg] styled the container instead of its
paragraphs. *)|Prose_elementname->combine(Class("prose-"^name^":"^cls))Descendant(prose_element_inner_selectorname)(* Pseudo-elements, aria/data, structural, media, peer, form state *)|_->pseudo_element_selectorclsmodifier(** Check if a modifier generates a hover rule *)letis_hover=functionHover|Group_hover|Peer_hover->true|_->false(* Modifiers whose whole effect is a media query. A group or peer negation
builds a selector, and a media query has no negated selector form, so one of
these cannot be its inner: [rule.ml] answers such a pair with no rules at
all, which reaches the author as a class that silently does nothing. The list
is the one [Rule.media_condition_of_modifier] answers for, plus [Hover] and
[Device_hocus], which gate on [\@media (hover: hover)]. *)letrenders_as_media=function|Hover|Device_hocus|Dark|Motion_safe|Motion_reduce|Contrast_more|Contrast_less|Print|Portrait|Landscape|Forced_colors|Inverted_colors|Pointer_none|Pointer_coarse|Pointer_fine|Any_pointer_none|Any_pointer_coarse|Any_pointer_fine|Noscript->true|_->false(* A variant whose rendering gates on [\@media (hover: hover)]: [hover] itself,
its group, peer, named, ancestor and [has-] forms. [has-hover] holds the
state as a name. *)letrecinvolves_hover=function|Hover|Group_hover|Peer_hover|Device_hocus|Has"hover"->true|Named_group(m,_)|Named_peer(m,_)|In_state(m,_)|Has_variantm->involves_hoverm|_->falseletwrapmstyles=matchstyleswith|[]->Utility.Group[]|_->Utility.Group(List.map(funt->Utility.Modified(m,t))styles)(* State variants *)lethover=wrapHoverletfocus=wrapFocusletactive=wrapActiveletdisabled=wrapDisabledletfocus_within=wrapFocus_withinletfocus_visible=wrapFocus_visible(* Group/peer markers *)letgroup=Interactivity.groupletpeer=Interactivity.peer(* Group/peer variants *)letgroup_hover=wrapGroup_hoverletgroup_focus=wrapGroup_focusletpeer_hover=wrapPeer_hoverletpeer_focus=wrapPeer_focusletgroup_hocus=wrapGroup_hocusletpeer_hocus=wrapPeer_hocus(* :has() helpers *)lethasselectorstyles=wrap(Hasselector)stylesletgroup_has?nameselectorstyles=wrap(Group_has(selector,name))stylesletpeer_has?nameselectorstyles=wrap(Peer_has(selector,name))styles(* Theme/motion/contrast *)letdark=wrapDarkletmotion_safe=wrapMotion_safeletmotion_reduce=wrapMotion_reduceletcontrast_more=wrapContrast_moreletcontrast_less=wrapContrast_lessletstarting=wrapStarting(* Pseudo-elements *)letbefore=wrapPseudo_beforeletafter=wrapPseudo_after(* Check if a utility already has a responsive modifier *)letrechas_responsive_modifier=function|Utility.Base_->false|Utility.Modified(Responsive_,_)->true|Utility.Modified(_,t)->has_responsive_modifiert|Utility.Groupstyles->List.existshas_responsive_modifierstyles|Utility.Important(_,t)->has_responsive_modifiert|Utility.Aliased(_,t)|Utility.Theme_bound(_,t)->has_responsive_modifiert(* Validate no nested responsive modifiers *)letvalidate_no_nested_responsivestyles=List.iter(funstyle->ifhas_responsive_modifierstylethenfailwith"Cannot apply responsive modifiers to styles that already have \
responsive modifiers")styles(* Responsive - with validation to prevent nesting *)letsmstyles=validate_no_nested_responsivestyles;wrap(Responsive`Sm)stylesletmdstyles=validate_no_nested_responsivestyles;wrap(Responsive`Md)stylesletlgstyles=validate_no_nested_responsivestyles;wrap(Responsive`Lg)stylesletxlstyles=validate_no_nested_responsivestyles;wrap(Responsive`Xl)stylesletxl2styles=validate_no_nested_responsivestyles;wrap(Responsive`Xl_2)styles(* Max responsive variants *)letmax_smstyles=validate_no_nested_responsivestyles;wrap(Max_responsive`Sm)stylesletmax_mdstyles=validate_no_nested_responsivestyles;wrap(Max_responsive`Md)stylesletmax_lgstyles=validate_no_nested_responsivestyles;wrap(Max_responsive`Lg)stylesletmax_xlstyles=validate_no_nested_responsivestyles;wrap(Max_responsive`Xl)stylesletmax_xl2styles=validate_no_nested_responsivestyles;wrap(Max_responsive`Xl_2)styles(* Arbitrary breakpoint variants *)(* A caller with a bare number has written no bracket, so spell one the way
[min-[600px]] reads. *)letarbitrary_pxpx:Style.arbitrary_px={px;text=Pp.floatpx^"px"}letmin_arbitrarypxstyles=validate_no_nested_responsivestyles;wrap(Min_arbitrary(arbitrary_pxpx))stylesletmax_arbitrarypxstyles=validate_no_nested_responsivestyles;wrap(Max_arbitrary(arbitrary_pxpx))styles(* ARIA/Peer/Data variants *)letpeer_checkedstyles=wrapPeer_checkedstylesletaria_checkedstyles=wrapAria_checkedstylesletaria_expandedstyles=wrapAria_expandedstylesletaria_selectedstyles=wrapAria_selectedstylesletaria_disabledstyles=wrapAria_disabledstylesletdata_statevaluestyle=Utility.Modified(Data_statevalue,style)letdata_variantvaluestyle=Utility.Modified(Data_variantvalue,style)letdata_customkeyvaluestyle=Utility.Modified(Data_custom(key,value),style)letdata_activestyles=wrapData_activestylesletdata_inactivestyles=wrapData_inactivestyles(* Structural pseudo-class variants *)letfirststyles=wrapFirststylesletlaststyles=wrapLaststylesletonlystyles=wrapOnlystylesletoddstyles=wrapOddstylesletevenstyles=wrapEvenstylesletfirst_of_typestyles=wrapFirst_of_typestylesletlast_of_typestyles=wrapLast_of_typestylesletonly_of_typestyles=wrapOnly_of_typestylesletnthexprstyles=wrap(Nth(Style.nth_exprexpr))stylesletnth_lastexprstyles=wrap(Nth_last(Style.nth_exprexpr))stylesletemptystyles=wrapEmptystyles(* Form state variants *)letcheckedstyles=wrapCheckedstylesletindeterminatestyles=wrapIndeterminatestylesletdefaultstyles=wrapDefaultstylesletrequiredstyles=wrapRequiredstylesletvalidstyles=wrapValidstylesletinvalidstyles=wrapInvalidstylesletin_rangestyles=wrapIn_rangestylesletout_of_rangestyles=wrapOut_of_rangestylesletplaceholder_shownstyles=wrapPlaceholder_shownstylesletautofillstyles=wrapAutofillstylesletread_onlystyles=wrapRead_onlystylesletread_writestyles=wrapRead_writestylesletoptionalstyles=wrapOptionalstylesletopen_styles=wrapOpenstylesletenabledstyles=wrapEnabledstyleslettargetstyles=wrapTargetstylesletvisitedstyles=wrapVisitedstylesletinertstyles=wrapInertstylesletuser_validstyles=wrapUser_validstylesletuser_invalidstyles=wrapUser_invalidstyles(* Group/peer structural variants *)letgroup_firststyles=wrapGroup_firststylesletgroup_laststyles=wrapGroup_laststylesletgroup_onlystyles=wrapGroup_onlystylesletgroup_oddstyles=wrapGroup_oddstylesletgroup_evenstyles=wrapGroup_evenstylesletgroup_first_of_typestyles=wrapGroup_first_of_typestylesletgroup_last_of_typestyles=wrapGroup_last_of_typestylesletgroup_only_of_typestyles=wrapGroup_only_of_typestylesletpeer_firststyles=wrapPeer_firststylesletpeer_laststyles=wrapPeer_laststylesletpeer_onlystyles=wrapPeer_onlystylesletpeer_oddstyles=wrapPeer_oddstylesletpeer_evenstyles=wrapPeer_evenstylesletpeer_first_of_typestyles=wrapPeer_first_of_typestylesletpeer_last_of_typestyles=wrapPeer_last_of_typestylesletpeer_only_of_typestyles=wrapPeer_only_of_typestyles(* More group/peer state variants *)letgroup_activestyles=wrapGroup_activestylesletgroup_visitedstyles=wrapGroup_visitedstylesletgroup_disabledstyles=wrapGroup_disabledstylesletgroup_checkedstyles=wrapGroup_checkedstylesletgroup_emptystyles=wrapGroup_emptystylesletgroup_requiredstyles=wrapGroup_requiredstylesletgroup_validstyles=wrapGroup_validstylesletgroup_invalidstyles=wrapGroup_invalidstylesletgroup_indeterminatestyles=wrapGroup_indeterminatestylesletgroup_defaultstyles=wrapGroup_defaultstylesletgroup_openstyles=wrapGroup_openstylesletgroup_targetstyles=wrapGroup_targetstylesletpeer_activestyles=wrapPeer_activestylesletpeer_visitedstyles=wrapPeer_visitedstylesletpeer_disabledstyles=wrapPeer_disabledstylesletpeer_emptystyles=wrapPeer_emptystylesletpeer_requiredstyles=wrapPeer_requiredstylesletpeer_validstyles=wrapPeer_validstylesletpeer_invalidstyles=wrapPeer_invalidstylesletpeer_indeterminatestyles=wrapPeer_indeterminatestylesletpeer_defaultstyles=wrapPeer_defaultstylesletpeer_openstyles=wrapPeer_openstylesletpeer_targetstyles=wrapPeer_targetstylesletgroup_optionalstyles=wrapGroup_optionalstylesletpeer_optionalstyles=wrapPeer_optionalstylesletgroup_read_onlystyles=wrapGroup_read_onlystylesletpeer_read_onlystyles=wrapPeer_read_onlystylesletgroup_read_writestyles=wrapGroup_read_writestylesletpeer_read_writestyles=wrapPeer_read_writestylesletgroup_inertstyles=wrapGroup_inertstylesletpeer_inertstyles=wrapPeer_inertstylesletgroup_user_validstyles=wrapGroup_user_validstylesletpeer_user_validstyles=wrapPeer_user_validstylesletgroup_user_invalidstyles=wrapGroup_user_invalidstylesletpeer_user_invalidstyles=wrapPeer_user_invalidstylesletgroup_placeholder_shownstyles=wrapGroup_placeholder_shownstylesletpeer_placeholder_shownstyles=wrapPeer_placeholder_shownstylesletgroup_autofillstyles=wrapGroup_autofillstylesletpeer_autofillstyles=wrapPeer_autofillstylesletgroup_in_rangestyles=wrapGroup_in_rangestylesletpeer_in_rangestyles=wrapPeer_in_rangestylesletgroup_out_of_rangestyles=wrapGroup_out_of_rangestylesletpeer_out_of_rangestyles=wrapPeer_out_of_rangestylesletgroup_focus_withinstyles=wrapGroup_focus_withinstylesletpeer_focus_withinstyles=wrapPeer_focus_withinstylesletgroup_focus_visiblestyles=wrapGroup_focus_visiblestylesletpeer_focus_visiblestyles=wrapPeer_focus_visiblestylesletgroup_enabledstyles=wrapGroup_enabledstylesletpeer_enabledstyles=wrapPeer_enabledstyles(* Pseudo-element variants *)letmarkerstyles=wrapPseudo_markerstylesletselectionstyles=wrapPseudo_selectionstylesletplaceholderstyles=wrapPseudo_placeholderstylesletbackdropstyles=wrapPseudo_backdropstylesletfilestyles=wrapPseudo_filestylesletfirst_letterstyles=wrapPseudo_first_letterstylesletfirst_linestyles=wrapPseudo_first_linestylesletdetails_contentstyles=wrapPseudo_details_contentstylesletchildrenstyles=wrapChildrenstylesletdescendantsstyles=wrapDescendantsstyles(* Directionality variants *)letltrstyles=wrapLtrstylesletrtlstyles=wrapRtlstyles(* Media type variants *)letprintstyles=wrapPrintstylesletportraitstyles=wrapPortraitstylesletlandscapestyles=wrapLandscapestylesletforced_colorsstyles=wrapForced_colorsstylesletsupportscondstyles=wrap(Supports_conditioncond)styles(* Prose element variants *)letprose_headingsstyles=wrap(Prose_element"headings")stylesletprose_pstyles=wrap(Prose_element"p")stylesletprose_astyles=wrap(Prose_element"a")stylesletprose_strongstyles=wrap(Prose_element"strong")stylesletprose_emstyles=wrap(Prose_element"em")stylesletprose_codestyles=wrap(Prose_element"code")stylesletprose_prestyles=wrap(Prose_element"pre")stylesletprose_olstyles=wrap(Prose_element"ol")stylesletprose_ulstyles=wrap(Prose_element"ul")stylesletprose_listyles=wrap(Prose_element"li")stylesletprose_blockquotestyles=wrap(Prose_element"blockquote")stylesletprose_h1styles=wrap(Prose_element"h1")stylesletprose_h2styles=wrap(Prose_element"h2")stylesletprose_h3styles=wrap(Prose_element"h3")stylesletprose_h4styles=wrap(Prose_element"h4")stylesletprose_h5styles=wrap(Prose_element"h5")stylesletprose_h6styles=wrap(Prose_element"h6")stylesletprose_dlstyles=wrap(Prose_element"dl")stylesletprose_dtstyles=wrap(Prose_element"dt")stylesletprose_ddstyles=wrap(Prose_element"dd")stylesletprose_tablestyles=wrap(Prose_element"table")stylesletprose_trstyles=wrap(Prose_element"tr")stylesletprose_picturestyles=wrap(Prose_element"picture")stylesletprose_imgstyles=wrap(Prose_element"img")stylesletprose_videostyles=wrap(Prose_element"video")stylesletprose_figurestyles=wrap(Prose_element"figure")stylesletprose_figcaptionstyles=wrap(Prose_element"figcaption")stylesletprose_hrstyles=wrap(Prose_element"hr")stylesletprose_thstyles=wrap(Prose_element"th")stylesletprose_tdstyles=wrap(Prose_element"td")stylesletprose_theadstyles=wrap(Prose_element"thead")stylesletprose_kbdstyles=wrap(Prose_element"kbd")stylesletprose_leadstyles=wrap(Prose_element"lead")styles(* Parse modifiers (responsive, states) from class string. Handles brackets
properly so has-[:checked]:bg-red-500 parses as modifiers=["has-[:checked]"]
and base_class="bg-red-500" *)letof_stringclass_str=letlen=String.lengthclass_strinletrecsplit_partsacccurrent_startibracket_depthparen_depth=ifi>=lenthen(* End of string - add final part *)letpart=String.subclass_strcurrent_start(len-current_start)inList.rev(part::acc)elsematchclass_str.[i]with|'['->split_partsacccurrent_start(i+1)(bracket_depth+1)paren_depth|']'->split_partsacccurrent_start(i+1)(max0(bracket_depth-1))paren_depth|'('->split_partsacccurrent_start(i+1)bracket_depth(paren_depth+1)|')'->split_partsacccurrent_start(i+1)bracket_depth(max0(paren_depth-1))|':'whenbracket_depth=0&&paren_depth=0->(* Split here - colon outside brackets and parens *)letpart=String.subclass_strcurrent_start(i-current_start)insplit_parts(part::acc)(i+1)(i+1)00|_->split_partsacccurrent_start(i+1)bracket_depthparen_depthinletparts=split_parts[]0000inmatchList.revpartswith|[]->([],class_str)|cls::modifiers->(List.revmodifiers,cls)(* Convert modifier to its string prefix *)letgroup_state_modifiers=Style.group_state_modifiersletis_has_shorthand=Style.is_has_shorthand(* One table, in [Style]: this renders the class name that [Utility.to_class]
puts in the class attribute, and the selector built here has to be the
selector that class matches. Two tables drift, and the drift is invisible
until a class fails to select its own rule. *)letpp_modifier=Style.pp_modifier(* Find matching closing bracket, handling nested brackets *)letmatching_brackets=letlen=String.lengthsinletrecloopidepth=ifi>=lenthenNoneelsematchs.[i]with|'['->loop(i+1)(depth+1)|']'whendepth=0->Somei|']'->loop(i+1)(depth-1)|'('->loop(i+1)(depth+1)|')'->loop(i+1)(depth-1)|_->loop(i+1)depthinloop00(* Extract bracketed content from a string like "prefix-[content]" *)letextract_bracket_content~prefixs=ifString.starts_with~prefixsthenletrest=String.subs(String.lengthprefix)(String.lengths-String.lengthprefix)inmatchmatching_bracketrestwith|Someiwheni=String.lengthrest-1->(* Closing bracket must be at end - reject trailing chars like /foo *)Some(String.subrest0i)|_->NoneelseNone(* Extract bracketed content allowing an optional /name suffix *)(* Extract name suffix from "rest" after prefix, e.g. "checked/name" ->
("checked", Some "name") *)letsplit_namerest=matchString.index_optrest'/'with|Somei->letbase=String.subrest0iinletname=String.subrest(i+1)(String.lengthrest-i-1)inifname<>""then(base,Somename)else(rest,None)|None->(rest,None)(* Try parsing has-shorthand modifiers like has-checked,
group-has-checked/name *)(* Known aria shorthand names that map to [aria-X=true] *)letis_aria_shorthand=function|"busy"|"checked"|"disabled"|"expanded"|"hidden"|"pressed"|"readonly"|"required"|"selected"->true|_->false(* Try parsing group-aria-*/peer-aria-* shorthand with optional /name *)lettry_aria_shorthands=ifString.starts_with~prefix:"group-aria-"s&&String.lengths>11thenletrest=String.subs11(String.lengths-11)inletbase,name=split_namerestinifis_aria_shorthandbasethenSome(Group_aria(base,name))elseNoneelseifString.starts_with~prefix:"peer-aria-"s&&String.lengths>10thenletrest=String.subs10(String.lengths-10)inletbase,name=split_namerestinifis_aria_shorthandbasethenSome(Peer_aria(base,name))elseNoneelseNonelettry_has_shorthands=ifString.starts_with~prefix:"has-"s&&String.lengths>4thenletrest=String.subs4(String.lengths-4)inifis_has_shorthandrestthenSome(Hasrest)elseNoneelseifString.starts_with~prefix:"group-has-"s&&String.lengths>10thenletrest=String.subs10(String.lengths-10)inletbase,name=split_namerestinifis_has_shorthandbasethenSome(Group_has(base,name))elseNoneelseifString.starts_with~prefix:"peer-has-"s&&String.lengths>9thenletrest=String.subs9(String.lengths-9)inletbase,name=split_namerestinifis_has_shorthandbasethenSome(Peer_has(base,name))elseNoneelseNone(* A bare arbitrary variant with no [&] anchor compounds onto the utility's own
class, so it has to be a single compound selector: [[code]] and [[.line]] are
fine, [[>img]] and [[@media_print]] are not. The bracket is read as a
selector rather than scanned for combinator characters, because [~] and [|]
spell an attribute operator as well as a combinator: [[data-size~=large]] is
a compound and [[p_~_span]] is not, and only the grammar separates them. [_]
stands for a space here, the decoding {!nest_selector} applies. *)letis_compound_selectorinner=lets=Parse.decode_underscoresinnerinletcursor=Cascade.Cursor.of_stringsinmatchCss.Selector.read_strict_selector_listcursorwith|exception(Cascade.Cursor.Parse_error_|Invalid_argument_)->false|List_|Combined_|Relative_->false|_->(* The reader stops at the first thing it cannot use, so trailing rubbish
has to be refused here: it is not part of the compound. *)Cascade.Cursor.wscursor;Cascade.Cursor.is_donecursor(* A plain identifier: what a group/peer name or a bare data attribute may be
spelled with. *)letis_plain_identstr=str<>""&&String.for_all(func->(c>='a'&&c<='z')||(c>='A'&&c<='Z')||(c>='0'&&c<='9')||c='-'||c='_')str(* An aria- or data- variant names an attribute: [aria-[modal]] matches
[[aria-modal]]. An argument that leaves the name empty gives [[aria-]], a
selector browsers parse and keep although nothing ever matches it, so it is
not a variant. An underscore stands for a space, so it is empty as well. *)letnames_attributeexpr=letdecoded=Parse.decode_underscoresexprindecoded<>""&&String.equaldecoded(String.trimdecoded)(* [group-data-dragging] is [group-data-[dragging]] with a different spelling,
so it keeps its own class name. *)lettry_bare_datasprefixmake=letplen=String.lengthprefixinifnot(String.starts_with~prefixs&&String.lengths>plen)thenNoneelseletbase,name=split_name(String.subsplen(String.lengths-plen))inifis_plain_identbase&&names_attributebasethenSome(makebasename)elseNoneletextract_bracket_content_with_name~prefixs=ifString.starts_with~prefixsthenletrest=String.subs(String.lengthprefix)(String.lengths-String.lengthprefix)inmatchmatching_bracketrestwith|Someiwheni=String.lengthrest-1->(* No suffix after bracket *)Some(String.subrest0i,None)|Someiwheni<String.lengthrest-1&&rest.[i+1]='/'->(* /name suffix after bracket *)letcontent=String.subrest0iinletname=String.subrest(i+2)(String.lengthrest-i-2)inifname<>""thenSome(content,Somename)elseNone|_->NoneelseNone(* Parse a CSS length from a string like "600px", "40rem", "100vh". Arbitrary
value syntax is decoded first, so [min-[calc(1000px+12em)]] becomes a
spec-valid [calc(1000px + 12em)] the length reader accepts. *)letparse_css_lengths:Style.arbitrary_lengthoption=Option.map(funlen:Style.arbitrary_length->{len;text=s})(Parse.arbitrary_lengths)(* Parse a pixel value from a string like "600px" or "600", keeping the
spelling: it is what the variant's own class is named after. The digits are a
CSS number, which is not OCaml's: [0x600px] is neither a number nor a length,
and reading it as an OCaml float turned [min-[0x600px]] into a live 1536px
breakpoint out of a value Tailwind leaves for the browser to drop. *)letparse_px_values=letdigits=ifString.ends_with~suffix:"px"sthenString.subs0(String.lengths-2)elsesinmatchCascade.Cursor.try_parse_full_errCss.Values.read_number(Cascade.Cursor.of_stringdigits)with|Ok(Numpx)->Some({px;text=s}:Style.arbitrary_px)|Ok_|Error_->None(* Parse a has-selector string as a relative CSS selector ([:has()] accepts a
bare leading combinator, e.g. [>div] or [~img]), with [&] resolved to the
universal selector via {!Cascade.Nest.substitute}, the same substitution
{!nest_selector} performs for anchored templates. *)lethas_relative_selectors=letsel=Css.Selector.read_relative(Cascade.Cursor.of_string(Parse.decode_underscoress))inCascade.Nest.substitute~parent:Css.Selector.universalsel(* Validate that a has-selector string can be parsed as a CSS selector. Rejects
invalid selectors like "@media_print" at parse time. *)letis_valid_has_selectorsel=tryignore(has_relative_selectorsel);truewithCascade.Cursor.Parse_error_|Invalid_argument_->false(* Find Tailwind's match operator ($=, ^=, *=, ~=, |= or bare =) in the raw,
not-yet-underscore-decoded bracket content, skipping over anything of that
shape written inside quotes. Cascade's own selector reader cannot do this
step: an unquoted operator can only be told apart from the same character
inside a quoted value by scanning for the quotes, and that scan has to run
before the value is handed to the reader at all. *)letdata_match_operatorraw_expr=letlen=String.lengthraw_exprinletin_quotes=reffalseinletrecgoi=ifi>=lenthenNoneelsematchraw_expr.[i]with|'"'|'\''->in_quotes:=not!in_quotes;go(i+1)|('$'|'^'|'*'|'~'|'|')when(not!in_quotes)&&i+1<len&&raw_expr.[i+1]='='->Some(i,2)|'='whennot!in_quotes->Some(i,1)|_->go(i+1)ingo0(* Split a trailing case-sensitivity flag off a decoded value remainder:
Selectors 4 requires whitespace before [i]/[s], which is what Tailwind's own
underscore convention spells too once the underscore before the flag decodes
to a space (e.g. [data-[foo=bar_i]] -> "bar i"). A value that is itself
quoted never matches this: its last character is the closing quote, and the
flag can only follow that, as in [data-[foo='bar'_i]] -> "'bar' i" decoding
to a value of "'bar'" and a flag. *)letsplit_trailing_flagvalue=letn=String.lengthvalueinifn>=2&&value.[n-1]='i'&&value.[n-2]=' 'then(String.trim(String.subvalue0(n-2))," i")elseifn>=2&&value.[n-1]='s'&&value.[n-2]=' 'then(String.trim(String.subvalue0(n-2))," s")else(value,"")letis_already_quotedvalue=letn=String.lengthvalueinn>=2&&((value.[0]='"'&&value.[n-1]='"')||(value.[0]='\''&&value.[n-1]='\''))(* Wrap a decoded, not-already-quoted value in quotes CSS requires but
Tailwind's own bracket sugar lets the author skip, e.g. the space in
[data-[foo=bar_baz]] -> "bar baz". {!Css.Selector.pp_attribute_match} drops
the quotes again when minified and the value does not need them, so adding
them here never changes what a plain identifier value renders as. *)letquote_valuevalue=letq=ifString.containsvalue'"'then'\''else'"'inString.make1q^value^String.make1q(* Read the reconstructed [[data-<expr>]] through cascade's own selector reader
and pull the pieces [parse_data_expr] hands back out of it. *)letdata_attribute_of_bracketbracket=matchCss.Selector.of_stringbracketwith|Css.Selector.Attribute(_,Css.Selector.Dataname,m,flag)->("data-"^name,m,flag)|_->invalid_arg("not a data attribute: "^bracket)(* Parse a [data-[...]] bracket body (the interior text, no brackets) into an
attribute name, match operator and optional case-sensitivity flag. Tailwind
underscore-decodes the whole expression and only then reads it, so the value
and flag - and, once a value is quoted, the quotes themselves - are valid CSS
only after that decoding; {!Css.Selector.of_string} reads the reconstructed
[[data-<expr>]] and does the actual work: choosing the match operator,
validating the attribute name, and reading a quoted value's escapes the same
way a browser would. Raises on a malformed expression; callers that see
arbitrary user input should validate through {!is_valid_data_attr_expr}
first. *)letparse_data_exprraw_expr=matchdata_match_operatorraw_exprwith|None->letname=String.trim(Parse.decode_underscoresraw_expr)indata_attribute_of_bracket("[data-"^name^"]")|Some(op_pos,op_len)->letlen=String.lengthraw_exprinletname=String.trim(Parse.decode_underscores(String.subraw_expr0op_pos))in(* Validated here, on its own: folded straight into the reconstructed text
below, a name ending in one of [$ ^ * ~ |] would recombine with the
operator that follows it into a different, two-character one ([foo^]
then [=] reads back as [foo] then [^=]). None of those characters is
one {!Css.Selector.attribute} accepts in a name, so this rules the
recombination out before it can happen. *)ignore(Css.Selector.attribute("data-"^name)Css.Selector.Presence);letop_text=String.subraw_exprop_posop_leninletrest=String.trim(Parse.decode_underscores(String.subraw_expr(op_pos+op_len)(len-op_pos-op_len)))inletvalue,flag_text=split_trailing_flagrestinletvalue_text=ifis_already_quotedvaluethenvalueelsequote_valuevalueindata_attribute_of_bracket("[data-"^name^op_text^value_text^flag_text^"]")(* Validate a data-[...] bracket expression by running it through the same
reading [parse_data_expr] does at render time. *)letis_valid_data_attr_exprexpr=matchparse_data_exprexprwith|_->true|exception(Invalid_argument_|Cascade.Cursor.Parse_error_)->false(* The query a bracket [@media] at-rule names, [[@media_print]] or
[[@media(width>=600px)]], with underscores read as spaces; [None] when what
follows [@media] is no query the grammar reads, so a half-written one is
refused where the modifier is parsed rather than raised from the render. *)letbracket_media_conditioninner=letn=String.lengthinnerinifString.starts_with~prefix:"@media"inner&&n>7&&(inner.[6]='_'||inner.[6]='(')thenletcond=String.trim(Parse.decode_underscores(String.subinner6(n-6)))inmatchCss.Media.of_string_strictcondwith|media->Somemedia|exceptionCascade.Cursor.Parse_error_->NoneelseNone(* An at-rule in brackets is a variant too: [[@supports(display:grid)]],
[[@media_print]] and [[@starting-style]] wrap the utility rather than select
it. *)lettry_bracket_at_rules=ifString.lengths>=3&&s.[0]='['&&s.[String.lengths-1]=']'thenletinner=String.subs1(String.lengths-2)inifinner="@starting-style"||String.starts_with~prefix:"@supports"inner&&String.lengthinner>9||Option.is_some(bracket_media_conditioninner)thenSome(At_ruleinner)elseNoneelseNoneletis_supports_ident_charc=(c>='a'&&c<='z')||(c>='A'&&c<='Z')||(c>='0'&&c<='9')||c='-'(* The [@supports] keywords a condition joins or negates with. Written against a
parenthesis, [not(display:grid)], the keyword is the name of a function
token, which the grammar reads as an unknown feature that never holds, so
each gets the spaces Tailwind gives it. Only at the top level: inside
[selector(...)] a [:not(a)] is a pseudo-class, not the keyword. *)letspace_supports_keywordscond=letn=String.lengthcondinletbuf=Buffer.create(n+8)inletkeyword_ati=List.find_opt(funk->letl=String.lengthkini+l<=n&&String.subcondil=k&&(i=0||not(is_supports_ident_charcond.[i-1]))&&(i+l=n||not(is_supports_ident_charcond.[i+l])))["and";"not";"or"]inletrecgoidepth=ifi<nthenmatchcond.[i]with|'('->Buffer.add_charbuf'(';go(i+1)(depth+1)|')'->Buffer.add_charbuf')';go(i+1)(depth-1)|cwhendepth=0->(matchkeyword_atiwith|Somek->letlen=Buffer.lengthbufiniflen>0&&Buffer.nthbuf(len-1)<>' 'thenBuffer.add_charbuf' ';Buffer.add_stringbufk;letnext=i+String.lengthkinifnext<n&&cond.[next]<>' 'thenBuffer.add_charbuf' ';gonextdepth|None->Buffer.add_charbufc;go(i+1)depth)|c->Buffer.add_charbufc;go(i+1)depthingo00;String.trim(Buffer.contentsbuf)(* [prop: value] is a property test only when what stands before the colon is a
property name. In [not(display:grid)] it is [not(display], and splitting
there handed the property reader [grid)] for a value. *)letsupports_property_testcond=matchString.index_optcond':'with|None->None|Somei->letprop=String.trim(String.subcond0i)inifprop<>""&&String.for_allis_supports_ident_charpropthenSome(prop,String.trim(String.subcond(i+1)(String.lengthcond-i-1)))elseNoneletnormalize_supports_conditioncondition_str=letcond=Parse.decode_underscorescondition_strinifString.starts_with~prefix:"--"cond&&String.lengthcond>2&¬(String.containscond':')then(* Bare custom property: --test tests against var(--tw), built directly
rather than assembled as "(--test: var(--tw))" and re-parsed. *)Css.Supports.propertycond"var(--tw)"elseifString.starts_with~prefix:"("cond&&String.ends_with~suffix:")"condthen(* Already parenthesised, so it is the condition the author wrote; only the
real grammar reader can make sense of arbitrary authored text. *)Css.Supports.of_string(space_supports_keywordscond)elsematchsupports_property_testcondwith|Some(prop,value)->(* [prop: value], the property test Tailwind emits, built directly. *)Css.Supports.propertypropvalue|NonewhenString.containscond'('->(* A negation, a function such as [selector(...)] or
[font-format(opentype)], or a condition joined with [and]/[or]; again
arbitrary authored text, so the real grammar reader parses it. *)Css.Supports.of_string(space_supports_keywordscond)|None->(* Bare property name: backdrop-filter tests against var(--tw), the same
expansion as the bare custom property above. A lone identifier is not
a condition the CSS grammar has a production for, so leaving it
raised a parse error out of the scanner. *)Css.Supports.propertycond"var(--tw)"(* Validate that a supports-[...] bracket normalizes to a condition the
[@supports] grammar has a production for. An empty or half-written one used
to reach the condition reader and raise from there, and a property test whose
value is no declaration value raised [Failure] out of the property reader. *)letis_valid_supports_conditioncond=cond<>""&&tryignore(normalize_supports_conditioncond);truewithCascade.Cursor.Parse_error_|Invalid_argument_|Failure_->false(* Which spelling an [nth-*] argument was written in. A bare number reads both
ways and they are different classes, so the reader records the one it saw
rather than deciding again when it prints. *)letbracketedexpr:Style.nth_expr={expr;bracketed=true}letbareexpr:Style.nth_expr={expr;bracketed=false}(* Validate that an nth-*-[...] bracket holds an An+B expression (Selectors 4
sec. 9.3). The reader raises on anything else, so the expression is checked
where the modifier is parsed, like the has- selector above. *)letis_valid_nth_exprexpr=tryignore(parse_nth_selectorexpr);truewithCascade.Cursor.Parse_error_|Invalid_argument_->false(* Try parsing a bracketed modifier, returning Some if matched *)(* Build the list of bracket pattern matchers for a given input string *)letbracket_named_patternss=let(let*)=Option.bindin(* Every aria- and data- bracket here takes an attribute expression, so the
argument has to name an attribute. *)lettry_attr?(extra=fun_->true)prefixmake=let*expr=extract_bracket_content~prefixsinifnames_attributeexpr&&extraexprthenSome(makeexpr)elseNoneinlettry_named?(extra=fun_->true)prefixmake=let*expr,name=extract_bracket_content_with_name~prefixsinifnames_attributeexpr&&extraexprthenSome(makeexprname)elseNoneinlettry_named_hasprefixmake=let*sel,name=extract_bracket_content_with_name~prefixsinifis_valid_has_selectorselthenSome(makeselname)elseNonein[(fun()->try_named"group-aria-["(funen->Group_aria(e,n)));(fun()->try_named"peer-aria-["(funen->Peer_aria(e,n)));(fun()->try_attr"aria-["(funexpr->Aria_bracketexpr));(fun()->try_named"group-data-["~extra:is_valid_data_attr_expr(funen->Group_data("["^e^"]",n)));(fun()->try_named"peer-data-["~extra:is_valid_data_attr_expr(funen->Peer_data("["^e^"]",n)));(* Bare shorthand: [group-data-dragging] is [group-data-[dragging]] with a
different spelling, so it keeps its own class name. *)(fun()->try_bare_datas"group-data-"(funen->Group_data(e,n)));(fun()->try_bare_datas"peer-data-"(funen->Peer_data(e,n)));(fun()->try_attr"data-["~extra:is_valid_data_attr_expr(funexpr->Data_bracketexpr));(fun()->try_named_has"group-has-["(funsn->Group_has(s,n)));(fun()->try_named_has"peer-has-["(funsn->Peer_has(s,n)));(fun()->let*sel=extract_bracket_content~prefix:"has-["sinifis_valid_has_selectorselthenSome(Hassel)elseNone);](* Every [nth-*-[...]] arm goes through [try_nth], so the bracket flag is set in
one place. *)letbracket_value_patternss=let(let*)=Option.bindinlettry_withprefixparsemake=let*content=extract_bracket_content~prefixsinlet*value=parsecontentinSome(makevalue)inlettry_nthprefixmake=let*expr=extract_bracket_content~prefixsinifis_valid_nth_exprexprthenSome(make(bracketedexpr))elseNonein[(fun()->try_with"min-["parse_px_value(funpx->Min_arbitrarypx));(fun()->try_with"max-["parse_px_value(funpx->Max_arbitrarypx));(fun()->try_with"min-["parse_css_length(funl->Min_arbitrary_lengthl));(fun()->try_with"max-["parse_css_length(funl->Max_arbitrary_lengthl));(fun()->try_nth"nth-last-of-type-["(fune->Nth_last_of_typee));(fun()->try_nth"nth-of-type-["(fune->Nth_of_typee));(fun()->try_nth"nth-last-["(fune->Nth_laste));(fun()->try_nth"nth-["(fune->Nthe));(fun()->let*cond=extract_bracket_content~prefix:"supports-["sinifis_valid_supports_conditioncondthenSome(Supports_conditioncond)elseNone);(fun()->try_with"group-["(funsel->ifsel<>""&&(String.containssel'&'||is_compound_selectorsel)thenSomeselelseNone)(funsel->Group_arbitrarysel));(fun()->try_with"peer-["(funsel->ifsel<>""&&(String.containssel'&'||is_compound_selectorsel)thenSomeselelseNone)(funsel->Peer_arbitrarysel));(fun()->try_bracket_at_rules);(fun()->ifString.lengths>=3&&s.[0]='['&&s.[String.lengths-1]=']'thenletinner=String.subs1(String.lengths-2)inifString.containsinner'&'||is_compound_selectorinnerthenSome(Arbitrary_selectorinner)elseNoneelseNone);]letbracket_patternss=bracket_named_patternss@bracket_value_patternss(* Try supports-<property> shorthand: supports-grid → Supports_property
"grid" *)lettry_supports_shorthands=ifString.starts_with~prefix:"supports-"s&&String.lengths>9&&(not(String.containss'['))&¬(String.containss'/')thenletprop=String.subs9(String.lengths-9)inSome(Supports_propertyprop)elseNonelettry_bracketed_modifiers=matchList.find_map(funf->f())(bracket_patternss)with|Some_asr->r|None->try_supports_shorthands(* Try to parse numeric nth patterns: nth-N, nth-last-N, nth-of-type-N,
nth-last-of-type-N where N is a positive integer *)lettry_numeric_nths=lettry_prefixprefixmake=letplen=String.lengthprefixinifString.starts_with~prefixs&&String.lengths>plenthenletrest=String.subsplen(String.lengths-plen)inifStyle.is_numericrestthenSome(makerest)elseNoneelseNoneinmatchtry_prefix"nth-last-of-type-"(funn->Nth_last_of_type(baren))with|Some_asr->r|None->(matchtry_prefix"nth-of-type-"(funn->Nth_of_type(baren))with|Some_asr->r|None->(matchtry_prefix"nth-last-"(funn->Nth_last(baren))with|Some_asr->r|None->try_prefix"nth-"(funn->Nth(baren))))(* Simple modifiers - direct string to modifier mapping *)letsimple_modifiers=[(* Responsive breakpoints *)("sm",Responsive`Sm);("md",Responsive`Md);("lg",Responsive`Lg);("xl",Responsive`Xl);("2xl",Responsive`Xl_2);(* Min responsive breakpoints (explicit min-width, same as unprefixed) *)("min-sm",Min_responsive`Sm);("min-md",Min_responsive`Md);("min-lg",Min_responsive`Lg);("min-xl",Min_responsive`Xl);("min-2xl",Min_responsive`Xl_2);(* Max responsive breakpoints *)("max-sm",Max_responsive`Sm);("max-md",Max_responsive`Md);("max-lg",Max_responsive`Lg);("max-xl",Max_responsive`Xl);("max-2xl",Max_responsive`Xl_2);(* Interactive states *)("hover",Hover);("focus",Focus);("active",Active);("disabled",Disabled);("focus-within",Focus_within);("focus-visible",Focus_visible);(* Appearance *)("dark",Dark);("motion-safe",Motion_safe);("motion-reduce",Motion_reduce);("contrast-more",Contrast_more);("contrast-less",Contrast_less);("forced-colors",Forced_colors);("inverted-colors",Inverted_colors);("pointer-none",Pointer_none);("pointer-coarse",Pointer_coarse);("pointer-fine",Pointer_fine);("any-pointer-none",Any_pointer_none);("any-pointer-coarse",Any_pointer_coarse);("any-pointer-fine",Any_pointer_fine);("noscript",Noscript);("print",Print);("portrait",Portrait);("landscape",Landscape);("ltr",Ltr);("rtl",Rtl);(* Group states *)("group-hover",Group_hover);("group-focus",Group_focus);("group-active",Group_active);("group-visited",Group_visited);("group-disabled",Group_disabled);("group-checked",Group_checked);("group-empty",Group_empty);("group-required",Group_required);("group-valid",Group_valid);("group-invalid",Group_invalid);("group-indeterminate",Group_indeterminate);("group-default",Group_default);("group-open",Group_open);("group-target",Group_target);("group-first",Group_first);("group-last",Group_last);("group-only",Group_only);("group-odd",Group_odd);("group-even",Group_even);("group-first-of-type",Group_first_of_type);("group-last-of-type",Group_last_of_type);("group-only-of-type",Group_only_of_type);("group-optional",Group_optional);("group-read-only",Group_read_only);("group-read-write",Group_read_write);("group-inert",Group_inert);("group-user-valid",Group_user_valid);("group-user-invalid",Group_user_invalid);("group-placeholder-shown",Group_placeholder_shown);("group-autofill",Group_autofill);("group-in-range",Group_in_range);("group-out-of-range",Group_out_of_range);("group-focus-within",Group_focus_within);("group-focus-visible",Group_focus_visible);("group-enabled",Group_enabled);("group-hocus",Group_hocus);(* Peer states *)("peer-hover",Peer_hover);("peer-focus",Peer_focus);("peer-checked",Peer_checked);("peer-active",Peer_active);("peer-visited",Peer_visited);("peer-disabled",Peer_disabled);("peer-empty",Peer_empty);("peer-required",Peer_required);("peer-valid",Peer_valid);("peer-invalid",Peer_invalid);("peer-indeterminate",Peer_indeterminate);("peer-default",Peer_default);("peer-open",Peer_open);("peer-target",Peer_target);("peer-first",Peer_first);("peer-last",Peer_last);("peer-only",Peer_only);("peer-odd",Peer_odd);("peer-even",Peer_even);("peer-first-of-type",Peer_first_of_type);("peer-last-of-type",Peer_last_of_type);("peer-only-of-type",Peer_only_of_type);("peer-optional",Peer_optional);("peer-read-only",Peer_read_only);("peer-read-write",Peer_read_write);("peer-inert",Peer_inert);("peer-user-valid",Peer_user_valid);("peer-user-invalid",Peer_user_invalid);("peer-placeholder-shown",Peer_placeholder_shown);("peer-autofill",Peer_autofill);("peer-in-range",Peer_in_range);("peer-out-of-range",Peer_out_of_range);("peer-focus-within",Peer_focus_within);("peer-focus-visible",Peer_focus_visible);("peer-enabled",Peer_enabled);("peer-hocus",Peer_hocus);(* ARIA variants *)("aria-checked",Aria_checked);("aria-expanded",Aria_expanded);("aria-selected",Aria_selected);("aria-disabled",Aria_disabled);(* Structural pseudo-classes *)("first",First);("last",Last);("only",Only);("odd",Odd);("even",Even);("first-of-type",First_of_type);("last-of-type",Last_of_type);("only-of-type",Only_of_type);("empty",Empty);(* Form states *)("checked",Checked);("indeterminate",Indeterminate);("default",Default);("required",Required);("valid",Valid);("invalid",Invalid);("in-range",In_range);("out-of-range",Out_of_range);("placeholder-shown",Placeholder_shown);("autofill",Autofill);("read-only",Read_only);("read-write",Read_write);("optional",Optional);("open",Open);("enabled",Enabled);("target",Target);("visited",Visited);("inert",Inert);("user-valid",User_valid);("user-invalid",User_invalid);(* Pseudo-elements *)("before",Pseudo_before);("after",Pseudo_after);("marker",Pseudo_marker);("selection",Pseudo_selection);("placeholder",Pseudo_placeholder);("backdrop",Pseudo_backdrop);("file",Pseudo_file);("first-letter",Pseudo_first_letter);("first-line",Pseudo_first_line);("details-content",Pseudo_details_content);(* Other *)("starting",Starting);("*",Children);("**",Descendants);("hocus",Hocus);("device-hocus",Device_hocus);(* Container queries *)("@3xs",ContainerContainer_3xs);("@2xs",ContainerContainer_2xs);("@xs",ContainerContainer_xs);("@sm",ContainerContainer_sm);("@md",ContainerContainer_md);("@lg",ContainerContainer_lg);("@xl",ContainerContainer_xl);("@2xl",ContainerContainer_2xl);("@3xl",ContainerContainer_3xl);("@4xl",ContainerContainer_4xl);("@5xl",ContainerContainer_5xl);("@6xl",ContainerContainer_6xl);("@7xl",ContainerContainer_7xl);](* [simple_modifiers] holds the built-in breakpoints as a fixed set, so a
[@theme] block that removed one is only visible through the scheme: [md:]
then names a breakpoint the project does not have, and stops resolving the
way an unknown variant does. *)letmodifier_breakpoint_is_definedtheme=function|Responsivebp|Min_responsivebp|Max_responsivebp->Scheme.has_breakpointtheme(Style.pp_modifier(Responsivebp))|_->true(* Look a variant up in [simple_modifiers], honouring the theme's
breakpoints. *)letlookup_simplethemes=matchList.assoc_optssimple_modifierswith|Somemwhenmodifier_breakpoint_is_definedthemem->Somem|Some_|None->None(* Try looking up a custom breakpoint (e.g., "10xl", "min-10xl", "max-10xl") *)lettry_custom_breakpointbreakpointss=(* Direct name: e.g., "10xl" → Custom_responsive *)ifList.memsbreakpointsthenSome(Custom_responsives)elseifString.starts_with~prefix:"min-"s&&String.lengths>4then(* min-<name>: e.g., "min-10xl" → Min_custom *)letname=String.subs4(String.lengths-4)inifList.memnamebreakpointsthenSome(Min_customname)elseNoneelseifString.starts_with~prefix:"max-"s&&String.lengths>4thenletname=String.subs4(String.lengths-4)inifList.memnamebreakpointsthenSome(Max_customname)elseNoneelseNone(** Try not-* shorthand patterns that aren't in simple_modifiers or bracket
patterns. These are modifiers like data-foo, has-checked, nth-2 that work as
shorthands in the not-* context. *)lettry_not_shorthandinner=matchtry_supports_shorthandinnerwith|Somem->Some(Notm)|None->ifString.starts_with~prefix:"data-"inner&&String.lengthinner>5then(* data-X shorthand — attribute presence check *)letattr=String.subinner5(String.lengthinner-5)inSome(Not(Data_custom(attr,"")))elseifString.starts_with~prefix:"nth-"inner&&String.lengthinner>4then(* nth-X shorthand — :nth-child(X) *)letexpr=String.subinner4(String.lengthinner-4)inSome(Not(Nth(Style.nth_exprexpr)))elseNone(** Check if a modifier is compatible with not-* negation. Pseudo-elements,
starting style and children/descendants cannot be negated; a container query
can, as [\@container not (width >= 28rem)]. *)letis_not_compatible=function|Pseudo_before|Pseudo_after|Pseudo_marker|Pseudo_selection|Pseudo_placeholder|Pseudo_backdrop|Pseudo_file|Pseudo_first_letter|Pseudo_first_line|Pseudo_details_content|Starting|Children|Descendants|Prose_element_->false(* A bracket [@media] at-rule wraps the utility in a query and has no selector
to negate or to look for; [not-[@media ...]] has its own reading, and
Tailwind compiles nothing for [has-[@media_print]]. *)|At_rulecontentwhenOption.is_some(bracket_media_conditioncontent)->false(* A bracket [@supports] condition has its own [not-] reading too, which
negates the condition; read as a plain variant it negated the class. *)|At_rulecontentwhenString.starts_with~prefix:"@supports"content&&String.lengthcontent>9->false|_->true(* A variant that is an at-rule and nothing else: a media, breakpoint, supports
or container query, or the negation of one. *)letrecat_rule_only=function|Responsive_|Min_responsive_|Max_responsive_|Min_arbitrary_|Max_arbitrary_|Min_arbitrary_length_|Max_arbitrary_length_|Custom_responsive_|Min_custom_|Max_custom_|Supports_property_|Supports_condition_|Container_|Starting->true|At_rulecontent|Not_bracketcontent->Option.is_some(bracket_media_conditioncontent)||String.starts_with~prefix:"@supports"content&&String.lengthcontent>9|Notm->at_rule_onlym|m->renders_as_mediam&¬(involves_hoverm)(* The variants a [group-not-] or [peer-not-] holds. A plain [not-hover] is fine
- it negates the selector and keeps the hover media gate - but a group or
peer negation has only the selector, so an inner with an at-rule half, a
breakpoint or a container as much as [hover], leaves it nothing to emit, and
Tailwind compiles nothing for it. *)letscoped_negation_holdsm=is_not_compatiblem&&(not(at_rule_onlym))&¬(involves_hoverm)(* The variants a [has-] or [in-] holds: those with a selector of their own. An
at-rule variant has none. A negation has one unless it negates an at-rule,
and [not-hover] pairs its selector with [\@media not (hover: hover)], which a
compound cannot carry: Tailwind compiles nothing for [has-md], [has-not-dark]
or [in-not-group-hover]. [hover] itself keeps its gate around the compound,
so [has-hover] is fine. *)letrechas_selector=function|Notm->is_not_compatiblem&&has_selectorm&¬(involves_hoverm)|m->is_not_compatiblem&¬(at_rule_onlym)(* [not-[...]] whose content is neither a media condition nor a pseudo-class
negates the content read as a selector, so text the selector grammar cannot
read - or can read only a prefix of - is not a negation at all. The reader
raises rather than answering, and it stops at the first thing it cannot use,
so the cursor has to be exhausted too: a trailing remainder would otherwise
be dropped and the rule would negate less than the class says. *)letreads_as_selectorcontent=lets=Parse.decode_underscorescontentinletcursor=Cascade.Cursor.of_stringsinmatchCss.Selector.readcursorwith|exception(Cascade.Cursor.Parse_error_|Invalid_argument_)->false|_->Cascade.Cursor.wscursor;Cascade.Cursor.is_donecursor(* The condition a bracket [@supports] at-rule names under [not-], with
underscores read as spaces: [(display:grid)] or [not (display:grid)]. [None]
for a compound [and]/[or] condition, which has no single negation, and for
text the condition grammar does not read. *)letbracket_supports_conditioncontent=letn=String.lengthcontentinifString.starts_with~prefix:"@supports"content&&n>9thenletcond=String.trim(Parse.decode_underscores(String.subcontent9(n-9)))inmatchCss.Supports.of_stringcondwith|Css.Supports.And_|Css.Supports.Or_|Css.Supports.Not(Css.Supports.And_|Css.Supports.Or_)->None|condition->Somecondition|exception(Cascade.Cursor.Parse_error_|Invalid_argument_|Failure_)->NoneelseNone(** Check if bracket content is valid for not-[...] patterns. Rejects combinator
selectors (+, >, ~), media conditions with commas, and bare selectors. *)letis_valid_not_bracket_contentcontent=ifString.lengthcontent=0thenfalseelse(* Reject combinator selectors: +img, >img, ~img *)letfirst=content.[0]iniffirst='+'||first='>'||first='~'thenfalseelseif(* Reject media conditions with commas (complex media) *)(String.starts_with~prefix:"@media"content&&String.lengthcontent>6)||String.starts_with~prefix:"@media_"content&&String.lengthcontent>7thennot(String.containscontent',')elseifString.starts_with~prefix:"@supports"content&&String.lengthcontent>9thenOption.is_some(bracket_supports_conditioncontent)elseiffirst=':'thentrueelsereads_as_selectorcontent(** Try parsing a not-[...] bracket pattern. Returns the Not_bracket modifier
for pseudo-class or media bracket content. *)lettry_not_bracketinner=ifString.starts_with~prefix:"["innerthenletrest=String.subinner1(String.lengthinner-1)inmatchmatching_bracketrestwith|Someiwheni=String.lengthrest-1->letcontent=String.subrest0iinifis_valid_not_bracket_contentcontentthenSome(Not_bracketcontent)elseNone(* Content after the bracket, [not-[:checked]/foo] among it: a named
not-bracket variant is invalid, and so is any other tail. *)|Some_|None->NoneelseNone(** Parse group-not-* or peer-not-* pattern. Splits the rest into inner modifier
and optional /name suffix. *)letparse_group_peer_not_innerrest=(* Check for bracket content first: [...]/name or [...] *)ifString.starts_with~prefix:"["restthenletafter_bracket=String.subrest1(String.lengthrest-1)inmatchmatching_bracketafter_bracketwith|Somei->letcontent=String.subafter_bracket0iinletremainder=String.subafter_bracket(i+1)(String.lengthafter_bracket-i-1)inletname=ifString.starts_with~prefix:"/"remainder&&String.lengthremainder>1thenSome(String.subremainder1(String.lengthremainder-1))elseNoneinSome(Not_bracketcontent,name)|None->Noneelse(* Non-bracket: split on / for name suffix *)matchString.index_optrest'/'with|Somei->letinner_str=String.subrest0iinletname=String.subrest(i+1)(String.lengthrest-i-1)inletinner_mod=matchList.assoc_optinner_strsimple_modifierswith|Somemwhenscoped_negation_holdsm->Somem|Some_|None->NoneinOption.map(funm->(m,Somename))inner_mod|None->(matchList.assoc_optrestsimple_modifierswith|Somemwhenscoped_negation_holdsm->Some(m,None)|Some_|None->None)(* Try to parse compound named group variants: not-group-STATE/name,
has-group-STATE/name, in-group-STATE/name, group-peer-STATE/name *)lettry_compound_named_groups=lettry_matchprefixmake=letplen=String.lengthprefixinifString.starts_with~prefixs&&String.lengths>plenthenletrest=String.subsplen(String.lengths-plen)inletbase,name_opt=split_namerestinmatchname_optwith(* [group-hover/[]] is not a name Tailwind accepts. *)|Somenamewhenis_plain_identname->(matchList.assoc_optbasegroup_state_modifierswith|Somem->Some(makemname)|None->None)|Some_|None->NoneelseNonein(* Longest prefix first: [group-peer-X/n] must not be read as [group-] with
the state [peer-X]. *)List.find_map(funf->f())[(fun()->try_match"not-group-"(funmn->Not_named_group(m,n)));(fun()->try_match"has-group-"(funmn->Has_named_group(m,n)));(fun()->try_match"in-group-"(funmn->In_named_group(m,n)));(fun()->try_match"group-peer-"(funmn->Group_peer_named(m,n)));(fun()->try_match"group-"(funmn->Named_group(m,n)));(fun()->try_match"peer-"(funmn->Named_peer(m,n)));](* Try in-* pattern: in-[selector] or in-data-attr *)lettry_in_modifiers=ifnot(String.starts_with~prefix:"in-"s&&String.lengths>3)thenNoneelseletrest=String.subs3(String.lengths-3)inifString.starts_with~prefix:"["restthenletafter=String.subrest1(String.lengthrest-1)inmatchmatching_bracketafterwith|Someiwheni=String.lengthafter-1->Some(In_bracket(String.subafter0i))|_->NoneelseifString.starts_with~prefix:"data-"rest&&String.lengthrest>5&¬(String.containsrest'[')thenSome(In_data(String.subrest5(String.lengthrest-5)))else(* [in-focus]: an ancestor in that state, the same state names the other
variants take. *)matchList.assoc_optrestgroup_state_modifierswith|Somem->Some(In_state(m,rest))|None->None(* Try not-in-[...] pattern *)lettry_not_in_modifiers=ifnot(String.starts_with~prefix:"not-in-["s&&String.lengths>8)thenNoneelseletrest=String.subs8(String.lengths-8)inmatchmatching_bracketrestwith|Someiwheni=String.lengthrest-1->Some(Not(In_bracket(String.subrest0i)))|_->None(* Try group-not-* and peer-not-* patterns *)lettry_group_peer_nots=lettry_prefixprefixmake=letplen=String.lengthprefixinifString.starts_with~prefixs&&String.lengths>plenthenletrest=String.subsplen(String.lengths-plen)inmatchparse_group_peer_not_innerrestwith|Some(inner,name)->Some(makeinnername)|None->NoneelseNoneinmatchtry_prefix"group-not-"(funin->Group_not(i,n))with|Some_asr->r|None->try_prefix"peer-not-"(funin->Peer_not(i,n))(* Try not-* prefix: wrap inner modifier in Not *)lettry_not_modifierthemes=ifnot(String.starts_with~prefix:"not-"s&&String.lengths>4)thenNoneelseletinner=String.subs4(String.lengths-4)inmatchList.assoc_optinnersimple_modifierswith|Somemwhenis_not_compatiblem->ifmodifier_breakpoint_is_definedthememthenSome(Notm)elseNone|Some_->None|None->letfns=[(fun()->try_not_bracketinner);(fun()->matchtry_bracketed_modifierinnerwith|Somemwhenis_not_compatiblem->Some(Notm)|Some_->None|None->None);(fun()->try_not_shorthandinner);]inList.find_map(funf->f())fns(* Try bare data-X or aria-X shorthand *)lettry_bare_data_arias=ifString.starts_with~prefix:"data-"s&&String.lengths>5&&(not(String.containss'['))&¬(String.containss'/')thenSome(Data_custom(String.subs5(String.lengths-5),""))elseifString.starts_with~prefix:"aria-"s&&String.lengths>5&&(not(String.containss'['))&¬(String.containss'/')thenletexpr=String.subs5(String.lengths-5)inifnames_attributeexprthenSome(Aria_bracketexpr)elseNoneelseNone(* The order @tailwindcss/typography registers its element variants in, which is
the order it emits their rules in. A variant missing from here sorts before
every other, so its utility loses to one it should win against. *)letprose_element_variant_order=function|"headings"->96001|"h1"->96002|"h2"->96003|"h3"->96004|"h4"->96005|"h5"->96006|"h6"->96007|"p"->96008|"a"->96009|"blockquote"->96010|"figure"->96011|"figcaption"->96012|"strong"->96013|"em"->96014|"kbd"->96015|"code"->96016|"pre"->96017|"ol"->96018|"ul"->96019|"li"->96020|"dl"->96021|"dt"->96022|"dd"->96023|"table"->96024|"thead"->96025|"tr"->96026|"th"->96027|"td"->96028|"img"->96029|"picture"->96030|"video"->96031|"hr"->96032|"lead"->96033|_->96000(* One name per variant @tailwindcss/typography registers. A name outside this
set is not a variant there either, so the class is rejected rather than
compiled into a selector Tailwind never produces. *)letis_prose_element_name=function|"headings"|"p"|"a"|"strong"|"em"|"code"|"pre"|"ol"|"ul"|"li"|"dl"|"dt"|"dd"|"blockquote"|"h1"|"h2"|"h3"|"h4"|"h5"|"h6"|"img"|"picture"|"video"|"figure"|"figcaption"|"hr"|"table"|"tr"|"th"|"td"|"thead"|"kbd"|"lead"->true|_->false(* Try parsing prose-* element variant modifier *)lettry_prose_elements=ifString.starts_with~prefix:"prose-"s&&String.lengths>6thenletname=String.subs6(String.lengths-6)inifis_prose_element_namenamethenSome(Prose_elementname)elseNoneelseNone(* Parse the container-query modifier forms that are not plain table entries:
[@min-<size>], [@max-<size>], [@\[<len>\]], [@min-\[<len>\]], and
[@max-\[<len>\]]. The bare [@<size>] forms are in [simple_modifiers]. *)letcontainer_size_of_string=function|"3xs"->SomeContainer_3xs|"2xs"->SomeContainer_2xs|"xs"->SomeContainer_xs|"sm"->SomeContainer_sm|"md"->SomeContainer_md|"lg"->SomeContainer_lg|"xl"->SomeContainer_xl|"2xl"->SomeContainer_2xl|"3xl"->SomeContainer_3xl|"4xl"->SomeContainer_4xl|"5xl"->SomeContainer_5xl|"6xl"->SomeContainer_6xl|"7xl"->SomeContainer_7xl|_->None(* The theme token a container-query arbitrary value reads through [theme()],
e.g. [breakpoint-lg] for [@min-[theme(--breakpoint-lg)]]. Tailwind spells the
lookup [theme(--x)] and [--theme(--x)]. *)letcontainer_theme_tokencontent=lets=String.trimcontentinlets=Option.value(Parse.bare_names)~default:sinmatchParse.call_body"theme"swith|Somebodywhenbody<>""->letinner=String.trimbodyinSome(ifString.starts_with~prefix:"--"innerthenString.subinner2(String.lengthinner-2)elseinner)|Some_|None->None(* A container-query arbitrary value may reference a theme token, e.g.
[@min-[theme(--breakpoint-lg)]]. [theme(--breakpoint-lg)] resolves to the
[--breakpoint-lg] default (64rem) via the same token table the [lg:] variant
publishes, so a plain length parse falls back to token resolution. *)letparse_container_lengthcontent=matchCss.parse_lengthcontentwith|Some_aslen->len|None->Option.bind(Option.bind(container_theme_tokencontent)Scheme.token_default)Css.parse_length(* Resolving a [theme()] reference into the query does not consume the token:
Tailwind writes its binding into [@layer theme] as well, so a consumer
reading [--breakpoint-lg] off the sheet still finds it. The value is the
registered default's own text, the way the theme layer emits every other
token it holds; the declaration rides on the utility's properties, where the
theme extractor collects it and the utilities layer filters it out. *)letreccontainer_query_theme_decls(q:container_query)=letof_bracketraw=matchcontainer_theme_tokenrawwith|None->[]|Somename->(matchScheme.token_defaultnamewith|None->[]|Somecss->[Css.custom_property~layer:"theme"("--"^name)css])inmatchqwith|Container_len(raw,_)|Container_len_cmp(_,raw,_)->of_bracketraw|Container_size(_,inner)|Container_scoped(_,inner)->container_query_theme_declsinner|Container_3xs|Container_2xs|Container_xs|Container_sm|Container_md|Container_lg|Container_xl|Container_2xl|Container_3xl|Container_4xl|Container_5xl|Container_6xl|Container_7xl|Container_theme_|Container_named_->[](* [@sm/main] aims a size query at the container named [main]. The name is the
tail after the last [/]; the head is any other container-query spelling. *)(* A size the project's [@theme] declared as [--container-<name>], read the
way the built-in scale is; the scale's own names stay built in. *)lettheme_container_sizethemename=matchcontainer_size_of_stringnamewith|Some_->None|None->ifParse.is_valid_theme_namename&&Option.is_some(Option.bind(Scheme.tokentheme("container-"^name))Css.parse_length)thenSome(Container_themename)elseNone(* Match ["<prefix>[<len>]"] and build a modifier from the parsed length. *)letbracketed_containersprefixmk=letplen=String.lengthprefixandslen=String.lengthsinifString.starts_with~prefix:(prefix^"[")s&&String.ends_with~suffix:"]"s&&slen>plen+2thenletraw=String.subs(plen+1)(slen-plen-2)inmatchparse_container_lengthrawwith|Somelen->Some(mkrawlen)|None->NoneelseNoneletrectry_container_query?(theme=Scheme.default)s=letbracketed=bracketed_containersin(* Match ["<prefix><size>"] against the named size scale, then against the
sizes the project declared. *)letsizedprefixcmp=letplen=String.lengthprefixinifString.starts_with~prefixs&&String.lengths>plenthenletname=String.subsplen(String.lengths-plen)inmatchcontainer_size_of_stringnamewith|Someq->Some(Container(Container_size(cmp,q)))|None->Option.map(funq->Container(Container_size(cmp,q)))(theme_container_sizethemename)elseNoneinifString.lengths<2||s.[0]<>'@'thenNoneelseList.find_map(funf->f())[(fun()->bracketed"@"(funrawlen->Container(Container_len(raw,len))));(fun()->bracketed"@min-"(funrawlen->Container(Container_len_cmp(Min,raw,len))));(fun()->bracketed"@max-"(funrawlen->Container(Container_len_cmp(Max,raw,len))));(fun()->sized"@min-"Min);(fun()->sized"@max-"Max);(fun()->Option.map(funq->Containerq)(theme_container_sizetheme(String.subs1(String.lengths-1))));(fun()->try_scoped_container_query~themes);]andtry_scoped_container_query?(theme=Scheme.default)s=matchString.rindex_opts'/'with|None->None|Somei->(letname=String.subs(i+1)(String.lengths-i-1)inifname=""thenNoneelselethead=String.subs0iinmatchmatchList.assoc_optheadsimple_modifierswith|Some_asm->m|None->try_container_query~themeheadwith|Some(Containerq)->Some(Container(Container_scoped(name,q)))|_->None)letcontainer_query_of_token?themetoken=matchList.assoc_opttokensimple_modifierswith|Some(Containerq)->Someq|Some_->None|None->(matchtry_container_query?themetokenwith|Some(Containerq)->Someq|Some_|None->None)(* The [--container-*] scale a size query reads, held the way the breakpoints
are: a size the [@theme] block removed names nothing, under [@min-], [@max-]
and a [/name] tail alike. *)letreccontainer_size_is_definedtheme=function|Container_size(_,inner)|Container_scoped(_,inner)->container_size_is_definedthemeinner|(Container_3xs|Container_2xs|Container_xs|Container_sm|Container_md|Container_lg|Container_xl|Container_2xl|Container_3xl|Container_4xl|Container_5xl|Container_6xl|Container_7xl|Container_theme_)asq->not(Scheme.is_removedtheme("container-"^Style.container_size_nameq))|Container_named_|Container_len_|Container_len_cmp_->true(* Parse a modifier string into a typed Style.modifier *)letrecparse_modifier~(theme:Scheme.t)s:modifieroption=letfns=[(fun()->lookup_simplethemes);(fun()->try_container_query~themes);(fun()->try_bracketed_modifiers);(fun()->try_aria_shorthands);(fun()->try_has_shorthands);(fun()->try_has_variant~themes);(fun()->try_numeric_nths);(fun()->try_compound_named_groups);(fun()->try_in_modifiers);(fun()->try_not_in_modifiers);(fun()->try_group_peer_nots);(fun()->try_group_peer_not_variant~themes);(* Before [try_not_modifier]: a container variant handles its own [not-]
(negating the structural condition), not the generic [Not] wrapper. *)(fun()->try_container_variantthemes);(fun()->try_not_modifierthemes);(fun()->try_bare_data_arias);(fun()->try_prose_elements);(fun()->try_custom_breakpoint(Scheme.breakpoint_namestheme)s);(fun()->try_custom_variantthemes);(* Last: a [not-] over any other variant negates the selector it produces.
The readings above come first because several [not-] spellings need
their own handling. *)(fun()->try_not_of_modifier~themes);(fun()->try_in_variant~themes);]inmatchList.find_map(funf->f())fnswith|Some(Containerq)whennot(container_size_is_definedthemeq)->None|m->m(* [group-not-has-[...]] and [peer-not-...]: the inner is any variant, read on
its own. The reading above only knows the simple state names and a bare
bracket. *)andtry_group_peer_not_variant~themes=lettry_prefixprefixmake=letplen=String.lengthprefixinifnot(String.starts_with~prefixs&&String.lengths>plen)thenNoneelseletbase,name=split_name(String.subsplen(String.lengths-plen))inmatchparse_modifier~themebasewith|Somemwhenscoped_negation_holdsm->Some(makemname)|Some_|None->Noneinmatchtry_prefix"group-not-"(funin->Group_not(i,n))with|Some_asr->r|None->try_prefix"peer-not-"(funin->Peer_not(i,n))(* [has-<variant>]: the argument is any variant, and that variant's own selector
goes inside [:has()]. The shorthand reading only knows the state names and a
bracket, so [has-peer-checked] fell through. *)andtry_has_variant~themes=ifString.starts_with~prefix:"has-"s&&String.lengths>4thenmatchparse_modifier~theme(String.subs4(String.lengths-4))with|Somemwhenhas_selectorm->Some(Has_variantm)|Some_|None->Noneelsematchtry_scoped_has_variant~theme"group-has-"swith|Some(m,name)->Some(Group_has_variant(m,name))|None->(matchtry_scoped_has_variant~theme"peer-has-"swith|Some(m,name)->Some(Peer_has_variant(m,name))|None->None)(* [group-has-<variant>] and [peer-has-<variant>] hold a variant the way [has-]
does. A state name or a bracket keeps the reading [try_has_shorthand] and the
bracket patterns give it. *)andtry_scoped_has_variant~themeprefixs=letn=String.lengthprefixinifString.starts_with~prefixs&&String.lengths>nthenletbase,name=split_name(String.subsn(String.lengths-n))inifbase=""||base.[0]='['||is_has_shorthandbasethenNoneelsematchparse_modifier~themebasewith|Somemwhenhas_selectorm->Some(m,name)|Some_|None->NoneelseNone(* A [not-] over any variant, including another negation: [not-not-md] is [md]
again. A negation with two halves has no single negation, so [not-not-hover]
reads as nothing, which is what Tailwind compiles for it. *)andtry_not_of_modifier~themes=ifnot(String.starts_with~prefix:"not-"s&&String.lengths>4)thenNoneelsematchparse_modifier~theme(String.subs4(String.lengths-4))with|Some(Notinner)wheninvolves_hoverinner->None|Somemwhenis_not_compatiblem->Some(Notm)|Some_|None->None(* [in-<variant>]: an ancestor the variant's selector matches. The readings
above know a bracket, a data attribute, a named group and the state names;
any other variant with a selector reads here, as [has-] does, and
[in-not-focus], [in-has-focus] and [in-group-hover] all read. *)andtry_in_variant~themes=ifString.starts_with~prefix:"in-"s&&String.lengths>3thenletrest=String.subs3(String.lengths-3)inmatchparse_modifier~themerestwith|Somemwhenhas_selectorm->Some(In_state(m,rest))|Some_|None->NoneelseNone(* Apply a list of modifier strings to a base utility *)letapply?(theme=Scheme.default)modifiersbase_utility=(* Convert utility to a list for wrapping *)letto_list=function|Utility.Groupstyles->styles|single->[single]in(* Apply a single parsed modifier to an accumulated utility *)letapply_oneaccmodifier_str=Option.bindacc(funu->Option.map(funm->wrapm(to_listu))(parse_modifier~thememodifier_str))in(* Apply modifiers in reverse order so that the first modifier in the string
(e.g., "dark" in "dark:hover:...") ends up as the outermost wrapper
(Modified(Dark, Modified(Hover, base))). This matches how the programmatic
API works: dark [ hover [ ... ] ] *)List.fold_leftapply_one(Somebase_utility)(List.revmodifiers)(** {1 Variant Ordering}
The Tailwind v4 cascade order of variants, written once. A {!Slot.t} names
one position in that order and {!Slot.rank} is the only place a number
appears; the three readings below - a modifier constructor, a class-name
token, a media condition a rule is nested in - each land on a slot rather
than on a scale of their own. Ordering lives here, in the module that owns
modifier semantics, rather than in the assembly pipeline. *)moduleSlot=struct(* One position in the variant cascade. Named separately from [Style.modifier]
because several modifiers share a position: every [group-*] spelling sorts
where [group-] sorts, and a [not-X] sorts where X does. *)typet=|Child(** [*:]: every direct child. *)|Descendant(** [**:]: every descendant. *)|Negation(** [not-X:], ordered inside the slot by the X it negates. *)|Group|Peer|Pseudo_first_letter|Pseudo_first_line|Pseudo_marker|Pseudo_selection|Pseudo_file|Pseudo_placeholder|Pseudo_backdrop|Pseudo_details_content|Pseudo_before|Pseudo_after|First|Last|Only|Odd|Even|First_of_type|Last_of_type|Only_of_type|Visited|Target|Open|Default|Checked|Indeterminate|Placeholder_shown|Autofill|Optional|Required|Valid|Invalid|User_valid|User_invalid|In_range|Out_of_range|Read_only|Read_write|Empty|Focus_within|Hover|Focus|Focus_visible|Active|Enabled|Disabled|Inert|Ancestor(** [in-*]: a state on an ancestor. *)|Has|Aria_named|Aria_arbitrary|Data_named|Data_arbitrary|Nth|Nth_last|Nth_of_type|Nth_last_of_type|Hocus|Supports|Motion_safe|Motion_reduce|Contrast_more|Contrast_less|Pointer|Any_pointer|Breakpoint(** Every [sm:]/[max-lg:]/[min-[32rem]:] spelling. *)|Portrait|Landscape|Ltr|Rtl|Dark|Print|Forced_colors|Noscript|Inverted_colors|Starting|Custom(** A [\@custom-variant] the theme registers. *)|Proseofstring(** [prose-X:], ordered by the element X names. *)|Arbitrary(** [[&>*]:], [not-[...]:] and the other bracket variants. *)|Container_query(* The cascade order itself. Every other function in this section maps onto a
slot; this is the only place a position is a number. The gaps leave room to
insert a slot without renumbering its neighbours. *)letrank=function|Child->100|Descendant->200|Negation->300|Group->500|Peer->600|Pseudo_first_letter->1000|Pseudo_first_line->1000|Pseudo_marker->1100|Pseudo_selection->1200|Pseudo_file->1300|Pseudo_placeholder->1400|Pseudo_backdrop->1401|Pseudo_details_content->1500|Pseudo_before->1600|Pseudo_after->1601|First->10100|Last->10200|Only->10300|Odd->10400|Even->10500|First_of_type->10600|Last_of_type->10700|Only_of_type->10800|Visited->10900|Target->11000|Open->11100|Default->11200|Checked->11300|Indeterminate->11400|Placeholder_shown->11500|Autofill->11600|Optional->11700|Required->11800|Valid->11900|Invalid->12000|User_valid->12010|User_invalid->12020|In_range->12100|Out_of_range->12200|Read_only->12300|Read_write->12310|Empty->12400|Focus_within->12500|Hover->20000|Focus->30100|Focus_visible->30200|Active->30300|Enabled->30400|Disabled->30500|Inert->30550|Ancestor->30560|Has->30600|Aria_named->30700|Aria_arbitrary->30790|Data_named->30800|Data_arbitrary->30810|Nth->30900|Nth_last->30910|Nth_of_type->30920|Nth_last_of_type->30930|Hocus->35000|Supports->40000|Motion_safe->50000|Motion_reduce->50100|Contrast_more->50200|Contrast_less->50300|Breakpoint->60000|Container_query->65000|Portrait->70000|Landscape->70100|Ltr->80000|Rtl->80100|Dark->90000|Starting->90500|Print->91000|Forced_colors->92000|Inverted_colors->93100|Pointer->93200|Any_pointer->93300|Noscript->93400|Custom->95500|Proseelement->prose_element_variant_orderelement|Arbitrary->100000end(* The slot a class-name token sorts in, or [None] when the token names no
variant this table knows. The token is one modifier of a class name, the part
between two ":" (["hover"], ["group-has-checked"], ["@min-[64rem]"]). *)letslot_of_prefixprefix:Slot.toption=letstarts_withprefix'=String.starts_with~prefix:prefix'prefixinmatchprefixwith|"*"->SomeSlot.Child|"**"->SomeSlot.Descendant|"first-letter"->SomeSlot.Pseudo_first_letter|"first-line"->SomeSlot.Pseudo_first_line|"marker"->SomeSlot.Pseudo_marker|"selection"->SomeSlot.Pseudo_selection|"file"->SomeSlot.Pseudo_file|"placeholder"->SomeSlot.Pseudo_placeholder|"backdrop"->SomeSlot.Pseudo_backdrop|"details-content"->SomeSlot.Pseudo_details_content|"before"->SomeSlot.Pseudo_before|"after"->SomeSlot.Pseudo_after|"first"->SomeSlot.First|"last"->SomeSlot.Last|"only"->SomeSlot.Only|"odd"->SomeSlot.Odd|"even"->SomeSlot.Even|"first-of-type"->SomeSlot.First_of_type|"last-of-type"->SomeSlot.Last_of_type|"only-of-type"->SomeSlot.Only_of_type|"visited"->SomeSlot.Visited|"target"->SomeSlot.Target|"open"->SomeSlot.Open|"default"->SomeSlot.Default|"checked"->SomeSlot.Checked|"indeterminate"->SomeSlot.Indeterminate|"placeholder-shown"->SomeSlot.Placeholder_shown|"autofill"->SomeSlot.Autofill|"optional"->SomeSlot.Optional|"required"->SomeSlot.Required|"valid"->SomeSlot.Valid|"invalid"->SomeSlot.Invalid|"user-valid"->SomeSlot.User_valid|"user-invalid"->SomeSlot.User_invalid|"in-range"->SomeSlot.In_range|"out-of-range"->SomeSlot.Out_of_range|"read-only"->SomeSlot.Read_only|"read-write"->SomeSlot.Read_write|"empty"->SomeSlot.Empty|"focus-within"->SomeSlot.Focus_within|"hover"->SomeSlot.Hover|"focus"->SomeSlot.Focus|"focus-visible"->SomeSlot.Focus_visible|"active"->SomeSlot.Active|"enabled"->SomeSlot.Enabled|"disabled"->SomeSlot.Disabled|"inert"->SomeSlot.Inert|"data-custom"|"data-active"|"data-inactive"->SomeSlot.Data_named|"hocus"|"device-hocus"->SomeSlot.Hocus|"motion-safe"->SomeSlot.Motion_safe|"motion-reduce"->SomeSlot.Motion_reduce|"contrast-more"->SomeSlot.Contrast_more|"contrast-less"->SomeSlot.Contrast_less|"portrait"->SomeSlot.Portrait|"landscape"->SomeSlot.Landscape|"ltr"->SomeSlot.Ltr|"rtl"->SomeSlot.Rtl|"dark"->SomeSlot.Dark|"print"->SomeSlot.Print|"forced-colors"->SomeSlot.Forced_colors|"noscript"->SomeSlot.Noscript|"inverted-colors"->SomeSlot.Inverted_colors|"starting"->SomeSlot.Starting|"sm"|"md"|"lg"|"xl"|"2xl"->SomeSlot.Breakpoint|_whenstarts_with"group-"->SomeSlot.Group|_whenstarts_with"peer-"->SomeSlot.Peer|_whenstarts_with"has-"->SomeSlot.Has|_whenstarts_with"aria-"->(* [aria-] with nothing after it, and [aria-[expr]], take the arbitrary
position; the shorthand names take the named one. *)ifString.lengthprefix>5&&prefix.[5]<>'['thenSomeSlot.Aria_namedelseSomeSlot.Aria_arbitrary|_whenstarts_with"data-"->ifString.lengthprefix>5&&prefix.[5]='['thenSomeSlot.Data_arbitraryelseSomeSlot.Data_named|_whenstarts_with"supports"->SomeSlot.Supports|_whenstarts_with"pointer-"->SomeSlot.Pointer|_whenstarts_with"any-pointer-"->SomeSlot.Any_pointer|_whenstarts_with"min-"||starts_with"max-"->SomeSlot.Breakpoint(* [nth-3], [nth-last-of-type-2]: the count is the argument, not part of the
variant name. Longest spelling first, so [nth-last-of-type-] is not read as
an [nth-last-] with a strange argument. *)|_whenstarts_with"nth-last-of-type-"->SomeSlot.Nth_last_of_type|_whenstarts_with"nth-of-type-"->SomeSlot.Nth_of_type|_whenstarts_with"nth-last-"->SomeSlot.Nth_last|_whenstarts_with"nth-"->SomeSlot.Nth(* [in-focus], [in-data-open], [in-[.foo]]: a state on an ancestor. The
pseudo-classes that merely start with the same letters ([in-range],
[inert], [invalid]) are matched by name above. *)|_whenstarts_with"in-"->SomeSlot.Ancestor(* Every [not-X] sorts together, ahead of the variants it can negate;
[variant_inner_order] puts the X back inside the slot. *)|_whenstarts_with"not-"->SomeSlot.Negation|_whenstarts_with"prose-"->Some(Slot.Prose(String.subprefix6(String.lengthprefix-6)))|_whenstarts_with"["->SomeSlot.Arbitrary|_whenstarts_with"@"->SomeSlot.Container_query|_->None(* The slot a media condition sorts in. A rule can take its position from the
query it is nested in rather than from its class name: [dark:group-hover]
writes a [(hover: hover)] query, and sorts where a bare [hover] sorts. *)letslot_of_media_cond(cond:Css.Media.t):Slot.toption=letopenCss.Mediainmatchcondwith|Cond(Feature(Plain(Hover,IdentHover)))->SomeSlot.Hover|Cond(Feature(Plain(Prefers_reduced_motion,IdentNo_preference)))->SomeSlot.Motion_safe|Cond(Feature(Plain(Prefers_reduced_motion,IdentReduce)))->SomeSlot.Motion_reduce|Cond(Feature(Plain(Prefers_contrast,IdentMore)))->SomeSlot.Contrast_more|Cond(Feature(Plain(Prefers_contrast,IdentLess)))->SomeSlot.Contrast_less|Cond(Feature(Plain(Orientation,IdentPortrait)))->SomeSlot.Portrait|Cond(Feature(Plain(Orientation,IdentLandscape)))->SomeSlot.Landscape|Cond(Feature(Plain(Prefers_color_scheme,(IdentDark|IdentLight))))->SomeSlot.Dark|Type{prefix=None;type_=Print;trailing=None}->SomeSlot.Print|Cond(Feature(Plain(Forced_colors,IdentActive)))->SomeSlot.Forced_colors|Cond(Feature(Plain(Inverted_colors,IdentInverted)))->SomeSlot.Inverted_colors|_->None(* The variant wrapped by one compound token, if any. Keeping this extraction in
one place lets ordering follow group-not-has-peer-not-data-active all the way
to its data predicate instead of stopping at the first [not] or [has]
slot. *)letvariant_inner_tokentoken=letaftern=String.subtokenn(String.lengthtoken-n)inletanchor_innern=letinner=afternin(* A slash inside an arbitrary selector belongs to the selector. Named
simple states have no brackets, so their suffix can be removed here. *)ifString.containsinner'['theninnerelsefst(split_nameinner)inifString.starts_with~prefix:"group-"tokenthenSome(anchor_inner6)elseifString.starts_with~prefix:"peer-"tokenthenSome(anchor_inner5)elseifString.starts_with~prefix:"not-"tokenthenSome(after4)elseifString.starts_with~prefix:"has-"tokenthenSome(after4)else(* [in-focus] names a state on an ancestor; [in-range] names one on the
element itself, and the table matches that by name. *)matchslot_of_prefixtokenwith|SomeSlot.Ancestor->Some(after3)|Some_|None->Noneletvariant_order_of_prefix?themeprefix=matchthemewith|SomethemewhenOption.is_some(try_custom_variantthemeprefix)->(* Tailwind registers [dark] before it reads [@custom-variant]. Replacing
that exact registration changes its body but retains its slot. *)ifString.equalprefix"dark"thenSlot.rankSlot.DarkelseSlot.rankSlot.Custom(* A breakpoint the project declared, [xs:] under [--breakpoint-xs], sorts
with the built-in ones; the table above knows the built-in names alone. *)|SomethemewhenScheme.has_breakpointthemeprefix->Slot.rankSlot.Breakpoint|Some_|None->(matchslot_of_prefixprefixwithSomeslot->Slot.rankslot|None->0)letrecvariant_inner_order_path?themetoken=matchvariant_inner_tokentokenwith|None->[]|Someinner->letorder=variant_order_of_prefix?themeinneriniforder=0then[]elseorder::variant_inner_order_path?themeinner(* The first wrapped slot retained for callers that only need the immediate
compound variant. *)letvariant_inner_ordertoken=matchvariant_inner_order_pathtokenwithorder::_->order|[]->0letvariant_order_of_media_condcond=matchslot_of_media_condcondwithSomeslot->Slot.rankslot|None->0