123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248(* 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 Css_values.mli *)openCss_syntaxtypelength={px:float;pct:float}typecolor={r:int;g:int;b:int;a:float}typecontext={em:float;rem:float;viewport_width:float;viewport_height:float}letresolve(l:length)(base:float):float=l.px+.(l.pct*.base/.100.)letpx(p:float):length={px=p;pct=0.}letzero=px0.letblack={r=0;g=0;b=0;a=1.}lettransparent={r=0;g=0;b=0;a=0.}letparts(cs:componentlist):componentlist=List.filter(func->c<>TokenWhitespace)cs(*****************************************************************************)(* var() *)(*****************************************************************************)exceptionUnresolvedletsubstitute(lookup:string->componentlistoption)(cs:componentlist):componentlistoption=letrecgo(depth:int)(cs:componentlist):componentlist=ifdepth>32thenraiseUnresolved(* a cycle: --a: var(--a) *)elseList.concat_map(fun(c:component)->matchcwith|Func(f,args)whenString.lowercase_asciif="var"->(letname,fallback=matchsplit_onCommaargswith|first::rest->(to_string(trimfirst),ifrest=[]thenNoneelseSome(trim(List.concat(List.mapi(funir->ifi=0thenrelseTokenComma::r)rest))))|[]->raiseUnresolvedinmatch(lookupname,fallback)with|Somev,_->go(depth+1)v|None,Somef->go(depth+1)f|None,None->raiseUnresolved)|Func(f,args)->[Func(f,godepthargs)]|Block(o,inside)->[Block(o,godepthinside)]|c->[c])csinmatchgo0cswithv->Somev|exceptionUnresolved->None(*****************************************************************************)(* Lengths and calc() *)(*****************************************************************************)letaddab={px=a.px+.b.px;pct=a.pct+.b.pct}letscaleka={px=k*.a.px;pct=k*.a.pct}letunit_length(ctx:context)(f:float)(u:string):lengthoption=matchuwith|"px"->Some(pxf)|"em"->Some(px(f*.ctx.em))|"rem"->Some(px(f*.ctx.rem))|"ex"|"ch"->Some(px(f*.ctx.em*.0.5))|"pt"->Some(px(f*.96./.72.))|"pc"->Some(px(f*.16.))|"in"->Some(px(f*.96.))|"cm"->Some(px(f*.96./.2.54))|"mm"->Some(px(f*.96./.25.4))|"vw"->Some(px(f*.ctx.viewport_width/.100.))|"vh"->Some(px(f*.ctx.viewport_height/.100.))|"vmin"->Some(px(f*.Float.minctx.viewport_widthctx.viewport_height/.100.))|"vmax"->Some(px(f*.Float.maxctx.viewport_widthctx.viewport_height/.100.))|_->None(* calc()'s expression: a sum of products, a number or a length each
* term (a number alone multiplies or divides a length) *)typeterm=Numoffloat|Lenoflengthletreclength(ctx:context)(c:component):lengthoption=matchcwith|Token(Dimension(f,u))->unit_lengthctxfu|Token(Percentagef)->Some{px=0.;pct=f}|Token(Number0.)->Somezero|Func(f,args)->(matchString.lowercase_asciifwith|"calc"->(matchcalcctxargswithSome(Lenl)->Somel|Some(Num0.)->Somezero|_->None)|("min"|"max"|"clamp")asf->(letls=List.map(funa->calcctxa)(split_onCommaargs)inifList.exists(funl->matchlwithSome(Len_)->false|_->true)lsthenNoneelseletls=List.map(functionSome(Lenl)->l|_->zero)lsinletpickcmp=List.fold_left(funab->ifcmpb.pxa.pxthenbelsea)(List.hdls)lsinmatch(f,ls)with|"min",_::_->Some(pick(<))|"max",_::_->Some(pick(>))|"clamp",[lo;v;hi]->Some(ifv.px<lo.pxthenloelseifv.px>hi.pxthenhielsev)|_->None)|_->None)|Block('(',inside)->(matchcalcctxinsidewithSome(Lenl)->Somel|_->None)|_->Noneandcalc(ctx:context)(cs:componentlist):termoption=letcs=partscsinletoperand(c:component):termoption=matchcwith|Token(Numberf)->Some(Numf)|Block('(',inside)->calcctxinside|c->Option.map(funl->Lenl)(lengthctxc)in(* the products first, then the sums: a precedence of two levels *)lettimes(a:term)(op:char)(b:term):termoption=match(a,op,b)with|Numx,'*',Numy->Some(Num(x*.y))|Numx,'*',Lenl|Lenl,'*',Numx->Some(Len(scalexl))|Numx,'/',Numywheny<>0.->Some(Num(x/.y))|Lenl,'/',Numywheny<>0.->Some(Len(scale(1./.y)l))|_->Noneinletplus(a:term)(sign:float)(b:term):termoption=match(a,b)with|Numx,Numy->Some(Num(x+.(sign*.y)))|Lenx,Leny->Some(Len(addx(scalesigny)))|Num0.,Leny->Some(Len(scalesigny))|Lenx,Num0.->Some(Lenx)|_->Noneinletrecproduct(acc:term)(cs:componentlist):(term*componentlist)option=matchcswith|Token(Delim(('*'|'/')asop))::c::rest->(matchoperandcwithSomeb->Option.bind(timesaccopb)(funt->producttrest)|None->None)|rest->Some(acc,rest)inletrecsum(acc:term)(cs:componentlist):termoption=matchcswith|[]->Someacc|Token(Delim(('+'|'-')asop))::c::rest->(matchoperandcwith|Someb->(matchproductbrestwith|Some(b,rest)->Option.bind(plusacc(ifop='+'then1.else-1.)b)(funt->sumtrest)|None->None)|None->None)(* "- 2em" read by the tokenizer as a negative number or dimension *)|(Token(Numberf)asc)::restwhenf<0.->sumacc(Token(Delim'+')::c::rest)|(Token(Dimension(f,_))asc)::restwhenf<0.->sumacc(Token(Delim'+')::c::rest)|_->Noneinmatchcswith|c::rest->(matchoperandcwithSomet->Option.bind(producttrest)(fun(t,rest)->sumtrest)|None->None)|[]->None(*****************************************************************************)(* Colours *)(*****************************************************************************)letnamed=[("aliceblue",0xf0f8ff);("antiquewhite",0xfaebd7);("aqua",0x00ffff);("aquamarine",0x7fffd4);("azure",0xf0ffff);("beige",0xf5f5dc);("bisque",0xffe4c4);("black",0x000000);("blanchedalmond",0xffebcd);("blue",0x0000ff);("blueviolet",0x8a2be2);("brown",0xa52a2a);("burlywood",0xdeb887);("cadetblue",0x5f9ea0);("chartreuse",0x7fff00);("chocolate",0xd2691e);("coral",0xff7f50);("cornflowerblue",0x6495ed);("cornsilk",0xfff8dc);("crimson",0xdc143c);("cyan",0x00ffff);("darkblue",0x00008b);("darkcyan",0x008b8b);("darkgoldenrod",0xb8860b);("darkgray",0xa9a9a9);("darkgreen",0x006400);("darkgrey",0xa9a9a9);("darkkhaki",0xbdb76b);("darkmagenta",0x8b008b);("darkolivegreen",0x556b2f);("darkorange",0xff8c00);("darkorchid",0x9932cc);("darkred",0x8b0000);("darksalmon",0xe9967a);("darkseagreen",0x8fbc8f);("darkslateblue",0x483d8b);("darkslategray",0x2f4f4f);("darkslategrey",0x2f4f4f);("darkturquoise",0x00ced1);("darkviolet",0x9400d3);("deeppink",0xff1493);("deepskyblue",0x00bfff);("dimgray",0x696969);("dimgrey",0x696969);("dodgerblue",0x1e90ff);("firebrick",0xb22222);("floralwhite",0xfffaf0);("forestgreen",0x228b22);("fuchsia",0xff00ff);("gainsboro",0xdcdcdc);("ghostwhite",0xf8f8ff);("gold",0xffd700);("goldenrod",0xdaa520);("gray",0x808080);("green",0x008000);("greenyellow",0xadff2f);("grey",0x808080);("honeydew",0xf0fff0);("hotpink",0xff69b4);("indianred",0xcd5c5c);("indigo",0x4b0082);("ivory",0xfffff0);("khaki",0xf0e68c);("lavender",0xe6e6fa);("lavenderblush",0xfff0f5);("lawngreen",0x7cfc00);("lemonchiffon",0xfffacd);("lightblue",0xadd8e6);("lightcoral",0xf08080);("lightcyan",0xe0ffff);("lightgoldenrodyellow",0xfafad2);("lightgray",0xd3d3d3);("lightgreen",0x90ee90);("lightgrey",0xd3d3d3);("lightpink",0xffb6c1);("lightsalmon",0xffa07a);("lightseagreen",0x20b2aa);("lightskyblue",0x87cefa);("lightslategray",0x778899);("lightslategrey",0x778899);("lightsteelblue",0xb0c4de);("lightyellow",0xffffe0);("lime",0x00ff00);("limegreen",0x32cd32);("linen",0xfaf0e6);("magenta",0xff00ff);("maroon",0x800000);("mediumaquamarine",0x66cdaa);("mediumblue",0x0000cd);("mediumorchid",0xba55d3);("mediumpurple",0x9370db);("mediumseagreen",0x3cb371);("mediumslateblue",0x7b68ee);("mediumspringgreen",0x00fa9a);("mediumturquoise",0x48d1cc);("mediumvioletred",0xc71585);("midnightblue",0x191970);("mintcream",0xf5fffa);("mistyrose",0xffe4e1);("moccasin",0xffe4b5);("navajowhite",0xffdead);("navy",0x000080);("oldlace",0xfdf5e6);("olive",0x808000);("olivedrab",0x6b8e23);("orange",0xffa500);("orangered",0xff4500);("orchid",0xda70d6);("palegoldenrod",0xeee8aa);("palegreen",0x98fb98);("paleturquoise",0xafeeee);("palevioletred",0xdb7093);("papayawhip",0xffefd5);("peachpuff",0xffdab9);("peru",0xcd853f);("pink",0xffc0cb);("plum",0xdda0dd);("powderblue",0xb0e0e6);("purple",0x800080);("rebeccapurple",0x663399);("red",0xff0000);("rosybrown",0xbc8f8f);("royalblue",0x4169e1);("saddlebrown",0x8b4513);("salmon",0xfa8072);("sandybrown",0xf4a460);("seagreen",0x2e8b57);("seashell",0xfff5ee);("sienna",0xa0522d);("silver",0xc0c0c0);("skyblue",0x87ceeb);("slateblue",0x6a5acd);("slategray",0x708090);("slategrey",0x708090);("snow",0xfffafa);("springgreen",0x00ff7f);("steelblue",0x4682b4);("tan",0xd2b48c);("teal",0x008080);("thistle",0xd8bfd8);("tomato",0xff6347);("turquoise",0x40e0d0);("violet",0xee82ee);("wheat",0xf5deb3);("white",0xffffff);("whitesmoke",0xf5f5f5);("yellow",0xffff00);("yellowgreen",0x9acd32)]letof_rgb(n:int):color={r=(nlsr16)land255;g=(nlsr8)land255;b=nland255;a=1.}lethex(h:string):coloroption=letdi=int_of_string_opt("0x"^String.make2h.[i])andddi=int_of_string_opt("0x"^String.subhi2)inletmkrgba=match(r,g,b,a)withSomer,Someg,Someb,Somea->Some{r;g;b;a=float_of_inta/.255.}|_->NoneinmatchString.lengthhwith|3->mk(d0)(d1)(d2)(Some255)|4->mk(d0)(d1)(d2)(d3)|6->mk(dd0)(dd2)(dd4)(Some255)|8->mk(dd0)(dd2)(dd4)(dd6)|_->Noneletclamp255(f:float):int=max0(min255(int_of_float(Float.roundf)))(* hsl to rgb: CSS Color's own algorithm *)lethsl(h:float)(s:float)(l:float):int*int*int=leth=Float.rem(Float.remh360.+.360.)360./.360.inlethuem1m2h=leth=ifh<0.thenh+.1.elseifh>1.thenh-.1.elsehinifh*.6.<1.thenm1+.((m2-.m1)*.h*.6.)elseifh*.2.<1.thenm2elseifh*.3.<2.thenm1+.((m2-.m1)*.((2./.3.)-.h)*.6.)elsem1inletm2=ifl<=0.5thenl*.(s+.1.)elsel+.s-.(l*.s)inletm1=(l*.2.)-.m2in(clamp255(255.*.huem1m2(h+.(1./.3.))),clamp255(255.*.huem1m2h),clamp255(255.*.huem1m2(h-.(1./.3.))))letcolor~(current:color)(cs:componentlist):coloroption=matchpartscswith|[Token(Hashh)]->hexh|[Token(Identn)]->(matchString.lowercase_asciinwith|"transparent"->Sometransparent|"currentcolor"->Somecurrent|n->Option.mapof_rgb(List.assoc_optnnamed))|[Func(f,args)]->((* the arguments, commas or not, and an alpha after a / or a fourth *)letitems=List.filter(func->matchcwithTokenComma|Token(Delim'/')->false|_->true)(partsargs)inletnumc=matchcwithToken(Numbern)->Somen|Token(Percentagep)->Some(p*.2.55)|_->Noneinletalphac=matchcwithToken(Numbern)->Somen|Token(Percentagep)->Some(p/.100.)|_->Noneinletpctc=matchcwithToken(Percentagep)->Some(p/.100.)|Token(Numbern)->Some(n/.100.)|_->Noneinletdegc=matchcwithToken(Numbern)->Somen|Token(Dimension(n,"deg"))->Somen|Token(Dimension(n,"turn"))->Some(n*.360.)|_->Noneinmatch(String.lowercase_asciif,items)with|("rgb"|"rgba"),[r;g;b]->(match(numr,numg,numb)withSomer,Someg,Someb->Some{r=clamp255r;g=clamp255g;b=clamp255b;a=1.}|_->None)|("rgb"|"rgba"),[r;g;b;a]->(match(numr,numg,numb,alphaa)withSomer,Someg,Someb,Somea->Some{r=clamp255r;g=clamp255g;b=clamp255b;a}|_->None)|("hsl"|"hsla"),h::s::l::rest->(match(degh,pcts,pctl)with|Someh,Somes,Somel->letr,g,b=hslhslinleta=matchrestwith[a]->Option.value(alphaa)~default:1.|_->1.inSome{r;g;b;a}|_->None)|_->None)|_->None