1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027(** CSS container query condition types *)openSyntaxtypecomponent_values=Values.component_valuestypet=|Min_width_remoffloat|Min_width_pxofint|Namedofstring*t|Styleof{query:style_query;uppercase:bool}|Scroll_stateof{query:scroll_state_query;uppercase:bool}|Andoft*t|Oroft*t|Notoft|Feature_queryofMedia.tandstyle_query=|Booleanofstring|Declarationof{name:string;value:component_values}|Rangeofstyle_range|Allofstyle_query*style_query|Anyofstyle_query*style_query|Negofstyle_queryandstyle_range=|Compareof{left:component_values;op:range_operator;right:component_values;}|Intervalof{lower:component_values;lower_op:range_operator;name:string;upper_op:range_operator;upper:component_values;}andrange_operator=Lt|Lte|Eq|Gt|Gteandscroll_state_query=|Stateof{name:string;value:string}|Bothofscroll_state_query*scroll_state_query|Eitherofscroll_state_query*scroll_state_query|Negatedofscroll_state_query(* Format float without trailing period (24. -> 24, 24.5 -> 24.5) *)letformat_remf=lets=string_of_floatfinifString.ends_with~suffix:"."sthenString.subs0(String.lengths-1)elsesletstring_of_componentscvs=Cursor.string_of_components_verbatim~trim:truecvsletstring_of_range_operator=function|Lt->"<"|Lte->"<="|Eq->"="|Gt->">"|Gte->">="(* CSS Syntax 3 (ED) sec. 4.3.7: a [style()] query names a custom property, and
an escape can carry a [;] or a [}] into that name, so it is written back with
the escapes that read the same name (see [Properties.pp_property]). *)letrecstring_of_style_query~minify=function|Booleanname->Parser.escape_identname|Declaration{name;value}->letsep=ifminifythen":"else": "inString.concat""[Parser.escape_identname;sep;string_of_componentsvalue]|Rangerange->letsep=ifminifythen""else" "inletparts=matchrangewith|Compare{left;op;right}->[string_of_componentsleft;sep;string_of_range_operatorop;sep;string_of_componentsright;]|Interval{lower;lower_op;name;upper_op;upper}->[string_of_componentslower;sep;string_of_range_operatorlower_op;sep;Parser.escape_identname;sep;string_of_range_operatorupper_op;sep;string_of_componentsupper;]inString.concat""parts|All(a,b)->String.concat""[style_query_operand~minifya;" and ";style_query_operand~minifyb;]|Any(a,b)->String.concat""[style_query_operand~minifya;" or ";style_query_operand~minifyb]|Negq->String.concat""["not ";style_query_operand~minifyq]andstyle_query_operand~minifyq=String.concat""["(";string_of_style_query~minifyq;")"]letrecstring_of_scroll_state_query~minify=function|State{name;value}->letsep=ifminifythen":"else": "inString.concat""[name;sep;value]|Both(a,b)->String.concat""["(";string_of_scroll_state_query~minifya;") and (";string_of_scroll_state_query~minifyb;")";]|Either(a,b)->String.concat""["(";string_of_scroll_state_query~minifya;") or (";string_of_scroll_state_query~minifyb;")";]|Negatedq->String.concat""["not (";string_of_scroll_state_query~minifyq;")"]letrecminified_condition=function|And(a,b)->String.concat""[minified_operanda;" and ";minified_operandb]|Or(a,b)->String.concat""[minified_operanda;" or ";minified_operandb]|Notc->"not "^minified_operandc|t->to_string_with~pretty:false~minify:truetandminified_operand=function|(Min_width_rem_|Min_width_px_|Style_|Scroll_state_|Feature_query_)ast->to_string_with~pretty:false~minify:truet|t->String.concat""["(";minified_conditiont;")"]andto_string_with~pretty~minifyt=matchtwith|Min_width_remrem->(* The [width>=] range upgrade is a target-fact rewrite applied by
[lower_for_minify] in the optimize phase, not here. *)letsep=ifpretty&¬minifythen": "else":"inString.concat""["(min-width";sep;format_remrem;"rem)"]|Min_width_pxpx->letsep=ifpretty&¬minifythen": "else":"inString.concat""["(min-width";sep;Int.to_stringpx;"px)"]|Named(name,cond)->String.concat""[Parser.escape_identname;" ";to_string_with~pretty~minifycond]|Style{query;uppercase}->lethead=ifuppercasethen"STYLE("else"style("inString.concat""[head;string_of_style_query~minifyquery;")"]|Scroll_state{query;uppercase}->lethead=ifuppercasethen"SCROLL-STATE("else"scroll-state("inString.concat""[head;string_of_scroll_state_query~minifyquery;")"]|And(a,b)->ifminifythenminified_conditiontelseString.concat""["(";to_string_with~pretty~minifya;" and ";to_string_with~pretty~minifyb;")";]|Or(a,b)->ifminifythenminified_conditiontelseString.concat""["(";to_string_with~pretty~minifya;" or ";to_string_with~pretty~minifyb;")";]|Notc->ifminifythenminified_conditiontelseString.concat""["(not ";to_string_with~pretty~minifyc;")"]|Feature_queryf->Media.to_string~minifyfletto_string?(minify=false)t=to_string_with~pretty:false~minifytletto_stylesheet_string?(minify=false)t=to_string_with~pretty:true~minifytletppctxt=Pp.stringctx(to_string~minify:(Pp.minifiedctx)t)(* A style query holds component values, which carry the source positions they
were read from, so a structural walk would call two spellings of one query
different. Compare what the query asks, not where it was written. *)letstyle_query_rank=function|Boolean_->0|Declaration_->1|Range_->2|All_->3|Any_->4|Neg_->5letcompare_values(a:component_values)b=List.compareComponent.compareabletcompare_range_operator(a:range_operator)b=Stdlib.compareabletstyle_range_rank=functionCompare_->0|Interval_->1letrecfirst_difference=function|[]->0|field::rest->letc=field()inifc<>0thencelsefirst_differencerestletcompare_style_range(r1:style_range)(r2:style_range)=match(r1,r2)with|Comparec1,Comparec2->first_difference[(fun()->compare_range_operatorc1.opc2.op);(fun()->compare_valuesc1.leftc2.left);(fun()->compare_valuesc1.rightc2.right);]|Intervali1,Intervali2->first_difference[(fun()->String.comparei1.namei2.name);(fun()->compare_range_operatori1.lower_opi2.lower_op);(fun()->compare_range_operatori1.upper_opi2.upper_op);(fun()->compare_valuesi1.loweri2.lower);(fun()->compare_valuesi1.upperi2.upper);]|(Compare_|Interval_),_->Int.compare(style_range_rankr1)(style_range_rankr2)letreccompare_style_queryq1q2=match(q1,q2)with|Booleann1,Booleann2->String.comparen1n2|Declarationd1,Declarationd2->letc=String.compared1.named2.nameinifc<>0thencelsecompare_valuesd1.valued2.value|Ranger1,Ranger2->compare_style_ranger1r2|All(a1,b1),All(a2,b2)|Any(a1,b1),Any(a2,b2)->letc=compare_style_querya1a2inifc<>0thencelsecompare_style_queryb1b2|Nega,Negb->compare_style_queryab|_->Int.compare(style_query_rankq1)(style_query_rankq2)letreccomparet1t2=match(t1,t2)with|Min_width_remr1,Min_width_remr2->Float.comparer1r2|Min_width_pxp1,Min_width_pxp2->Int.comparep1p2|Named(n1,c1),Named(n2,c2)->letname_cmp=String.comparen1n2inifname_cmp<>0thenname_cmpelsecomparec1c2|Style{query=q1;_},Style{query=q2;_}->compare_style_queryq1q2|Scroll_state{query=q1;_},Scroll_state{query=q2;_}->Stdlib.compareq1q2|And(a1,b1),And(a2,b2)->letc=comparea1a2inifc<>0thencelsecompareb1b2|Or(a1,b1),Or(a2,b2)->letc=comparea1a2inifc<>0thencelsecompareb1b2|Nota,Notb->compareab|Feature_queryq1,Feature_queryq2->Media.compareq1q2|_->Stdlib.comparet1t2letcompare_scroll_state_query(a:scroll_state_query)b=Stdlib.compareabtypekind=Min_width|Otherletequal_kind(a:kind)b=a=bletreckind=function|Min_width_rem_|Min_width_px_->Min_width|Named(_,cond)->kindcond|And_|Or_|Not_|Style_|Scroll_state_|Feature_query_->Other(* Container feature queries reuse [Media] features, so they share the same
minify-time grammar upgrades. The dedicated [Min_width_*] shorthands carry an
implicit lower [width] bound, so they lower to the same [width>=V] range. *)letwidth_gel:t=Feature_query(Media.Cond(Media.Feature(Media.Range(Media.Width,Media.Ge,Media.Lengthl))))(* A container [not (X)] wraps the query rather than the condition, so the bound
{!Media.negated_bound} flips sits one level down. *)letnegated_bound=function|Feature_query(Media.Condcond)->(matchMedia.negated_boundcondwith|Someflipped->Some(Feature_query(Media.Condflipped))|None->None)|_->Noneletreclower_for_minifyc=matchcwith|Feature_queryq->letq'=Media.lower_for_minifyqinifq'==qthencelseFeature_queryq'|Min_width_remrem->width_ge(Values.Remrem)|Min_width_pxpx->width_ge(Values.Px(float_of_intpx))|Named(name,cond)->letcond'=lower_for_minifycondinifcond'==condthencelseNamed(name,cond')|And(a,b)->leta'=lower_for_minifyaandb'=lower_for_minifybinifa'==a&&b'==bthencelseAnd(a',b')|Or(a,b)->leta'=lower_for_minifyaandb'=lower_for_minifybinifa'==a&&b'==bthencelseOr(a',b')|Notc'->(letc''=lower_for_minifyc'inmatchnegated_boundc''with|Someflipped->flipped|None->ifc''==c'thencelseNotc'')|Style_|Scroll_state_->cletrecdrop_leading_whitespace=function|component::restwhenComponent.is_whitespacecomponent->drop_leading_whitespacerest|components->componentslettrim_componentscomponents=components|>drop_leading_whitespace|>List.rev|>drop_leading_whitespace|>List.revletcomponents_empty=function[]->true|_::_->falseletident_component=function|Component.Preserved{kind=Token.Identname;_}->Somename|_->Noneletident_iswordcomponent=matchident_componentcomponentwith|Somename->String.equal(String.lowercase_asciiname)word|None->falseletsplit_keywordwordcomponents=letrecloopbefore=function|[]->None|component::afterwhenident_iswordcomponent->Some(trim_components(List.revbefore),trim_componentsafter)|component::after->loop(component::before)afterinloop[]componentslethas_keywordwordcomponents=Option.is_some(split_keywordwordcomponents)letsingle_paren_bodycomponents=matchtrim_componentscomponentswith|[Component.Block{node={opening=Token.Paren;value;closed=true};_};]->Somevalue|_->Noneletis_condition_function=function|Component.Func{node={name;terminated=true;_};_}->List.mem(String.lowercase_asciiname)["style";"scroll-state"]|_->falseletis_query_operandcomponents=matchtrim_componentscomponentswith|[Component.Block{node={opening=Token.Paren;closed=true;_};_};]->true|[component]->is_condition_functioncomponent|_->falseletstarts_query=function|Component.Block{node={opening=Token.Paren;closed=true;_};_}::_->true|component::_whenis_condition_functioncomponent->true|first::_whenident_is"not"first->true|_->false(* CSS Containment 3 section 4: [<container-name>] excludes the keywords [none],
[and], [not], [or]; without this guard [Container.of_string "not (width)"]
would split as [Named ("not", "(width)")] instead of [Not (width)]. *)letis_reserved_container_namename=matchString.lowercase_asciinamewith|"none"|"and"|"not"|"or"->true|_->falseletsplit_named_componentscomponents=matchdrop_leading_whitespacecomponentswith|Component.Preserved{kind=Token.Identname;_}::(Component.Preserved{kind=Token.Whitespace_;_}::_asafter)whennot(is_reserved_container_namename)->letquery=trim_componentsafterinifstarts_queryquerythenSome(name,query)elseNone|_->Noneletis_custom_propertyname=Custom_property_name.is_validnamelethas_semicolon_component=List.exists(function|Component.Preserved{kind=Token.Semicolon;_}->true|_->false)letstyle_strip_ws=List.filter(function|Component.Preserved{kind=Token.Whitespace_;_}->false|_->true)(* CSS Conditional Rules 5 sec. 5.4 builds <style-range> out of <mf-comparison>
and <mf-lt> / <mf-gt>, and Media Queries 4 sec. 3 spells those [<mf-lt> = '<'
'='?], [<mf-gt> = '>' '='?], [<mf-eq> = '='] and [<mf-comparison> = <mf-lt> |
<mf-gt> | <mf-eq>]. So a bare [=] is a style range operator exactly as it is
a media feature one, and [same_direction] is what keeps it out of the two
interval branches, which take <mf-lt> twice or <mf-gt> twice. *)lettake_range_operator=function|Component.Preserved{kind=Token.Delim"<";_}::Component.Preserved{kind=Token.Delim"=";_}::rest->Some(Lte,rest)|Component.Preserved{kind=Token.Delim">";_}::Component.Preserved{kind=Token.Delim"=";_}::rest->Some(Gte,rest)|Component.Preserved{kind=Token.Delim"<";_}::rest->Some(Lt,rest)|Component.Preserved{kind=Token.Delim">";_}::rest->Some(Gt,rest)|Component.Preserved{kind=Token.Delim"=";_}::rest->Some(Eq,rest)|_->Noneletsame_directionlower_opupper_op=match(lower_op,upper_op)with|(Lt|Lte),(Lt|Lte)|(Gt|Gte),(Gt|Gte)->true|(Lt|Lte|Eq|Gt|Gte),_->falseletsplit_before_range_operatorcvs=letrecloopbeforerest=matchtake_range_operatorrestwith|Some(op,after)->Some(List.revbefore,op,after)|None->(matchrestwith[]->None|cv::rest->loop(cv::before)rest)inloop[]cvsletstyle_intervallowerlower_opmiddleupper_opupper=matchmiddlewith|[prop]->(matchident_componentpropwith|Somenamewhenis_custom_propertyname&&upper<>[]&&same_directionlower_opupper_op->Some(Range(Interval{lower;lower_op;name;upper_op;upper}))|Some_|None->None)|_->Noneletstyle_range_querycvs=matchsplit_before_range_operator(style_strip_wscvs)with|Some(left,op,rest)whenleft<>[]->(matchsplit_before_range_operatorrestwith|None->ifrest=[]thenNoneelseSome(Range(Compare{left;op;right=rest}))|Some(middle,upper_op,upper)->style_intervalleftopmiddleupper_opupper)|Some_|None->None(* Every failure below is an [@container] prelude failure, and the slice of the
query that failed carries the span the caret must point at. [t] anchors the
smallest enclosing construct, for a failure that has no components of its
own. *)leterrtcvsreason=letat=matchcvswith[]->t|_::_->Cursor.subtcvsinCursor.err_conditionat~at_rule:"@container"reasonletstyle_leaf_declarationtname_componentsvalue=match(style_strip_wsname_components,style_strip_wsvalue)with|[name_component],stripped_valuewhennot(has_semicolon_componentvalue)->(matchident_componentname_componentwith|Somenamewhenstripped_value<>[]||is_custom_propertyname->Declaration{name;value}|Some_|None->errtname_components"invalid style() container query")|_->errtname_components"invalid style() container query"letstyle_leaf_booleantcomponents=matchstyle_strip_wscomponentswith|[name_component]->(matchident_componentname_componentwith(* CSS Conditional Rules 5 section 6.2: a boolean [style()] query tests
whether a custom property has any value, so the ident must start with
[--]. A bare property name like [style(color)] is not a valid boolean
form. *)|Somenamewhenis_custom_propertyname->Booleanname|_->errtcomponents"invalid style() container query")|_->errtcomponents"invalid style() container query"letstyle_leaf_componentstcomponents=matchsplit_top_level_coloncomponentswith|Some(name_components,value)->style_leaf_declarationtname_componentsvalue|None->(matchstyle_range_querycomponentswith|Somequery->query|None->style_leaf_booleantcomponents)letrecstyle_query_componentstcomponents=letcomponents=trim_componentscomponentsinifcomponents_emptycomponentsthenerrtcomponents"empty style() container query";matchsplit_top_level_coloncomponentswith|Some_->style_leaf_componentstcomponents|None->style_query_operatortcomponentsandstyle_query_operatortcomponents=letlevel,unwrapped=matchsingle_paren_bodycomponentswith|Somebody->(trim_componentsbody,true)|None->(components,false)inifhas_keyword"and"level&&has_keyword"or"levelthenerrtlevel"mixed style() operators require grouping"elsematchsplit_keyword"or"levelwith|Some(lhs,rhs)->Any(style_query_componentstlhs,style_query_componentstrhs)|None->style_query_conjunctiont~components~level~unwrappedandstyle_query_conjunctiont~components~level~unwrapped=matchsplit_keyword"and"levelwith|Some(lhs,rhs)->All(style_query_componentstlhs,style_query_componentstrhs)|None->style_query_unaryt~components~level~unwrappedandstyle_query_unaryt~components~level~unwrapped=matchlevelwith|first::restwhenident_is"not"first->Neg(style_query_componentstrest)|_whenunwrapped->style_query_componentstlevel|_->style_leaf_componentstcomponentsletstyle_bodyt~uppercase(fn:Component.funcComponent.node)=lett=Cursor.subt[Component.Funcfn]inStyle{query=style_query_componentstfn.node.arguments;uppercase}letscroll_state_value_allowednamevalue=matchnamewith|"stuck"->(matchvaluewith|"top"|"right"|"bottom"|"left"|"block-start"|"block-end"|"inline-start"|"inline-end"|"none"->true|_->false)|"snapped"->(matchvaluewith|"block"|"inline"|"x"|"y"|"both"->true|_->false)|"scrollable"->(matchvaluewith|"top"|"right"|"bottom"|"left"|"block"|"inline"|"x"|"y"|"block-start"|"block-end"|"inline-start"|"inline-end"->true|_->false)|"scrolled"->(matchvaluewith|"top"|"right"|"bottom"|"left"|"block"|"inline"|"x"|"y"|"block-start"|"block-end"|"inline-start"|"inline-end"->true|_->false)|_->falseletscroll_state_query_leaftcomponents=matchsplit_top_level_coloncomponentswith|Some(name_components,value_components)->(match(style_strip_wsname_components,style_strip_wsvalue_components)with|[name_component],[value_component]->(match(ident_componentname_component,ident_componentvalue_component)with|Somename,Somevalue->letname=String.lowercase_asciinameinletvalue=String.lowercase_asciivalueinifscroll_state_value_allowednamevaluethenState{name;value}elseerrtvalue_components"invalid scroll-state() container query"|Some_,None|None,Some_|None,None->errtcomponents"invalid scroll-state() container query")|_->errtcomponents"invalid scroll-state() container query")|None->errtcomponents"invalid scroll-state() container query"letrecscroll_state_query_componentstcomponents=letcomponents=trim_componentscomponentsinifcomponents_emptycomponentsthenerrtcomponents"empty scroll-state() container query";matchsplit_top_level_coloncomponentswith|Some_->scroll_state_query_leaftcomponents|None->scroll_state_query_operatortcomponentsandscroll_state_query_operatortcomponents=letlevel,unwrapped=matchsingle_paren_bodycomponentswith|Somebody->(trim_componentsbody,true)|None->(components,false)inifhas_keyword"and"level&&has_keyword"or"levelthenerrtlevel"mixed scroll-state() operators require grouping"elsematchsplit_keyword"or"levelwith|Some(lhs,rhs)->Either(scroll_state_query_componentstlhs,scroll_state_query_componentstrhs)|None->scroll_state_query_conjunctiont~components~level~unwrappedandscroll_state_query_conjunctiont~components~level~unwrapped=matchsplit_keyword"and"levelwith|Some(lhs,rhs)->Both(scroll_state_query_componentstlhs,scroll_state_query_componentstrhs)|None->scroll_state_query_unaryt~components~level~unwrappedandscroll_state_query_unaryt~components~level~unwrapped=matchlevelwith|first::restwhenident_is"not"first->Negated(scroll_state_query_componentstrest)|_whenunwrapped->scroll_state_query_componentstlevel|_->scroll_state_query_leaftcomponentsletscroll_state_bodyt~uppercase(fn:Component.funcComponent.node)=lett=Cursor.subt[Component.Funcfn]inScroll_state{query=scroll_state_query_componentstfn.node.arguments;uppercase}typequery_surface=|Style_funcof{canonical_name:bool;fn:Component.funcComponent.node}|Scroll_state_funcof{canonical_name:bool;fn:Component.funcComponent.node;}|Parenthesized_feature|Other_querytyperange_direction=Lt_range|Gt_rangeletrange_direction_of_component=function|Component.Preserved{kind=Token.Delim"<";_}->SomeLt_range|Component.Preserved{kind=Token.Delim">";_}->SomeGt_range|_->Noneletrecstrip_ws=function|Component.Preserved{kind=Token.Whitespace_;_}::rest->strip_wsrest|cvs->cvsletnon_wscvs=List.filter(function|Component.Preserved{kind=Token.Whitespace_;_}->false|_->true)cvslethas_opposing_interval_componentscvs=letrecfirst_op=function|[]->None|cv::rest->(matchrange_direction_of_componentcvwith|Some_asop->op|None->first_oprest)inletrecsecond_opseen_first=function|[]->None|cv::rest->(match(range_direction_of_componentcv,seen_first)with|Some_,false->second_optruerest|Someop,true->Someop|None,_->second_opseen_firstrest)inmatch(first_opcvs,second_opfalsecvs)with|SomeLt_range,SomeGt_range|SomeGt_range,SomeLt_range->true|_->falselethas_dangling_range_operatorcvs=matchList.rev(non_wscvs)with|Component.Preserved{kind=Token.Delim("<"|">");_}::_->true|_->falseletclassify_query_surfacetcomponents=matchtrim_componentscomponentswith|[(Component.Func({node={name;terminated;_};_}asfn)ascv)]->(ifnotterminatedthenerrt[cv]"unmatched container query function";letlower=String.lowercase_asciinameinletcanonical_name=name=lowerinmatchlowerwith|"style"->Style_func{canonical_name;fn}|"scroll-state"->Scroll_state_func{canonical_name;fn}|_->Other_query)|[(Component.Block{node={opening=Token.Paren;value;closed};_}ascv);]->ifnotclosedthenerrt[cv]"unmatched container query parentheses";letvalue=strip_wsvalueinifcomponents_emptyvaluethenOther_queryelseifhas_dangling_range_operatorvaluethenerrtvalue"dangling range operator in container query"elseifhas_opposing_interval_componentsvaluethenerrtvalue"opposing interval operators in container query"elseParenthesized_feature|_->Other_query(* Lift a typed [Media.t] into a [Feature_query]. The container parser only
accepts single-feature media leaves at this point (compound forms are peeled
off by [unnamed_of_string] before [atom_of_string]), so anything that's not a
single feature is a parse error. *)letsingle_feature_of_media(media:Media.t)=matchmediawith|Media.Cond(Media.Feature_)->Somemedia|Media.Cond_|Media.List_|Media.Type_->Noneletspecific_of_componentstcomponents=iftrim_componentscomponents|>components_emptythenerrtcomponents"empty container query";matchclassify_query_surfacetcomponentswith|Style_func{canonical_name;fn}->style_bodyt~uppercase:(notcanonical_name)fn|Scroll_state_func{canonical_name;fn}->scroll_state_bodyt~uppercase:(notcanonical_name)fn|Parenthesized_feature->errtcomponents"unrecognised container feature query"|Other_query->errtcomponents"not a container-specific query"letunresolved_media_featurecomponents=matchtrim_componentscomponentswith|[Component.Block{node={opening=Token.Paren;value;closed=true};_};]->(matchsplit_top_level_colonvaluewith|Some(name_components,value_components)->(matchstyle_strip_wsname_componentswith|[name_component]->(matchident_componentname_componentwith|Somenamewhen(not(trim_componentsvalue_components|>components_empty))&&Component.has_varvalue_components->letvalue=string_of_componentsvalue_componentsinSome(Feature_query(Media.Cond(Media.Feature(Media.Plain(Media.name_of_stringname,Media.value_of_stringvalue)))))|Some_|None->None)|_->None)|None->None)|_->Noneletrecstrip_outer_componentscomponents=matchsingle_paren_bodycomponentswith|Somebody->strip_outer_componentsbody|None->trim_componentscomponentsletatom_of_componentstcomponents=letcomponents=trim_componentscomponentsinletstripped=strip_outer_componentscomponentsinmatchclassify_query_surfacetstrippedwith|_whenComponent.has_varcomponents->(matchunresolved_media_featurecomponentswith|Somequery->query|None->specific_of_componentststripped)|Style_func_|Scroll_state_func_->specific_of_componentststripped|Parenthesized_feature|Other_query->(letsource=string_of_componentscomponentsinmatchMedia.of_string_strictsourcewith|Media.Cond(Media.Feature(Media.Plain(Media.MinMedia.Width,value)))asmedia->(matchvaluewith|Media.Length(Values.Remrem)->Min_width_remrem|Media.Length(Values.Pxpx)whenFloat.is_integerpx->Min_width_px(int_of_floatpx)|_->Feature_querymedia)|media->(matchsingle_feature_of_mediamediawith|Somef->Feature_queryf|None->errtcomponents"not a container feature query")|exceptionError.Parse_error_->specific_of_componentststripped)letrecunnamed_of_componentstcomponents=letcomponents=trim_componentscomponentsinifcomponents_emptycomponentsthenerrtcomponents"empty container query";letlevel=matchsingle_paren_bodycomponentswith|SomebodywhenOption.is_none(split_top_level_colonbody)->trim_componentsbody|Some_|None->componentsinifhas_keyword"and"level&&has_keyword"or"levelthenerrtlevel"mixed container query operators require grouping"elseunnamed_or_componentst~componentslevelandunnamed_or_componentst~componentslevel=matchsplit_keyword"or"levelwith|Some(lhs,rhs)->Or(unnamed_of_componentstlhs,unnamed_of_componentstrhs)|None->unnamed_and_componentst~componentslevelandunnamed_and_componentst~componentslevel=matchsplit_keyword"and"levelwith|Some(lhs,rhs)->And(unnamed_of_componentstlhs,unnamed_of_componentstrhs)|None->unnamed_unary_componentst~componentslevelandunnamed_unary_componentst~components=function|first::restwhenident_is"not"first->ifnot(is_query_operandrest)thenerrtrest"container query: 'not' requires a query-in-parens operand";Not(unnamed_of_componentstrest)|_->atom_of_componentstcomponentsletreadt=letcomponents=Cursor.remainingtinmatchsplit_named_componentscomponentswith|Some(name,query)->Named(name,unnamed_of_componentstquery)|None->unnamed_of_componentstcomponentsletof_strings=read(Cursor.of_strings)letfeaturenamevalue=Feature_query(Media.featurenamevalue)letstyle?valueprop=letquery=matchvaluewith|None->ifis_custom_propertypropthenBooleanpropelseinvalid_arg"Container.style: boolean style queries require a custom property"|Somevalue->Declaration{name=prop;value=Cursor.remaining(Cursor.of_stringvalue)}inStyle{query;uppercase=false}letscroll_statenamevalue=Scroll_state{query=State{name;value};uppercase=false}(* ===== Canonical form ===== *)(* [normalize] is the one place that decides which spellings name one container
query, and its invariant runs one way only:
normalize a = normalize b => a and b select the same containers
Nothing promises the converse, and nothing should: two equivalent queries may
normalise apart, which costs a merge and never correctness. So every rewrite
below is one of the spec's own equivalences, never a heuristic. *)(* CSS Conditional Rules 5 sec. 6.1: a [<size-feature>] is spelled like a media
feature, so the [min-] prefix, the value-first bound and the two interval
directions are the Media Queries 4 sec. 2.4.3 and sec. 2.4.4 equivalences
here too. [Media.normalize] owns that fold; the [Min_width_*] shorthands are
cascade's compact spelling of [(min-width: V)], so they enter it as that
feature rather than being folded a second way. *)letmin_widthl:Media.t=Media.Cond(Media.Feature(Media.Plain(Media.MinMedia.Width,Media.Lengthl)))(* CSS Syntax 3 (ED) sec. 5.5.6 consumes a declaration by discarding the
whitespace after the colon and removing the trailing whitespace tokens, so a
[<style-feature-plain>] means the same however much space the source left
around its value. A [<style-range>] carries no whitespace at all: the parser
strips it before splitting on the comparison. *)letrecnormalize_style_query(q:style_query):style_query=matchqwith|Declaration{name;value}->letvalue'=trim_componentsvalueinifvalue'==valuethenqelseDeclaration{name;value=value'}|All(a,b)->leta'=normalize_style_queryaandb'=normalize_style_querybinifa'==a&&b'==bthenqelseAll(a',b')|Any(a,b)->leta'=normalize_style_queryaandb'=normalize_style_querybinifa'==a&&b'==bthenqelseAny(a',b')|Nega->leta'=normalize_style_queryainifa'==athenqelseNega'|Boolean_|Range_->q(* Rebuild only what changed, as [lower_for_minify] does: [equal] runs this on
both sides of every merge test, and the queries it is asked about are usually
already normal. *)letrecnormalize(c:t):t=matchcwith|Min_width_remrem->Feature_query(Media.normalize(min_width(Remrem)))|Min_width_pxpx->Feature_query(Media.normalize(min_width(Px(float_of_intpx))))|Feature_queryq->letq'=Media.normalizeqinifq'==qthencelseFeature_queryq'|Named(name,cond)->letcond'=normalizecondinifcond'==condthencelseNamed(name,cond')(* CSS Values 4 sec. 9: function names are ASCII case-insensitive, so the
[STYLE(] / [SCROLL-STATE(] spelling the AST keeps for round-trip says
nothing about which query this is. *)|Style{query;uppercase}->letquery'=normalize_style_queryqueryinifquery'==query&¬uppercasethencelseStyle{query=query';uppercase=false}|Scroll_state{query;uppercase}->ifnotuppercasethencelseScroll_state{query;uppercase=false}|And(a,b)->leta'=normalizeaandb'=normalizebinifa'==a&&b'==bthencelseAnd(a',b')|Or(a,b)->leta'=normalizeaandb'=normalizebinifa'==a&&b'==bthencelseOr(a',b')|Nota->(leta'=normalizeainmatchnegated_bounda'with|Someflipped->flipped|None->ifa'==athencelseNota')(* CSS Conditional 5 sec. 6.2: a [style()] query reads the property it names on
the query container; a range's operands may name it too. *)letrecqueried_names(c:t):stringlist=letof_range_value(value:component_values)=matchvaluewith|[Component.Preserved{kind=Token.Identname;_}]->[name]|_->[]inletof_range=function|Compare{left;right;_}->of_range_valueleft@of_range_valueright|Interval{lower;name;upper;_}->(name::of_range_valuelower)@of_range_valueupperinletrecof_style=function|Booleanname|Declaration{name;_}->[name]|Rangerange->of_rangerange|All(a,b)|Any(a,b)->of_stylea@of_styleb|Negq->of_styleqinmatchcwith|Min_width_rem_|Min_width_px_|Feature_query_|Scroll_state_->[]|Named(_,q)|Notq->queried_namesq|Style{query;_}->of_stylequery|And(a,b)|Or(a,b)->queried_namesa@queried_namesb(* Written out rather than left to the structural operators: [equal] is the gate
on block merging, and a comparison that walks a runtime representation is how
a spelling difference becomes a merge. *)letequal_components(a:component_values)b=List.equalComponent.equalabletequal_range_operator(a:range_operator)b=match(a,b)with|Lt,Lt|Lte,Lte|Eq,Eq|Gt,Gt|Gte,Gte->true|(Lt|Lte|Eq|Gt|Gte),_->falseletequal_style_range(a:style_range)b=match(a,b)with|Comparea,Compareb->equal_range_operatora.opb.op&&equal_componentsa.leftb.left&&equal_componentsa.rightb.right|Intervala,Intervalb->String.equala.nameb.name&&equal_range_operatora.lower_opb.lower_op&&equal_range_operatora.upper_opb.upper_op&&equal_componentsa.lowerb.lower&&equal_componentsa.upperb.upper|(Compare_|Interval_),_->falseletrecequal_style_query(a:style_query)b=match(a,b)with|Booleana,Booleanb->String.equalab|Declarationa,Declarationb->String.equala.nameb.name&&equal_componentsa.valueb.value|Rangea,Rangeb->equal_style_rangeab|All(a1,b1),All(a2,b2)|Any(a1,b1),Any(a2,b2)->equal_style_querya1a2&&equal_style_queryb1b2|Nega,Negb->equal_style_queryab|(Boolean_|Declaration_|Range_|All_|Any_|Neg_),_->falseletrecequal_scroll_state_query(a:scroll_state_query)b=match(a,b)with|Statea,Stateb->String.equala.nameb.name&&String.equala.valueb.value|Both(a1,b1),Both(a2,b2)|Either(a1,b1),Either(a2,b2)->equal_scroll_state_querya1a2&&equal_scroll_state_queryb1b2|Negateda,Negatedb->equal_scroll_state_queryab|(State_|Both_|Either_|Negated_),_->false(* A [<container-name>] is a [<custom-ident>] (Conditional Rules 5 sec. 5.4), so
it is compared by the name the parser unescaped, not by how it was
spelled. *)letrecequal_normalized(a:t)b=match(a,b)with|Min_width_rema,Min_width_remb->Float.equalab|Min_width_pxa,Min_width_pxb->Int.equalab|Named(n1,c1),Named(n2,c2)->String.equaln1n2&&equal_normalizedc1c2|Stylea,Styleb->equal_style_querya.queryb.query|Scroll_statea,Scroll_stateb->equal_scroll_state_querya.queryb.query|And(a1,b1),And(a2,b2)|Or(a1,b1),Or(a2,b2)->equal_normalizeda1a2&&equal_normalizedb1b2|Nota,Notb->equal_normalizedab|Feature_querya,Feature_queryb->Media.equalab|((Min_width_rem_|Min_width_px_|Named_|Style_|Scroll_state_|And_|Or_|Not_|Feature_query_),_)->falseletequalab=equal_normalized(normalizea)(normalizeb)