@@ -1980,11 +1980,361 @@ const compareRules = (before, after, signatures) => {
19801980 return "" ;
19811981} ;
19821982
1983+ /**
1984+ * One stylesheet printed two ways that must style every element alike.
1985+ * @typedef {{ name: string, before: string, after: string } } CascadePair
1986+ */
1987+
1988+ /**
1989+ * What one pair styled differently, per element, and how much was compared.
1990+ * @typedef {{ name: string, elements: number, properties: number, differences: string[] } } CascadeReport
1991+ */
1992+
1993+ /**
1994+ * Runs in the page. Builds, per pair, elements its selectors reach — a chain per
1995+ * selector, and one element per type carrying every class and attribute named,
1996+ * so rules meet in one cascade — then reads each under `before` and `after`.
1997+ * Only what a rule held by one sheet alone declares can differ, so only that is read.
1998+ * @param {{ pairs: CascadePair[], types: string[] } } input the pairs, and element types every pair nests in each other
1999+ * @returns {CascadeReport[] } one report per pair, in order
2000+ */
2001+ const compareCascades = ( { pairs, types } ) => {
2002+ const REPORTED = 5 ;
2003+
2004+ /**
2005+ * @param {string } text an identifier as the CSSOM serializes it
2006+ * @returns {string } the name it spells
2007+ */
2008+ const unescapeIdentifier = ( text ) =>
2009+ text . replace ( / \\ (?: ( [ 0 - 9 a - f A - F ] { 1 , 6 } ) ? | ( [ \s \S ] ) ) / g, ( _ , hex , other ) =>
2010+ hex === undefined ? other : String . fromCodePoint ( Number . parseInt ( hex , 16 ) )
2011+ ) ;
2012+
2013+ /**
2014+ * @param {string } list a selector list
2015+ * @returns {string[] } its selectors
2016+ */
2017+ const splitList = ( list ) => {
2018+ /** @type {string[] } */
2019+ const out = [ ] ;
2020+ let depth = 0 ;
2021+ let quote = "" ;
2022+ let start = 0 ;
2023+ for ( let i = 0 ; i < list . length ; i ++ ) {
2024+ const char = list [ i ] ;
2025+ if ( char === "\\" ) {
2026+ i ++ ;
2027+ } else if ( quote !== "" ) {
2028+ if ( char === quote ) quote = "" ;
2029+ } else if ( char === '"' || char === "'" ) {
2030+ quote = char ;
2031+ } else if ( char === "(" || char === "[" ) {
2032+ depth ++ ;
2033+ } else if ( char === ")" || char === "]" ) {
2034+ depth -- ;
2035+ } else if ( char === "," && depth === 0 ) {
2036+ out . push ( list . slice ( start , i ) . trim ( ) ) ;
2037+ start = i + 1 ;
2038+ }
2039+ }
2040+ out . push ( list . slice ( start ) . trim ( ) ) ;
2041+ return out ;
2042+ } ;
2043+
2044+ /** @typedef {{ combinator: string, type: string, classes: string[], id: string, attributes: [string, string][] } } Compound */
2045+
2046+ /**
2047+ * A selector as the compounds an element chain has to carry, pseudos left out:
2048+ * the element is still built, whether or not one matches it.
2049+ * @param {string } selector one selector
2050+ * @returns {Compound[] | null } the chain, or null for a shape not read here
2051+ */
2052+ const parseSelector = ( selector ) => {
2053+ const IDENTIFIER =
2054+ / ^ (?: [ - \w \u00A0 - \uFFFF ] | \\ (?: [ 0 - 9 a - f A - F ] { 1 , 6 } ? | [ \s \S ] ) ) + / ;
2055+ const ATTRIBUTE =
2056+ / ^ \[ \s * ( [ - \w ] + ) \s * (?: [ ~ | ^ $ * ] ? = \s * ( " (?: [ ^ " \\ ] | \\ .) * " | ' (?: [ ^ ' \\ ] | \\ .) * ' | [ ^ \s \] ] + ) \s * [ i I s S ] ? \s * ) ? \] / ;
2057+ /** @type {Compound[] } */
2058+ const chain = [ ] ;
2059+ /** @type {Compound } */
2060+ let current = {
2061+ combinator : "" ,
2062+ type : "" ,
2063+ classes : [ ] ,
2064+ id : "" ,
2065+ attributes : [ ]
2066+ } ;
2067+ let started = false ;
2068+ let at = 0 ;
2069+ while ( at < selector . length ) {
2070+ const rest = selector . slice ( at ) ;
2071+ const combinator = / ^ [ \s > + ~ ] + / . exec ( rest ) ;
2072+ if ( combinator !== null ) {
2073+ if ( started ) {
2074+ chain . push ( current ) ;
2075+ const named = combinator [ 0 ] . replace ( / \s / g, "" ) ;
2076+ current = {
2077+ combinator : named === "" ? " " : named ,
2078+ type : "" ,
2079+ classes : [ ] ,
2080+ id : "" ,
2081+ attributes : [ ]
2082+ } ;
2083+ started = false ;
2084+ }
2085+ at += combinator [ 0 ] . length ;
2086+ continue ;
2087+ }
2088+ started = true ;
2089+ const char = rest [ 0 ] ;
2090+ if ( char === "*" ) {
2091+ at ++ ;
2092+ } else if ( char === "." || char === "#" ) {
2093+ const name = IDENTIFIER . exec ( rest . slice ( 1 ) ) ;
2094+ if ( name === null ) return null ;
2095+ if ( char === "." ) current . classes . push ( unescapeIdentifier ( name [ 0 ] ) ) ;
2096+ else current . id = unescapeIdentifier ( name [ 0 ] ) ;
2097+ at += 1 + name [ 0 ] . length ;
2098+ } else if ( char === "[" ) {
2099+ const attribute = ATTRIBUTE . exec ( rest ) ;
2100+ if ( attribute === null ) return null ;
2101+ const value = attribute [ 2 ] === undefined ? "" : attribute [ 2 ] ;
2102+ current . attributes . push ( [
2103+ attribute [ 1 ] ,
2104+ unescapeIdentifier ( / ^ [ " ' ] / . test ( value ) ? value . slice ( 1 , - 1 ) : value )
2105+ ] ) ;
2106+ at += attribute [ 0 ] . length ;
2107+ } else if ( char === ":" ) {
2108+ const name = / ^ : : ? [ - \w ] + / . exec ( rest ) ;
2109+ if ( name === null ) return null ;
2110+ at += name [ 0 ] . length ;
2111+ if ( selector [ at ] === "(" ) {
2112+ for ( let depth = 0 ; at < selector . length ; at ++ ) {
2113+ if ( selector [ at ] === "(" ) depth ++ ;
2114+ else if ( selector [ at ] === ")" && -- depth === 0 ) break ;
2115+ }
2116+ at ++ ;
2117+ }
2118+ } else {
2119+ const name = IDENTIFIER . exec ( rest ) ;
2120+ // A namespace, a nesting selector: the chain would not say what it matches.
2121+ if ( name === null ) return null ;
2122+ current . type = unescapeIdentifier ( name [ 0 ] ) . toLowerCase ( ) ;
2123+ at += name [ 0 ] . length ;
2124+ }
2125+ }
2126+ chain . push ( current ) ;
2127+ return chain ;
2128+ } ;
2129+
2130+ /**
2131+ * @param {Compound } compound what the element carries
2132+ * @returns {Element } the element
2133+ */
2134+ const build = ( compound ) => {
2135+ /** @type {Element } */
2136+ let element ;
2137+ try {
2138+ element = document . createElement ( compound . type || "div" ) ;
2139+ } catch ( _err ) {
2140+ element = document . createElement ( "div" ) ;
2141+ }
2142+ if ( compound . classes . length > 0 ) {
2143+ element . setAttribute ( "class" , compound . classes . join ( " " ) ) ;
2144+ }
2145+ if ( compound . id !== "" ) element . id = compound . id ;
2146+ for ( const [ name , value ] of compound . attributes ) {
2147+ try {
2148+ element . setAttribute ( name , value ) ;
2149+ } catch ( _err ) {
2150+ // A name the DOM will not take is one no element carries either.
2151+ }
2152+ }
2153+ return element ;
2154+ } ;
2155+
2156+ /**
2157+ * Every style rule a sheet holds, keyed by the text it reads as in its
2158+ * context and counted, so the rules only one of two sheets holds stand out.
2159+ * @param {CSSRuleList } rules the rules
2160+ * @param {string } context the conditions and layers around them
2161+ * @param {Map<string, { count: number, rule: CSSStyleRule }> } out what was found
2162+ * @returns {Map<string, { count: number, rule: CSSStyleRule }> } the same map
2163+ */
2164+ const inventory = ( rules , context , out ) => {
2165+ for ( const rule of rules ) {
2166+ const grouping = /** @type {CSSConditionRule & CSSLayerBlockRule } */ (
2167+ /** @type {unknown } */ ( rule )
2168+ ) ;
2169+ const inside =
2170+ grouping . conditionText !== undefined
2171+ ? `${ context } @${ grouping . conditionText } `
2172+ : grouping . name !== undefined
2173+ ? `${ context } @layer ${ grouping . name } `
2174+ : context ;
2175+ if ( rule instanceof CSSStyleRule ) {
2176+ const key = `${ context } |${ rule . cssText } ` ;
2177+ const found = out . get ( key ) ;
2178+ if ( found === undefined ) out . set ( key , { count : 1 , rule } ) ;
2179+ else found . count ++ ;
2180+ }
2181+ const nested = /** @type {CSSGroupingRule } */ (
2182+ /** @type {unknown } */ ( rule )
2183+ ) . cssRules ;
2184+ if ( nested !== undefined ) inventory ( nested , inside , out ) ;
2185+ }
2186+ return out ;
2187+ } ;
2188+
2189+ const style = document . createElement ( "style" ) ;
2190+ document . head . append ( style ) ;
2191+ const root = document . createElement ( "div" ) ;
2192+ document . body . append ( root ) ;
2193+ /** @type {CascadeReport[] } */
2194+ const reports = [ ] ;
2195+ for ( const pair of pairs ) {
2196+ root . textContent = "" ;
2197+ style . textContent = pair . after ;
2198+ const afterRules = inventory (
2199+ /** @type {CSSStyleSheet } */ ( style . sheet ) . cssRules ,
2200+ "" ,
2201+ new Map ( )
2202+ ) ;
2203+ style . textContent = pair . before ;
2204+ const beforeRules = inventory (
2205+ /** @type {CSSStyleSheet } */ ( style . sheet ) . cssRules ,
2206+ "" ,
2207+ new Map ( )
2208+ ) ;
2209+ /** @type {Set<string> } */
2210+ const properties = new Set ( ) ;
2211+ for ( const [ one , other ] of [
2212+ [ beforeRules , afterRules ] ,
2213+ [ afterRules , beforeRules ]
2214+ ] ) {
2215+ for ( const [ key , { count, rule } ] of one ) {
2216+ const there = other . get ( key ) ;
2217+ if ( there !== undefined && there . count === count ) continue ;
2218+ for ( let i = 0 ; i < rule . style . length ; i ++ ) {
2219+ properties . add ( rule . style . item ( i ) ) ;
2220+ }
2221+ }
2222+ }
2223+ /** @type {Set<string> } */
2224+ const named = new Set ( [ "div" , ...types ] ) ;
2225+ /** @type {Set<string> } */
2226+ const classes = new Set ( ) ;
2227+ /** @type {Map<string, string> } */
2228+ const attributes = new Map ( ) ;
2229+ for ( const { rule } of beforeRules . values ( ) ) {
2230+ for ( const selector of splitList ( rule . selectorText ) ) {
2231+ const chain = parseSelector ( selector ) ;
2232+ if ( chain === null ) continue ;
2233+ /** @type {Element | null } */
2234+ let last = null ;
2235+ for ( const compound of chain ) {
2236+ if ( compound . type !== "" ) named . add ( compound . type ) ;
2237+ for ( const name of compound . classes ) classes . add ( name ) ;
2238+ for ( const [ name , value ] of compound . attributes ) {
2239+ attributes . set ( name , value ) ;
2240+ }
2241+ const element = build ( compound ) ;
2242+ if (
2243+ last !== null &&
2244+ ( compound . combinator === "+" || compound . combinator === "~" )
2245+ ) {
2246+ last . after ( element ) ;
2247+ } else {
2248+ ( last === null ? root : last ) . append ( element ) ;
2249+ }
2250+ last = element ;
2251+ }
2252+ }
2253+ }
2254+ /** @type {Compound } */
2255+ const everything = {
2256+ combinator : "" ,
2257+ type : "" ,
2258+ classes : [ ...classes ] ,
2259+ id : "" ,
2260+ attributes : [ ...attributes ]
2261+ } ;
2262+ for ( const type of named ) {
2263+ const outer = build ( { ...everything , type } ) ;
2264+ outer . append ( build ( { ...everything , type : "div" } ) ) ;
2265+ root . append ( outer ) ;
2266+ for ( const inner of types ) {
2267+ const parent = build ( { ...everything , type, classes : [ ] } ) ;
2268+ parent . append ( build ( { ...everything , type : inner , classes : [ ] } ) ) ;
2269+ parent . append ( build ( { ...everything , type : inner } ) ) ;
2270+ root . append ( parent ) ;
2271+ }
2272+ }
2273+ const tracked = [ ...properties ] ;
2274+ const elements = [
2275+ document . documentElement ,
2276+ document . body ,
2277+ root ,
2278+ ...root . querySelectorAll ( "*" )
2279+ ] ;
2280+ const PSEUDOS = [ null , "::before" , "::after" ] ;
2281+ /**
2282+ * @returns {string[][] } every tracked value of every element and pseudo
2283+ */
2284+ const read = ( ) => {
2285+ // Swapping the sheet restarts every animation, which a read would
2286+ // otherwise catch part way through interpolating.
2287+ for ( const running of document . getAnimations ( ) ) running . cancel ( ) ;
2288+ /** @type {string[][] } */
2289+ const out = [ ] ;
2290+ for ( const element of elements ) {
2291+ for ( const pseudo of PSEUDOS ) {
2292+ const computed = getComputedStyle ( element , pseudo ) ;
2293+ out . push ( tracked . map ( ( name ) => computed . getPropertyValue ( name ) ) ) ;
2294+ }
2295+ }
2296+ return out ;
2297+ } ;
2298+ const before = read ( ) ;
2299+ style . textContent = pair . after ;
2300+ const after = read ( ) ;
2301+ /** @type {string[] } */
2302+ const differences = [ ] ;
2303+ for ( let i = 0 ; i < before . length && differences . length < REPORTED ; i ++ ) {
2304+ /** @type {string[] } */
2305+ const moved = [ ] ;
2306+ for ( let at = 0 ; at < tracked . length ; at ++ ) {
2307+ if ( before [ i ] [ at ] !== after [ i ] [ at ] ) {
2308+ moved . push ( `${ tracked [ at ] } : ${ before [ i ] [ at ] } -> ${ after [ i ] [ at ] } ` ) ;
2309+ }
2310+ }
2311+ if ( moved . length === 0 ) continue ;
2312+ const element = elements [ Math . floor ( i / PSEUDOS . length ) ] ;
2313+ const pseudo = PSEUDOS [ i % PSEUDOS . length ] || "" ;
2314+ const tag = element . outerHTML . slice (
2315+ 0 ,
2316+ element . outerHTML . indexOf ( ">" ) + 1
2317+ ) ;
2318+ differences . push ( `${ tag . slice ( 0 , 160 ) } ${ pseudo } ${ moved . join ( "; " ) } ` ) ;
2319+ }
2320+ reports . push ( {
2321+ name : pair . name ,
2322+ elements : elements . length ,
2323+ properties : tracked . length ,
2324+ differences
2325+ } ) ;
2326+ }
2327+ style . remove ( ) ;
2328+ root . remove ( ) ;
2329+ return reports ;
2330+ } ;
2331+
19832332module . exports = {
19842333 benchmarkDocuments,
19852334 benchmarkStylesheets,
19862335 buildCorpus,
19872336 collectFixtures,
2337+ compareCascades,
19882338 compareRules,
19892339 conditionSignatures,
19902340 installHelpers,
0 commit comments