12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182(** CSS Selectors - types and pretty printing *)openSyntaxincludeSelector_intfletpp_component_values=Values.pp_component_valuesletread_component_values=Values.read_component_values(** Helper function for invalid identifiers *)leterr_invalid_identifiernamereason=invalid_arg(String.concat""["CSS identifier '";name;"' ";reason])(* CSS identifier validation functions *)letis_valid_nmstartc=(c>='a'&&c<='z')||(c>='A'&&c<='Z')||c='_'||Char.codec>127||c='\\'let is_valid_nmcharc=is_valid_nmstartc||(c>='0'&&c<='9')||c='-'letpp_attr_flagctx=function|SomeInsensitive->Pp.charctx' ';Pp.charctx'i'|SomeSensitive->Pp.charctx' ';Pp.charctx's'|None->()(* CSS Selectors 4 sec. 6.2 accepts an [<ident-token>] or [<string-token>] as an
attribute match value. Quotes can drop only when this text is a CSS ident. *)letattr_value_needs_quotingvalue=ifvalue=""thentrueelseletfirst=value.[0]in(* Numbers need quoting; double-dash identifiers are valid CSS idents. *)iffirst>='0'&&first<='9'thentrueelse(* Must quote if contains non-identifier characters *)not(String.for_all(function|'a'..'z'|'A'..'Z'|'0'..'9'|'-'|'_'->true|cwhenChar.codec>=0xA0->true(* Unicode chars U+00A0 and higher *)|_->false)value)(* Helper to pretty-print attribute values with smart quoting. *)letpp_quoted_attr_valuequotectxvalue=Pp.charctxquote;String.iter(func->ifc=quotethen(Pp.charctx'\\';Pp.char ctxc)elsePp.charctxc)value;Pp.char ctxquoteletpp_attr_value?quotectxvalue=(* Under minify, drop the surrounding quotes when the value is a CSS ident
since CSS Selectors 4 sec. 6.2 accepts the ident and string forms at the
same grammar position and the bare form is shorter. The non-minified path
keeps the quotes for source fidelity - the user who wrote [type="text"]
expects to read [type="text"] back, even if [type=text] would parse the
same way. *)ifString.containsvalue'\\'thenPp.stringctxvalueelseifPp.minifiedctx&¬(attr_value_needs_quotingvalue)thenPp.stringctxvalueelsematchquote with|Somequotewhennot(Pp.minifiedctx)->pp_quoted_attr_valuequotectxvalue|_->Pp.quoted_stringctxvalue(* Helper to print a token with pretty spacing when not minifying. *)letpp_token:stringPp.t=functxtoken->ifPp.minifiedctxthenPp.stringctxtokenelse(Pp.spacectx();Pp.stringctxtoken;Pp.spacectx())letpp_attribute_match :attribute_matchPp.t=functx->function|Presence->()|Exactvalue->Pp.charctx'=';pp_attr_valuectxvalue|Exact_quoted(value,quote)->Pp.charctx'=';pp_attr_value~quotectxvalue|Whitespace_listvalue->Pp.stringctx"~=";pp_attr_valuectxvalue|Whitespace_list_quoted(value,quote)->Pp.stringctx"~=";pp_attr_value~quotectxvalue|Hyphen_listvalue->Pp.stringctx"|=";pp_attr_valuectxvalue|Hyphen_list_quoted (value,quote)->Pp.stringctx"|=";pp_attr_value~quotectxvalue|Prefixvalue->Pp.stringctx"^=";pp_attr_valuectxvalue|Prefix_quoted(value,quote)->Pp.stringctx"^=";pp_attr_value ~quotectxvalue|Suffixvalue->Pp.stringctx"$=";pp_attr_valuectxvalue|Suffix_quoted(value,quote)->Pp.stringctx"$=";pp_attr_value ~quotectxvalue|Substringvalue->Pp.stringctx"*=";pp_attr_valuectxvalue|Substring_quoted(value,quote)->Pp.stringctx"*=";pp_attr_value ~quotectx valueletskip_css_escapenamei=(* Skip escaped sequence: either next char or up to 6 hex digits + optional
space *)letlen =String.lengthnameinincri;if!i>=lenthen()elseletstart=!iinlet recconsume_hex n=ifn=6|| !i>=len then()elseifis_hex name.[!i]then(incri;consume_hex(n+1))inconsume_hex0;if!i=startthenincri(* single escaped char *)elseif!i<len&&name.[!i]=' 'thenincriletvalidate_identifier_startname=ifString.lengthname=0thenerr_invalid_identifiername"cannot be empty";letfirst_char=name.[0]iniffirst_char>='0'&&first_char<='9'thenerr_invalid_identifiername"cannot start with digit";ifString.lengthname>=2then(ifCustom_property_name.has_prefixnamethenerr_invalid_identifier name"cannot start with '--' (reserved for custom properties)";ifname.[0]='-'&&name.[1]>='0'&&name.[1]<='9'thenerr_invalid_identifiername"cannot start with '-' followed by digit")letidentifier_char_valididxc=ifidx=0thenis_valid_nmstartc||c='-'elseis_valid_nmcharcletinvalid_identifier_char_messagecidx=String.concat""["contains invalid character '";String.make1c;"' at position ";Int.to_stringidx;]letvalidate_css_identifiername=validate_identifier_startname;letlen=String.lengthnameinleti=ref0inwhile!i<lendoletc=name.[!i]inifc='\\'thenskip_css_escapenameielseletidx=!iinif(not(identifier_char_valididxc))&&Char.codec<=127thenerr_invalid_identifier name(invalid_identifier_char_messagecidx);incridoneletelement?nsname=validate_css_identifiername;Element(ns,name)(* Looser validation for class names: allow Tailwind-style tokens and escape at
print time. Only reject control/unprintable characters and double-dash prefix. *)letvalidate_serializable_class name=ifString.length name=0thenerr_invalid_identifiername"cannot be empty";ifCustom_property_name.has_prefixnamethenerr_invalid_identifiername"cannot start with '--' (reserved for custom properties)";String.iter(func->letcode=Char.codecinifcode<0x20||code=0x7Fthenerr_invalid_identifiername"contains control character")namelet class_name=validate_serializable_classname;Classnameletidname=validate_serializable_classname;Idnameletuniversal=UniversalNoneletuniversal_nsns=Universal(Somens)(** Parse an ARIA attribute name into its structured type *)letaria_attr_of_string :string->aria_attr=Aria.of_string(** Categorize an attribute name into its structured type *)letattr_name_of_string name=letlen=String.lengthnameiniflen>5&&String.subname05="aria-"thenmatcharia_attr_of_stringnamewith|attr->Ariaattr|exceptionInvalid_argument_->Regularnameelseiflen>5&&String.subname05="data-"thenData(String.subname5(len-5))elseRegularname(** Convert attr_name back to string for printing *)letstring_of_aria_attr:aria_attr->string=Aria.to_stringletstring_of_attr_name=function|Ariaa->string_of_aria_attra|Datas->"data-"^s|Regulars->sletpp_aria_attr:aria_attrPp.t=functxa->Pp.stringctx(string_of_aria_attra)letread_aria_attrt:aria_attr=lets=Cursor.identtinmatcharia_attr_of_stringswith|v->v|exceptionInvalid_argumentmsg->Cursor.errtmsgletpp_attr_name:attr_namePp.t=functxa->Pp.stringctx(string_of_attr_namea)letread_attr_namet:attr_name=lets=Cursor.identtinattr_name_of_stringsletattribute?ns?flagnamematch_type=validate_css_identifier name;letattr_name=attr_name_of_stringnameinAttribute(ns,attr_name,match_type,flag)(* Convenience: build a class selector from a raw class token. Escaping happens
in [pp]/[to_string]. Equivalent to [class_]. *)(* Convert a hex digit character to its integer value *)letint_of_hexc=matchcwith|'0'..'9'->Char.codec-Char.code'0'|'a'..'f'->Char.codec-Char.code'a'+10|'A'..'F'->Char.codec-Char.code'A'+10|_->invalid_arg"not a hex digit"(* Unescape CSS escapes per CSS Syntax 3 sec. 4.3.7: \XX...XX (1-6 hex) or \X
(any char). Handles both hex escapes (e.g., \3A for ':') and simple escapes
(e.g., \:). *)(* Helper to process hex escape sequences. Returns (codepoint, next_index) *)letprocess_hex_escapesilen=letrecconsume_hexaccnidx=ifn=6||idx>=len||not(is_hexs.[idx])then(acc,idx)elseconsume_hex((acc*16)+int_of_hexs.[idx])(n+1)(idx+1)inletcodepoint,next_idx=consume_hex00(i+1)in(* Skip optional whitespace after hex escape *)letfinal_idx=ifnext_idx<len&&s.[next_idx]=' 'thennext_idx+1elsenext_idxin(codepoint,final_idx)letunescape_selector_names=letlen=String.lengthsinletbuf=Buffer.createleninletprocess_escapei=ifi+1>=lenthen(* Trailing backslash - ignore *)lenelseifis_hexs.[i+1]then(letcodepoint,final_idx=process_hex_escape silenin(* CSS Syntax 3 (ED) sec. 4.3.7: U+0000, surrogates, and out-of-range code
points are replaced with U+FFFD rather than passed through. *)letcp=ifcodepoint<=0||codepoint>0x10FFFF||(codepoint>=0xD800&&codepoint<=0xDFFF)then0xFFFDelsecodepointinBuffer.add_utf_8_ucharbuf(Uchar.of_intcp);final_idx)else((* Simple escape: just take the next character literally *)Buffer.add_charbufs.[i+1];i+2)inletrecloopi=ifi>=lenthen()elseifs.[i]<>'\\'then(Buffer.add_charbufs.[i];loop(i+1))elseloop(process_escapei)inloop0;Buffer.contentsbuf(* Simple readers that don't need recursion *)letensure_call_donetname=Cursor.wst;ifnot(Cursor.is_donet)thenCursor.errt("unexpected tokens after "^name)letread_lang_ranget=matchCursor.ident_opttwith|Somelang->lang|None->(matchCursor.string_opttwith|Somelang->lang|None->Cursor.err_expectedt"language range")letread_lang_contentt=letlangs=Cursor.list~sep:Cursor.comma~at_least:1read_lang_rangetinensure_call_donet"lang";Langlangs(* CSS Values 4 sec. 4.1 interprets a keyword ASCII case-insensitively, so the
two directionalities Selectors 4 sec. 7.1 names reach one node however they
are spelled. Any other identifier is no keyword, so it keeps its case. *)letdir_keywordident=matchString.lowercase_asciiidentwith|("ltr"|"rtl")askeyword->keyword|_->identletread_dir_contentt=(* Selectors 4 sec. 7.1: the argument is a single identifier, and one other
than [ltr] or [rtl] "is not invalid, but does not match anything". Keeping
it verbatim also leaves room for a directionality a later markup spec
defines. *)letdir=dir_keyword(Cursor.identt)inensure_call_donet"dir";Dirdirletread_state_contentt=letname=Cursor.identtinensure_call_donet"state";Statenameletread_heading_contentt=ensure_call_donet"heading";Headingletread_active_view_transition_contentt=letnames=Cursor.list~sep:Cursor.comma~at_least:1Cursor.identtinensure_call_donet"active view transition type";Active_view_transition_type(Somenames)letread_langt=Cursor.call"lang"tread_lang_contentletread_dirt=Cursor.call"dir"tread_dir_contentletread_statet=Cursor.call"state"tread_state_contentletread_headingt=Cursor.call"heading"tread_heading_contentletread_active_view_transition_typet=Cursor.call"active-view-transition-type"tread_active_view_transition_contentletread_part_contentt=(* CSS Shadow 1 section 5.4 [::part()]: a whitespace-separated list of ident
tokens, *not* comma-separated. *)letrecread_identsacc=Cursor.wst;ifCursor.is_donetthenList.revaccelseletname=Cursor.identtinread_idents(name::acc)inletidents=read_idents[]inifidents=[]thenCursor.err_expectedt"part name"elsePartidentsletread_partt=Cursor.call"part"tread_part_contentletreccombines1combs2=matchs2with|Listselectors->(* When combining with a List, distribute the combinator over each
element *)List(List.map(combines1comb)selectors)|_->(* For all other cases, create a Combined node *)Combined(s1,comb,s2)let(++)s1s2=combines1Descendants2let(>>)s1s2=combines1Childs2letwhereselectors=Whereselectorsletlistselectors=matchselectorswith|[]->invalid_arg"CSS selector list cannot be empty"|_->Listselectorsletunescape_attribute_value=unescape_selector_nameletis_compound_list=functionList_->true|_->falseletas_list=functionListsels->Somesels|_->Noneletcompoundselectors=Compoundselectorsleterr_expectedtwhat=Cursor.err_expectedtwhat(** Parse attribute value (quoted or unquoted) *)letread_attribute_value_identts(loc:Loc.t)=Cursor.skipt;letraw=matchCursor.sourcetwith|Somesource->Some(String.subsourceloc.start_pos(loc.end_pos-loc.start_pos))|None->NoneinmatchrawwithSomerawwhenString.containsraw'\\'->raw|_->sletformat_attribute_numbern(unit:stringoption)=matchunitwith|None->ifFloat.is_integernthenstring_of_int(int_of_floatn)elsestring_of_floatn|Someu->ifFloat.is_integernthenstring_of_int(int_of_floatn)^uelsestring_of_floatn^uletread_attribute_value_unquotedt=matchCursor.peektwith|Some(Component.Preserved{kind=Token.Idents;loc;_})->read_attribute_value_identtsloc|Some(Component.Preserved{kind=Token.Number_tok_;_})|Some(Component.Preserved{kind=Token.Dimension_;_})|Some(Component.Preserved{kind=Token.Percentage_;_})->letn,unit=Cursor.number_with_unittinformat_attribute_numbernunit|_->""letread_attribute_valuet=(* Check if we start with a quote - if so, we MUST parse as quoted string *)letvalue,quote=matchCursor.string_with_quote_opttwith|Some(s,quote)->(s,Somequote)|None->(read_attribute_value_unquotedt,None)in(* CSS spec allows empty quoted strings but not empty unquoted values *)ifvalue=""&&Option.is_nonequotethenmatchCursor.peektwith|None->Cursor.err_expected_but_eoft"']'"|Some_->Cursor.err_invalidt"attribute value"else(value,quote)(* Selectors 4 sec. 5.1 spells a class as a [.] "immediately followed by" an
ident, and sec. 3.5 a pseudo-class or pseudo-element as its colons followed
by the name. Every other reader has the cursor skip whitespace for it, so
these three look for it themselves: [. x] names no class, and reading past
the space gave the author [.x]. *)letexpect_adjacenttwhat=matchCursor.peek_rawtwith|SomecwhenComponent.is_whitespacec->Cursor.err_invalidt(String.concat""["whitespace before the ";what])|_->()(** Parse a class selector (.classname) *)letread_classt=Cursor.expect'.'t;expect_adjacentt"class name";letname=Cursor.ident~keep_case:truetin(* No validation needed: Cursor.ident already enforces CSS identifier syntax,
including parser-valid double-dash identifiers such as .--x. *)Classname(** Parse an ID selector ([#id]). Per CSS Selectors sec. 6.7, an ID must be an
ident-type hash; unrestricted hashes such as digit-only [#123] are not valid
IDs. *)letread_idt=matchCursor.peektwith|Some(Component.Preserved{kind=Token.Hash{value;hash_flag=Token.Id};_})->Cursor.skipt;Idvalue|Some(Component.Preserved{kind=Token.Hash_;_})->Cursor.err_invalidt"expected identifier"|_->Cursor.err_expectedt"'#'"letpeek_is_namespaced_namet=matchCursor.peektwith|Some(Component.Preserved{kind=Token.Ident_;_})->true|Some(Component.Preserved{kind=Token.Delim"*";_})->true|_->falseletlookahead_bare_pipe_nst=(* Bare ['|'] selects the default (no) namespace. Selectors Level 4 section
6.2 distinguishes [[|attr]] from [[attr]]: the former explicitly matches
the empty namespace, the latter matches any. *)Cursor.lookahead(funt->matchCursor.peek_delimtwith|Some'|'->let_=Cursor.nexttin(* Reject ['|='] (the dash-match operator). *)ifCursor.peek_delimt=Some'='thenfalseelse(* Bare ['|'] is only a namespace prefix when a namespaced element
or attribute name follows it. *)peek_is_namespaced_namet|_->false)t(* CSS Selectors 4 sec. 15.2: [||] is the column combinator, so a pipe opening
one never separates a namespace prefix from a name. *)letat_column_combinatort=Cursor.lookahead(funt->Cursor.try_kind_pair(Token.Delim"|")(Token.Delim"|")t)tletread_prefixed_nst=letp=Cursor.ident~keep_case:truetin(* Avoid treating '|=' as a namespace separator: peek for the pair. *)letis_eq_pair=Cursor.lookahead(funt->Cursor.try_kind_pair(Token.Delim"|")(Token.Delim"=")t)tinifis_eq_pair||at_column_combinatortthenCursor.errt"not a namespace";Cursor.expect'|'t;Prefixpletread_ns_innert=ifCursor.try_kind_pair(Token.Delim"*")(Token.Delim"|")tthen(* Another pipe means [*||td]: the universal selector, then a column. *)ifCursor.peek_delimt=Some'|'thenCursor.errt"not a namespace"elseAnyelseiflookahead_bare_pipe_nstthen(Cursor.expect'|'t;None)elseread_prefixed_nstletread_nst:nsoption=Cursor.optionread_ns_innert(** Parse a namespaced type or universal selector *)letread_type_or_universalt=letns=read_nstin(* Now read the selector itself *)matchCursor.peek_delimtwith|Some'*'->(Cursor.skipt;matchnswithNone->universal|Somens->universal_nsns)|_->(letname=Cursor.ident~keep_case:truetin(* Cursor.ident is the parser contract here. Constructors can keep their
stricter policy, but parsed CSS identifiers such as --x are valid. *)matchnswith|None->Element(None,name)|Somens->Element(Somens,name))(** Parse attribute selector [attr] or [attr=value] *)lettry_shadow_piercingt=(* Legacy [>>>]: three consecutive [>] delims; emit Shadow_piercing only when
the entire run matches. *)letsnap=Cursor.savetinifCursor.try_kind(Token.Delim">")t&&Cursor.try_kind(Token.Delim">")t&&Cursor.try_kind(Token.Delim">")tthentrueelse(Cursor.restoretsnap;false)lettry_shadow_deept=(* Legacy [/deep/]: [/] [ident "deep"] [/]. *)letsnap=Cursor.savetinifCursor.try_kind(Token.Delim"/")t&&Cursor.try_ident"deep"t&&Cursor.try_kind(Token.Delim"/")tthentrueelse(Cursor.restoretsnap;false)letread_combinatort=iftry_shadow_piercingtthenShadow_piercingelseiftry_shadow_deeptthenShadow_deepelsematchCursor.peek_delimtwith|Some'>'->Cursor.skipt;Child|Some'+'->Cursor.skipt;Next_sibling|Some'~'->Cursor.skipt;Subsequent_sibling|Some'|'whenCursor.try_kind_pair(Token.Delim"|")(Token.Delim"|")t->Column|Some'!'->Cursor.errt"invalid combinator character"|NonewhenCursor.is_donet->Cursor.errt"empty combinator"|_->Descendantletattribute_matchconscons_quoted(value,quote)=matchquotewithSomequote->cons_quotedvaluequote|None->consvaluelettry_attribute_opcconscons_quotedt:attribute_matchoption=ifCursor.try_kind_pair(Token.Delim(String.make1c))(Token.Delim"=")tthenSome(attribute_matchconscons_quoted(read_attribute_valuet))elseNonelettry_whitespace_list_matcht=try_attribute_op'~'(funv->Whitespace_listv)(funvq->Whitespace_list_quoted(v,q))tlettry_hyphen_list_matcht=try_attribute_op'|'(funv->Hyphen_listv)(funvq->Hyphen_list_quoted(v,q))tlettry_prefix_matcht=try_attribute_op'^'(funv->Prefixv)(funvq->Prefix_quoted(v,q))tlettry_suffix_matcht=try_attribute_op'$'(funv->Suffixv)(funvq->Suffix_quoted(v,q))tlettry_substring_matcht=try_attribute_op'*'(funv->Substringv)(funvq->Substring_quoted(v,q))tletread_exact_or_presencet=ifCursor.peek_delimt=Some'='then(Cursor.skipt;attribute_match(funv->Exactv)(funvq->Exact_quoted(v,q))(read_attribute_valuet))elsePresenceletread_attribute_matcht:attribute_match=lettry_ops=[try_whitespace_list_match;try_hyphen_list_match;try_prefix_match;try_suffix_match;try_substring_match;]inletrecfind=function|[]->read_exact_or_presencet|op::rest->(matchoptwithSomev->v|None->findrest)infindtry_opsletread_attr_flagt:attr_flagoption=Cursor.wst;Cursor.option(funt->matchCursor.ident_opttwith|SomeswhenString.lowercase_asciis="i"->Insensitive|SomeswhenString.lowercase_asciis="s"->Sensitive|Somes->Cursor.errt~got:s"'i' or 's'"|None->Cursor.err_unexpectedt)tletread_attributet=Cursor.brackets(funinner->Cursor.wsinner;letns=read_nsinnerinletattr=Cursor.ident~keep_case:trueinnerinCursor.wsinner;letmatcher=read_attribute_matchinnerinCursor.wsinner;letflag=read_attr_flaginnerinCursor.wsinner;ifnot(Cursor.is_doneinner)thenCursor.err_invalidinner"trailing tokens in attribute selector";letattr_name=attr_name_of_stringattrinAttribute(ns,attr_name,matcher,flag))t(** Parse the An+B microsyntax (Selectors 4 section 13.3.1 / CSS Syntax 3 (ED)
section 6) as shape patterns over the component stream: [odd]/[even], bare
[<integer>], [<n-dimension>] with optional offset, the [5n-5]/[5n-] token
variants, and the [n]/[-n]/[n-5]/... ident forms with an optional leading
[+]. Idents are case-insensitive (CSS Values 4 sec. 4.1); [+ n] (whitespace
after [+]) is invalid since [+] is lexically part of the ident form. *)(* Numeric helpers: split an arbitrary ident's tail into an optional [-digits]
suffix, for ndashdigit / ndash / n patterns. *)letall_digitss=String.lengths>0&&String.for_all(func->c>='0'&&c<='9')sletndashdigit_bunit_=letn=String.lengthunit_inifn>=3&&Char.lowercase_asciiunit_.[0]='n'&&unit_.[1]='-'thenlettail=String.subunit_2(n-2)inifall_digitstailthenint_of_string_opttailelseNoneelseNoneletis_ndashunit_=String.lengthunit_=2&&Char.lowercase_asciiunit_.[0]='n'&&unit_.[1]='-'letis_n_unitunit_=String.lengthunit_=1&&Char.lowercase_asciiunit_.[0]='n'letdashndashdigit_bident=letn=String.lengthidentinifn>=4&&ident.[0]='-'&&Char.lowercase_asciiident.[1]='n'&&ident.[2]='-'thenlettail=String.subident3(n-3)inifall_digitstailthenint_of_string_opttailelseNoneelseNoneletis_n_identident=String.lowercase_asciiident="n"letis_neg_n_identident=String.lowercase_asciiident="-n"letis_ndash_identident=String.lengthident=2&&Char.lowercase_asciiident.[0]='n'&&ident.[1]='-'letis_dashndash_identident=String.lengthident=3&&ident.[0]='-'&&Char.lowercase_asciiident.[1]='n'&&ident.[2]='-'(* A signed number's repr starts with [+] or [-]; signless means it doesn't. *)letrepr_is_signed(number:Token.number)=letr=number.reprinString.lengthr>0&&(r.[0]='+'||r.[0]='-')letinteger_tokent(number:Token.number)=matchToken.integer_optnumberwith|Somevalue->value|Nonewhennumber.number_flag=Token.Integer->Cursor.err_invalidt"integer outside supported range"|None->Cursor.err_expectedt"integer"(* Parse a [<signless-integer>] (no leading [+]/[-] in the token repr). *)letread_signless_integert=matchCursor.peektwith|Some(Component.Preserved{kind=Token.Number_tokn;_})whennot(repr_is_signedn)->letvalue=integer_tokentninCursor.skipt;value|_->Cursor.err_expectedt"signless integer"(* Parse a [<signed-integer>]: a single number token whose repr starts with [+]
or [-]. *)letread_signed_integer_optt=matchCursor.peektwith|Some(Component.Preserved{kind=Token.Number_tokn;_})whenrepr_is_signedn&&n.number_flag=Token.Integer->(matchToken.integer_optnwith|Somevalue->Cursor.skipt;Somevalue|None->None)|_->None(* After an [<n-dimension>] or n-ident, consume the optional offset tail: -
nothing (EOF / comma / close-paren in enclosing context), -
[<signed-integer>] (a single signed number), - ['+' | '-']
[<signless-integer>] (explicit operator + unsigned int). *)letread_an_tailt=matchread_signed_integer_opttwith|Somen->n|None->(matchCursor.peek_delimtwith|Some'+'->Cursor.skipt;read_signless_integert|Some'-'->Cursor.skipt;-read_signless_integert|_->0)(* Reject a leading [+] that is separated from the following ident by
whitespace. Per the grammar, the ['+'? n] form does not admit whitespace
between the [+] and [n]. *)letensure_no_ws_after_plust=matchCursor.peek_rawtwith|Some(Component.Preserved{kind=Token.Whitespace_;_})->Cursor.err_invalidt"whitespace after '+'"|_->()(* Dimension forms [<n-dimension>, <ndashdigit-dimension>, <ndash-dimension>]
from CSS Syntax 3 (ED) section 6.2. Assumes the cursor is positioned on a
[Dimension] component. *)letread_nth_dimensiontnumberunit_=letcoefficient=integer_tokentnumberinifis_n_unitunit_then(Cursor.skipt;An_plus_b(coefficient,read_an_tailt))elsematchndashdigit_bunit_with|Someb->Cursor.skipt;An_plus_b(coefficient,-b)|None->ifis_ndashunit_then(Cursor.skipt;An_plus_b(coefficient,-read_signless_integert))elseCursor.err_expectedt"An+B dimension (n / n-N / n-)"(* Fall-through for ident forms not caught by the explicit predicates: must be
[<ndashdigit-ident>] [n-<digits>] or [<dashndashdigit-ident>]
[-n-<digits>]. *)letread_nth_ident_tailts=matchndashdigit_bswith|Someb->Cursor.skipt;An_plus_b(1,-b)|None->(matchdashndashdigit_bswith|Someb->Cursor.skipt;An_plus_b(-1,-b)|None->Cursor.errt("not an An+B ident: "^s))(* After a leading [+] delim, only the positive ident forms are valid. *)letread_nth_after_plust=ensure_no_ws_after_plust;matchCursor.peektwith|Some(Component.Preserved{kind=Token.Idents;_})whenis_n_idents->Cursor.skipt;An_plus_b(1,read_an_tailt)|Some(Component.Preserved{kind=Token.Idents;_})whenis_ndash_idents->Cursor.skipt;An_plus_b(1,-read_signless_integert)|Some(Component.Preserved{kind=Token.Idents;_})->(matchndashdigit_bswith|Someb->Cursor.skipt;An_plus_b(1,-b)|None->Cursor.err_expectedt"An+B after '+'")|_->Cursor.err_expectedt"An+B after '+'"letread_ntht:nth=Cursor.wst;matchCursor.peektwith|Some(Component.Preserved{kind=Token.Idents;_})whenString.lowercase_asciis="odd"->Cursor.skipt;Odd|Some(Component.Preserved{kind=Token.Idents;_})whenString.lowercase_asciis="even"->Cursor.skipt;Even|Some(Component.Preserved{kind=Token.Number_tok{number_flag=Integer;_};_})->Index(Cursor.intt)|Some(Component.Preserved{kind=Token.Dimension{number;unit_};_})->read_nth_dimensiontnumberunit_|Some(Component.Preserved{kind=Token.Idents;_})whenis_n_idents->Cursor.skipt;An_plus_b(1,read_an_tailt)|Some(Component.Preserved{kind=Token.Idents;_})whenis_neg_n_idents->Cursor.skipt;An_plus_b(-1,read_an_tailt)|Some(Component.Preserved{kind=Token.Idents;_})whenis_ndash_idents->Cursor.skipt;An_plus_b(1,-read_signless_integert)|Some(Component.Preserved{kind=Token.Idents;_})whenis_dashndash_idents->Cursor.skipt;An_plus_b(-1,-read_signless_integert)|Some(Component.Preserved{kind=Token.Idents;_})->read_nth_ident_tailts|Some(Component.Preserved{kind=Token.Delim"+";_})->Cursor.skipt;read_nth_after_plust|_->Cursor.errt"expected 'odd', 'even', or An+B expression"(** Pretty print nth expression *)letpp_nth:nthPp.t=functx->function(* Source-shape preserving: the parser keeps [Odd]/[Even] for the keyword
spellings and [An_plus_b (2, 1)] / [An_plus_b (2, 0)] for the explicit An+B
forms, so the printer just emits whichever the author actually wrote in
pretty mode. Under minify, CSS Selectors 4 14 makes [2n+1]/[odd] and
[2n]/[even] spec-equivalent; pick the shorter spelling. *)|An_plus_b(2,1)whenPp.minifiedctx->Pp.stringctx"odd"|EvenwhenPp.minifiedctx->Pp.stringctx"2n"|Odd->Pp.stringctx"odd"|Even->Pp.stringctx"even"|Indexn->Pp.intctxn|An_plus_b(a,b)->ifa=0thenPp.intctxbelse((* Print coefficient *)ifa=1thenPp.stringctx"n"elseifa=-1thenPp.stringctx"-n"else(Pp.intctxa;Pp.charctx'n');(* Print offset *)ifb>0then(Pp.charctx'+';Pp.intctxb)elseifb<0thenPp.intctxb(* b = 0: print nothing *))(* Pseudo-class and pseudo-element identifier mappings *)letpseudo_class_base_idents=[(* Interactive *)("hover",Hover);("active",Active);("focus",Focus);("focus-visible",Focus_visible);("focus-within",Focus_within);("target",Target);("target-within",Target_within);(* Link *)("link",Link);("visited",Visited);("any-link",Any_link);("local-link",Local_link);(* Structural *)("root",Root);("empty",Empty);("first-child",First_child);("last-child",Last_child);("only-child",Only_child);("first-of-type",First_of_type);("last-of-type",Last_of_type);("only-of-type",Only_of_type);(* Input *)("enabled",Enabled);("disabled",Disabled);("read-only",Read_only);("read-write",Read_write);("placeholder-shown",Placeholder_shown);("default",Default);("checked",Checked);("indeterminate",Indeterminate);("blank",Blank);("valid",Valid);("invalid",Invalid);("in-range",In_range);("out-of-range",Out_of_range);("required",Required);("optional",Optional);("user-invalid",User_invalid);("user-valid",User_valid);("inert",Inert);("autofill",Autofill);(* Display *)("fullscreen",Fullscreen);("modal",Modal);("picture-in-picture",Picture_in_picture);("popover-open",Popover_open);("open",Open);(* CSS Modules scope keywords - non-standard but emitted by tooling *)("local",Local_scope);("global",Global_scope);(* Paged *)("left",Left);("right",Right);("first",First);(* Component *)("defined",Defined);("scope",Scope);("host",HostNone);(* Media *)("playing",Playing);("paused",Paused);("seeking",Seeking);("buffering",Buffering);("stalled",Stalled);("muted",Muted);("volume-locked",Volume_locked);("current",Current);("past",Past);("future",Future);(* View transitions *)("active-view-transition",Active_view_transition);]letscrollbar_state_ident=function|Horizontal->"horizontal"|Vertical->"vertical"|Decrement->"decrement"|Increment->"increment"|Start->"start"|End->"end"|Double_button->"double-button"|Single_button->"single-button"|No_button->"no-button"|Corner_present->"corner-present"|Window_inactive->"window-inactive"letscrollbar_part_ident=function|Scrollbar->"-webkit-scrollbar"|Button->"-webkit-scrollbar-button"|Track->"-webkit-scrollbar-track"|Track_piece->"-webkit-scrollbar-track-piece"|Thumb->"-webkit-scrollbar-thumb"|Corner->"-webkit-scrollbar-corner"|Resizer->"-webkit-resizer"(* WebKit, "Styling Scrollbars". Both engines read these on any element, so they
are ordinary pseudo-classes here; which pseudo-elements take them is
[pseudo_element_allows]'s business. *)letpseudo_class_scrollbar_idents=List.map(funstate->(scrollbar_state_identstate,Scrollbar_statestate))[Horizontal;Vertical;Decrement;Increment;Start;End;Double_button;Single_button;No_button;Corner_present;Window_inactive;]letpseudo_element_legacy_identsform=[(* Legacy pseudo-elements: parser records [Single] or [Double] colon for
tests and compatibility, but the printer canonicalizes pretty output to
the modern double-colon spelling. *)("before",Beforeform);("after",Afterform);("first-letter",First_letterform);("first-line",First_lineform);]letpseudo_element_modern_idents=[(* Modern pseudo-elements *)("backdrop",Backdrop);("marker",Marker);("placeholder",Placeholder);("selection",Selection);("target-text",Target_text);("spelling-error",Spelling_error);("grammar-error",Grammar_error);("file-selector-button",File_selector_button);("view-transition",View_transition);]letpseudo_vendor_idents=[(* Vendor-specific *)("-moz-focusring",Moz_focusring);("-webkit-any",Webkit_any);("-webkit-autofill",Webkit_autofill);("-moz-placeholder",Moz_placeholder);("-webkit-input-placeholder",Webkit_input_placeholder);("-ms-input-placeholder",Ms_input_placeholder);("-moz-ui-invalid",Moz_ui_invalid);("-moz-ui-valid",Moz_ui_valid);("-webkit-scrollbar",Webkit_scrollbarScrollbar);("-webkit-search-cancel-button",Webkit_search_cancel_button);("-webkit-search-decoration",Webkit_search_decoration);(* Webkit datetime pseudo-elements *)("-webkit-datetime-edit-fields-wrapper",Webkit_datetime_edit_fields_wrapper);("-webkit-date-and-time-value",Webkit_date_and_time_value);("-webkit-datetime-edit",Webkit_datetime_edit);("-webkit-datetime-edit-year-field",Webkit_datetime_edit_year_field);("-webkit-datetime-edit-month-field",Webkit_datetime_edit_month_field);("-webkit-datetime-edit-day-field",Webkit_datetime_edit_day_field);("-webkit-datetime-edit-hour-field",Webkit_datetime_edit_hour_field);("-webkit-datetime-edit-minute-field",Webkit_datetime_edit_minute_field);("-webkit-datetime-edit-second-field",Webkit_datetime_edit_second_field);("-webkit-datetime-edit-millisecond-field",Webkit_datetime_edit_millisecond_field);("-webkit-datetime-edit-meridiem-field",Webkit_datetime_edit_meridiem_field);("-webkit-inner-spin-button",Webkit_inner_spin_button);("-webkit-outer-spin-button",Webkit_outer_spin_button);("-webkit-calendar-picker-indicator",Webkit_calendar_picker_indicator);("-webkit-details-marker",Webkit_details_marker);("details-content",Details_content);](* Names the [::] reader takes and the [:] reader does not. Chrome 151 and
WebKit 26.5 drop [:cue] and [:-webkit-scrollbar-thumb] while keeping the [::]
spelling of both, so these stay out of [pseudo_class_all_idents];
[::-webkit-scrollbar] itself keeps the single-colon spelling cascade has
always read for it and stays in [pseudo_vendor_idents].
WebVTT 1 sec. 8.2 requires "the ::cue, ::cue(selector), ::cue-region and
::cue-region(selector) pseudo-elements", and sec. 8.2.1 and sec. 8.2.3 define
the argument-less pair the [calls] table above cannot reach. Chrome, WebKit
and Lightning CSS all take [::cue]; only Lightning CSS takes [::cue-region],
the other two implement neither of its forms. *)letpseudo_element_double_colon_idents=("cue",CueNone)::("cue-region",Cue_regionNone)::List.map(funpart->(scrollbar_part_identpart,Webkit_scrollbarpart))[Button;Track;Track_piece;Thumb;Corner;Resizer](* Every [::] form, and only those: a pseudo-element names a box other than the
originating element, where an element-tied pseudo-class ([:hover], [:focus])
only narrows which elements match. An unrecognised [::foo] belongs here too.
Selectors 4 sec. 16 builds a pseudo-compound out of
[<pseudo-element-selector> <pseudo-class-selector>*] whatever the
pseudo-element's name is, so the shape rules that read this predicate cannot
wait for cascade to learn the name; the framework-only [::deep] / [::v-deep]
/ [::ng-deep] are unknown [::] names like any other, since Selectors 4 has no
piercing combinator. *)letis_pseudo_element=function|Before_|After_|First_letter_|First_line_|Backdrop|Marker|Placeholder|Selection|Target_text|Spelling_error|Grammar_error|File_selector_button|Moz_placeholder|Webkit_input_placeholder|Ms_input_placeholder|Webkit_scrollbar_|Webkit_search_cancel_button|Webkit_search_decoration|Webkit_datetime_edit_fields_wrapper|Webkit_date_and_time_value|Webkit_datetime_edit|Webkit_datetime_edit_year_field|Webkit_datetime_edit_month_field|Webkit_datetime_edit_day_field|Webkit_datetime_edit_hour_field|Webkit_datetime_edit_minute_field|Webkit_datetime_edit_second_field|Webkit_datetime_edit_millisecond_field|Webkit_datetime_edit_meridiem_field|Webkit_inner_spin_button|Webkit_outer_spin_button|Webkit_calendar_picker_indicator|Webkit_details_marker|Details_content|Part_|Slotted_|Cue_|Cue_region_|Highlight_|View_transition|View_transition_group_|View_transition_image_pair_|View_transition_old_|View_transition_new_|Unknown_pseudo_element_|Unknown_pseudo_element_call_->true|_->falseletrecanyp=function|Compoundxsassel->List.exists(anyp)xs||psel|Combined(a,_,b)assel->anypa||anypb||psel|Relative(_,b)assel->anypb||psel|Listxsassel->List.exists(anyp)xs||psel|(Isxs|Wherexs|Notxs|Hasxs|Moz_any_callxs|Webkit_any_callxs|Slottedxs|Current_ofxs)assel->List.exists(anyp)xs||psel|(Nth_child(_,Somexs)|Nth_last_child(_,Somexs)|Nth_of_type(_,Somexs)|Nth_last_of_type(_,Somexs)|Host(Somexs)|Cue(Somexs)|Cue_region(Somexs)|Host_contextxs)assel->List.exists(anyp)xs||psel|Part_assel->psel|s->pslethas_pseudo_elementsel=anyis_pseudo_elementsellethas_unknown_pseudo_class=any(function|Unknown_pseudo_class_|Unknown_pseudo_class_call_->true|_->false)(* CSS Selectors 4 17.1: a forgiving [:is()] / [:where()] with no surviving
valid argument matches nothing, and a compound or combined selector that
contains such a sub-selector inherits the same behaviour. A selector list
matches nothing only when every entry does. Useful for dropping dead rules
under [Optimize.stylesheet]. *)letrecmatches_nothing=function|Is[]|Where[]->true|Compoundxs->List.existsmatches_nothingxs|Combined(a,_,b)->matches_nothinga||matches_nothingb|Relative(_,b)->matches_nothingb|List[]->true|Listxs->List.for_allmatches_nothingxs|_->false(* CSS Selectors 4 sec. 3.6.3: what may follow a pseudo-element in its compound
is a pseudo-class, and sec. 3.6.4 adds a sub-pseudo-element
([pseudo_element_allows_sub]). A class, id, type or attribute selector after
the pseudo-element makes the compound invalid whatever the pseudo-element
is. *)letis_pe_action=function|Element_|Class_|Id_|Universal_|Attribute_|Nesting->false|sel->not(is_pseudo_elementsel)(* CSS Selectors 4 sec. 9. *)letis_user_action_pseudo_class=function|Hover|Active|Focus|Focus_visible|Focus_within->true|_->false(* CSS Selectors 4 sec. 13: the tree-structural pseudo-classes, the ones that
answer a question about the element's place among its siblings. *)letis_structural_pseudo_class=function|Root|Empty|First_child|Last_child|Only_child|First_of_type|Last_of_type|Only_of_type|Nth_child_|Nth_last_child_|Nth_of_type_|Nth_last_of_type_->true|_->false(* Which pseudo-classes each pseudo-element takes after it. CSS Selectors 4 sec.
3.6.3 allows the logical combinations and hands the rest of the list to
"other specifications", so the rows below come from CSS Pseudo-Elements 4
sec. 5 for the element-backed pseudo-elements and, for the UA widgets whose
list only exists in the engines, from Chrome and WebKit, taking a
pseudo-class as allowed when either engine keeps the rule. *)letrecpseudo_element_allowspepc=matchpewith(* A name no engine recognises: cascade keeps it so a pseudo-element newer
than this list survives a format pass, and knows nothing about its rules,
so it keeps taking any pseudo-class after it. [::cue-region()] keeps the
same bargain: Chrome 151 and WebKit 26.5 implement neither of its forms. *)|Unknown_pseudo_element_|Unknown_pseudo_element_call_|Moz_placeholder|Ms_input_placeholder|Cue_region(Some_)->true|pe->(matchpcwith(* Sec. 3.6.3 allows the logical combinations after every pseudo-element
and passes the row below on to their arguments; what an argument the
row refuses costs is the argument list's own business. [:is()] and
[:where()] take a [<forgiving-selector-list>] (sec. 4.1), which drops
it and leaves a selector that still parses and matches nothing, so both
engines keep the rule; the [:-moz-any()] / [:-webkit-any()] aliases
read back the same way. *)|Is_|Where_|Moz_any_call_|Webkit_any_call_->true(* [:not()] takes an unforgiving list, so the refused argument takes the
compound down with it: both engines drop [::before:not(:hover)] and
keep [::part(p):not(:hover)]. *)|Notargs->List.for_all(pseudo_element_allows_argumentpe)args(* WebKit, "Styling Scrollbars": a scrollbar part reports its own state,
and [:window-inactive] reaches past the scrollbar to a selection and to
a shadow part, where both engines take it. Past that they disagree one
cell each way (only WebKit takes the other ten after [::part()], only
Chrome takes [:window-inactive] after [::details-content]), so the list
stops where they agree. *)|Scrollbar_statestate->(match(pe,state)with|Webkit_scrollbar_,_->true|(Selection|Part_),Window_inactive->true|_->false)(* Same forward-compatibility bargain as an unknown pseudo-element. *)|Unknown_pseudo_class_|Unknown_pseudo_class_call_->true|pc->(matchpewith(* CSS Pseudo-Elements 4 sec. 5: an element-backed pseudo-element
takes what a real element takes, bar the pseudo-classes that would
report on the tree it sits in. *)|Part_|Details_content->(matchpcwith|Has_->false|pc->not(is_structural_pseudo_classpc))(* A scrollbar part takes no focus, and reports which part of which
scrollbar it is through the state pseudo-classes below. *)|Webkit_scrollbar_->(matchpcwith|Hover|Active|Enabled|Disabled->true|_->false)(* CSS View Transitions 1 sec. 3.1: [:only-child] matches a view
transition pseudo with no sibling in the pseudo-element tree. *)|View_transition_group_|View_transition_image_pair_|View_transition_old_|View_transition_new_->(matchpcwithOnly_child->true|_->false)(* The UA widgets that stand in for a real control, and the cue and
region boxes WebVTT 1 sec. 8.2.1 and sec. 8.2.3 define: all three
engines keep [::cue:focus-within] and drop [::cue:enabled].
[::cue(...)] selects inside the cue and takes none of them. *)|CueNone|Cue_regionNone|Placeholder|File_selector_button|Webkit_input_placeholder|Webkit_search_cancel_button|Webkit_search_decoration|Webkit_datetime_edit_fields_wrapper|Webkit_date_and_time_value|Webkit_datetime_edit|Webkit_datetime_edit_year_field|Webkit_datetime_edit_month_field|Webkit_datetime_edit_day_field|Webkit_datetime_edit_hour_field|Webkit_datetime_edit_minute_field|Webkit_datetime_edit_second_field|Webkit_datetime_edit_millisecond_field|Webkit_datetime_edit_meridiem_field|Webkit_inner_spin_button|Webkit_outer_spin_button|Webkit_calendar_picker_indicator|Webkit_details_marker->is_user_action_pseudo_classpc|_->false))(* An argument of a logical combination sits where the pseudo-element's own
pseudo-classes sit, so it reads as a pseudo-compound tail: pseudo-classes the
pseudo-element takes, and nothing else. A combinator or a selector list makes
the argument a whole complex selector, which no pseudo-element takes. *)andpseudo_element_allows_argumentpe=function|Compoundcomponents->List.for_all(pseudo_element_allows_argumentpe)components|Combined_|Relative_|List_->false|c->is_pe_actionc&&pseudo_element_allowspec(* CSS Pseudo-Elements 4 sec. 4 names the tree-abiding pseudo-elements, sec. 7.1
lists [::backdrop] and [::view-transition] among them, and sec. 5 adds that
"element-backed pseudo-elements are always tree-abiding". [::part()] is
element-backed and so belongs here on that reading, and is left out because
sec. 5 also says it never matches after a pseudo-element. *)letis_tree_abiding_pseudo_element=function|Before_|After_|Marker|Placeholder|Backdrop|View_transition|Details_content|File_selector_button->true|_->false(* Which pseudo-elements each pseudo-element takes after it, in the same
compound. CSS Selectors 4 sec. 3.6.4 refuses the shape "unless the
corresponding sub-pseudo-element is explicitly defined to exist in another
specification", so the rows below are those definitions and nothing else:
[::before::before] goes, [::before::marker] stays.
CSS Pseudo-Elements 4 sec. 4.2 defines the [::marker] of a [::before] or
[::after] that is a list item, and rules [::marker::marker] out. Sec. 5 lets
an element-backed pseudo-element take every pseudo-element "just as if the
pseudo-element were a type selector", bar [::part()], which "never matches"
there; cascade turns that never-matching row into a refusal, as it does one
row up for [:has()] and the structural pseudo-classes. Sec. 5.1 files
[::file-selector-button] as element-backed, but Chrome 151, WebKit 27 and
Lightning CSS all drop [::file-selector-button::before], so the originating
row keeps the [::part()] / [::details-content] pair [pseudo_element_allows]
carries.
CSS Shadow 1 sec. 3.2.4 gives [::slotted()] a narrower row of its own: "the
::slotted() pseudo-element can be followed by a tree-abiding pseudo-element".
The engines show the difference, dropping [::slotted(a)::first-line] while
keeping [::part(x)::first-line].
One adjacent pair at a time, so a longer chain is as valid as each of its
links: Chrome keeps [::part(x)::before::marker] and drops
[::part(x)::marker::before].
An unmodelled [::] name keeps the bargain [pseudo_element_allows] makes for
it: cascade preserves the name rather than judging what may follow it. *)letpseudo_element_allows_subpesub=matchpewith|Unknown_pseudo_element_|Unknown_pseudo_element_call_->true|Part_|Details_content->(matchsubwithPart_->false|_->true)|Slotted_->is_tree_abiding_pseudo_elementsub|Before_|After_->(matchsubwithMarker->true|_->false)|_->false(* CSS Selectors 4 sec. 3.6.5: a pseudo-element defined to have internal
structure may be followed by a child or descendant combinator, and a selector
with a combinator after the pseudo-element is invalid otherwise. Nothing
shipping claims that structure, so the exception stays empty: Chrome 151 and
WebKit 26.5 drop [::part(x) > .b], [::details-content > div] and
[::first-letter em] alike, and the Servo selectors crate raises
UnexpectedSelectorAfterPseudoElement for the same shapes.
A [::] name cascade does not model is exempt, for the reason
[pseudo_element_allows] already exempts it from sec. 3.6.3: cascade preserves
such a name rather than judging it. test/interop/lightning carries seven
rules of that kind ([.foo ::deep .bar], [.foo ::unknown(.foo) .bar] and their
kin) and all six reference minifiers keep every one verbatim, while Lightning
CSS rejects the identical shape as soon as the name is one it knows
([::-moz-placeholder .b]). Dropping the exemption would delete those
rules. *)letis_modelled_pseudo_element=function|Unknown_pseudo_element_|Unknown_pseudo_element_call_->false|sel->is_pseudo_elementsel(* Only the compound's own components. A pseudo-element inside a functional
pseudo-class argument belongs to that argument's own rules, so
[.a:has(.b::before) .c] is not this one's to judge. *)letbars_following_combinator=function|Compoundcomponents->List.existsis_modelled_pseudo_elementcomponents|sel->is_modelled_pseudo_elementsel(* The same sec. 3.6.5 rule read off a built selector rather than applied while
reading one. Nesting reaches a selector the reader would refuse, because it
joins a valid parent to a valid child without either passing the check. The
two tooling combinators are exempt for the reason [check_combinator] exempts
them: no engine parses them, so the rule never reaches them. *)letrechas_combinator_after_pseudo_element=function|Listbranches->List.existshas_combinator_after_pseudo_elementbranches|Combined(left,comb,right)->(matchcombwith|Shadow_piercing|Shadow_deep->false|Descendant|Child|Next_sibling|Subsequent_sibling|Column->bars_following_combinatorleft)||has_combinator_after_pseudo_elementleft||has_combinator_after_pseudo_elementright|_->false(* Why a compound may not carry [s] after the pseudo-element it follows.
[read_compound] turns each into its own message and [has_refused_simple_-
after_pseudo_element] reads the same answer off a built selector, so the two
cannot drift. [seen] is the compound's earlier components, most recent
first. *)typecompound_refusal=|Sub_pseudo_element(** sec. 3.6.4, via [pseudo_element_allows_sub]. *)|Simple_after_pseudo_element(** sec. 3.6.3, via [is_pe_action]. *)|Pseudo_class_after_pseudo_element(** sec. 3.6.3, via [pseudo_element_allows]. *)letcompound_refusal~seens=matchList.find_optis_pseudo_elementseenwith|Somepewhenis_pseudo_elements->ifpseudo_element_allows_subpesthenOption.NoneelseOption.SomeSub_pseudo_element|Some_whennot(is_pe_actions)->Option.SomeSimple_after_pseudo_element|Somepewhennot(pseudo_element_allowspes)->Option.SomePseudo_class_after_pseudo_element|_->Option.None(* The sec. 3.6.3 / 3.6.4 companion of [has_combinator_after_pseudo_element]:
nesting extends a pseudo-element's own compound the same way it puts a
combinator after one, out of a valid parent and a valid child that neither
one meets the reader's check. A pseudo-element inside a functional
pseudo-class argument is that argument's own business, as it is for
[bars_following_combinator]. *)(* Substituting [&] splices the parent compound in as one component, so the walk
flattens a nested [Compound] rather than treating it as a simple selector the
pseudo-element would refuse outright. *)letrecrefuses_compoundseen=function|[]->false|Compoundinner::rest->refuses_compoundseen(inner@rest)|c::rest->(matchcompound_refusal~seencwith|Some_->true|None->refuses_compound(c::seen)rest)letrechas_refused_simple_in_compound=function|Listbranches->List.existshas_refused_simple_in_compoundbranches|Combined(left,_,right)->has_refused_simple_in_compoundleft||has_refused_simple_in_compoundright|Relative(_,right)->has_refused_simple_in_compoundright|Compoundcomponents->refuses_compound[]components|_->false(* The merged lists are static across the lifetime of the program (every
constituent is a [let] binding above); memoise them so the [@] cons-chain
only happens once instead of per [:foo] / [::foo] pseudo read. *)letpseudo_class_all_idents_lazy=lazy(pseudo_class_base_idents@pseudo_class_scrollbar_idents@pseudo_element_legacy_identsSingle@pseudo_element_modern_idents@pseudo_vendor_idents)letpseudo_class_all_idents()=Lazy.forcepseudo_class_all_idents_lazyletpseudo_element_unknown_idents=lazy(pseudo_element_modern_idents@pseudo_vendor_idents@pseudo_element_double_colon_idents@pseudo_element_legacy_identsDouble)letread_unknown_pseudo_class_call~all_identst=matchCursor.peektwith|Some(Component.Func{node={name;arguments;_};_})->(* CSS Selectors 4 sec. 3.5: a known non-functional pseudo ([:checked],
[:hover], ...) called with parens ([:checked()]) is invalid. Reject so
the rule reader drops it rather than passing through as an unknown
call. *)letlower=String.lowercase_asciinameinletis_known_non_functional=List.exists(fun(n,_)->String.lowercase_asciin=lower)all_identsinifis_known_non_functionalthenCursor.err_invalidt("pseudo-class is not functional: "^name);Cursor.skipt;Unknown_pseudo_class_call(name,arguments)|_->Cursor.err_expectedt"pseudo-class call"letread_unknown_pseudo_class_identt=matchCursor.ident_opttwith|Somename->Unknown_pseudo_classname|None->Cursor.err_expectedt"pseudo-class"letread_unknown_pseudo_class~all_identst=Cursor.one_of[read_unknown_pseudo_class_call~all_idents;read_unknown_pseudo_class_ident;]tletrecforgiving_take_next_in_segmenttacc=matchCursor.next_rawtwith|None->List.revacc|Somecv->forgiving_take_segmentt(cv::acc)andforgiving_take_segmenttacc=letis_comma=function|Component.Preserved{kind=Token.Comma;_}->true|_->falseinmatchCursor.peek_rawtwith|None->List.revacc|Somecvwhenis_commacv->ignore(Cursor.next_rawt:Component.toption);List.revacc|Some_->forgiving_take_next_in_segmenttaccletread_forgiving_segmentread_itemtacc=letitem=Cursor.subt(forgiving_take_segmentt[])inmatchread_itemitemwith|sel->Cursor.wsitem;ifCursor.is_doneitemthensel::accelseacc|exceptionCursor.Parse_error_->accletread_nth_exprt=letexpr=read_nthtinCursor.wst;ifnot(Cursor.is_donet)thenCursor.errt"unexpected tokens after An+B expression";exprletread_nth_col_contentt=Nth_col(read_nth_exprt)letread_nth_last_col_contentt=Nth_last_col(read_nth_exprt)letread_nth_colt=Cursor.call"nth-col"tread_nth_col_contentletread_nth_last_colt=Cursor.call"nth-last-col"tread_nth_last_col_contentletread_highlight_contentt=(* ::highlight() takes a single custom-ident per CSS Custom Highlight API sec.
3.1; comma-separated names are rejected. *)letname=Cursor.identtinensure_call_donet"highlight";Highlight[name]letread_vt_class_selectort:vt_class_selector=(* CSS View Transitions 2 sec. 10.4 [<vt-class-selector>] = [<vt-name>?
[.<custom-ident>]*]. The name is [<custom-ident> | *]; either the name or
at least one class must be present. *)Cursor.wst;letname=matchCursor.peek_delimtwith|Some'*'->Cursor.skipt;Some"*"|Some'.'->None|_->Some(Cursor.identt)inletrecread_classesacc=matchCursor.peek_delimtwith|Some'.'->Cursor.skipt;letcls=Cursor.identtinread_classes(cls::acc)|_->List.revaccinletclasses=read_classes[]in{name;classes}letread_view_transition_group_contentt=letsel=read_vt_class_selectortinensure_call_donet"view transition group";View_transition_groupselletread_vt_image_pair_contentt=letsel=read_vt_class_selectortinensure_call_donet"view transition image pair";View_transition_image_pairselletread_view_transition_old_contentt=letsel=read_vt_class_selectortinensure_call_donet"view transition old";View_transition_oldselletread_view_transition_new_contentt=letsel=read_vt_class_selectortinensure_call_donet"view transition new";View_transition_newselletrecread_selector_list_tailread_itemtacc=letsel=read_itemtinletacc=sel::accinCursor.wst;ifCursor.comma_opttthen(Cursor.wst;ifCursor.is_donetthenCursor.errt"expected at least one selector";read_selector_list_tailread_itemtacc)elseifCursor.is_donetthenList.revaccelseCursor.errt"unexpected tokens after selector"letread_selector_list_withread_itemt=Cursor.wst;ifCursor.is_donetthenCursor.errt"expected at least one selector"elseread_selector_list_tailread_itemt[]letread_forgiving_listread_itemt=letrecloopacc=ifCursor.is_donetthenList.revaccelseloop(read_forgiving_segmentread_itemtacc)inloop[](* Forward declarations for mutually recursive functions *)letrecread_complex_listt=read_selector_list_withread_complextandread_forgiving_complex_listt=read_forgiving_listread_forgiving_complex_itemtandread_forgiving_complex_itemt=letsel=read_complextinifhas_pseudo_elementselthenCursor.errt"pseudo-element not allowed here";ifhas_unknown_pseudo_classselthenCursor.errt"unknown pseudo-class";sel(** Read nth selector with optional "of S" clause *)andread_nth_selectort:nth*tlistoption=letexpr=read_nthtinCursor.wst;(* Check for "of S" clause *)letof_clause=Cursor.option(funt->Cursor.expect_string"of"t;Cursor.wst;Cursor.list~sep:Cursor.comma~at_least:1read_complext)tin(* Per Selectors Level 4 section 13.3.1, the An+B (plus optional [of S]) must
consume the entire [<nth-child>] argument list. Leftover tokens (e.g.
[:nth-child(1 - n)] or [:nth-child(2 n + 2)]) are a parse error, not a
silently-dropped tail. *)Cursor.wst;ifnot(Cursor.is_donet)thenCursor.errt"unexpected tokens after An+B expression";(expr,of_clause)(** Parse a relative selector (used inside :has()). A relative selector can
start with a combinator (+, >, ~) without a left operand. *)andread_relative_selectort=Cursor.wst;matchCursor.peek_delimtwith|Some('+'|'>'|'~')->letcomb=read_combinatortinCursor.wst;letright=read_complextinRelative(comb,right)|Some'/'whenCursor.lookaheadtry_shadow_deept->letcomb=read_combinatortinCursor.wst;letright=read_complextinRelative(comb,right)|_->read_complextandread_relative_selector_listt=read_selector_list_withread_relative_selectort(* Helper readers for functional pseudo-class content *)andread_is_contentt=Is(read_forgiving_complex_listt)andread_moz_any_contentt=Moz_any_call(read_forgiving_complex_listt)andread_webkit_any_contentt=Webkit_any_call(read_forgiving_complex_listt)andread_has_contentt=letselectors=read_relative_selector_listtinletcontains_hassel=any(functionHas_->true|_->false)selinList.iter(funsel->ifcontains_hasselthenCursor.errt":has() cannot contain :has()";ifhas_pseudo_elementselthenCursor.errt":has() cannot contain pseudo-elements";ifhas_unknown_pseudo_classselthenCursor.errt":has() cannot contain an unknown pseudo-class")selectors;Hasselectorsandread_not_contentt=letselectors=read_complex_listtin(* CSS Selectors 4 sec. 4.3: [:not()] takes a [<complex-real-selector-list>],
which sec. 16 builds out of [<compound-selector>]s alone, with no
[<pseudo-compound-selector>] and so no pseudo-element. It is also
non-forgiving, so a pseudo-element or an unknown selector anywhere in the
argument invalidates the whole rule instead of just its own item. Top-level
lists keep unknown pseudo-classes for forward compatibility. *)List.iter(funsel->ifhas_pseudo_elementselthenCursor.errt":not() cannot contain pseudo-elements";ifhas_unknown_pseudo_classselthenCursor.errt":not() cannot contain an unknown pseudo-class")selectors;Notselectorsandread_where_contentt=Where(read_forgiving_complex_listt)andread_local_contentt=Local_call(read_complex_listt)andread_global_contentt=Global_call(read_complex_listt)andread_nth_child_contentt=letexpr,of_sel=read_nth_selectortinNth_child(expr,of_sel)andread_nth_last_child_contentt=letexpr,of_sel=read_nth_selectortinNth_last_child(expr,of_sel)andread_nth_of_type_contentt=letexpr,of_sel=read_nth_selectortinNth_of_type(expr,of_sel)andread_nth_last_type_contentt=letexpr,of_sel=read_nth_selectortinNth_last_of_type(expr,of_sel)andread_host_contentt=Host(Cursor.optionread_complex_listt)andread_host_context_contentt=Host_context(read_complex_listt)andread_current_contentt=Current_of(read_complex_listt)(* Read helper functions for functional pseudo-classes *)andread_ist=Cursor.call"is"tread_is_contentandread_moz_anyt=Cursor.call"-moz-any"tread_moz_any_contentandread_webkit_anyt=Cursor.call"-webkit-any"tread_webkit_any_contentandread_hast=Cursor.call"has"tread_has_contentandread_nott=Cursor.call"not"tread_not_contentandread_wheret=Cursor.call"where"tread_where_contentandread_localt=Cursor.call"local"tread_local_contentandread_globalt=Cursor.call"global"tread_global_contentandread_nth_childt=Cursor.call"nth-child"tread_nth_child_contentandread_nth_last_childt=Cursor.call"nth-last-child"tread_nth_last_child_contentandread_nth_of_typet=Cursor.call"nth-of-type"tread_nth_of_type_contentandread_nth_last_of_typet=Cursor.call"nth-last-of-type"tread_nth_last_type_contentandread_hostt=Cursor.call"host"tread_host_contentandread_host_contextt=Cursor.call"host-context"tread_host_context_contentandread_currentt=Cursor.call"current"tread_current_content(* Helper readers for pseudo-element functions that need recursion *)andread_slotted_contentt=(* CSS Shadow 1 section 3.2.4 [::slotted()] takes a single compound selector;
comma-separated lists are a syntax error. *)letsel=read_complextinCursor.wst;ifnot(Cursor.is_donet)thenCursor.errt"::slotted() accepts a single compound selector";Slotted[sel]andread_cue_contentt=letsels=read_complex_listtinCue(Somesels)andread_cue_region_contentt=letsels=read_complex_listtinCue_region(Somesels)andread_slottedt=Cursor.call"slotted"tread_slotted_contentandread_cuet=Cursor.call"cue"tread_cue_contentandread_cue_regiont=Cursor.call"cue-region"tread_cue_region_contentandpseudo_class_calls_lazy=lazy[("is",read_is);("-moz-any",read_moz_any);("-webkit-any",read_webkit_any);("has",read_has);("not",read_not);("where",read_where);("local",read_local);("global",read_global);("nth-child",read_nth_child);("nth-last-child",read_nth_last_child);("nth-of-type",read_nth_of_type);("nth-last-of-type",read_nth_last_of_type);("nth-col",read_nth_col);("nth-last-col",read_nth_last_col);("lang",read_lang);("dir",read_dir);("state",read_state);("host",read_host);("host-context",read_host_context);("current",read_current);("heading",read_heading);("active-view-transition-type",read_active_view_transition_type);]andpseudo_class_calls()=Lazy.forcepseudo_class_calls_lazy(** Parse pseudo-class (:hover, :nth-child(2n+1), etc.) *)andread_pseudo_class?(allow_unknown=false)t=ifnot(Cursor.colont)thenCursor.err_expectedt"':'";expect_adjacentt"pseudo-class name";letall_idents=pseudo_class_all_idents()inletcalls=pseudo_class_calls()inletread_unknown=read_unknown_pseudo_class~all_identsinifallow_unknownthenCursor.enum_or_calls"pseudo-class"all_idents~calls~default:read_unknowntelseCursor.enum_or_calls"pseudo-class"all_idents~callst(** Parse pseudo-element (::before, ::after, etc.) *)andread_pseudo_elementt=ifnot(Cursor.try_kind_pairToken.ColonToken.Colont)thenCursor.err_expectedt"'::'";expect_adjacentt"pseudo-element name";Cursor.enum_calls[("part",read_part);("slotted",read_slotted);("cue",read_cue);("cue-region",read_cue_region);("highlight",funt->Cursor.call"highlight"tread_highlight_content);("view-transition-group",funt->Cursor.call"view-transition-group"tread_view_transition_group_content);("view-transition-image-pair",funt->Cursor.call"view-transition-image-pair"tread_vt_image_pair_content);("view-transition-old",funt->Cursor.call"view-transition-old"tread_view_transition_old_content);("view-transition-new",funt->Cursor.call"view-transition-new"tread_view_transition_new_content);]~default:(funt->letread_unknown_callt=(* Unknown functional pseudo-element: keep the call body verbatim so the
printer can re-emit the exact same source. *)matchCursor.peektwith|Some(Component.Func{node={name;arguments;_};_})->Cursor.skipt;Unknown_pseudo_element_call(name,arguments)|_->Cursor.err_expectedt"pseudo-element call"inletread_unknown_identt=matchCursor.ident_opttwith|Somename->Unknown_pseudo_elementname|None->Cursor.err_expectedt"pseudo-element"inCursor.enum"pseudo-element"(Lazy.forcepseudo_element_unknown_idents)~default:(funt->Cursor.one_of[read_unknown_call;read_unknown_ident]t)t)t(** Parse a simple selector (one part). Does not skip leading whitespace -- the
caller (read_compound) uses whitespace as a compound / descendant boundary
marker. *)andread_simple?(allow_unknown_pseudo_class=false)t=matchCursor.peek_delimtwith|Some'.'->read_classt|Some('*'|'|')->read_type_or_universalt|Some'&'->Cursor.skipt;Nesting|_->(ifCursor.peek_hasht<>Nonethenread_idtelseifCursor.peek_blockt=SomeToken.Squarethenread_attributetelseifCursor.peek_colontthen(* [::] for pseudo-element, [:] for pseudo-class. *)letsnap=Cursor.savetinifCursor.try_kind_pairToken.ColonToken.Colontthen(Cursor.restoretsnap;read_pseudo_elementt)elseread_pseudo_class~allow_unknown:allow_unknown_pseudo_classtelsematchCursor.peek_identtwith|Some_->read_type_or_universalt|None->err_expectedt"selector")(** Parse a compound selector (multiple simple selectors without spaces).
Leading whitespace is skipped; whitespace {e between} simple selectors stops
the compound (it marks the descendant combinator at the enclosing
complex-selector level). *)andread_compoundt=Cursor.wst;letcan_start()=matchCursor.peek_rawtwith|Some(Component.Preserved{kind=Token.Whitespace_;_})->false|_->(matchCursor.peek_delimtwith|Some('.'|'*'|'&')->true(* A pipe extends the compound only as the [|name] prefix; a [||] ends
it so [read_complex] can take the column combinator. *)|Some'|'->lookahead_bare_pipe_nst|_->false)||Cursor.peek_hasht<>None||Cursor.peek_colont||Cursor.peek_blockt=SomeToken.Square||Cursor.peek_identt<>Noneinletprepend_simpleacc=(* CSS Selectors 4 sec. 3.5: tolerate unknown pseudo-classes for forward
compat (vendor pseudos, authored future-pseudos must round-trip).
Non-forgiving rejection lives where the spec requires it: inside
[:not()]/[:has()] ([read_not_content]/[read_has_content]) and in the rule
reader when a whole selector list is unknown. *)lets=read_simple~allow_unknown_pseudo_class:truetinmatchcompound_refusal~seen:accswith|SomeSub_pseudo_element->Cursor.errt"pseudo-element not allowed after this pseudo-element"|SomeSimple_after_pseudo_element->Cursor.errt"pseudo-element must be last in compound selector"|SomePseudo_class_after_pseudo_element->Cursor.errt"pseudo-class not allowed after this pseudo-element"|None->s::accinletrecloopacc=ifcan_start()thenloop(prepend_simpleacc)elseaccinmatchloop[]with|[]->err_expectedt"at least one selector"|[s]->s|selectors->compound(List.revselectors)(** Parse a complex selector (with combinators) *)andread_complext=letleft=read_compoundtinCursor.wst;letcan_start_selector()=(matchCursor.peek_delimtwith|Some('.'|'*'|'|'|'&')->true|_->false)||Cursor.peek_hasht<>None||Cursor.peek_colont||Cursor.peek_blockt=SomeToken.Square||Cursor.peek_identt<>Nonein(* CSS Selectors 4 sec. 3.6.5, via [bars_following_combinator]. [>>>] and
[/deep/] are Vue / Angular tooling spellings rather than CSS combinators,
and no engine parses either, so the rule never reaches them and cascade
passes them through as it does an unmodelled pseudo-element name. *)letcheck_combinator=function|Shadow_piercing|Shadow_deep->()|Descendant|Child|Next_sibling|Subsequent_sibling|Column->ifbars_following_combinatorleftthenCursor.errt"combinator not allowed after a pseudo-element"inletread_combined()=letcomb=read_combinatortincheck_combinatorcomb;Cursor.wst;combineleftcomb(read_complext)inmatchCursor.peek_delimtwith|Some'|'whenCursor.lookahead(funt->Cursor.try_kind_pair(Token.Delim"|")(Token.Delim"|")t)t->read_combined()|Some('>'|'+'|'~')->read_combined()|Some'/'whenCursor.lookaheadtry_shadow_deept->read_combined()|_->ifCursor.peek_commat||Cursor.is_donetthenleftelseifcan_start_selector()then(check_combinatorDescendant;combineleftDescendant(read_complext))elseleft(* CSS Selectors 4 section 3.9: the top-level rule selector list is an
unforgiving site. [read_compound] keeps [Unknown_pseudo_class] so vendor and
forward-compat pseudos round-trip, but one at top level is a spec deviation;
raise so [Selector.of_string ".ok,:future-pseudo"] surfaces a
[Parse_error]. *)letvalidate_unforgiving_pseudotsel=ifhas_unknown_pseudo_classselthenCursor.errt"unknown pseudo-class in unforgiving selector list"letread_selector_listt=Cursor.with_contextt"list"@@fun()->Cursor.wst;(* Parse the selector list manually to properly handle trailing commas *)letreccollect_listacc=letsel=read_complextinletacc=sel::accinCursor.wst;ifCursor.comma_opttthen(Cursor.wst;(* After a comma, we must have another selector - trailing commas are
invalid *)collect_listacc)elseList.revaccinletselectors=collect_list[]inletresult=matchselectorswith[s]->s|selectors->Listselectorsinvalidate_unforgiving_pseudotresult;resultletread_strict_selector_listt=Cursor.with_contextt"list"@@fun()->letresult=matchread_complex_listtwith[s]->s|selectors->Listselectorsinvalidate_unforgiving_pseudotresult;resultletreadt=letselector=read_selector_listtinCursor.wst;ifnot(Cursor.is_donet)thenCursor.errt"unexpected characters after selector";selectorletread_relativet=letselectors=read_relative_selector_listtinCursor.wst;ifnot(Cursor.is_donet)thenCursor.errt"unexpected characters after selector";letresult=matchselectorswith[s]->s|_->Listselectorsin(* A nested style rule's prelude is a selector list like any other, so it is
unforgiving too. *)validate_unforgiving_pseudotresult;result(* CSS Nesting 1 sec. 3: a nested selector is implicitly relative to [&], so a
leading [& <combinator>] is redundant: [& .bar] -> [.bar], [& > .bar] -> [>
.bar]. Only a leading [&] that is the whole left operand of a combinator is
removed; [&.bar] (compound) and a deeper [&] stay. *)letrecdrop_redundant_nesting_prefix(sel:t):t=matchselwith|Combined(Nesting,Descendant,right)->right|Combined(Nesting,comb,right)->Relative(comb,right)|Listsels->List(List.mapdrop_redundant_nesting_prefixsels)|other->other(* CSS Syntax 3 (ED) sec. 4.3.11 builds an identifier from ident code points -
letters, digits, [-], [_] and anything non-ASCII - and escapes. The shortcut
below reads the whole string as one name, so anything else in it means the
string is not a name: selector punctuation ([:], [>], [,] ...) but equally a
[}] or a [;], which name nothing and used to be taken for an element. *)letis_ident_code_pointc=matchcwith|'a'..'z'|'A'..'Z'|'0'..'9'|'_'|'-'->true|c->Char.codec>=0x80letis_unescaped_selector_syntaxsstart=letlen=String.lengthsinletrecloopi=ifi>=lenthenfalseelsematchs.[i]with|'\\'->letj=refiinskip_css_escapesj;loop!j|cwhenis_ident_code_pointc->loop(i+1)|_->trueinloopstartlethas_invalid_css_escapes=letlen=String.lengthsinletrecloopi=ifi>=lenthenfalseelseifs.[i]<>'\\'thenloop(i+1)elseifi+1>=lenthentrueelsematchs.[i+1]with|'\n'|'\r'|'\012'->true|_->letj=refiinskip_css_escapesj;loop!jinloop0letcan_fallback_shortcuts=letlen=String.lengthsinlen>0&&(not(has_invalid_css_escapes))&&matchs.[0]with|'.'|'#'->len>1&¬(is_unescaped_selector_syntaxs1)|_->not(is_unescaped_selector_syntaxs0)(* Use the full selector parser; fall back to the single-token shortcut for
['.foo' / '#foo' / 'foo'] when the cursor parser would reject the input. *)letof_string_fallbacks=matchs.[0]with|'.'->class_(unescape_selector_name(String.subs1(String.lengths-1)))|'#'->id(unescape_selector_name(String.subs1(String.lengths-1)))|_->Element(None,unescape_selector_names)letof_strings=tryread(Cursor.of_strings)withCursor.Parse_error_asexn->ifnot(can_fallback_shortcuts)thenraiseexnelseof_string_fallbacks(** Pretty print a function-like pseudo-class or pseudo-element *)letpp_func:'a.Pp.ctx->prefix:string->string->'aPp.t->'a->unit=functx~prefixnamecontent_ppvalue->Pp.stringctxprefix;Pp.stringctxname;Pp.charctx'(';content_ppctxvalue;Pp.charctx')'(** Helper functions for common patterns *)letpseudoctxname=Pp.stringctx(":"^name)letelemctxname=Pp.stringctx("::"^name)letvendorctxname=Pp.stringctx(":-"^name)letvendor_elemctxname=Pp.stringctx("::-"^name)(* CSS Selectors 4 sec. 3.6.1 keeps [:before] (CSS 2.1) as a deprecated
compatibility spelling for the four original pseudo-elements. Minified output
uses the shorter valid alias; pretty output preserves the parsed colon form
so the authored spelling round-trips. *)letlegacy_elemctxformname=letprefix=ifPp.minifiedctxthen":"elsematchformwithSingle->":"|Double->"::"inPp.stringctx(prefix^name)letfuncctxnamepp_contentvalue=pp_funcctx~prefix:":"namepp_contentvalueletelem_funcctxnamepp_contentvalue=pp_funcctx~prefix:"::"namepp_contentvalueletpp_vt_class_selectorctx(sel:vt_class_selector)=(matchsel.namewithSomen->Pp.stringctxn|None->());List.iter(funcls->Pp.charctx'.';Pp.stringctxcls)sel.classesletpp_combinatorctx=function|Descendant->Pp.spacectx()|Child->pp_tokenctx">"|Next_sibling->pp_tokenctx"+"|Subsequent_sibling->pp_tokenctx"~"|Column->pp_tokenctx"||"|Shadow_piercing->pp_tokenctx">>>"|Shadow_deep->pp_tokenctx"/deep/"letpp_relative_combinatorctx=function|Descendant->Pp.spacectx()|Child->Pp.stringctx">";Pp.space_if_prettyctx()|Next_sibling->Pp.stringctx"+";Pp.space_if_prettyctx()|Subsequent_sibling->Pp.stringctx"~";Pp.space_if_prettyctx()|Column->Pp.stringctx"||";Pp.space_if_prettyctx()|Shadow_piercing->Pp.stringctx">>>";Pp.space_if_prettyctx()|Shadow_deep->Pp.stringctx"/deep/";Pp.space_if_prettyctx()letstrsctxstrings=Pp.list~sep:Pp.commaPp.stringctxstringsletlang_rangectxstring=ifattr_value_needs_quotingstringthenPp.quoted_stringctxstringelsePp.stringctxstringletlangsctxstrings=Pp.list~sep:Pp.commalang_rangectxstringslethex_digits="0123456789abcdef"letadd_selector_hex_escapebufcode=Buffer.add_charbuf'\\';letrecemitnacc=match(n,acc)with|0,[]->Buffer.add_charbuf'0'|0,digits->List.iter(Buffer.add_charbuf)digits|_->emit(n/16)(hex_digits.[nmod16]::acc)inemitcode[];Buffer.add_charbuf' 'letfirst_needs_hex_escapename=matchString.lengthnamewith|0->false|len->letfirst=name.[0]in(first>='0'&&first<='9')||(first='-'&&len>1&&name.[1]>='0'&&name.[1]<='9')letadd_selector_asciibuf~first_needs_hex_escapeic=ifi=0&&first_needs_hex_escapethenadd_selector_hex_escapebuf(Char.codec)elsematchcwith|'\x00'..'\x1F'|'\x7F'->add_selector_hex_escapebuf(Char.codec)|'['->Buffer.add_stringbuf"\\["|']'->Buffer.add_stringbuf"\\]"|'\\'->Buffer.add_stringbuf"\\\\"|'('->Buffer.add_stringbuf"\\("|')'->Buffer.add_stringbuf"\\)"|','->Buffer.add_stringbuf"\\,"|'/'->Buffer.add_stringbuf"\\/"|':'->Buffer.add_stringbuf"\\:"|'%'->Buffer.add_stringbuf"\\%"|'.'->Buffer.add_stringbuf"\\."|'#'->Buffer.add_stringbuf"\\#"|' '->Buffer.add_stringbuf"\\ "|'"'->Buffer.add_stringbuf"\\\""|'\''->Buffer.add_stringbuf"\\'"|'@'->Buffer.add_stringbuf"\\@"|'*'->Buffer.add_stringbuf"\\*"|'>'->Buffer.add_stringbuf"\\>"|'+'->Buffer.add_stringbuf"\\+"|'~'->Buffer.add_stringbuf"\\~"|'&'->Buffer.add_stringbuf"\\&"|'^'->Buffer.add_stringbuf"\\^"|'$'->Buffer.add_stringbuf"\\$"|'='->Buffer.add_stringbuf"\\="|'!'->Buffer.add_stringbuf"\\!"|'|'->Buffer.add_stringbuf"\\|"|cwhenis_valid_nmcharc->Buffer.add_charbufc|c->Buffer.add_charbuf'\\';Buffer.add_charbufcletadd_selector_ucharbuf~add_asciiiu=letcp=Uchar.to_intuinifcp<0x80thenadd_asciii(Char.chrcp)elseadd_selector_hex_escapebufcpletadd_selector_malformedbuf~add_asciinameilen=forj=itoi+len-1doletc=name.[j]inifChar.codec<0x80thenadd_asciijcelseadd_selector_hex_escapebuf(Char.codec)done(* Fast path: most identifiers in real CSS are pure ASCII without any of the
characters the full escaper would need to handle. [is_safe_nmchar] is the
subset of {!is_valid_nmchar} that also rules out the leading-digit /
leading-dash-digit pattern so the unmodified bytes are a valid CSS ident
already. *)(** Escape a class or ID name for use inside a selector, following CSS section
9.1 rules: hex-escape control bytes and leading digits (or a leading dash
followed by a digit), and backslash-escape the punctuation characters that
otherwise terminate or reframe the selector. *)letis_safe_nmchar=function|'a'..'z'|'A'..'Z'|'0'..'9'|'_'|'-'->true|_->false(* Toplevel: [let exception] allocates the constructor on every call, and both
of these run once per selector name in the optimizer's inner loops. *)exceptionChar_rejectedletname_is_plain_ascii_identname=letlen=String.lengthnameiniflen=0thenfalseelseiffirst_needs_hex_escapenamethenfalseelsetryfori=0tolen-1doifnot(is_safe_nmcharname.[i])thenraiseChar_rejecteddone;truewithChar_rejected->falseletname_is_asciiname=letlen=String.lengthnameintryfori=0tolen-1doifChar.code(String.unsafe_getnamei)>=0x80thenraiseChar_rejecteddone;truewithChar_rejected->falseletescape_selector_namename=ifString.lengthname=0then""elseifname="-"then"\\-"elseifname_is_plain_ascii_identnamethennameelseletbuf=Buffer.create(String.lengthname*2)inletfirst_needs_hex_escape=first_needs_hex_escapenameinletadd_ascii=add_selector_asciibuf~first_needs_hex_escapein(* A name with escapable punctuation but no byte >= 0x80 (the common
Tailwind case: [hover:p-4], [w-1/2], [bg-[#fff]]) has no multi-byte
sequence to decode, so escape it byte by byte and skip the decoder, which
would box a [Uchar] per character. [String.iteri]'s index is the byte
offset, which equals the fold's index for single-byte input. *)ifname_is_asciinamethenString.iteriadd_asciinameelsebeginletfolder()i=function|Common.String.Scalaru->add_selector_ucharbuf~add_asciiiu|Common.String.Malformedlen->add_selector_malformedbuf~add_asciinameileninCommon.String.utf8_foldfolder()nameend;Buffer.contentsbufletpp_nsctx=function|Any->Pp.stringctx"*|"|None->(* Explicit "no namespace" prefix [(|)] -- distinct from omitting the
prefix entirely, which is encoded by passing [None : ns option]. *)Pp.charctx'|'|Prefixp->Pp.stringctx(escape_selector_namep);Pp.charctx'|'(** Pretty print nth function with optional "of" clause *)letreccan_follow_nth_of=function|Element(None,_)|Element(Some(Prefix_),_)|Universal(Some(Prefix_))->false|Compound(first::_)|Combined(first,_,_)|List(first::_)->can_follow_nth_offirst|_->trueletpp_nth_col_funcctxnameexpr=letpp_nth_colctx=function|Odd->Pp.stringctx"odd"|Even->Pp.stringctx"even"|expr->pp_nthctxexprinPp.charctx':';Pp.stringctxname;Pp.charctx'(';pp_nth_colctxexpr;Pp.charctx')'letcomma_spacectx()=Pp.stringctx", "(* CSS Selectors 4 3.5: when the universal selector [*] is not the only
component of a compound, the [*] may be omitted. Namespaced universals
([ns|*], [*|*]) carry namespace information and are preserved. *)letdrop_redundant_universal=function|[_]assingleton->singleton|components->letkept=List.filter(functionUniversalNone->false|_->true)componentsinifkept=[]thencomponentselsekept(* CSS Selectors 4 3.5: a compound selector that "contains a type selector or
universal selector [...] must come first in the sequence". So a rewrite that
splices a wrapped selector into the surrounding compound has to leave a
type-bearing argument wrapped: spliced, the two names fuse and [.a:is(code)]
reads as [.acode], a class nobody wrote. *)letreccarries_type_selector=function|Element_|Universal_->true|Compoundparts->List.existscarries_type_selectorparts|_->false(* Which elements [HTML] gives each state pseudo-class pair to. The three sets
differ. [:enabled] and [:disabled] split every form control plus [optgroup],
[option] and [fieldset] (HTML sec. 4.15, sec. 4.16.3). [:valid] and
[:invalid] split a [form] and a [fieldset] whole, but reach a form control
only while it is a candidate for constraint validation, which a disabled,
readonly or [type=hidden] one is not. [:required] and [:optional] split
[select] and [textarea]; [:optional] wants an [input] "to which the required
attribute applies", and it does not apply in e.g. the Hidden state. *)letenabled_carriers=["button";"fieldset";"input";"optgroup";"option";"select";"textarea"]letvalidity_carriers=["fieldset";"form"]letoptionality_carriers=["select";"textarea"]letstate_pair=function|Enabled->Some(Disabled,enabled_carriers)|Disabled->Some(Enabled,enabled_carriers)|Valid->Some(Invalid,validity_carriers)|Invalid->Some(Valid,validity_carriers)|Required->Some(Optional,optionality_carriers)|Optional->Some(Required,optionality_carriers)|_->None(* Whether every element [sel] can select is one of [carriers]. A type selector
proves it, and [:is()]/[:where()] prove it when every branch does; a class,
an attribute, a negation or [:has()] constrains something other than the
subject's element type, so it proves nothing. Selectors 4 sec. 3.5: the
subject of a complex selector is its rightmost compound. A namespaced type
selector is left out: the prefix binds to a namespace declared elsewhere in
the stylesheet, and these names are the HTML ones. *)letrecselects_onlycarriers=function|Element(None,name)->List.mem(String.lowercase_asciiname)carriers|Compoundparts->List.exists(selects_onlycarriers)parts|Isbranches|Wherebranches->branches<>[]&&List.for_all(selects_onlycarriers)branches|Combined(_,_,right)->selects_onlycarriersright|_->false(* CSS Selectors 4 sec. 12.1.1, 12.3.1 and 12.3.3: an element outside a pair's
set matches neither half, so [X:not(:enabled)] is [X:disabled] only inside a
compound that proves its subject carries the state. Which elements those are
is a [HTML] fact rather than one the CSS text proves, hence the
[enforce_spec] gate. *)letfold_state_negationsctxcomponents=if(not(Pp.minifiedctx))||ctx.Pp.enforce_specthencomponentselseList.map(function|Not[state]ascomponent->(matchstate_pairstatewith|Some(complement,carriers)whenList.exists(selects_onlycarriers)components->complement|_->component)|component->component)componentsletrecpp_nth_funcctxnameexprof_sel=Pp.charctx':';Pp.stringctxname;Pp.charctx'(';pp_nthctxexpr;(matchof_selwith|Somesels->Pp.stringctx" of";(matchselswith|first::_whenPp.minifiedctx&&can_follow_nth_offirst->()|_->Pp.charctx' ');Pp.list~sep:Pp.commappctxsels|None->());Pp.charctx')'andselsctxselectors=Pp.list~sep:Pp.commappctxselectorsandsels_nested_function_listsctxselectors=Pp.list~sep:Pp.commapp_nested_function_listsctxselectorsandspaced_sels_nested_function_listsctxselectors=Pp.list~sep:comma_spacepp_nested_function_listsctxselectorsandpp_nested_function_listsctx=function|Isselectors->funcctx"is"spaced_sels_nested_function_listsselectors|Whereselectors->funcctx"where"spaced_sels_nested_function_listsselectors|Compoundselectors->List.iter(pp_nested_function_listsctx)(fold_state_negationsctxselectors)|Combined(left,comb,right)->pp_nested_function_listsctxleft;pp_combinatorctxcomb;pp_nested_function_listsctxright|Relative(comb,right)->pp_relative_combinatorctxcomb;pp_nested_function_listsctxright|Listselectors->Pp.list~sep:Pp.commapp_nested_function_listsctxselectors|selector->ppctxselectorandpp:tPp.t=functx->function|Element(ns,name)->Pp.optionpp_nsctxns;Pp.stringctx(escape_selector_namename)|Classname->Pp.charctx'.';Pp.stringctx(escape_selector_namename)|Idname->Pp.charctx'#';Pp.stringctx(escape_selector_namename)|Universalns->Pp.optionpp_nsctxns;Pp.charctx'*'|Attribute(ns,attr_name,match_type,flag)->Pp.charctx'[';Pp.optionpp_nsctxns;Pp.stringctx(string_of_attr_nameattr_name);pp_attribute_matchctxmatch_type;pp_attr_flagctxflag;Pp.charctx']'(* Simple pseudo-classes *)|Hover->pseudoctx"hover"|Active->pseudoctx"active"|Focus->pseudoctx"focus"|Focus_visible->pseudoctx"focus-visible"|Focus_within->pseudoctx"focus-within"|Target->pseudoctx"target"|Link->pseudoctx"link"|Visited->pseudoctx"visited"|Any_link->pseudoctx"any-link"|Local_link->pseudoctx"local-link"|Target_within->pseudoctx"target-within"|Scope->pseudoctx"scope"|Root->pseudoctx"root"|Empty->pseudoctx"empty"|First_child->pseudoctx"first-child"|Last_child->pseudoctx"last-child"|Only_child->pseudoctx"only-child"|First_of_type->pseudoctx"first-of-type"|Last_of_type->pseudoctx"last-of-type"|Only_of_type->pseudoctx"only-of-type"|Enabled->pseudoctx"enabled"|Disabled->pseudoctx"disabled"|Read_only->pseudoctx"read-only"|Read_write->pseudoctx"read-write"|Placeholder_shown->pseudoctx"placeholder-shown"|Default->pseudoctx"default"|Checked->pseudoctx"checked"|Indeterminate->pseudoctx"indeterminate"|Blank->pseudoctx"blank"|Valid->pseudoctx"valid"|Invalid->pseudoctx"invalid"|In_range->pseudoctx"in-range"|Out_of_range->pseudoctx"out-of-range"|Required->pseudoctx"required"|Optional->pseudoctx"optional"|User_invalid->pseudoctx"user-invalid"|User_valid->pseudoctx"user-valid"|Inert->pseudoctx"inert"|Autofill->pseudoctx"autofill"|Fullscreen->pseudoctx"fullscreen"|Modal->pseudoctx"modal"|Picture_in_picture->pseudoctx"picture-in-picture"|Left->pseudoctx"left"|Right->pseudoctx"right"|First->pseudoctx"first"|Defined->pseudoctx"defined"|Playing->pseudoctx"playing"|Paused->pseudoctx"paused"|Seeking->pseudoctx"seeking"|Buffering->pseudoctx"buffering"|Stalled->pseudoctx"stalled"|Muted->pseudoctx"muted"|Volume_locked->pseudoctx"volume-locked"|Future->pseudoctx"future"|Past->pseudoctx"past"|Current->pseudoctx"current"|Popover_open->pseudoctx"popover-open"|Open->pseudoctx"open"|Unknown_pseudo_classname->pseudoctxname|Unknown_pseudo_class_call(name,args)->pp_funcctx~prefix:":"name(functxargs->Pp.stringctx(ifPp.minifiedctxthenParser.to_string_minifiedargselseParser.string_of_componentsargs))args|Local_scope->pseudoctx"local"|Global_scope->pseudoctx"global"|Local_callselectors->funcctx"local"selsselectors|Global_callselectors->funcctx"global"selsselectors(* Legacy pseudo-elements (use single colon in minified mode) *)|Beforeform->legacy_elemctxform"before"|Afterform->legacy_elemctxform"after"|First_letterform->legacy_elemctxform"first-letter"|First_lineform->legacy_elemctxform"first-line"(* Modern double-colon pseudo-elements *)|Backdrop->elemctx"backdrop"|Marker->elemctx"marker"|Placeholder->elemctx"placeholder"|Selection->elemctx"selection"|Target_text->elemctx"target-text"|Spelling_error->elemctx"spelling-error"|Grammar_error->elemctx"grammar-error"|File_selector_button->elemctx"file-selector-button"(* Vendor-specific pseudo-classes *)|Moz_focusring->vendorctx"moz-focusring"|Moz_any_callselectors->funcctx"-moz-any"selsselectors|Webkit_any->vendorctx"webkit-any"|Webkit_any_callselectors->funcctx"-webkit-any"selsselectors|Webkit_autofill->vendorctx"webkit-autofill"|Moz_ui_invalid->vendorctx"moz-ui-invalid"|Moz_ui_valid->vendorctx"moz-ui-valid"|Scrollbar_statestate->pseudoctx(scrollbar_state_identstate)(* Vendor-specific pseudo-elements *)|Moz_placeholder->vendor_elemctx"moz-placeholder"|Webkit_input_placeholder->vendor_elemctx"webkit-input-placeholder"|Ms_input_placeholder->vendor_elemctx"ms-input-placeholder"|Webkit_scrollbarpart->elemctx(scrollbar_part_identpart)|Webkit_search_cancel_button->vendor_elemctx"webkit-search-cancel-button"|Webkit_search_decoration->vendor_elemctx"webkit-search-decoration"(* Webkit datetime pseudo-elements *)|Webkit_datetime_edit_fields_wrapper->vendor_elemctx"webkit-datetime-edit-fields-wrapper"|Webkit_date_and_time_value->vendor_elemctx"webkit-date-and-time-value"|Webkit_datetime_edit->vendor_elemctx"webkit-datetime-edit"|Webkit_datetime_edit_year_field->vendor_elemctx"webkit-datetime-edit-year-field"|Webkit_datetime_edit_month_field->vendor_elemctx"webkit-datetime-edit-month-field"|Webkit_datetime_edit_day_field->vendor_elemctx"webkit-datetime-edit-day-field"|Webkit_datetime_edit_hour_field->vendor_elemctx"webkit-datetime-edit-hour-field"|Webkit_datetime_edit_minute_field->vendor_elemctx"webkit-datetime-edit-minute-field"|Webkit_datetime_edit_second_field->vendor_elemctx"webkit-datetime-edit-second-field"|Webkit_datetime_edit_millisecond_field->vendor_elemctx"webkit-datetime-edit-millisecond-field"|Webkit_datetime_edit_meridiem_field->vendor_elemctx"webkit-datetime-edit-meridiem-field"|Webkit_inner_spin_button->vendor_elemctx"webkit-inner-spin-button"|Webkit_outer_spin_button->vendor_elemctx"webkit-outer-spin-button"|Webkit_calendar_picker_indicator->vendor_elemctx"webkit-calendar-picker-indicator"|Webkit_details_marker->vendor_elemctx"webkit-details-marker"|Details_content->elemctx"details-content"(* Functional pseudo-elements *)|Partidents->elem_funcctx"part"(Pp.list~sep:Pp.spacePp.string)idents|Slottedselectors->elem_funcctx"slotted"selsselectors|CueNone->elemctx"cue"|Cue(Someselectors)->elem_funcctx"cue"selsselectors|Cue_regionNone->elemctx"cue-region"|Cue_region(Someselectors)->elem_funcctx"cue-region"selsselectors(* Functional pseudo-classes *)|IsselectorswhenPp.minifiedctx&&List.sortcompareselectors=[Link;Visited]->(* CSS Selectors 4 sec. 8.1: [:any-link] is defined as equivalent to
[:is(:link, :visited)], same specificity and shorter. *)ppctxAny_link|Isselectors->funcctx"is"selsselectors|Whereselectors->funcctx"where"selsselectors|Not[Not[inner]]whenPp.minifiedctx&¬(carries_type_selectorinner)->(* CSS Selectors 4 sec. 4.3: double negation [:not(:not(X))] is
spec-equivalent to [X] (and shorter under minify). [X] is spliced into
whatever compound holds the [:not()], so a type-bearing one stays
wrapped (Selectors 4 3.5, [carries_type_selector]). *)ppctxinner|Not[Dir"ltr"]whenPp.minifiedctx&¬ctx.Pp.enforce_spec->(* CSS Selectors 4 sec. 7.1 leaves directionality to the document
language, so CSS alone does not make [ltr] and [rtl] a partition: an
element the language gives no directionality matches neither. [HTML]
makes it one for every element in an HTML document, which is the host
fact [enforce_spec] drops. *)funcctx"dir"Pp.string"rtl"|Not[Dir"rtl"]whenPp.minifiedctx&¬ctx.Pp.enforce_spec->funcctx"dir"Pp.string"ltr"|Notselectors->funcctx"not"selsselectors|Hasselectors->funcctx"has"sels_nested_function_listsselectors|Nth_child(Index1,None)whenPp.minifiedctx->(* CSS Selectors 4 14: [:nth-child(1)] is spec-equivalent to
[:first-child]; the keyword form is shorter. *)Pp.stringctx":first-child"|Nth_last_child(Index1,None)whenPp.minifiedctx->(* Likewise [:nth-last-child(1)] is [:last-child]. *)Pp.stringctx":last-child"|Nth_of_type(Index1,None)whenPp.minifiedctx->Pp.stringctx":first-of-type"|Nth_last_of_type(Index1,None)whenPp.minifiedctx->Pp.stringctx":last-of-type"|Nth_child(expr,of_sel)->pp_nth_funcctx"nth-child"exprof_sel|Nth_last_child(expr,of_sel)->pp_nth_funcctx"nth-last-child"exprof_sel|Nth_of_type(expr,of_sel)->pp_nth_funcctx"nth-of-type"exprof_sel|Nth_last_of_type(expr,of_sel)->pp_nth_funcctx"nth-last-of-type"exprof_sel|Nth_colexpr->pp_nth_col_funcctx"nth-col"expr|Nth_last_colexpr->pp_nth_col_funcctx"nth-last-col"expr|Dirdir->funcctx"dir"Pp.stringdir|Langnames->funcctx"lang"langsnames|Statename->funcctx"state"Pp.stringname|Current_ofselectors->funcctx"current"selsselectors|HostNone->pseudoctx"host"|Host(Someselectors)->funcctx"host"selsselectors|Host_contextselectors->funcctx"host-context"selsselectors|Heading->Pp.stringctx":heading()"|Active_view_transition->pseudoctx"active-view-transition"|Active_view_transition_typeNone->Pp.stringctx":active-view-transition-type()"|Active_view_transition_type(Somet)->funcctx"active-view-transition-type"strst|Highlightnames->elem_funcctx"highlight"strsnames|View_transition->elemctx"view-transition"|View_transition_groupsel->elem_funcctx"view-transition-group"pp_vt_class_selectorsel|View_transition_image_pairsel->elem_funcctx"view-transition-image-pair"pp_vt_class_selectorsel|View_transition_oldsel->pp_funcctx~prefix:"::""view-transition-old"pp_vt_class_selectorsel|View_transition_newsel->pp_funcctx~prefix:"::""view-transition-new"pp_vt_class_selectorsel|Unknown_pseudo_elementname->elemctxname|Unknown_pseudo_element_call(name,args)->pp_funcctx~prefix:"::"name(functxargs->Pp.stringctx(ifPp.minifiedctxthenParser.to_string_minifiedargselseParser.string_of_componentsargs))args|Compoundselectors->List.iter(ppctx)(fold_state_negationsctxselectors)|Combined(left,comb,right)->ppctxleft;pp_combinatorctxcomb;ppctxright|Relative(comb,right)->pp_relative_combinatorctxcomb;ppctxright|Listselectors->Pp.list~sep:Pp.commappctxselectors|Nesting->Pp.charctx'&'openCommonletlist_map_preserve=List.map_preserveletlist_same=List.same(** Recursively map over all selectors in the tree. [f] is applied bottom-up; a
node whose children are unchanged and for which [f] returns its argument
keeps its physical identity. *)letrecmapfnode=letlstctorxs=letxs'=list_map_preserve(mapf)xsinifxs'==xsthennodeelsectorxs'inletnode'=matchnodewith|Combined(left,combinator,right)->letleft'=mapfleftandright'=mapfrightinifleft'==left&&right'==rightthennodeelseCombined(left',combinator,right')|Relative(combinator,right)->letright'=mapfrightinifright'==rightthennodeelseRelative(combinator,right')|Compoundxs->lst(funxs->Compoundxs)xs|Wherexs->lst(funxs->Wherexs)xs|Isxs->lst(funxs->Isxs)xs|Notxs->lst(funxs->Notxs)xs|Hasxs->lst(funxs->Hasxs)xs|Moz_any_callxs->lst(funxs->Moz_any_callxs)xs|Webkit_any_callxs->lst(funxs->Webkit_any_callxs)xs|Listxs->lst(funxs->Listxs)xs|Nth_child(nth,Somexs)->lst(funxs->Nth_child(nth,Somexs))xs|Nth_last_child(nth,Somexs)->lst(funxs->Nth_last_child(nth,Somexs))xs|Nth_of_type(nth,Somexs)->lst(funxs->Nth_of_type(nth,Somexs))xs|Nth_last_of_type(nth,Somexs)->lst(funxs->Nth_last_of_type(nth,Somexs))xs|Host(Somexs)->lst(funxs->Host(Somexs))xs|Current_ofxs->lst(funxs->Current_ofxs)xs|Host_contextxs->lst(funxs->Host_contextxs)xs|Slottedxs->lst(funxs->Slottedxs)xs|Cue(Somexs)->lst(funxs->Cue(Somexs))xs|Cue_region(Somexs)->lst(funxs->Cue_region(Somexs))xs|other->otherinfnode'(* Dedup and sort an unordered selector list by minified-printed form so that
permutations of the same alternatives collapse to a single canonical AST. *)letcanonicalize_unordered_listselectors=letseen=Hashtbl.create(List.lengthselectors)inletuniq=List.filter_map(funs->letkey=Pp.to_string~minify:trueppsinifHashtbl.memseenkeythenNoneelse(Hashtbl.addseenkey();Some(key,s)))selectorsinList.sort(fun(k1,_)(k2,_)->String.comparek1k2)uniq|>List.mapsnd(* [map] rewrites a compound's components before the compound itself, so the
[Is] branch below has already spliced a single-argument [:is()] into this
list. A component the reader refuses after the pseudo-element can only have
arrived that way, so the compound puts the wrapper back: Selectors 4 sec. 16
builds a pseudo-compound out of [<pseudo-element-selector>
<pseudo-class-selector>*] and sec. 3.6.3 makes the pseudo-class list per
pseudo-element. *)letrewrap_pseudo_compoundcomponents=letrecloopseen=function|[]->[]|c::restwhenis_pseudo_elementc->c::loop(Somec)rest|c::rest->letc=matchseenwith|Somepewhennot(is_pe_actionc&&pseudo_element_allowspec)->Is[c]|_->cinc::loopseenrestinifList.existsis_pseudo_elementcomponentsthenloopNonecomponentselsecomponents(* A compound in canonical form: the universal a longer compound implies is gone
(Selectors 4 sec. 3.5) and a pseudo-element keeps its own compound. *)letcanonical_compoundcomponents=matchrewrap_pseudo_compound(drop_redundant_universalcomponents)with|[single]->single|components->Compoundcomponents(* Substituting [&] splices a complex parent into a compound's leading slot,
giving [Compound [Combined (.L, a); :where(.dark)]] where reading the same
selector back gives [Combined (.L, Compound [a; :where(.dark)])]. The two
serialise alike and only the second is a compound in the grammar's sense, a
sequence of simple selectors, so the trailing components move onto the
subject. Without this the two spellings stay structurally distinct while
printing identically, and every structural comparison reads them apart. The
subject is a compound built here, after [map] has visited the components, so
it takes its canonical form here too: [svg *] under [&:where(.dark)] is [svg
:where(.dark)], as reading that text gives. *)letreclift_leading_combinator=function|Combined(left,comb,right)::rest->letsubject=matchlift_leading_combinator(right::rest)with|Somelifted->lifted|None->canonical_compound(right::rest)inSome(Combined(left,comb,subject))|_->None(* CSS Selectors 4 sec. 4.2: a single-argument [:is(s)] matches the same
elements as [s] with the same specificity, so it reduces to [s]. Sound only
when [s] is a single compound: a combinator ([Combined] / [Relative]) or a
[List] makes [:is()] a grouping boundary that cannot be spliced into the
surrounding compound. A type or universal selector cannot be spliced either:
this rewrite is node-local and cannot see whether the [:is()] heads its
compound, and only there may such a selector stand
([carries_type_selector]). *)letcanonicalize_isnodeselectors=letsorted=canonicalize_unordered_listselectorsinmatchsortedwith|[single]whenmatchsinglewith|Combined_|Relative_|List_->false|_->not(carries_type_selectorsingle)->single|_->iflist_samesortedselectorsthennodeelseIssorted(* Canonicalise so selectors denoting the same thing are structurally equal:
drop the implied [*] from a multi-part compound ([*.foo] -> [.foo]), collapse
a one-part compound, and dedup/sort selector-list alternatives by printed
form (both [List] and the set-based [:is]/[:where]/[:not]/[:has] lists, incl.
the [:-moz-any]/[:-webkit-any] aliases, whose order Selectors 4 makes
irrelevant to matching and specificity). [map] hands each node over without
saying where it sits, so every rewrite below has to be sound in any position;
the one that needs the top of a rule selector is [canonicalize]. *)letcanonicalize_nodessel=map(funnode->letcanonctorselectors=letsorted=canonicalize_unordered_listselectorsiniflist_samesortedselectorsthennodeelsectorsortedinmatchnodewith|Compoundcomponents->(matchlift_leading_combinatorcomponentswith|Somelifted->lifted|None->(matchcanonical_compoundcomponentswith|Compoundcomponents'whenlist_samecomponents'components->node|canonical->canonical))|Listselectors->canon(funxs->Listxs)selectors|Whereselectors->canon(funxs->Wherexs)selectors|Isselectors->canonicalize_isnodeselectors|Notselectors->canon(funxs->Notxs)selectors|Hasselectors->canon(funxs->Hasxs)selectors|Moz_any_callselectors->canon(funxs->Moz_any_callxs)selectors|Webkit_any_callselectors->canon(funxs->Webkit_any_callxs)selectors|Nth_child(nth,Someselectors)->canon(funxs->Nth_child(nth,Somexs))selectors|Nth_last_child(nth,Someselectors)->canon(funxs->Nth_last_child(nth,Somexs))selectors|Nth_of_type(nth,Someselectors)->canon(funxs->Nth_of_type(nth,Somexs))selectors|Nth_last_of_type(nth,Someselectors)->canon(funxs->Nth_last_of_type(nth,Somexs))selectors|other->other)selletis_sels=Isselslethassels=Hasselsletnotselectors=Notselectorsletnth_child?of_nth=Nth_child(nth,of_)lethost?selectors()=Hostselectors(* ========================= *)(* Analysis helpers *)(* ========================= *)lethas_focussel=any(functionFocus->true|_->false)sellethas_focus_withinsel=any(functionFocus_within->true|_->false)sellethas_focus_visiblesel=any(functionFocus_visible->true|_->false)selletzero_specificity={ids=0;classes=0;elements=0}letadd_specificityab={ids=a.ids+b.ids;classes=a.classes+b.classes;elements=a.elements+b.elements;}letcompare_specificityab=matchcomparea.idsb.idswith|0->(matchcomparea.classesb.classeswith|0->comparea.elementsb.elements|n->n)|n->nletmax_specificityxs=List.fold_left(funaccx->ifcompare_specificityxacc>0thenxelseacc)zero_specificityxsletrecspecificity=function|Id_->{ids=1;classes=0;elements=0}|Class_|Attribute_|Hover|Active|Focus|Focus_visible|Focus_within|Target|Link|Visited|Any_link|Local_link|Target_within|Scope|Root|Empty|First_child|Last_child|Only_child|First_of_type|Last_of_type|Only_of_type|Enabled|Disabled|Read_only|Read_write|Placeholder_shown|Default|Checked|Indeterminate|Blank|Valid|Invalid|In_range|Out_of_range|Required|Optional|User_invalid|User_valid|Inert|Autofill|Fullscreen|Modal|Picture_in_picture|Left|Right|First|Defined|Playing|Paused|Seeking|Buffering|Stalled|Muted|Volume_locked|Future|Past|Current|Popover_open|Open|Moz_focusring|Webkit_any|Webkit_autofill|Unknown_pseudo_class_|Unknown_pseudo_class_call_|Moz_placeholder|Webkit_input_placeholder|Ms_input_placeholder|Moz_ui_invalid|Moz_ui_valid|Scrollbar_state_|Webkit_scrollbarScrollbar|Webkit_search_cancel_button|Webkit_search_decoration|Webkit_datetime_edit_fields_wrapper|Webkit_date_and_time_value|Webkit_datetime_edit|Webkit_datetime_edit_year_field|Webkit_datetime_edit_month_field|Webkit_datetime_edit_day_field|Webkit_datetime_edit_hour_field|Webkit_datetime_edit_minute_field|Webkit_datetime_edit_second_field|Webkit_datetime_edit_millisecond_field|Webkit_datetime_edit_meridiem_field|Webkit_inner_spin_button|Webkit_outer_spin_button|Webkit_calendar_picker_indicator|Webkit_details_marker|Details_content|Nth_col_|Nth_last_col_|Dir_|Lang_|State_|Active_view_transition|Active_view_transition_type_|Heading|Local_scope|Global_scope->{ids=0;classes=1;elements=0}|Element_->{ids=0;classes=0;elements=1}|Universal_|Nesting->zero_specificity|Before_|After_|First_letter_|First_line_|Backdrop|Marker|Placeholder|Selection|Target_text|Spelling_error|Grammar_error|File_selector_button|Part_|View_transition|View_transition_group_|View_transition_image_pair_|View_transition_old_|View_transition_new_|Unknown_pseudo_element_|Unknown_pseudo_element_call_(* Sec. 17 counts a pseudo-element in C. [::-webkit-scrollbar] is read with
one colon too and weighs a class above, as every [pseudo_vendor_idents]
name does; the parts are [::]-only. *)|Webkit_scrollbar(Button|Track|Track_piece|Thumb|Corner|Resizer)|CueNone|Cue_regionNone->{ids=0;classes=0;elements=1}|Where_->zero_specificity|Isxs|Moz_any_callxs|Webkit_any_callxs|Notxs|Hasxs|Current_ofxs->xs|>List.mapspecificity|>max_specificity|Nth_child(_,of_)|Nth_last_child(_,of_)|Nth_of_type(_,of_)|Nth_last_of_type(_,of_)->add_specificity{ids=0;classes=1;elements=0}(matchof_with|None->zero_specificity|Somexs->xs|>List.mapspecificity|>max_specificity)|HostNone->{ids=0;classes=1;elements=0}|Host(Somexs)|Host_contextxs->add_specificity{ids=0;classes=1;elements=0}(xs|>List.mapspecificity|>max_specificity)|Slottedxs|Cue(Somexs)|Cue_region(Somexs)->add_specificity{ids=0;classes=0;elements=1}(xs|>List.mapspecificity|>max_specificity)|Local_callxs|Global_callxs->xs|>List.mapspecificity|>max_specificity|Highlight_->{ids=0;classes=0;elements=1}|Compoundxs->List.fold_left(funaccsel->add_specificityacc(specificitysel))zero_specificityxs|Combined(a,_,b)->add_specificity(specificitya)(specificityb)|Relative(_,sel)->specificitysel|Listxs->xs|>List.mapspecificity|>max_specificity(* Selectors 4 sec. 4.2 replaces the specificity of [:is()] with that of its
most specific argument, so splitting [:is(s1, s2, ...)] into the selector
list [s1, s2, ...] holds the weight of every match only when the arguments
already agree on one specificity. The arguments also have to be structurally
simple: [&] is out because [specificity] reads [Nesting] as zero while CSS
Nesting 1 sec. 3 weighs it as the most specific selector in the parent rule,
so the equality below would pass on a weight it never measured. *)letrecis_unwrap_safe_is_arg:t->bool=function|Element_|Class_|Id_|Universal_|Attribute_->true|Compoundparts->List.for_allis_unwrap_safe_is_argparts|_->falseletis_unwrap_safeselectors=List.for_allis_unwrap_safe_is_argselectors&&matchList.mapspecificityselectorswith|[]->false|s::rest->List.for_all(funs'->s'=s)rest(* Sound only where the [:is()] is a whole rule selector, or a whole member of
one top-level list: there the split lands in a selector list, which weighs
each branch on its own. Nested inside a [Compound] or a [Combined] it is a
grouping boundary and the split would change what matches. A lone argument
goes further than [canonicalize_is] can: that one is node-local and has to
leave a type or universal argument wrapped rather than splice it into a
compound it cannot see, and here there is no compound to splice into. *)letrectop_level_is_unwrapsel=matchselwith|Is[single]whenis_unwrap_safe_is_argsingle->single|Isselectorswhenis_unwrap_safeselectors->Listselectors|Listselectors->letexpanded=List.concat_map(funsel->matchtop_level_is_unwrapselwith|Listmembers->members|other->[other])selectorsiniflist_sameexpandedselectorsthenselelseList(canonicalize_unordered_listexpanded)|_->selletcanonicalizesel=top_level_is_unwrap(canonicalize_nodessel)letto_string?minifyt=Pp.to_string?minifypptletto_buffer?minifybuft=Pp.to_buffer?minifybufpptletexists_classpredsel=any(functionClassname->predname|_->false)selletrecfirst_class=function|Classn->Somen|Compoundxs->List.find_mapfirst_classxs|Combined(a,_,_)->first_classa|List(h::_)->first_classh|Isxs|Wherexs|Notxs|Hasxs|Moz_any_callxs|Webkit_any_callxs|Slottedxs|Current_ofxs->(matchxswith[]->None|h::_->first_classh)|Cue(Some(h::_))|Cue_region(Some(h::_))->first_classh|Part_->None|_->Noneletcontains_modifier_colonsel=exists_class(funname->String.containsname':')sel(** Check if selector contains :where(.group) - used for group-* modifiers.
Note: can't use 'any' because it transforms Where to List before calling p.
*)letis_group_class=function|Classs->s="group"||(String.lengths>6&&String.subs06="group/")|_->falseletrechas_group_marker=function|Wherexs->List.exists(function|selwhenis_group_classsel->true|Compoundcs->List.existsis_group_classcs|other->has_group_markerother)xs|Compoundxs->List.existshas_group_markerxs|Combined(a,_,b)->has_group_markera||has_group_markerb|Relative(_,b)->has_group_markerb|Listxs->List.existshas_group_markerxs|Cue(Somexs)|Cue_region(Somexs)->List.existshas_group_markerxs|Isxs|Notxs|Hasxs|Moz_any_callxs|Webkit_any_callxs|Slottedxs|Current_ofxs->List.existshas_group_markerxs|_->false(** Check if selector contains :where(.peer) - used for peer-* modifiers *)letis_peer_class=function|Classs->s="peer"||(String.lengths>5&&String.subs05="peer/")|_->falseletrechas_peer_marker=function|Wherexs->List.exists(function|selwhenis_peer_classsel->true|Compoundcs->List.existsis_peer_classcs|other->has_peer_markerother)xs|Compoundxs->List.existshas_peer_markerxs|Combined(a,_,b)->has_peer_markera||has_peer_markerb|Relative(_,b)->has_peer_markerb|Listxs->List.existshas_peer_markerxs|Cue(Somexs)|Cue_region(Somexs)->List.existshas_peer_markerxs|Isxs|Notxs|Hasxs|Moz_any_callxs|Webkit_any_callxs|Slottedxs|Current_ofxs->List.existshas_peer_markerxs|_->false(** Check if selector uses the :is(:where(...)) pattern used by group-* and
peer-* variants. *)lethas_is_where_patternsel=has_group_markersel||has_peer_markersel(** A "newer" pseudo-class with limited browser support: it must not be combined
in a selector list with an [:is(:where())] variant, since a browser that
lacks it drops the whole rule, whereas the forgiving variant would survive.
*)letis_newer_pseudo_class=function|User_valid|User_invalid->true|_->false(** Check if a selector directly uses a newer pseudo-class (not nested inside
:is()/:where() which provides forgiving parsing). *)letrechas_newer_pseudo_class=function|User_valid|User_invalid->true|Compoundxs->List.existshas_newer_pseudo_classxs|Combined(a,_,b)->has_newer_pseudo_classa||has_newer_pseudo_classb|Relative(_,b)->has_newer_pseudo_classb|Listxs->List.existshas_newer_pseudo_classxs|Current_ofxs->List.existshas_newer_pseudo_classxs(* Stop recursion at forgiving selectors -- :is()/:where() have forgiving
parsing, so newer pseudo-classes inside them don't cause the whole rule to
fail *)|Is_|Where_|Moz_any_call_|Webkit_any_call_->false|Notxs|Hasxs->List.existshas_newer_pseudo_classxs|_->falseletmodifier_prefixsel=matchfirst_classselwith|Option.None->Option.None|Option.Someclass_name->(matchString.index_optclass_name':'with|Option.Someidx->Option.Some(String.subclass_name0(idx+1))|Option.None->Option.None)let(&&)sel1sel2=compound[sel1;sel2]let(||)s1s2=combines1Columns2letcompare(a:t)b=Stdlib.compareab