local C={} local INHERIT={color=1,["font-weight"]=1,["font-style"]=1,["text-align"]=1,["white-space"]=1, visibility=1,["list-style-type"]=1,["font-size"]=1,["text-transform"]=1,["line-height"]=1, ["letter-spacing"]=1,cursor=1} local KEEP={display=1,visibility=1,color=1,["background-color"]=1,["background-image"]=1,background=1, width=1,["max-width"]=1,["min-width"]=1,height=1,["min-height"]=1,["max-height"]=1, margin=1,["margin-top"]=1,["margin-bottom"]=1,["margin-left"]=1,["margin-right"]=1, padding=1,["padding-top"]=1,["padding-bottom"]=1,["padding-left"]=1,["padding-right"]=1, border=1,["border-top"]=1,["border-bottom"]=1,["border-left"]=1,["border-right"]=1,["border-color"]=1, ["border-width"]=1,["border-style"]=1,["border-radius"]=1, ["text-align"]=1,["font-weight"]=1,["font-size"]=1,["font-style"]=1,["white-space"]=1,float=1, position=1,["flex-direction"]=1,flex=1,["flex-grow"]=1,["flex-basis"]=1,["flex-wrap"]=1, ["flex-flow"]=1,["justify-content"]=1,["align-items"]=1,gap=1,["column-gap"]=1,["row-gap"]=1, ["grid-template-columns"]=1,["grid-column"]=1,["list-style"]=1,["list-style-type"]=1, ["text-transform"]=1,opacity=1,overflow=1,["line-height"]=1,clear=1,["vertical-align"]=1, ["text-decoration"]=1,["object-fit"]=1,["background-size"]=1,["background-position"]=1,clip=1, ["clip-path"]=1,transform=1,top=1,left=1,right=1,bottom=1,["text-indent"]=1,content=1} local PSEUDO_EL={before=1,after=1} local BAD_PSEUDO={hover=1,focus=1,active=1,visited=1,before=1,after=1,["focus-within"]=1, ["focus-visible"]=1,placeholder=1,selection=1,["first-line"]=1,["first-letter"]=1,checked=1, disabled=1,target=1,marker=1,backdrop=1,["-webkit-scrollbar"]=1,["-moz-focus-inner"]=1} local function parseCompound(s) local c={classes={},attrs={},pseudo={}} local pos=1 local tag=s:match("^([%w%-%*]+)",pos) if tag then c.tag=tag:lower();pos=pos+#tag end if c.tag=="*"then c.tag=nil end while pos<=#s do local ch=s:sub(pos,pos) if ch=="#"then local id=s:match("^#([%w%-_]+)",pos) if not id then return nil end c.id=id;pos=pos+#id+1 elseif ch=="."then local cl=s:match("^%.([%w%-_]+)",pos) if not cl then return nil end c.classes[#c.classes+1]=cl;pos=pos+#cl+1 elseif ch=="["then local close=s:find("]",pos,true) if not close then return nil end local body=s:sub(pos+1,close-1) local name,op,val=body:match('^%s*([%w%-_:]+)%s*([~|%^%$%*]?=)%s*["\']?(.-)["\']?%s*[iI]?%s*$') if not name then name=body:match("^%s*([%w%-_:]+)%s*$")end if not name then return nil end c.attrs[#c.attrs+1]={name:lower(),op,val} pos=close+1 elseif ch==":"then local double=s:sub(pos+1,pos+1)==":" local name=s:match("^:+([%w%-]+)",pos) if not name then return nil end pos=pos+#name+(double and 2 or 1) name=name:lower() if name=="before"or name=="after"then c.pseudoEl=name elseif BAD_PSEUDO[name]then return nil end if c.pseudoEl==name then elseif name=="not"then local arg=s:match("^%((.-)%)",pos) if not arg then return nil end pos=pos+#arg+2 local inner=parseCompound(arg) if not inner then return nil end c.pseudo[#c.pseudo+1]={"not",inner} elseif name=="first-child"or name=="last-child"or name=="root"or name=="only-child"then c.pseudo[#c.pseudo+1]={name} elseif name=="nth-child"or name=="nth-of-type"or name=="is"or name=="where"or name=="has"then return nil else return nil end else return nil end end return c end local function parseSelector(sel) sel=sel:gsub("^%s+",""):gsub("%s+$","") if sel==""or sel:find("[+~]")then return nil end sel=sel:gsub("%s*>%s*"," > ") local parts,combs={},{} local nextComb=nil for tok in sel:gmatch("%S+")do if tok==">"then nextComb=">" else local c=parseCompound(tok) if not c then return nil end parts[#parts+1]=c combs[#parts]=nextComb or" " nextComb=nil end end if#parts==0 then return nil end local a,b,cc=0,0,0 for _,p in ipairs(parts)do if p.id then a=a+1 end b=b+#p.classes+#p.attrs+#p.pseudo if p.tag then cc=cc+1 end end return{parts=parts,combs=combs,spec=a*10000+b*100+cc} end local function hasClass(el,cl) local list=el.classSet if not list then list={} for w in(el.attrs.class or""):gmatch("%S+")do list[w]=true end el.classSet=list end return list[cl] end local function elementIndex(el) local p=el.parent if not p then return 1,1 end local idx,n=0,0 for _,c in ipairs(p.children)do if c.tag then n=n+1 if c==el then idx=n end end end return idx,n end local function matchCompound(el,c) if not el.tag or el.tag:sub(1,1)=="#"then return false end if c.tag and c.tag~=el.tag then return false end if c.id and el.attrs.id~=c.id then return false end for _,cl in ipairs(c.classes)do if not hasClass(el,cl)then return false end end for _,at in ipairs(c.attrs)do local v=el.attrs[at[1]] if v==nil then return false end local op,want=at[2],at[3] if op=="="and v~=want then return false elseif op=="~="then local ok=false for w in v:gmatch("%S+")do if w==want then ok=true end end if not ok then return false end elseif op=="^="and v:sub(1,#want)~=want then return false elseif op=="$="and(want==""or v:sub(-#want)~=want)then return false elseif op=="*="and not v:find(want,1,true)then return false elseif op=="|="and v~=want and v:sub(1,#want+1)~=want.."-"then return false end end for _,ps in ipairs(c.pseudo)do local k=ps[1] if k=="not"then if matchCompound(el,ps[2])then return false end elseif k=="root"then if el.tag~="html"then return false end else local i,n=elementIndex(el) if k=="first-child"and i~=1 then return false end if k=="last-child"and i~=n then return false end if k=="only-child"and n~=1 then return false end end end return true end local function matches(el,sel) local parts,combs=sel.parts,sel.combs local i=#parts if not matchCompound(el,parts[i])then return false end local node=el while i>1 do local comb=combs[i] i=i-1 if comb==">"then node=node.parent if not node or not matchCompound(node,parts[i])then return false end else repeat node=node.parent until not node or matchCompound(node,parts[i]) if not node then return false end end end return true end C.matches=matches local function mediaOk(q,vw) q=q:lower() if q:find("print")and not q:find("screen")then return false end if q:find("prefers%-color%-scheme%s*:%s*dark")then return false end for op,v in q:gmatch("(m[ai][xn])%-width%s*:%s*([%d%.]+)")do v=tonumber(v) if op=="max"and vw>v then return false end if op=="min"and vw0 then for d in(body..";"):gmatch("([^;]*);")do local prop,val=d:match("^%s*([%w%-]+)%s*:%s*(.-)%s*$") if prop then prop=prop:lower() local imp=false if val:find("!%s*important%s*$")then imp=true;val=val:gsub("%s*!%s*important%s*$","")end if KEEP[prop]or prop:sub(1,2)=="--"then if base and val:find("url%(")then val=val:gsub("url%(%s*['\"]?([^'\")]+)['\"]?%s*%)",function(u)return"url("..base(u)..")"end) end decls[#decls+1]={prop,val,imp} end end end end if#decls>0 then for _,sel in ipairs(sels)do order=order+1 rules[#rules+1]={sel=sel,decls=decls,order=order} end end end end end return rules end function C.index(rules) local idx={id={},class={},tag={},any={}} for _,r in ipairs(rules)do local last=r.sel.parts[#r.sel.parts] local bucket if last.id then bucket=idx.id[last.id]or{};idx.id[last.id]=bucket elseif#last.classes>0 then local k=last.classes[1]bucket=idx.class[k]or{};idx.class[k]=bucket elseif last.tag then bucket=idx.tag[last.tag]or{};idx.tag[last.tag]=bucket else bucket=idx.any end bucket[#bucket+1]=r end return idx end local NAMED={black=0x000000,white=0xFFFFFF,red=0xFF0000,green=0x008000,blue=0x0000FF,yellow=0xFFFF00, gray=0x808080,grey=0x808080,silver=0xC0C0C0,maroon=0x800000,purple=0x800080,fuchsia=0xFF00FF, lime=0x00FF00,olive=0x808000,navy=0x000080,teal=0x008080,aqua=0x00FFFF,orange=0xFFA500, pink=0xFFC0CB,brown=0xA52A2A,gold=0xFFD700,darkgray=0xA9A9A9,darkgrey=0xA9A9A9,lightgray=0xD3D3D3, lightgrey=0xD3D3D3,whitesmoke=0xF5F5F5,gainsboro=0xDCDCDC,dimgray=0x696969,crimson=0xDC143C, darkred=0x8B0000,darkblue=0x00008B,darkgreen=0x006400,royalblue=0x4169E1,steelblue=0x4682B4, dodgerblue=0x1E90FF,skyblue=0x87CEEB,tomato=0xFF6347,coral=0xFF7F50,salmon=0xFA8072, beige=0xF5F5DC,ivory=0xFFFFF0,khaki=0xF0E68C,indigo=0x4B0082,violet=0xEE82EE,orchid=0xDA70D6, tan=0xD2B48C,chocolate=0xD2691E,seagreen=0x2E8B57,slategray=0x708090,aliceblue=0xF0F8FF, ghostwhite=0xF8F8FF,snow=0xFFFAFA,honeydew=0xF0FFF0,mintcream=0xF5FFFA,lavender=0xE6E6FA} function C.color(v) if not v then return nil end v=v:lower():gsub("^%s+",""):gsub("%s+$","") if v=="transparent"or v=="none"or v==""then return nil,0 end local h=v:match("^#(%x+)$") if h then if#h==3 or#h==4 then local r,g,b=tonumber(h:sub(1,1),16),tonumber(h:sub(2,2),16),tonumber(h:sub(3,3),16) local a=#h==4 and tonumber(h:sub(4,4),16)/15 or 1 return(r*17)<<16|(g*17)<<8|(b*17),a elseif#h==6 or#h==8 then return tonumber(h:sub(1,6),16),#h==8 and tonumber(h:sub(7,8),16)/255 or 1 end return nil end local fn,args=v:match("^(rgba?)%s*%((.-)%)") if fn then local nums={} for n,pct in args:gmatch("([%d%.]+)(%%?)")do nums[#nums+1]=pct=="%"and tonumber(n)*2.55 or tonumber(n)end if#nums>=3 then local a=nums[4]or 1 if args:find("/%s*[%d%.]+%%")then a=a/2.55/100 end if a>1 then a=a/255 end local function c(x)return math.max(0,math.min(255,math.floor(x+0.5)))end return c(nums[1])<<16|c(nums[2])<<8|c(nums[3]),a end end local hs,hargs=v:match("^(hsla?)%s*%((.-)%)") if hs then local n={} for x in hargs:gmatch("[%d%.]+")do n[#n+1]=tonumber(x)end if#n>=3 then local hh,s,l=(n[1]%360)/360,n[2]/100,n[3]/100 local function f(k) local kk=(k+hh*12)%12 local a=s*math.min(l,1-l) return l-a*math.max(-1,math.min(kk-3,9-kk,1)) end local function c(x)return math.floor(x*255+0.5)end return c(f(0))<<16|c(f(8))<<8|c(f(4)),n[4]and(n[4]>1 and n[4]/100 or n[4])or 1 end end if NAMED[v]then return NAMED[v],1 end return nil end function C.px(v,ref,fontPx) if not v then return nil end v=v:lower() local n,unit=v:match("^%s*(-?[%d%.]+)%s*([%a%%]*)") n=tonumber(n) if not n then return nil end if unit=="px"or unit==""then return n end if unit=="%"then return ref and n*ref/100 or nil end if unit=="em"then return n*(fontPx or 16)end if unit=="rem"then return n*16 end if unit=="pt"then return n*4/3 end if unit=="vw"then return n*(C.vw or 1200)/100 end if unit=="vh"then return n*(C.vh or 700)/100 end if unit=="ch"then return n*8 end return nil end local DEFAULT_DISPLAY={div="block",p="block",ul="block",ol="block",li="list-item",h1="block", h2="block",h3="block",h4="block",h5="block",h6="block",header="block",footer="block", section="block",article="block",nav="block",main="block",aside="block",form="block", table="table",tr="table-row",td="table-cell",th="table-cell",thead="block",tbody="block", tfoot="block",blockquote="block",pre="block",hr="block",body="block",html="block", figure="block",figcaption="block",dl="block",dt="block",dd="block",address="block", center="block",fieldset="block",details="block",summary="block",caption="block", head="none",script="none",style="none",title="none",meta="none",link="none",noscript="none", template="none",svg="inline-block",iframe="none",object="none",select="inline-block", button="inline-block",input="inline-block",textarea="inline-block",img="inline-block",br="inline", video="none",audio="none",canvas="none",datalist="none",dialog="none",map="none",area="none", picture="inline",source="none"} local UA={ h1={["font-size"]="32px",["font-weight"]="bold",["margin-top"]="21px",["margin-bottom"]="21px"}, h2={["font-size"]="24px",["font-weight"]="bold",["margin-top"]="20px",["margin-bottom"]="20px"}, h3={["font-size"]="19px",["font-weight"]="bold",["margin-top"]="18px",["margin-bottom"]="18px"}, h4={["font-weight"]="bold",["margin-top"]="16px",["margin-bottom"]="16px"}, h5={["font-weight"]="bold"},h6={["font-weight"]="bold"}, p={["margin-top"]="16px",["margin-bottom"]="16px"}, ul={["padding-left"]="40px",["margin-top"]="16px",["margin-bottom"]="16px",["list-style-type"]="disc"}, ol={["padding-left"]="40px",["margin-top"]="16px",["margin-bottom"]="16px",["list-style-type"]="decimal"}, blockquote={["margin-left"]="40px",["margin-top"]="16px",["margin-bottom"]="16px"}, pre={["white-space"]="pre",["margin-top"]="16px",["margin-bottom"]="16px"}, b={["font-weight"]="bold"},strong={["font-weight"]="bold"},th={["font-weight"]="bold"}, i={["font-style"]="italic"},em={["font-style"]="italic"}, a={color="#0000ee",["text-decoration"]="underline"}, body={["margin"]="8px",color="#000000"}, hr={["border-top"]="1px solid #888888",["margin-top"]="8px",["margin-bottom"]="8px"}, center={["text-align"]="center"},td={padding="1px"},th={padding="1px",["text-align"]="center"}, button={["background-color"]="#e9e9ed",color="#000000",["padding-left"]="6px",["padding-right"]="6px"}, input={["background-color"]="#ffffff",color="#000000"},select={["background-color"]="#ffffff",color="#000000"}, textarea={["background-color"]="#ffffff",color="#000000"}, } local function resolveVars(v,st,depth) if not v:find("var(",1,true)or(depth or 0)>6 then return v end local out=v:gsub("var%(%s*(%-%-[%w%-_]+)%s*,?%s*([^()]*)%)",function(name,fb) local x=st[name] if x==nil or x==""then x=fb~=""and fb or""end return x end) return resolveVars(out,st,(depth or 0)+1) end local SHORT={margin=1,padding=1,background=1,flex=1,["flex-flow"]=1,["list-style"]=1,gap=1} local function expand(st,prop,val) if SHORT[prop]and val:find("var(",1,true)then st[prop]=val return end if prop=="margin"or prop=="padding"then local p={} for t in val:gmatch("%S+")do p[#p+1]=t end local t,r,b,l=p[1],p[2]or p[1],p[3]or p[1],p[4]or p[2]or p[1] st[prop.."-top"],st[prop.."-right"],st[prop.."-bottom"],st[prop.."-left"]=t,r,b,l elseif prop=="background"then local url=val:match("url%(%s*['\"]?([^'\")]+)['\"]?%s*%)") if url then st["background-image"]="url("..url..")"end local col for tok in val:gsub("url%b()",""):gsub("%b()",function(x)return x:gsub("%s","")end):gmatch("%S+")do if C.color(tok)or tok=="transparent"then col=tok end end if val:find("rgba?%b()")then col=val:match("(rgba?%b())")end if val:find("#%x+")and not col then col=val:match("(#%x+)")end if val:find("gradient")then col=val:match("gradient%(.-(#%x+)")or val:match("gradient%(.-(rgba?%b())")or col end if col then st["background-color"]=col end if not url and not col and val:match("^%s*none%s*$")then st["background-color"]="transparent"end elseif prop=="border"or prop=="border-top"or prop=="border-bottom"or prop=="border-left"or prop=="border-right"then st[prop]=val elseif prop=="flex"then local g=val:match("^%s*([%d%.]+)") if val=="none"then st["flex-grow"]="0"elseif val=="auto"then st["flex-grow"]="1"elseif g then st["flex-grow"]=g end local basis=val:match("^%s*[%d%.]+%s+[%d%.]+%s+(%S+)")or val:match("^%s*[%d%.]+%s+(%S+%a)$") if basis then st["flex-basis"]=basis end elseif prop=="flex-flow"then if val:find("wrap")then st["flex-wrap"]="wrap"end if val:find("column")then st["flex-direction"]="column"end elseif prop=="list-style"then if val:find("none")then st["list-style-type"]="none"end elseif prop=="gap"then st["column-gap"]=val:match("(%S+)%s*$") st["row-gap"]=val:match("^%s*(%S+)") else st[prop]=val end end function C.cascade(root,rules,vwPx,yield) local idx=C.index(rules) local function candidates(el) local list={} local function addAll(b)if b then for _,r in ipairs(b)do list[#list+1]=r end end end if el.attrs.id then addAll(idx.id[el.attrs.id])end for cl in(el.attrs.class or""):gmatch("%S+")do addAll(idx.class[cl])end addAll(idx.tag[el.tag]) addAll(idx.any) return list end local visited=0 local function visit(el,parentStyle) visited=visited+1 if yield and visited%100==0 then yield()end if not el.tag or el.tag=="#document"then for _,c in ipairs(el.children or{})do if c.tag then visit(c,parentStyle)end end return end local meta=parentStyle.__inh if not meta then meta={__index=function(_,k) if INHERIT[k]or(type(k)=="string"and k:sub(1,2)=="--")then return parentStyle[k]end end} rawset(parentStyle,"__inh",meta) end local st=setmetatable({},meta) st.display=DEFAULT_DISPLAY[el.tag]or"inline" for k,v in pairs(UA[el.tag]or{})do expand(st,k,v)end local hits,gen={},nil for _,r in ipairs(candidates(el))do if matches(el,r.sel)then local pe=r.sel.parts[#r.sel.parts].pseudoEl if pe then gen=gen or{} local g=gen[pe]or{spec=-1} for _,d in ipairs(r.decls)do if(d[1]=="content"or d[1]=="color")and r.sel.spec>=g.spec then g[d[1]]=d[2]end end g.spec=math.max(g.spec,r.sel.spec) gen[pe]=g else hits[#hits+1]=r end end end if gen then for pe,g in pairs(gen)do local c=g.content and g.content:match('^%s*"(.-)"%s*$')or g.content and g.content:match("^%s*'(.-)'%s*$") if c then c=c:gsub("\\a"," "):gsub("\\(%x%x%x%x?)",function(h)return utf8.char(tonumber(h,16))end) el.pseudo=el.pseudo or{} el.pseudo[pe]={text=c,color=g.color} end end end table.sort(hits,function(a,b)if a.sel.spec~=b.sel.spec then return a.sel.spec