buninu-browser 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,9 @@
4
4
  * model and parser here are project-owned and intentionally cover only the
5
5
  * properties the current pixel renderer consumes.
6
6
  */
7
+ import { compileSelector, matchesCompiled } from "../happy-dom/selector-matcher.js";
8
+ import { selectorKeys } from "./selector-keys.js";
9
+
7
10
 
8
11
  const BLOCK_TAGS = new Set([
9
12
  "ADDRESS", "ARTICLE", "ASIDE", "BLOCKQUOTE", "BODY", "DD", "DETAILS", "DIV", "DL", "DT",
@@ -253,6 +256,9 @@ export class StyleEngine {
253
256
  // Parsed and indexed rules for the last style sheet list and viewport:
254
257
  // restyles after script changes reuse them instead of re-parsing.
255
258
  #ruleCache = { styleSheets: null, key: "", index: null };
259
+ // Parsed rules by sheet text, for the viewport in #parsedSheetsKey.
260
+ #parsedSheets = new Map();
261
+ #parsedSheetsKey = "";
256
262
 
257
263
  /**
258
264
  * Computes the document's styles. With `dirty` (elements whose subtree's
@@ -306,10 +312,7 @@ export class StyleEngine {
306
312
  const topmost = topmostConnected(starts, root);
307
313
  for (const start of topmost) {
308
314
  const parent = start.parentElement;
309
- const ancestors = new Map();
310
- for (let node = parent; node; node = node.parentElement) {
311
- for (const key of ancestorFilterKeys(elementKeys(node))) ancestors.set(key, (ancestors.get(key) ?? 0) + 1);
312
- }
315
+ const ancestors = this.#ancestorsOf(start);
313
316
  this.#visit(start, this.#styles.get(parent), this.#customProperties.get(parent) ?? EMPTY_MAP, rules, ancestors);
314
317
  }
315
318
  // Elements whose own matches may have changed (:empty, :first-child,
@@ -332,10 +335,8 @@ export class StyleEngine {
332
335
  const cache = this.#ruleCache;
333
336
  this.#sheetChanges = null;
334
337
  if (cache.styleSheets !== styleSheets || cache.key !== key || cache.length !== styleSheets.length) {
335
- const counter = { order: 0 };
336
- const perSheet = styleSheets.map((source) => parseStyleSheet(source, environment, counter));
338
+ const perSheet = this.#parseSheets(styleSheets, key);
337
339
  const rules = perSheet.flat();
338
- if (counter.layers) rankLayers(counter.layers, rules);
339
340
  // Sheets added or removed since the last list (same viewport): only the
340
341
  // elements their rules match can change style, every other element
341
342
  // keeps the same rules in the same relative order.
@@ -345,6 +346,62 @@ export class StyleEngine {
345
346
  return this.#ruleCache.index;
346
347
  }
347
348
 
349
+ /**
350
+ * Each sheet's rules, in cascade order. A sheet is parsed once per text
351
+ * (and viewport) with layers of its own; putting a list together only
352
+ * renumbers the rules and merges the sheets' layer trees in order, so a
353
+ * script adding one <style> does not re-parse every other sheet. Of
354
+ * identical sheets only the last copy's rules are kept, as each beats the
355
+ * same rule of an earlier copy (the earlier copy still declares its layers
356
+ * first); a sheet with anonymous layers is kept in every copy, since under
357
+ * !important an earlier anonymous layer wins.
358
+ */
359
+ #parseSheets(styleSheets, key) {
360
+ if (this.#parsedSheetsKey !== key) {
361
+ this.#parsedSheets = new Map();
362
+ this.#parsedSheetsKey = key;
363
+ }
364
+ const parsed = this.#parsedSheets;
365
+ const entries = styleSheets.map((source) => {
366
+ let entry = parsed.get(source);
367
+ if (entry) parsed.delete(source);
368
+ else {
369
+ const counter = { order: 0 };
370
+ const rules = parseStyleSheet(source, environment, counter);
371
+ entry = { rules, layers: counter.layers.children.size ? counter.layers : null };
372
+ entry.anonymousLayers = entry.layers !== null && hasAnonymousLayer(entry.layers);
373
+ }
374
+ parsed.set(source, entry); // most recently used last
375
+ return entry;
376
+ });
377
+ for (const source of parsed.keys()) {
378
+ if (parsed.size <= MAX_PARSED_SHEETS + styleSheets.length) break;
379
+ parsed.delete(source);
380
+ }
381
+ const last = new Map();
382
+ entries.forEach((entry, index) => last.set(entry, index));
383
+ const root = newLayer(null);
384
+ const layerOf = new Map();
385
+ let order = 0;
386
+ const perSheet = entries.map((entry, index) => {
387
+ if (entry.layers) mergeLayers(entry.layers, root, layerOf);
388
+ const lastCopy = last.get(entry) === index;
389
+ if (!lastCopy && !entry.anonymousLayers) return [];
390
+ // Each kept copy but the last gets rule objects of its own.
391
+ const rules = lastCopy ? entry.rules : entry.rules.map((rule) => rule.copy());
392
+ for (const rule of rules) {
393
+ rule.order = order++;
394
+ rule.layer = rule.layerNode ? layerOf.get(rule.layerNode) : Infinity;
395
+ }
396
+ return rules;
397
+ });
398
+ if (root.children.size) {
399
+ rankLayerTree(root);
400
+ for (const rules of perSheet) for (const rule of rules) if (rule.layerNode) rule.layer = rule.layer.rank;
401
+ }
402
+ return perSheet;
403
+ }
404
+
348
405
  /** Elements matched by the rules of added or removed sheets, or null when that is unknown. */
349
406
  #elementsForSheetChanges(document) {
350
407
  const changes = this.#sheetChanges;
@@ -372,16 +429,24 @@ export class StyleEngine {
372
429
 
373
430
  /** Computes and stores one element's style and ::before/::after styles. */
374
431
  #styleOne(element, parentStyle, parentCustomProperties, rules, keys, ancestors) {
432
+ const previousStyle = this.#styles.get(element);
433
+ const previousCustom = this.#customProperties.get(element);
434
+ const previousPseudo = this.#pseudoStyles.get(element);
375
435
  // Inline style takes part in the cascade before custom properties are
376
436
  // resolved: style="--x: ..." must win over stylesheet values of --x.
377
437
  const declarations = withInlineStyle(element, cascadeDeclarations(element, rules, null, keys, ancestors));
378
- const customProperties = withCustomProperties(parentCustomProperties ?? EMPTY_MAP, declarations);
379
- const style = computeElementStyle(element, parentStyle, declarations, customProperties);
438
+ let customProperties = withCustomProperties(parentCustomProperties ?? EMPTY_MAP, declarations);
439
+ let style = computeElementStyle(element, parentStyle, declarations, customProperties);
440
+ // A descendant may be visited because an ancestor's selector state
441
+ // changed even though its final computed values did not. Preserve the
442
+ // immutable identities so retained render and layout subtrees survive.
443
+ if (sameValue(previousStyle, style)) style = previousStyle;
444
+ if (sameCustom(previousCustom, customProperties)) customProperties = previousCustom;
380
445
  // rem units resolve against the root element's computed font size.
381
446
  if (!parentStyle) environment = { ...environment, rootFontSize: style.fontSize };
382
447
  this.#styles.set(element, style);
383
448
  this.#customProperties.set(element, customProperties);
384
- const pseudoStyles = {};
449
+ let pseudoStyles = {};
385
450
  for (const pseudo of ["before", "after"]) {
386
451
  const pseudoDeclarations = cascadeDeclarations(element, rules, pseudo, keys, ancestors);
387
452
  if (!pseudoDeclarations.has("content")) continue;
@@ -390,13 +455,16 @@ export class StyleEngine {
390
455
  // `content: none | normal` generates no box (CSS Pseudo 4 §2.3).
391
456
  if (pseudoStyle.content != null && pseudoStyle.display !== "none") pseudoStyles[pseudo] = pseudoStyle;
392
457
  }
458
+ if (sameValue(previousPseudo ?? null, (pseudoStyles.before || pseudoStyles.after) ? pseudoStyles : null)) {
459
+ pseudoStyles = previousPseudo ?? {};
460
+ }
393
461
  if (pseudoStyles.before || pseudoStyles.after) this.#pseudoStyles.set(element, pseudoStyles);
394
462
  else this.#pseudoStyles.delete(element);
395
463
  this.restyled++;
396
464
  return { style, customProperties };
397
465
  }
398
466
 
399
- #visit(element, parentStyle, parentCustomProperties, rules, ancestors = new Map()) {
467
+ #visit(element, parentStyle, parentCustomProperties, rules, ancestors = new AncestorFilter()) {
400
468
  const keys = elementKeys(element);
401
469
  const { style, customProperties } = this.#styleOne(element, parentStyle, parentCustomProperties, rules, keys, ancestors);
402
470
  this.#visitChildren(element, keys, style, customProperties, rules, ancestors);
@@ -404,22 +472,15 @@ export class StyleEngine {
404
472
 
405
473
  #visitChildren(element, keys, style, customProperties, rules, ancestors) {
406
474
  // The element is an ancestor of everything below it.
407
- const own = ancestorFilterKeys(keys);
408
- for (const key of own) ancestors.set(key, (ancestors.get(key) ?? 0) + 1);
475
+ const own = ancestors.push(keys);
409
476
  for (const child of element.children) this.#visit(child, style, customProperties, rules, ancestors);
410
- for (const key of own) {
411
- const count = ancestors.get(key) - 1;
412
- if (count) ancestors.set(key, count);
413
- else ancestors.delete(key);
414
- }
477
+ ancestors.pop(own);
415
478
  }
416
479
 
417
480
  /** Ancestor filter counts for the elements above `element`. */
418
481
  #ancestorsOf(element) {
419
- const ancestors = new Map();
420
- for (let node = element.parentElement; node; node = node.parentElement) {
421
- for (const key of ancestorFilterKeys(elementKeys(node))) ancestors.set(key, (ancestors.get(key) ?? 0) + 1);
422
- }
482
+ const ancestors = new AncestorFilter();
483
+ for (let node = element.parentElement; node; node = node.parentElement) ancestors.push(elementKeys(node));
423
484
  return ancestors;
424
485
  }
425
486
 
@@ -461,6 +522,8 @@ function sameCustom(left, right) {
461
522
 
462
523
  const EMPTY_MAP = new Map();
463
524
  const MAX_SHEET_CHANGE_RULES = 300;
525
+ // Parsed sheets kept for reuse beyond those in use (sheets that come and go).
526
+ const MAX_PARSED_SHEETS = 64;
464
527
 
465
528
  /**
466
529
  * Rules from sheets present in only one of two sheet lists (by text), or null
@@ -571,8 +634,7 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
571
634
  // font-size and the font shorthand wins the cascade.
572
635
  const fontSize = declarations.get("font-size");
573
636
  const font = declarations.get("font");
574
- const cascadeKey = (declaration) => ({ important: false, specificity: 0, order: 0, ...declaration, important: Boolean(declaration.important) });
575
- if (font && (!fontSize || compareCascade(cascadeKey(font), cascadeKey(fontSize)) > 0)) {
637
+ if (font && (!fontSize || compareDeclarations(font, fontSize) > 0)) {
576
638
  const parsed = parseFontShorthand(resolveVariables(font.value, customProperties));
577
639
  if (parsed) setProperty(values, "font-size", parsed.size, parentStyle);
578
640
  } else if (fontSize) {
@@ -582,10 +644,7 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
582
644
  const colorScheme = declarations.get("color-scheme");
583
645
  if (colorScheme) applyDeclaration(values, colorScheme, customProperties, parentStyle);
584
646
  // Apply in cascade order so a later shorthand overrides earlier longhands and vice versa.
585
- const ordered = [...declarations.values()].sort((left, right) => compareCascade(
586
- { important: false, specificity: 0, order: 0, ...left, important: Boolean(left.important) },
587
- { important: false, specificity: 0, order: 0, ...right, important: Boolean(right.important) },
588
- ));
647
+ const ordered = [...declarations.values()].sort(compareDeclarations);
589
648
  for (const declaration of ordered) {
590
649
  if (declaration.name.startsWith("--") || declaration.name === "font-size" || declaration.name === "color-scheme") continue;
591
650
  // The shorthand's size was settled above; its other parts apply in order.
@@ -706,6 +765,13 @@ class StyleRule {
706
765
  get hasCustomProperties() {
707
766
  return this.#block.hasCustomProperties;
708
767
  }
768
+
769
+ /** The same rule as a separate object (its own order and layer). */
770
+ copy() {
771
+ const rule = new StyleRule(this.selector, this.#selectorText, this.pseudo, this.#block, this.order);
772
+ rule.layerNode = this.layerNode;
773
+ return rule;
774
+ }
709
775
  }
710
776
 
711
777
  /**
@@ -744,7 +810,7 @@ export function parseStyleSheet(source, viewport = environment, counter = { orde
744
810
  if (name === "layer") {
745
811
  // A named layer block, or an anonymous one (a layer of its own).
746
812
  const parent = layer ?? counter.layers;
747
- const inner = condition ? declareLayer(parent, condition) : declareLayer(parent, `\0anonymous${counter.order}`);
813
+ const inner = condition ? declareLayer(parent, condition) : declareLayer(parent, `${ANONYMOUS_LAYER}${counter.order}`);
748
814
  parseBlock(open + 1, close, inner);
749
815
  } else if ((name === "media" && mediaQueryMatches(condition, viewport)) || name === "supports") {
750
816
  parseBlock(open + 1, close, layer);
@@ -771,6 +837,28 @@ function newLayer(parent) {
771
837
  return { parent, children: new Map(), rank: Infinity };
772
838
  }
773
839
 
840
+ const ANONYMOUS_LAYER = "\0anonymous";
841
+
842
+ function hasAnonymousLayer(node) {
843
+ for (const [name, child] of node.children) if (name.startsWith(ANONYMOUS_LAYER) || hasAnonymousLayer(child)) return true;
844
+ return false;
845
+ }
846
+
847
+ /**
848
+ * Adds one sheet's layer tree to the layers of a sheet list, in first-seen
849
+ * order; `layerOf` maps each of the sheet's layers to the list's. Every
850
+ * anonymous layer is a layer of its own.
851
+ */
852
+ function mergeLayers(node, target, layerOf) {
853
+ for (const [name, child] of node.children) {
854
+ const anonymous = name.startsWith(ANONYMOUS_LAYER);
855
+ let merged = anonymous ? null : target.children.get(name);
856
+ if (!merged) target.children.set(anonymous ? `${name}\0${target.children.size}` : name, merged = newLayer(target));
857
+ layerOf.set(child, merged);
858
+ mergeLayers(child, merged, layerOf);
859
+ }
860
+ }
861
+
774
862
  /** Finds or creates a (possibly dotted) layer name under `parent`, in first-seen order. */
775
863
  function declareLayer(parent, name) {
776
864
  let node = parent;
@@ -787,12 +875,16 @@ function declareLayer(parent, name) {
787
875
  * layer's sublayers before its own rules; unlayered rules rank highest.
788
876
  */
789
877
  export function rankLayers(root, rules) {
878
+ rankLayerTree(root);
879
+ for (const rule of rules) rule.layer = rule.layerNode ? rule.layerNode.rank : Infinity;
880
+ }
881
+
882
+ function rankLayerTree(root) {
790
883
  let next = 0;
791
884
  (function visit(node) {
792
885
  for (const child of node.children.values()) visit(child);
793
886
  if (node !== root) node.rank = next++;
794
887
  })(root);
795
- for (const rule of rules) rule.layer = rule.layerNode ? rule.layerNode.rank : Infinity;
796
888
  }
797
889
 
798
890
  /** Splits a `font` shorthand value into size, line height, weight and family. */
@@ -823,34 +915,38 @@ function parseFontShorthand(value) {
823
915
  return null;
824
916
  }
825
917
 
918
+ // Character codes for the scanners below, which run over whole style sheets.
919
+ const QUOTE = 0x22, APOSTROPHE = 0x27, BACKSLASH = 0x5c, OPEN_PAREN = 0x28, CLOSE_PAREN = 0x29, OPEN_BRACE = 0x7b, CLOSE_BRACE = 0x7d;
920
+
826
921
  function findTopLevel(source, character, start, end) {
827
- let quote = "";
922
+ const target = character.charCodeAt(0);
923
+ let quote = 0;
828
924
  let depth = 0;
829
925
  for (let index = start; index < end; index++) {
830
- const current = source[index];
926
+ const current = source.charCodeAt(index);
831
927
  if (quote) {
832
- if (current === "\\") index++;
833
- else if (current === quote) quote = "";
834
- } else if (current === '"' || current === "'") quote = current;
835
- else if (current === "(") depth++;
836
- else if (current === ")") depth = Math.max(0, depth - 1);
837
- else if (current === character && depth === 0) return index;
838
- else if (current === "{" || current === "}") return character === current ? index : -1;
928
+ if (current === BACKSLASH) index++;
929
+ else if (current === quote) quote = 0;
930
+ } else if (current === QUOTE || current === APOSTROPHE) quote = current;
931
+ else if (current === OPEN_PAREN) depth++;
932
+ else if (current === CLOSE_PAREN) depth = Math.max(0, depth - 1);
933
+ else if (current === target && depth === 0) return index;
934
+ else if (current === OPEN_BRACE || current === CLOSE_BRACE) return target === current ? index : -1;
839
935
  }
840
936
  return -1;
841
937
  }
842
938
 
843
939
  function matchingBrace(source, open, end) {
844
940
  let depth = 0;
845
- let quote = "";
941
+ let quote = 0;
846
942
  for (let index = open; index < end; index++) {
847
- const current = source[index];
943
+ const current = source.charCodeAt(index);
848
944
  if (quote) {
849
- if (current === "\\") index++;
850
- else if (current === quote) quote = "";
851
- } else if (current === '"' || current === "'") quote = current;
852
- else if (current === "{") depth++;
853
- else if (current === "}" && --depth === 0) return index;
945
+ if (current === BACKSLASH) index++;
946
+ else if (current === quote) quote = 0;
947
+ } else if (current === QUOTE || current === APOSTROPHE) quote = current;
948
+ else if (current === OPEN_BRACE) depth++;
949
+ else if (current === CLOSE_BRACE && --depth === 0) return index;
854
950
  }
855
951
  return end;
856
952
  }
@@ -1452,10 +1548,14 @@ class RuleIndex {
1452
1548
  buckets = { id: new Map(), class: new Map(), attr: new Map(), tag: new Map(), universal: [] };
1453
1549
  this.#byPseudo.set(rule.pseudo, buckets);
1454
1550
  }
1455
- const parts = selectorParts(rule.selector);
1456
- rule.ancestorKeys = parts ? ancestorKeys(parts) : [];
1551
+ // Kept on the rule: a parsed sheet's rules are indexed again whenever the sheet list changes.
1552
+ if (rule.subjectKeys === undefined) {
1553
+ const { subject, ancestors } = selectorKeys(rule.selector);
1554
+ rule.ancestorIds = Int32Array.from(ancestors, filterKeyId);
1555
+ rule.subjectKeys = subject;
1556
+ }
1457
1557
  rule.seen = 0;
1458
- const keys = parts ? subjectKeys(parts.compounds.at(-1)) : null;
1558
+ const keys = rule.subjectKeys;
1459
1559
  if (!keys) {
1460
1560
  buckets.universal.push(rule);
1461
1561
  continue;
@@ -1472,7 +1572,7 @@ class RuleIndex {
1472
1572
  /**
1473
1573
  * Rules that may match `element` for `pseudo`, in cascade order.
1474
1574
  * @param {ReturnType<typeof elementKeys>} keys the element's own keys
1475
- * @param {Map<string, number>} ancestors keys present on its ancestors
1575
+ * @param {AncestorFilter} ancestors keys present on its ancestors
1476
1576
  */
1477
1577
  candidates(keys, pseudo, ancestors) {
1478
1578
  const buckets = this.#byPseudo.get(pseudo);
@@ -1490,7 +1590,7 @@ class RuleIndex {
1490
1590
  for (const rule of list) {
1491
1591
  if (rule.seen === stamp) continue;
1492
1592
  rule.seen = stamp;
1493
- if (rule.ancestorKeys.every((key) => ancestors.has(key))) result.push(rule);
1593
+ if (ancestors.hasAll(rule.ancestorIds)) result.push(rule);
1494
1594
  }
1495
1595
  }
1496
1596
  if (lists.length > 1) result.sort((left, right) => left.order - right.order);
@@ -1498,6 +1598,72 @@ class RuleIndex {
1498
1598
  }
1499
1599
  }
1500
1600
 
1601
+ /**
1602
+ * The elements that match each selector among `root`'s descendants and, for
1603
+ * an element, `root` itself (querySelectorAll() plus matches()), in one walk: selectors are bucketed by their subject's keys like
1604
+ * rules, so each element is only tested against selectors it may match.
1605
+ * Invalid selectors are left out of the result.
1606
+ * @param {Element | Document} root
1607
+ * @param {Iterable<string>} selectors
1608
+ * @param {Map<string, object | null>} [compiledSelectors] compiled selectors to reuse and add to
1609
+ * @returns {Map<string, Set<Element>>}
1610
+ */
1611
+ export function matchSelectors(root, selectors, compiledSelectors = new Map()) {
1612
+ const result = new Map();
1613
+ const buckets = { id: new Map(), class: new Map(), attr: new Map(), tag: new Map(), universal: [] };
1614
+ const sample = root.nodeType === 9 ? root.documentElement : root;
1615
+ for (const selector of selectors) {
1616
+ if (result.has(selector)) continue;
1617
+ // :scope means the root to querySelectorAll() but the element to matches().
1618
+ let compiled;
1619
+ if (sample && !selector.includes(":scope")) {
1620
+ compiled = compiledSelectors.get(selector);
1621
+ if (compiled === undefined) {
1622
+ compiled = compileSelector(sample, selector);
1623
+ if (compiled !== undefined) compiledSelectors.set(selector, compiled);
1624
+ }
1625
+ }
1626
+ if (compiled === null) continue;
1627
+ if (compiled === undefined) {
1628
+ try { result.set(selector, new Set(root.querySelectorAll(selector))); } catch { /* invalid */ }
1629
+ continue;
1630
+ }
1631
+ const entry = { compiled, matched: new Set(), seen: 0 };
1632
+ result.set(selector, entry.matched);
1633
+ const keys = selectorKeys(selector).subject;
1634
+ if (!keys) {
1635
+ buckets.universal.push(entry);
1636
+ continue;
1637
+ }
1638
+ for (const key of keys) {
1639
+ const list = buckets[key.kind].get(key.name);
1640
+ if (list) list.push(entry); else buckets[key.kind].set(key.name, [entry]);
1641
+ }
1642
+ }
1643
+ let stamp = 0;
1644
+ const test = (element, list) => {
1645
+ if (!list) return;
1646
+ for (const entry of list) {
1647
+ if (entry.seen === stamp) continue;
1648
+ entry.seen = stamp;
1649
+ if (matchesCompiled(element, entry.compiled)) entry.matched.add(element);
1650
+ }
1651
+ };
1652
+ const visit = (element) => {
1653
+ stamp++;
1654
+ const keys = elementKeys(element);
1655
+ test(element, buckets.universal);
1656
+ if (keys.id) test(element, buckets.id.get(keys.id));
1657
+ for (const name of keys.classes) test(element, buckets.class.get(name));
1658
+ for (const name of keys.attrs) test(element, buckets.attr.get(name));
1659
+ if (keys.tag) test(element, buckets.tag.get(keys.tag));
1660
+ for (const child of element.children) visit(child);
1661
+ };
1662
+ if (root.nodeType === 1) visit(root);
1663
+ else for (const child of root.children ?? []) visit(child);
1664
+ return result;
1665
+ }
1666
+
1501
1667
  /** The keys an element offers to selectors: id, classes, attribute names, type. */
1502
1668
  function elementKeys(element) {
1503
1669
  const id = element.getAttribute?.("id") || null;
@@ -1509,203 +1675,102 @@ function elementKeys(element) {
1509
1675
  return { id, classes, attrs, tag: tag ? String(tag).toLowerCase() : null };
1510
1676
  }
1511
1677
 
1512
- /** Ancestor filter keys for an element: "tag", "#id", ".class", "[attr". */
1513
- function ancestorFilterKeys(keys) {
1514
- const result = [];
1515
- if (keys.tag) result.push(keys.tag);
1516
- if (keys.id) result.push(`#${keys.id}`);
1517
- for (const name of keys.classes) result.push(`.${name}`);
1518
- for (const name of keys.attrs) result.push(`[${name}`);
1519
- return result;
1678
+ // Ancestor filter keys that some rule requires, numbered for AncestorFilter.
1679
+ const FILTER_KEY_IDS = new Map();
1680
+
1681
+ function filterKeyId(key) {
1682
+ let id = FILTER_KEY_IDS.get(key);
1683
+ if (id === undefined) FILTER_KEY_IDS.set(key, id = FILTER_KEY_IDS.size);
1684
+ return id;
1520
1685
  }
1521
1686
 
1522
1687
  /**
1523
- * Splits a complex selector into compounds and the combinators between them
1524
- * (" ", ">", "+", "~"), ignoring anything inside (), [] and quotes. Returns
1525
- * null for selectors this does not understand, which are then left unindexed.
1688
+ * Counts of the filter keys on the elements above the one being styled (a
1689
+ * counting filter, like Blink's ancestor Bloom filter but exact). Keys no
1690
+ * rule requires are not counted. Created after the rules are indexed.
1526
1691
  */
1527
- function selectorParts(selector) {
1528
- const compounds = [];
1529
- const combinators = [];
1530
- let current = "";
1531
- let pending = null;
1532
- let depth = 0;
1533
- for (let index = 0; index < selector.length; index++) {
1534
- const character = selector[index];
1535
- if (character === "\\") {
1536
- current += character + (selector[++index] ?? "");
1537
- continue;
1538
- }
1539
- if (character === "\"" || character === "'") {
1540
- const close = selector.indexOf(character, index + 1);
1541
- if (close < 0) return null;
1542
- current += selector.slice(index, close + 1);
1543
- index = close;
1544
- continue;
1545
- }
1546
- if (character === "(" || character === "[") depth++;
1547
- else if (character === ")" || character === "]") depth--;
1548
- if (depth === 0 && (character === " " || character === ">" || character === "+" || character === "~"
1549
- || character === "\t" || character === "\n" || character === "\r" || character === "\f")) {
1550
- if (current) {
1551
- compounds.push(current);
1552
- current = "";
1553
- pending = " ";
1554
- }
1555
- if (character === ">" || character === "+" || character === "~") {
1556
- if (pending === null && compounds.length === 0) return null;
1557
- pending = character;
1558
- }
1559
- continue;
1560
- }
1561
- if (pending !== null) {
1562
- combinators.push(pending);
1563
- pending = null;
1564
- }
1565
- current += character;
1566
- }
1567
- if (depth !== 0) return null;
1568
- if (current) compounds.push(current);
1569
- else if (pending !== null && compounds.length) return null;
1570
- return compounds.length ? { compounds, combinators } : null;
1571
- }
1572
-
1573
- const IDENTIFIER = /^(?:[\w-]|\\[^0-9a-fA-F\s]|[^\x00-\x7f])+/;
1574
-
1575
- /** An identifier at the start of `text`, unescaped; null when absent or hex-escaped. */
1576
- function leadingIdentifier(text) {
1577
- const match = IDENTIFIER.exec(text);
1578
- if (!match || text[match[0].length] === "\\") return null;
1579
- return match[0].replace(/\\(.)/g, "$1");
1580
- }
1692
+ class AncestorFilter {
1693
+ #counts = new Int32Array(Math.max(1, FILTER_KEY_IDS.size));
1581
1694
 
1582
- /** Keys of one compound selector, from its parts outside () (ids, classes, attribute names, type). */
1583
- function compoundKeys(compound) {
1584
- const keys = { ids: [], classes: [], attrs: [], tag: null, pseudo: [] };
1585
- if (compound.includes("|")) return keys;
1586
- const tag = /^[a-zA-Z][\w-]*/.exec(compound)?.[0];
1587
- if (tag) keys.tag = tag.toLowerCase();
1588
- let depth = 0;
1589
- for (let index = 0; index < compound.length; index++) {
1590
- const character = compound[index];
1591
- if (character === "\\") {
1592
- index++;
1593
- continue;
1594
- }
1595
- if (character === "(") {
1596
- depth++;
1597
- continue;
1598
- }
1599
- if (character === ")") {
1600
- depth--;
1601
- continue;
1602
- }
1603
- if (depth > 0) continue;
1604
- if (character === "[") {
1605
- const name = /^\s*([\w-]+)/.exec(compound.slice(index + 1))?.[1];
1606
- if (name) keys.attrs.push(name.toLowerCase());
1607
- // Skip to the closing bracket; quoted values may contain "]", "#" or ".".
1608
- let quote = null;
1609
- for (index++; index < compound.length; index++) {
1610
- const inner = compound[index];
1611
- if (inner === "\\") index++;
1612
- else if (quote) {
1613
- if (inner === quote) quote = null;
1614
- } else if (inner === "\"" || inner === "'") quote = inner;
1615
- else if (inner === "]") break;
1616
- }
1617
- } else if (character === "#" || character === ".") {
1618
- const name = leadingIdentifier(compound.slice(index + 1));
1619
- if (name && !(character === "." && /^\d/.test(name))) (character === "#" ? keys.ids : keys.classes).push(name);
1620
- } else if (character === ":") {
1621
- const name = /^:?([\w-]+)/.exec(compound.slice(index + 1))?.[1];
1622
- if (name) keys.pseudo.push({ name: name.toLowerCase(), start: index });
1695
+ /** Adds an element's keys; returns what pop() takes to remove them. */
1696
+ push(keys) {
1697
+ const ids = [];
1698
+ for (const key of ancestorFilterKeys(keys)) {
1699
+ const id = FILTER_KEY_IDS.get(key);
1700
+ if (id === undefined || id >= this.#counts.length) continue;
1701
+ this.#counts[id]++;
1702
+ ids.push(id);
1623
1703
  }
1704
+ return ids;
1624
1705
  }
1625
- return keys;
1626
- }
1627
1706
 
1628
- /**
1629
- * Bucket keys for the subject compound: one id, class, attribute or type key,
1630
- * or one per alternative of an :is()/:where() whose alternatives all have one.
1631
- */
1632
- function subjectKeys(compound) {
1633
- const keys = compoundKeys(compound);
1634
- if (keys.ids.length) return [{ kind: "id", name: keys.ids[0] }];
1635
- if (keys.classes.length) return [{ kind: "class", name: keys.classes[0] }];
1636
- if (keys.attrs.length) return [{ kind: "attr", name: keys.attrs[0] }];
1637
- if (keys.tag) return [{ kind: "tag", name: keys.tag }];
1638
- if (keys.pseudo.some(({ name }) => name === "root")) return [{ kind: "tag", name: "html" }];
1639
- for (const { name, start } of keys.pseudo) {
1640
- if (!["is", "where", "matches", "-webkit-any"].includes(name)) continue;
1641
- const open = compound.indexOf("(", start);
1642
- const close = open < 0 ? -1 : matchingParenthesis(compound, open);
1643
- if (close < 0) return null;
1644
- const alternatives = splitTopLevel(compound.slice(open + 1, close), ",").filter(Boolean);
1645
- const result = [];
1646
- for (const alternative of alternatives) {
1647
- // Only simple alternatives: complex ones inside :is() are left to the
1648
- // selector engine unindexed, so indexing never changes a result.
1649
- const parts = selectorParts(alternative.trim());
1650
- const inner = parts?.compounds.length === 1 ? subjectKeys(parts.compounds[0]) : null;
1651
- if (!inner) return null;
1652
- result.push(...inner);
1653
- }
1654
- return result.length ? result : null;
1707
+ pop(ids) {
1708
+ for (const id of ids) this.#counts[id]--;
1655
1709
  }
1656
- return null;
1657
- }
1658
1710
 
1659
- /** Keys that must be present on some ancestor for the selector to match. */
1660
- function ancestorKeys(parts) {
1661
- const keys = new Set();
1662
- for (let index = 0; index < parts.compounds.length - 1; index++) {
1663
- // Only compounds whose right-hand combinator is descendant or child are
1664
- // ancestors of the subject (a sibling's parent is the subject's parent).
1665
- if (parts.combinators[index] !== " " && parts.combinators[index] !== ">") continue;
1666
- const compound = compoundKeys(parts.compounds[index]);
1667
- if (compound.tag) keys.add(compound.tag);
1668
- for (const id of compound.ids) keys.add(`#${id}`);
1669
- for (const name of compound.classes) keys.add(`.${name}`);
1670
- for (const name of compound.attrs) keys.add(`[${name}`);
1711
+ hasAll(ids) {
1712
+ for (let index = 0; index < ids.length; index++) if (!(this.#counts[ids[index]] > 0)) return false;
1713
+ return true;
1671
1714
  }
1672
- return [...keys];
1673
1715
  }
1674
1716
 
1675
- function matchingParenthesis(text, open) {
1676
- let depth = 0;
1677
- for (let index = open; index < text.length; index++) {
1678
- if (text[index] === "\\") index++;
1679
- else if (text[index] === "(") depth++;
1680
- else if (text[index] === ")" && --depth === 0) return index;
1681
- }
1682
- return -1;
1717
+ /** Ancestor filter keys for an element: "tag", "#id", ".class", "[attr". */
1718
+ function ancestorFilterKeys(keys) {
1719
+ const result = [];
1720
+ if (keys.tag) result.push(keys.tag);
1721
+ if (keys.id) result.push(`#${keys.id}`);
1722
+ for (const name of keys.classes) result.push(`.${name}`);
1723
+ for (const name of keys.attrs) result.push(`[${name}`);
1724
+ return result;
1683
1725
  }
1684
1726
 
1685
- function cascadeDeclarations(element, rules, pseudo = null, keys = elementKeys(element), ancestors = new Map()) {
1727
+ function cascadeDeclarations(element, rules, pseudo = null, keys = elementKeys(element), ancestors = new AncestorFilter()) {
1686
1728
  const result = new Map();
1687
1729
  result.hasCustomProperties = false;
1688
1730
  for (const rule of rules.candidates(keys, pseudo, ancestors)) {
1689
1731
  if (!matchesSelector(element, rule)) continue;
1690
1732
  if (rule.hasCustomProperties) result.hasCustomProperties = true;
1733
+ const { specificity, order } = rule;
1734
+ const layer = rule.layer ?? Infinity;
1691
1735
  for (const declaration of rule.declarationList) {
1692
- const candidate = { ...declaration, specificity: rule.specificity, order: rule.order, layer: rule.layer ?? Infinity };
1693
- const current = result.get(candidate.name);
1694
- if (!current || compareCascade(candidate, current) >= 0) result.set(candidate.name, candidate);
1736
+ const current = result.get(declaration.name);
1737
+ if (current && compareCascadeParts(declaration.important, layer, specificity, order, current) < 0) continue;
1738
+ result.set(declaration.name, { name: declaration.name, value: declaration.value, important: declaration.important, specificity, order, layer });
1695
1739
  }
1696
1740
  }
1697
1741
  return result;
1698
1742
  }
1699
1743
 
1700
1744
  function compareCascade(left, right) {
1701
- if (left.important !== right.important) return left.important ? 1 : -1;
1745
+ return compareCascadeParts(left.important, left.layer ?? Infinity, left.specificity, left.order, right);
1746
+ }
1747
+
1748
+ /** compareCascade() for a left side given as its parts (no object for it). */
1749
+ function compareCascadeParts(important, layer, specificity, order, right) {
1750
+ if (important !== right.important) return important ? 1 : -1;
1702
1751
  // Cascade layers: later layers and unlayered styles win for normal
1703
1752
  // declarations; for !important the order is reversed.
1753
+ const rightLayer = right.layer ?? Infinity;
1754
+ if (layer !== rightLayer) return (layer > rightLayer ? 1 : -1) * (important ? -1 : 1);
1755
+ if (specificity !== right.specificity) return specificity - right.specificity;
1756
+ return order - right.order;
1757
+ }
1758
+
1759
+ /**
1760
+ * compareCascade() for declarations that may lack cascade fields (user-agent
1761
+ * and inline ones): absent means normal importance, specificity and order 0.
1762
+ */
1763
+ function compareDeclarations(left, right) {
1764
+ const leftImportant = Boolean(left.important);
1765
+ const rightImportant = Boolean(right.important);
1766
+ if (leftImportant !== rightImportant) return leftImportant ? 1 : -1;
1704
1767
  const leftLayer = left.layer ?? Infinity;
1705
1768
  const rightLayer = right.layer ?? Infinity;
1706
- if (leftLayer !== rightLayer) return (leftLayer > rightLayer ? 1 : -1) * (left.important ? -1 : 1);
1707
- if (left.specificity !== right.specificity) return left.specificity - right.specificity;
1708
- return left.order - right.order;
1769
+ if (leftLayer !== rightLayer) return (leftLayer > rightLayer ? 1 : -1) * (leftImportant ? -1 : 1);
1770
+ const leftSpecificity = "specificity" in left ? left.specificity : 0;
1771
+ const rightSpecificity = "specificity" in right ? right.specificity : 0;
1772
+ if (leftSpecificity !== rightSpecificity) return leftSpecificity - rightSpecificity;
1773
+ return ("order" in left ? left.order : 0) - ("order" in right ? right.order : 0);
1709
1774
  }
1710
1775
 
1711
1776
  const PSEUDO_ELEMENT = /(?:::?(before|after)|::([\w-]+))$/i;
@@ -1716,9 +1781,14 @@ const PSEUDO_ELEMENT = /(?:::?(before|after)|::([\w-]+))$/i;
1716
1781
  * pseudo-elements that are not rendered (::placeholder, ::selection, ...).
1717
1782
  */
1718
1783
  function splitPseudoElement(selectorText) {
1719
- const match = PSEUDO_ELEMENT.exec(selectorText);
1784
+ // A trailing pseudo-element starts at the last colon (or the one before
1785
+ // it): only the tail needs the regular expression.
1786
+ const colon = selectorText.lastIndexOf(":");
1787
+ if (colon < 0) return { selector: selectorText, pseudo: null };
1788
+ const offset = Math.max(0, colon - 1);
1789
+ const match = PSEUDO_ELEMENT.exec(offset ? selectorText.slice(offset) : selectorText);
1720
1790
  if (!match) return { selector: selectorText, pseudo: null };
1721
- const selector = selectorText.slice(0, match.index).trim() || "*";
1791
+ const selector = selectorText.slice(0, offset + match.index).trim() || "*";
1722
1792
  return { selector, pseudo: match[1] ? match[1].toLowerCase() : undefined };
1723
1793
  }
1724
1794
 
@@ -1780,7 +1850,18 @@ function addSpecificity(left, right) {
1780
1850
  function matchesSelector(element, rule) {
1781
1851
  if (rule.invalid || typeof element.matches !== "function") return false;
1782
1852
  try {
1783
- return element.matches(rule.selector);
1853
+ // Compiled once per rule; objects other than Happy DOM elements use matches().
1854
+ let compiled = rule.compiled;
1855
+ if (compiled === undefined) {
1856
+ compiled = compileSelector(element, rule.selector);
1857
+ if (compiled === undefined) return element.matches(rule.selector);
1858
+ rule.compiled = compiled;
1859
+ }
1860
+ if (compiled === null) {
1861
+ rule.invalid = true;
1862
+ return false;
1863
+ }
1864
+ return matchesCompiled(element, compiled);
1784
1865
  } catch {
1785
1866
  rule.invalid = true;
1786
1867
  return false;