123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)(* See Computed.mli *)openCss_syntaxmoduleV=Css_valuestypedisplay=|Inline|Block|Inline_block|List_item|Flex|Inline_flex|Grid|Table|Table_row_group|Table_row|Table_cell|Table_caption|Display_none|Contentstypeposition=Static|Relative|Absolute|Fixed|Stickytypesize=Auto|LenofCss_values.lengthtypefamily=Serif|Sans_serif|Monospacetypewhite_space=Normal|Pre|Nowrap|Pre_wrap|Pre_linetypetext_align=Align_left|Align_right|Align_center|Align_justifytypevertical_align=Baseline|Middle|Top|Bottom|Text_top|Text_bottom|Sub|Supertypeline_height=Line_normal|Factoroffloat|Line_pxoffloattypeside=Side_none|Side_left|Side_right|Side_bothtypeflex_direction=Row|Row_reverse|Column|Column_reversetypealign=Start|End|Center|Stretch|Space_between|Space_around|Space_evenly|Align_baselinetypet={display:display;position:position;float:side;clear:side;top:size;right:size;bottom:size;left:size;width:size;height:size;min_width:Css_values.length;min_height:Css_values.length;max_width:size;max_height:size;margin:size*size*size*size;padding:Css_values.length*Css_values.length*Css_values.length*Css_values.length;border_width:float*float*float*float;border_color:Css_values.color*Css_values.color*Css_values.color*Css_values.color;border_box:bool;color:Css_values.color;background:Css_values.color;background_image:stringoption;mask_image:stringoption;font_size:float;bold:bool;italic:bool;family:family;line_height:line_height;text_align:text_align;underline:bool;line_through:bool;uppercase:bool;white_space:white_space;vertical_align:vertical_align;list_style:string;visible:bool;overflow_hidden:bool;flex_direction:flex_direction;flex_wrap:bool;justify_content:align;align_items:align;align_self:alignoption;flex_grow:float;flex_shrink:float;flex_basis:size;row_gap:Css_values.length;column_gap:Css_values.length;custom:(string*Css_syntax.componentlist)list;}letzero=V.zeroletblack=V.blackletinitial:t={display=Inline;position=Static;float=Side_none;clear=Side_none;top=Auto;right=Auto;bottom=Auto;left=Auto;width=Auto;height=Auto;min_width=zero;min_height=zero;max_width=Auto;max_height=Auto;margin=(Lenzero,Lenzero,Lenzero,Lenzero);padding=(zero,zero,zero,zero);border_width=(0.,0.,0.,0.);border_color=(black,black,black,black);border_box=false;color=black;background=V.transparent;background_image=None;mask_image=None;font_size=16.;bold=false;italic=false;family=Serif;line_height=Line_normal;text_align=Align_left;underline=false;line_through=false;uppercase=false;white_space=Normal;vertical_align=Baseline;list_style="disc";visible=true;overflow_hidden=false;flex_direction=Row;flex_wrap=false;justify_content=Start;align_items=Stretch;align_self=None;flex_grow=0.;flex_shrink=1.;flex_basis=Auto;row_gap=zero;column_gap=zero;custom=[];}(*****************************************************************************)(* Shorthands *)(*****************************************************************************)letident(c:component):stringoption=matchcwithToken(Idents)->Some(String.lowercase_asciis)|_->Noneletsides=["top";"right";"bottom";"left"](* one to four values: top, right, bottom, left *)letfour(vs:componentlist):componentlistlistoption=matchV.partsvswith|[a]->Some[[a];[a];[a];[a]]|[a;b]->Some[[a];[b];[a];[b]]|[a;b;c]->Some[[a];[b];[c];[b]]|[a;b;c;d]->Some[[a];[b];[c];[d]]|_->Noneletborder_styles=["none";"hidden";"dotted";"dashed";"solid";"double";"groove";"ridge";"inset";"outset"]letis_width(c:component)=matchcwith|Token(Dimension_)|Token(Number0.)|Func_->true|Token(Idents)->List.mem(String.lowercase_asciis)["thin";"medium";"thick"]|_->false(* "1px solid #ccc", in any order *)letborder_parts(vs:componentlist):(string*componentlist)list=List.fold_left(funaccc->matchidentcwith|SomeswhenList.memsborder_styles->("style",[c])::acc|_whenis_widthc->("width",[c])::acc|_->("color",[c])::acc)[](V.partsvs)(* a url(x) or url("x"): x *)leturl_of(c:component):stringoption=matchcwith|Token(Urlu)->Someu|Func(f,args)whenString.lowercase_asciif="url"->(matchCss_syntax.trimargswith[Token(Stringu)]->Someu|_->None)|_->Noneletexpand((name,value):string*componentlist):(string*componentlist)list=letper_sideprefixsuffix=matchfourvaluewith|Somevs->List.map2(funsv->(prefix^s^suffix,v))sidesvs|None->[]inmatchnamewith|"margin"->per_side"margin-"""|"padding"->per_side"padding-"""|"inset"->per_side""""|"border-width"->per_side"border-""-width"|"border-style"->per_side"border-""-style"|"border-color"->per_side"border-""-color"|"border"->(* a border's style, if not said, is none: "border: 1px red" draws
* nothing; its width, medium *)letps=border_partsvalueinletwith_defaults=ps@(ifList.mem_assoc"style"psthen[]else[("style",[Token(Ident"none")])])inList.concat_map(funs->List.map(fun(k,v)->("border-"^s^"-"^k,v))with_defaults)sides|"border-top"|"border-right"|"border-bottom"|"border-left"->letps=border_partsvalueinletps=ps@ifList.mem_assoc"style"psthen[]else[("style",[Token(Ident"none")])]inList.map(fun(k,v)->(name^"-"^k,v))ps|"background"->(* its colour, where it has one (an image alone makes it
* transparent), and its picture, its first url() *)(matchList.find_opt(func->V.color~current:black[c]<>None)(V.partsvalue)with|Somec->[("background-color",[c])]|None->[("background-color",[Token(Ident"transparent")])])@[("background-image",matchList.find_opt(func->url_ofc<>None)(V.partsvalue)withSomec->[c]|None->[Token(Ident"none")])]|"font"->((* [style] [variant] [weight] size[/line-height] family *)letrecgoacc=function|[]->acc|c::rest->(matchidentcwith|Some("italic"|"oblique")->go(("font-style",[c])::acc)rest|Some("bold"|"bolder"|"lighter")->go(("font-weight",[c])::acc)rest|Some("normal"|"small-caps")->goaccrest|_->(matchcwith|Token(Numbern)whenn>=100.->go(("font-weight",[c])::acc)rest|_->(letacc=("font-size",[c])::accinmatchrestwith|Token(Delim'/')::lh::family->("line-height",[lh])::("font-family",family)::acc|family->("font-family",family)::acc)))ingo[](V.partsvalue))|"flex"->(letnumc=matchcwithToken(Numbern)->Somen|_->Noneinletnf=[Token(Numberf)]inmatchV.partsvaluewith|[c]whenidentc=Some"none"->[("flex-grow",n0.);("flex-shrink",n0.);("flex-basis",[Token(Ident"auto")])]|[c]whenidentc=Some"auto"->[("flex-grow",n1.);("flex-shrink",n1.);("flex-basis",[Token(Ident"auto")])]|[c]whennumc<>None->[("flex-grow",[c]);("flex-shrink",n1.);("flex-basis",[Token(Percentage0.)])]|[c]->[("flex-grow",n1.);("flex-shrink",n1.);("flex-basis",[c])]|[g;s]whennums<>None->[("flex-grow",[g]);("flex-shrink",[s]);("flex-basis",[Token(Percentage0.)])]|[g;b]->[("flex-grow",[g]);("flex-shrink",n1.);("flex-basis",[b])]|[g;s;b]->[("flex-grow",[g]);("flex-shrink",[s]);("flex-basis",[b])]|_->[])|"gap"|"grid-gap"->(matchV.partsvaluewith[a]->[("row-gap",[a]);("column-gap",[a])]|[a;b]->[("row-gap",[a]);("column-gap",[b])]|_->[])|"list-style"->(matchList.find_opt(func->matchidentcwithSomes->s<>"inside"&&s<>"outside"|None->false)(V.partsvalue)with|Somec->[("list-style-type",[c])]|None->[])|"overflow"|"overflow-x"|"overflow-y"->[("overflow",value)]|"text-decoration-line"->[("text-decoration",value)]|"flex-flow"->List.map(func->matchidentcwithSome("wrap"|"nowrap"|"wrap-reverse")->("flex-wrap",[c])|_->("flex-direction",[c]))(V.partsvalue)|_->[(name,value)](*****************************************************************************)(* The properties *)(*****************************************************************************)letinherited=["color";"font-size";"font-weight";"font-style";"font-family";"line-height";"text-align";"text-transform";"white-space";"visibility";"list-style-type";"text-decoration"]letdisplay_of(s:string):displayoption=matchswith|"inline"->SomeInline|"block"|"flow-root"|"block flow"->SomeBlock|"inline-block"|"inline-grid"|"inline-table"->SomeInline_block|"list-item"->SomeList_item|"flex"->SomeFlex|"inline-flex"->SomeInline_flex|"grid"->SomeGrid|"table"->SomeTable|"table-row-group"|"table-header-group"|"table-footer-group"->SomeTable_row_group|"table-row"->SomeTable_row|"table-cell"->SomeTable_cell|"table-caption"->SomeTable_caption|"none"|"table-column"|"table-column-group"->SomeDisplay_none|"contents"->SomeContents|_->Noneletalign_of(s:string):alignoption=matchswith|"flex-start"|"start"|"left"|"self-start"|"normal"->SomeStart|"flex-end"|"end"|"right"|"self-end"->SomeEnd|"center"->SomeCenter|"stretch"->SomeStretch|"space-between"->SomeSpace_between|"space-around"->SomeSpace_around|"space-evenly"->SomeSpace_evenly|"baseline"|"first baseline"->SomeAlign_baseline|_->Noneletwidth_keyword(s:string):floatoption=matchswith"thin"->Some1.|"medium"->Some3.|"thick"->Some5.|_->Noneletcompute(m:Cascade.media)~(root_font_size:float)~(parent:t)(declared:(string*componentlist)list):t=(* the custom properties first: inherited, overridden by this element's *)letcustom=List.fold_left(funacc(n,v)->ifString.lengthn>2&&String.subn02="--"then(n,v)::List.remove_assocnaccelseacc)parent.customdeclaredinletlookupn=List.assoc_optncustominletdecls=declared|>List.filter(fun(n,_)->not(String.lengthn>2&&String.subn02="--"))|>List.filter_map(fun(n,v)->Option.map(funv->(n,v))(V.substitutelookupv))|>List.concat_mapexpandin(* a property's value: this element's, else (if inherited) its
* parent's -- None, the caller's initial value *)letget(name:string):componentlistoption=matchList.assoc_optname(List.revdecls)with|Somev->(matchV.partsvwith|[Token(Identk)]whenString.lowercase_asciik="inherit"->Some[Token(Ident"inherit")]|[Token(Identk)]whenString.lowercase_asciik="initial"->None|[Token(Identk)]whenList.mem(String.lowercase_asciik)["unset";"revert";"revert-layer"]->ifList.memnameinheritedthenSome[Token(Ident"inherit")]elseNone|_->Somev)|None->ifList.memnameinheritedthenSome[Token(Ident"inherit")]elseNoneinletis_inheritv=matchvwithSome[Token(Ident"inherit")]->true|_->falseinletwordname=matchgetnamewithSomev->(matchV.partsvwith[c]->identc|cs->Some(String.lowercase_ascii(to_stringcs)))|None->Nonein(* font-size first: an em here is the parent's *)letfont_size=letpctx:V.context={em=parent.font_size;rem=root_font_size;viewport_width=m.width;viewport_height=m.height}inmatchget"font-size"with|vwhenis_inheritv->parent.font_size|Somev->(matchV.partsvwith|[Token(Percentagep)]->parent.font_size*.p/.100.|[Token(Identk)]->(matchString.lowercase_asciikwith|"xx-small"->9.|"x-small"->10.|"small"->13.|"medium"->16.|"large"->18.|"x-large"->24.|"xx-large"->32.|"xxx-large"->48.|"larger"->parent.font_size*.1.2|"smaller"->parent.font_size/.1.2|_->parent.font_size)|[c]->(matchV.lengthpctxcwithSomel->Float.max0.(V.resolvelparent.font_size)|None->parent.font_size)|_->parent.font_size)|None->16.inletctx:V.context={em=font_size;rem=root_font_size;viewport_width=m.width;viewport_height=m.height}inletlen_ofv=matchV.partsvwith[c]->V.lengthctxc|_->Nonein(* a property: [f] of its value, [inh] the parent's, [init] else *)letpropname~inh~initf=matchgetnamewith|vwhenis_inheritv->inh|Somev->(matchfvwithSomex->x|None->ifList.memnameinheritedtheninhelseinit)|None->initin(* [inh] the parent's value: what inherit asks, even of a property
* not inherited by default *)letlengthname~inh~init=propname~inh~initlen_ofinletsizename~inh~init=propname~inh~init(funv->matchV.partsvwith[c]whenidentc=Some"auto"||identc=Some"none"->SomeAuto|_->Option.map(funl->Lenl)(len_ofv))inlet(pmt,pmr,pmb,pml),(ppt,ppr,ppb,ppl)=(parent.margin,parent.padding)inletcolor=prop"color"~inh:parent.color~init:black(funv->V.color~current:parent.colorv)inletcolourname=propname~inh:color~init:color(funv->V.color~current:colorv)inletborders=letstyle=word("border-"^s^"-style")inletvisible=matchstylewithSome("none"|"hidden")|None->false|Some_->trueinletw=prop("border-"^s^"-width")~inh:3.~init:3.(funv->matchV.partsvwith|[Token(Identk)]->width_keyword(String.lowercase_asciik)|_->Option.map(funl->V.resolvel0.)(len_ofv))in((ifvisiblethenwelse0.),colour("border-"^s^"-color"))inlet(bt,ct),(br,cr),(bb,cb),(bl,cl)=(border"top",border"right",border"bottom",border"left")inletdecoration=word"text-decoration"inlethas_decorationk=matchdecorationwithSomed->List.memk(String.split_on_char' 'd)|None->falsein{display=(matchget"display"with|Somev->(matchdisplay_of(String.lowercase_ascii(to_string(V.partsv|>List.filteri(funi_->i=0))))withSomed->d|None->Inline)|None->Inline);position=(matchword"position"withSome"relative"->Relative|Some"absolute"->Absolute|Some"fixed"->Fixed|Some"sticky"->Sticky|_->Static);float=(matchword"float"withSome"left"->Side_left|Some"right"->Side_right|_->Side_none);clear=(matchword"clear"withSome"left"->Side_left|Some"right"->Side_right|Some"both"->Side_both|_->Side_none);top=size"top"~inh:parent.top~init:Auto;right=size"right"~inh:parent.right~init:Auto;bottom=size"bottom"~inh:parent.bottom~init:Auto;left=size"left"~inh:parent.left~init:Auto;width=size"width"~inh:parent.width~init:Auto;height=size"height"~inh:parent.height~init:Auto;min_width=length"min-width"~inh:parent.min_width~init:zero;min_height=length"min-height"~inh:parent.min_height~init:zero;max_width=size"max-width"~inh:parent.max_width~init:Auto;max_height=size"max-height"~inh:parent.max_height~init:Auto;margin=(size"margin-top"~inh:pmt~init:(Lenzero),size"margin-right"~inh:pmr~init:(Lenzero),size"margin-bottom"~inh:pmb~init:(Lenzero),size"margin-left"~inh:pml~init:(Lenzero));padding=(length"padding-top"~inh:ppt~init:zero,length"padding-right"~inh:ppr~init:zero,length"padding-bottom"~inh:ppb~init:zero,length"padding-left"~inh:ppl~init:zero);border_width=(bt,br,bb,bl);border_color=(ct,cr,cb,cl);border_box=word"box-sizing"=Some"border-box";color;background=prop"background-color"~inh:V.transparent~init:V.transparent(funv->V.color~current:colorv);background_image=(matchget"background-image"withSomev->List.find_mapurl_of(V.partsv)|None->None);mask_image=(matchget"mask-image"with|Somev->List.find_mapurl_of(V.partsv)|None->(matchget"-webkit-mask-image"withSomev->List.find_mapurl_of(V.partsv)|None->None));font_size;bold=prop"font-weight"~inh:parent.bold~init:false(funv->matchV.partsvwith|[Token(Numbern)]->Some(n>=600.)|[c]->(matchidentcwithSome("bold"|"bolder")->Sometrue|Some("normal"|"lighter")->Somefalse|_->None)|_->None);italic=prop"font-style"~inh:parent.italic~init:false(funv->matchV.partsvwith[c]->Option.map(funs->s="italic"||s="oblique")(identc)|_->None);family=prop"font-family"~inh:parent.family~init:Serif(funv->(* the first family we can tell: our pens have three faces *)letnames=List.map(funf->String.lowercase_ascii(to_string(trimf)))(split_onCommav)inList.find_map(funn->letn=String.concat""(String.split_on_char'"'n)inifList.memn["monospace";"courier";"courier new";"consolas";"menlo";"monaco";"sfmono-regular";"ui-monospace";"dejavu sans mono"]thenSomeMonospaceelseifList.memn["serif";"times";"times new roman";"georgia";"linux libertine"]thenSomeSerifelseifList.memn["sans-serif";"arial";"helvetica";"verdana";"system-ui";"-apple-system";"roboto";"segoe ui";"helvetica neue"]thenSomeSans_serifelseNone)names);line_height=prop"line-height"~inh:parent.line_height~init:Line_normal(funv->matchV.partsvwith|[Token(Numbern)]->Some(Factorn)|[Token(Percentagep)]->Some(Line_px(font_size*.p/.100.))|[c]whenidentc=Some"normal"->SomeLine_normal|[c]->Option.map(funl->Line_px(V.resolvel0.))(V.lengthctxc)|_->None);text_align=prop"text-align"~inh:parent.text_align~init:Align_left(funv->matchV.partsvwith|[c]->(matchidentcwith|Some("left"|"start")->SomeAlign_left|Some("right"|"end")->SomeAlign_right|Some("center"|"-webkit-center"|"-moz-center")->SomeAlign_center|Some"justify"->SomeAlign_justify|_->None)|_->None);underline=(ifdecoration=Nonethenparent.underlineelsehas_decoration"underline");line_through=(ifdecoration=Nonethenparent.line_throughelsehas_decoration"line-through");uppercase=prop"text-transform"~inh:parent.uppercase~init:false(funv->matchV.partsvwith[c]->Option.map(funs->s="uppercase")(identc)|_->None);white_space=prop"white-space"~inh:parent.white_space~init:Normal(funv->matchV.partsvwith|[c]->(matchidentcwith|Some"pre"->SomePre|Some"nowrap"->SomeNowrap|Some("pre-wrap"|"break-spaces")->SomePre_wrap|Some"pre-line"->SomePre_line|Some"normal"->SomeNormal|_->None)|_->None);vertical_align=(matchword"vertical-align"with|Some"middle"->Middle|Some"top"->Top|Some"bottom"->Bottom|Some"text-top"->Text_top|Some"text-bottom"->Text_bottom|Some"sub"->Sub|Some"super"->Super|_->Baseline);list_style=prop"list-style-type"~inh:parent.list_style~init:"disc"(funv->matchV.partsvwith[c]->identc|_->None);visible=prop"visibility"~inh:parent.visible~init:true(funv->matchV.partsvwith[c]->Option.map(funs->s="visible")(identc)|_->None)(* opacity: 0, what hides a checkbox that a label stands for: not
* drawn either (and nor is what is in it) *)&&(matchget"opacity"withSomev->(matchfloat_of_string_opt(String.trim(to_stringv))withSomeo->o>0.|None->true)|None->true);(* auto and scroll: a box that scrolls, here clipped (no scrollbar) *)(* "hidden auto": x, then y; either clipping clips here *)overflow_hidden=(matchget"overflow"with|Somev->List.exists(func->matchidentcwithSome("hidden"|"clip"|"auto"|"scroll")->true|_->false)(V.partsv)|None->false);flex_direction=(matchword"flex-direction"withSome"row-reverse"->Row_reverse|Some"column"->Column|Some"column-reverse"->Column_reverse|_->Row);flex_wrap=(matchword"flex-wrap"withSome("wrap"|"wrap-reverse")->true|_->false);justify_content=(matchOption.bind(word"justify-content")align_ofwithSomea->a|None->Start);align_items=(matchOption.bind(word"align-items")align_ofwithSomeStartwhenword"align-items"=Some"normal"->Stretch|Somea->a|None->Stretch);align_self=(matchword"align-self"withSome"auto"|None->None|Somes->align_ofs);flex_grow=prop"flex-grow"~inh:0.~init:0.(funv->matchV.partsvwith[Token(Numbern)]->Somen|_->None);flex_shrink=prop"flex-shrink"~inh:1.~init:1.(funv->matchV.partsvwith[Token(Numbern)]->Somen|_->None);flex_basis=size"flex-basis"~inh:parent.flex_basis~init:Auto;row_gap=length"row-gap"~inh:parent.row_gap~init:zero;column_gap=length"column-gap"~inh:parent.column_gap~init:zero;custom;}(*****************************************************************************)(* The browser's own sheet, and every element's style *)(*****************************************************************************)letuser_agent_sheet:Cascade.sheet={origin=User_agent;rules=parse_stylesheetUa_sheet.text}(* quirks mode's rules (WHATWG HTML, "Rendering", tables in quirks
* mode): a table does not inherit its surroundings' fonts and
* alignment -- pages of the 1990s <center>ed a table, not its text *)letquirks_sheet:Cascade.sheet={origin=User_agent;rules=parse_stylesheet"table { font-weight: initial; font-style: initial; font-size: initial; line-height: initial; white-space: initial; text-align: initial }";}(* the browser's sheets, before the page's *)letbrowser_sheets~(quirks:bool):Cascade.sheetlist=ifquirksthen[user_agent_sheet;quirks_sheet]else[user_agent_sheet]letstyles?visited?(quirks=false)(m:Cascade.media)(sheets:Cascade.sheetlist)(root:Dom.element):Dom.element->t=letua=browser_sheets~quirksinletdeclared=Cascade.cascade?visitedm(ua@sheets)rootinlettable:(int,Dom.element*t)Hashtbl.t=Hashtbl.create1024inletroot_style=computem~root_font_size:16.~parent:initial(declaredroot)in(* each element's from its parent's, down the tree *)letrecgo(e:Dom.element)(style:t)=Hashtbl.addtable(Hashtbl.hashe)(e,style);List.iter(fun(n:Dom.node)->matchnwith|Elementc->goc(computem~root_font_size:root_style.font_size~parent:style(declaredc))|Text_->())e.childreningorootroot_style;fune->matchCascade.find_elementtableewithSomes->s|None->initial