@react-x11/components 0.14.0 → 0.16.0

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.
Files changed (97) hide show
  1. package/dist/code-editor/node.d.ts +13 -0
  2. package/dist/code-editor/node.d.ts.map +1 -1
  3. package/dist/code-editor/node.js +23 -5
  4. package/dist/code-editor/node.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +209 -23
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +873 -175
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +25 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +307 -19
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/content.d.ts +11 -6
  14. package/dist/html/css/content.d.ts.map +1 -1
  15. package/dist/html/css/content.js +171 -12
  16. package/dist/html/css/content.js.map +1 -1
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +62 -9
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/shapes.d.ts +62 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -0
  22. package/dist/html/css/shapes.js +214 -0
  23. package/dist/html/css/shapes.js.map +1 -0
  24. package/dist/html/css/style.d.ts +42 -5
  25. package/dist/html/css/style.d.ts.map +1 -1
  26. package/dist/html/css/style.js +75 -27
  27. package/dist/html/css/style.js.map +1 -1
  28. package/dist/html/css/ua.js +15 -0
  29. package/dist/html/css/ua.js.map +1 -1
  30. package/dist/html/css/values.d.ts +5 -0
  31. package/dist/html/css/values.d.ts.map +1 -1
  32. package/dist/html/css/values.js +18 -9
  33. package/dist/html/css/values.js.map +1 -1
  34. package/dist/html/index.d.ts.map +1 -1
  35. package/dist/html/index.js +7 -0
  36. package/dist/html/index.js.map +1 -1
  37. package/dist/html/layout/block.d.ts +38 -7
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +160 -50
  40. package/dist/html/layout/block.js.map +1 -1
  41. package/dist/html/layout/boxes.d.ts +29 -2
  42. package/dist/html/layout/boxes.d.ts.map +1 -1
  43. package/dist/html/layout/boxes.js +159 -20
  44. package/dist/html/layout/boxes.js.map +1 -1
  45. package/dist/html/layout/css-grid.d.ts.map +1 -1
  46. package/dist/html/layout/css-grid.js +69 -19
  47. package/dist/html/layout/css-grid.js.map +1 -1
  48. package/dist/html/layout/flex.d.ts.map +1 -1
  49. package/dist/html/layout/flex.js +1648 -225
  50. package/dist/html/layout/flex.js.map +1 -1
  51. package/dist/html/layout/inline.d.ts +15 -3
  52. package/dist/html/layout/inline.d.ts.map +1 -1
  53. package/dist/html/layout/inline.js +23 -8
  54. package/dist/html/layout/inline.js.map +1 -1
  55. package/dist/html/layout/table.d.ts +27 -1
  56. package/dist/html/layout/table.d.ts.map +1 -1
  57. package/dist/html/layout/table.js +907 -323
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +36 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +174 -37
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts +4 -1
  64. package/dist/html/paint.d.ts.map +1 -1
  65. package/dist/html/paint.js +769 -204
  66. package/dist/html/paint.js.map +1 -1
  67. package/dist/html/resources.js +14 -1
  68. package/dist/html/resources.js.map +1 -1
  69. package/dist/html/svg.d.ts +50 -17
  70. package/dist/html/svg.d.ts.map +1 -1
  71. package/dist/html/svg.js +501 -83
  72. package/dist/html/svg.js.map +1 -1
  73. package/dist/html/widgets.d.ts.map +1 -1
  74. package/dist/html/widgets.js +57 -4
  75. package/dist/html/widgets.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/code-editor/node.ts +24 -5
  78. package/src/html/css/cascade.ts +987 -176
  79. package/src/html/css/color.ts +345 -18
  80. package/src/html/css/content.ts +173 -15
  81. package/src/html/css/parse.ts +60 -8
  82. package/src/html/css/shapes.ts +273 -0
  83. package/src/html/css/style.ts +123 -30
  84. package/src/html/css/ua.ts +15 -0
  85. package/src/html/css/values.ts +23 -9
  86. package/src/html/index.ts +7 -0
  87. package/src/html/layout/block.ts +169 -56
  88. package/src/html/layout/boxes.ts +198 -24
  89. package/src/html/layout/css-grid.ts +89 -22
  90. package/src/html/layout/flex.ts +1863 -219
  91. package/src/html/layout/inline.ts +27 -7
  92. package/src/html/layout/table.ts +989 -333
  93. package/src/html/node.ts +204 -37
  94. package/src/html/paint.ts +999 -252
  95. package/src/html/resources.ts +13 -1
  96. package/src/html/svg.ts +600 -78
  97. package/src/html/widgets.ts +61 -4
@@ -6,7 +6,8 @@
6
6
  // compiles a selector to a closure once and the closure is what runs per
7
7
  // element, and it has the combinator, `:nth-child(an+b)` and attribute-
8
8
  // operator cases right — so it is imported, through an adapter that answers
9
- // `:hover` from this renderer's own pointer state.
9
+ // `:hover` from this renderer's own pointer state, and pseudo-classes that
10
+ // answer `:focus` and its kin from which element's widget holds the focus.
10
11
  //
11
12
  // What is *not* delegated is the choice of which selectors to try. Testing
12
13
  // every rule against every element is O(rules × elements) and is what makes
@@ -21,7 +22,7 @@ import { Element as DomElement, isTag } from 'domhandler';
21
22
  import type { Element } from 'domhandler';
22
23
 
23
24
  import { attr, tagOf } from '../dom.js';
24
- import { svgSizeHint } from '../svg.js';
25
+ import { systemColorTable, usedColorScheme } from './color.js';
25
26
  import {
26
27
  asciiLower,
27
28
  escapeEnd,
@@ -33,9 +34,11 @@ import type { Declaration, StyleRule, Stylesheet } from './parse.js';
33
34
  import {
34
35
  applyDeclaration,
35
36
  blockify,
37
+ settleAlign,
36
38
  settleButton,
37
39
  settleContentVisibility,
38
40
  settleOverflow,
41
+ settleOutline,
39
42
  settleClamp,
40
43
  copyStyle,
41
44
  decorate,
@@ -54,6 +57,15 @@ import type { UnitContext } from './values.js';
54
57
  import { viewportUnit } from './values.js';
55
58
  import { customProperties, substituteIn } from './vars.js';
56
59
  import type { CustomProps } from './vars.js';
60
+ import {
61
+ ROOT_BOX_PROPS,
62
+ SHAPE_TAGS,
63
+ isShapeProp,
64
+ isUse,
65
+ shapeValue,
66
+ useHref,
67
+ } from './shapes.js';
68
+ import type { ShapeContext, ShapeStyle, ShapeStyles } from './shapes.js';
57
69
 
58
70
  /** Where a declaration came from. Higher wins before specificity is asked. */
59
71
  const enum Origin {
@@ -139,6 +151,18 @@ interface IndexedRule {
139
151
  compiled: boolean;
140
152
  /** Which rule this is, in a sharing key (`Cascade.sharedStyleFor`). */
141
153
  id: number;
154
+ /** For a rule asked of the elements in a drawing by their type, or of
155
+ * them all (`Cascade.shapeStyles`): the ids and classes it asks of its
156
+ * subject's ancestors (`ancestorKeys`). Null until a drawing asks, and
157
+ * in every other rule. */
158
+ needs: readonly string[] | null;
159
+ /** For a rule kept for drawings: whether it declares something a
160
+ * drawing's root does not have from its box's style. Null until asked. */
161
+ root: boolean | null;
162
+ /** The selector as it is matched in the tree a `<use>`'s copy is in
163
+ * (`Cascade.shapeStyles`), compiled once one is first asked. */
164
+ inCopy: ((el: Element) => boolean) | null;
165
+ inCopyCompiled: boolean;
142
166
  }
143
167
 
144
168
  /**
@@ -169,10 +193,12 @@ class RuleIndex {
169
193
  /** Whether any rule in here is pointer-sensitive, so the renderer knows
170
194
  * whether a pointer move can change the cascade at all. */
171
195
  hoverSensitive = false;
172
- /** The rules that test `:hover`, in buckets of their own: the ones whose
173
- * answers a pointer move can change, which is all that is asked to know
174
- * whether an element's did (`Cascade.pointerChanged`). Null until there
175
- * is one. */
196
+ /** …and focus-sensitive, for a widget taking the focus or giving it up. */
197
+ focusSensitive = false;
198
+ /** The rules that test `:hover` or the focus, in buckets of their own:
199
+ * the ones whose answers a pointer move or a focus change can change,
200
+ * which is all that is asked to know whether an element's did
201
+ * (`Cascade.pointerChanged`). Null until there is one. */
176
202
  pointer: RuleIndex | null = null;
177
203
  /** The buckets holding a rule an element cannot share its style under
178
204
  * (`UNSHAREABLE`): the ids, classes and tags whose elements compute
@@ -194,6 +220,11 @@ class RuleIndex {
194
220
  */
195
221
  reaches(el: Element): boolean {
196
222
  if (this.universal.length || this.byTag.has(tagOf(el))) return true;
223
+ return this.names(el);
224
+ }
225
+
226
+ /** Whether a bucket here is for `el`'s id, or for a class of its. */
227
+ names(el: Element): boolean {
197
228
  if (this.byId.size) {
198
229
  const id = attr(el, 'id');
199
230
  if (id && this.byId.has(id)) return true;
@@ -209,18 +240,23 @@ class RuleIndex {
209
240
  return false;
210
241
  }
211
242
 
212
- add(rule: StyleRule): void {
243
+ add(rule: StyleRule, key = rightmostKey(rule.selector)): void {
213
244
  this.size += 1;
214
245
  const indexed: IndexedRule = {
215
246
  rule,
216
247
  match: null,
217
248
  compiled: false,
218
249
  id: this._nextId++,
250
+ needs: null,
251
+ root: null,
252
+ inCopy: null,
253
+ inCopyCompiled: false,
219
254
  };
220
255
  if (rule.selector.includes(':hover') || rule.selector.includes(':active')) {
221
256
  this.hoverSensitive = true;
222
257
  }
223
- const key = rightmostKey(rule.selector);
258
+ const focus = FOCUS.test(rule.selector);
259
+ if (focus) this.focusSensitive = true;
224
260
  if (UNSHAREABLE.test(rule.selector)) {
225
261
  if (key.kind === 'id') this.ownStyleIds.add(key.name);
226
262
  else if (key.kind === 'class') this.ownStyleClasses.add(key.name);
@@ -228,7 +264,7 @@ class RuleIndex {
228
264
  else this.ownStyleEverywhere = true;
229
265
  }
230
266
  this._bucket(key).push(indexed);
231
- if (HOVER.test(rule.selector)) {
267
+ if (focus || HOVER.test(rule.selector)) {
232
268
  this.pointer ??= new RuleIndex();
233
269
  this.pointer.size += 1;
234
270
  this.pointer._bucket(key).push(indexed);
@@ -321,8 +357,18 @@ function splitPseudoElement(
321
357
  * handed none), so a selector that tests only it never changes as the
322
358
  * pointer moves — Wikipedia's buttons' `:focus:not(:active)` among them.
323
359
  * A press that sets it would have to be counted here too.
360
+ *
361
+ * The focus is the other state a change of reaches only some elements, and
362
+ * `kind` says which is asked for: `FOCUS_STATE` finds the compounds that
363
+ * test `:focus`, `:focus-visible` or `:focus-within` the same way. Either
364
+ * kind leaves the other's pseudo-classes out of a compound as well as its
365
+ * own, so what is left matches wherever the compound could, in any state —
366
+ * `a:focus:hover` is a compound of `a`'s, for both.
324
367
  */
325
- export function pointerCompounds(selector: string): PointerCompounds {
368
+ export function pointerCompounds(
369
+ selector: string,
370
+ kind: StateKind = HOVER_STATE,
371
+ ): PointerCompounds {
326
372
  const out: PointerCompounds = {
327
373
  compounds: [],
328
374
  siblings: false,
@@ -330,10 +376,33 @@ export function pointerCompounds(selector: string): PointerCompounds {
330
376
  has: [],
331
377
  followed: [],
332
378
  };
333
- scanComplex(selector, out);
379
+ scanComplex(selector, out, kind);
334
380
  return out;
335
381
  }
336
382
 
383
+ /** A state a change of reaches only the elements some compound names: which
384
+ * selectors mention it at all, which tests it anywhere, and which of the
385
+ * pseudo-classes `STATE_PSEUDO_AT` takes out of a compound are its own. */
386
+ export interface StateKind {
387
+ mentions: RegExp;
388
+ any: RegExp;
389
+ own: RegExp;
390
+ }
391
+
392
+ /** The pointer's: `:hover`, and `:active`, which nothing sets. */
393
+ export const HOVER_STATE: StateKind = {
394
+ mentions: /:(?:hover|active)/i,
395
+ any: /:hover(?![\w-])/i,
396
+ own: /^:hover$/i,
397
+ };
398
+
399
+ /** The focus's: `:focus`, `:focus-visible` and `:focus-within`. */
400
+ export const FOCUS_STATE: StateKind = {
401
+ mentions: /:focus/i,
402
+ any: /:focus(?:-visible|-within)?(?![\w-])/i,
403
+ own: /^:focus/i,
404
+ };
405
+
337
406
  export interface PointerCompounds {
338
407
  compounds: string[];
339
408
  siblings: boolean;
@@ -422,11 +491,15 @@ function closeOf(text: string, open: number): number {
422
491
  }
423
492
 
424
493
  /** `pointerCompounds` for one complex selector, into `out`. */
425
- function scanComplex(selector: string, out: PointerCompounds): void {
494
+ function scanComplex(
495
+ selector: string,
496
+ out: PointerCompounds,
497
+ kind: StateKind,
498
+ ): void {
426
499
  for (const { text, next } of compoundsOf(selector)) {
427
500
  const compoundsFrom = out.compounds.length;
428
501
  const hasFrom = out.has.length;
429
- const found = scanCompound(text, out);
502
+ const found = scanCompound(text, out, kind);
430
503
  const sibling = next === '+' || next === '~';
431
504
  if (found.args.length) {
432
505
  out.has.push({
@@ -458,11 +531,13 @@ function scanComplex(selector: string, out: PointerCompounds): void {
458
531
  * it (`bare`), whether it tests the element's own hover (`pointer`), the
459
532
  * arguments of each `:has()` of the element's that tests it below (`args`),
460
533
  * and whether a selector nested in it named another element's (`below`),
461
- * whose compounds went into `out`.
534
+ * whose compounds went into `out`. Of the focus, likewise, for
535
+ * `FOCUS_STATE`.
462
536
  */
463
537
  function scanCompound(
464
538
  text: string,
465
539
  out: PointerCompounds,
540
+ kind: StateKind,
466
541
  ): { bare: string; pointer: boolean; below: boolean; args: string[] } {
467
542
  let bare = '';
468
543
  let pointer = false;
@@ -487,9 +562,9 @@ function scanCompound(
487
562
  else if (c === '[') bracket += 1;
488
563
  else if (c === ']') bracket = Math.max(0, bracket - 1);
489
564
  else if (c === ':' && bracket === 0 && text[i + 1] !== ':') {
490
- const m = POINTER_PSEUDO_AT.exec(text.slice(i));
565
+ const m = STATE_PSEUDO_AT.exec(text.slice(i));
491
566
  if (m) {
492
- if (m[0].length === 6) pointer = true;
567
+ if (kind.own.test(m[0])) pointer = true;
493
568
  i += m[0].length - 1;
494
569
  continue;
495
570
  }
@@ -500,7 +575,7 @@ function scanCompound(
500
575
  const arg = text.slice(open + 1, close);
501
576
  const whole = text.slice(i, close + 1);
502
577
  i = close;
503
- if (!HOVER.test(arg)) {
578
+ if (!kind.any.test(arg)) {
504
579
  bare += whole;
505
580
  continue;
506
581
  }
@@ -521,15 +596,15 @@ function scanCompound(
521
596
  const own: string[] = [];
522
597
  let any = false;
523
598
  for (const entry of selectorsOf(arg)) {
524
- if (!HOVER.test(entry)) continue;
599
+ if (!kind.any.test(entry)) continue;
525
600
  const parts = compoundsOf(entry);
526
601
  if (parts.length !== 1) {
527
- scanComplex(entry, out);
602
+ scanComplex(entry, out, kind);
528
603
  below = true;
529
604
  continue;
530
605
  }
531
606
  // of this element itself
532
- const one = scanCompound(parts[0].text, out);
607
+ const one = scanCompound(parts[0].text, out, kind);
533
608
  if (one.below) below = true;
534
609
  if (!one.pointer && !one.args.length) continue;
535
610
  if (one.pointer) pointer = true;
@@ -556,32 +631,42 @@ function scanCompound(
556
631
  const FUNCTION_AT = /^:([\w-]+)\(/;
557
632
 
558
633
  /** `:hover` anywhere in a selector. */
559
- const HOVER = /:hover(?![\w-])/i;
634
+ const HOVER = HOVER_STATE.any;
635
+
636
+ /** `:focus`, `:focus-visible` or `:focus-within` anywhere in a selector. */
637
+ const FOCUS = FOCUS_STATE.any;
560
638
 
561
- /** `:hover` or `:active` at the start of a string, and nothing longer. */
562
- const POINTER_PSEUDO_AT = /^:(?:hover|active)(?![\w-])/i;
639
+ /** A pseudo-class of a state this renderer keeps — `:hover`, `:active`,
640
+ * `:focus` and its two kin — at the start of a string, and nothing
641
+ * longer. */
642
+ const STATE_PSEUDO_AT =
643
+ /^:(?:hover|active|focus(?:-visible|-within)?)(?![\w-])/i;
563
644
 
564
- /** `:hover` or `:active` anywhere in a selector. */
565
- const POINTER_PSEUDO = /:(?:hover|active)(?![\w-])/i;
645
+ /** Any of them anywhere in a selector. */
646
+ const STATE_PSEUDO = /:(?:hover|active|focus(?:-visible|-within)?)(?![\w-])/i;
566
647
 
567
648
  /**
568
649
  * A selector compiled to a matcher over this adapter. css-select keeps the
569
650
  * answers of a `:has()` above the subject, or of the ancestors a
570
651
  * descendant combinator tried under one, for as long as the matcher lives
571
652
  * (`cacheResults`) — which is the cascade's life, and a pointer move is no
572
- * new cascade. So a selector that tests the pointer keeps none: with them,
573
- * `#box:has(a:hover) .m` answered what it did before the first move, for
574
- * good.
653
+ * new cascade. So a selector that tests the pointer or the focus keeps
654
+ * none: with them, `#box:has(a:hover) .m` answered what it did before the
655
+ * first move, for good.
575
656
  */
576
657
  function compileSelector(
577
658
  selector: string,
578
659
  adapter: CssSelectAdapter,
660
+ pseudos: typeof PSEUDOS = PSEUDOS,
661
+ /** Whether what it answers may be kept: not where the adapter's tree
662
+ * moves under it, as a `<use>`'s copy's does (`Cascade.shapeStyles`). */
663
+ cache = true,
579
664
  ): (el: Element) => boolean {
580
665
  return compile(noEmptyWords(selector), {
581
666
  adapter,
582
667
  xmlMode: false,
583
- pseudos: PSEUDOS,
584
- cacheResults: !POINTER_PSEUDO.test(selector),
668
+ pseudos,
669
+ cacheResults: cache && !STATE_PSEUDO.test(selector),
585
670
  } as unknown as Parameters<typeof compile>[1]) as unknown as (
586
671
  node: Element,
587
672
  ) => boolean;
@@ -641,7 +726,14 @@ function rightmostKey(selector: string): {
641
726
  start = i + 1;
642
727
  }
643
728
  }
644
- const compound = selector.slice(start);
729
+ return compoundKey(selector.slice(start));
730
+ }
731
+
732
+ /** A compound selector's most selective key (`rightmostKey`). */
733
+ function compoundKey(compound: string): {
734
+ kind: 'id' | 'class' | 'tag' | 'any';
735
+ name: string;
736
+ } {
645
737
  let id: string | null = null;
646
738
  let cls: string | null = null;
647
739
  let tag: string | null = null;
@@ -675,6 +767,74 @@ function rightmostKey(selector: string): {
675
767
  return { kind: 'any', name: '' };
676
768
  }
677
769
 
770
+ /**
771
+ * The ids and classes a selector asks of its subject's ancestors: for each
772
+ * compound a descendant or a child combinator follows, its id, or else its
773
+ * first class, as `#name` or `.name`. Such a compound is an ancestor of
774
+ * the subject — one a sibling combinator follows is a sibling of one — so
775
+ * an element with no ancestor of some such name is not the rule's, which
776
+ * is known without matching it. A compound with neither asks nothing.
777
+ */
778
+ function ancestorKeys(selector: string): string[] {
779
+ const keys: string[] = [];
780
+ const compounds = compoundsOf(selector);
781
+ for (let i = 0; i < compounds.length - 1; i += 1) {
782
+ const { text, next } = compounds[i];
783
+ if (next !== ' ' && next !== '>') continue;
784
+ const key = compoundKey(text);
785
+ if (key.kind === 'id') keys.push(`#${key.name}`);
786
+ else if (key.kind === 'class') keys.push(`.${key.name}`);
787
+ }
788
+ return keys;
789
+ }
790
+
791
+ /** A drawing none of whose own elements a rule reaches. */
792
+ const NO_SHAPES: ReadonlyMap<Element, ShapeStyle> = new Map();
793
+
794
+ /** What the rules matched in the copy a `<use>` makes of an element: each
795
+ * element they reach, with its place in the copy and its rules in cascade
796
+ * order; how many elements the copy has; and the `<use>`s in it. */
797
+ interface CopyMatch {
798
+ rules: { el: Element; at: number; candidates: Candidate[] }[];
799
+ size: number;
800
+ uses: Element[];
801
+ }
802
+
803
+ /**
804
+ * What `Cascade.shapeStyles` keeps for the drawings of one build of the
805
+ * boxes (`BoxTree.shapeCopies`): the document's elements by their ids, and
806
+ * what the rules matched in the copy of each element a `<use>` names —
807
+ * the same whichever `<use>` makes it, so a page of hundreds of icons from
808
+ * one sprite matches each symbol once — and what that comes to for a
809
+ * drawing of each style. A build is made again whenever the document, its
810
+ * style sheets or a viewport a value reads change, and a pointer move
811
+ * changes nothing a copy is matched by: nothing in one is under the
812
+ * pointer, and a drawing whose style it changes has another style.
813
+ */
814
+ export class ShapeCopies {
815
+ /** @internal */
816
+ readonly matched = new Map<Element, CopyMatch>();
817
+ /** @internal What each copy comes to for a drawing of a style, and the
818
+ * key that names it. */
819
+ readonly resolved = new WeakMap<
820
+ ComputedStyle,
821
+ Map<Element, { into: Map<Element, ShapeStyle> | null; key: string }>
822
+ >();
823
+ constructor(readonly byId: (id: string) => Element | null) {}
824
+ }
825
+
826
+ /** What the rules asked of every element of a drawing, or of every one of
827
+ * a type, ask of its ancestors (`Cascade.shapeStyles`). */
828
+ interface ShapeNeeds {
829
+ /** The classes and the ids some such rule names in an ancestor. */
830
+ classes: Set<string>;
831
+ ids: Set<string>;
832
+ /** Whether a rule asked of every element asks nothing of its ancestors,
833
+ * and the types with such a rule. */
834
+ universal: boolean;
835
+ tags: Set<string>;
836
+ }
837
+
678
838
  function balancedEnd(
679
839
  text: string,
680
840
  from: number,
@@ -707,6 +867,138 @@ export interface PointerState {
707
867
 
708
868
  const NO_POINTER: PointerState = { hovered: new Set(), active: new Set() };
709
869
 
870
+ /** Where the focus is, for `:focus` and its kin: the element whose widget
871
+ * holds it, whether that shows (`:focus-visible`), and the element with
872
+ * every element around it (`:focus-within`). No element of a document
873
+ * takes the focus itself; a control's widget, mounted beside it, does. */
874
+ export interface FocusState {
875
+ element: Element | null;
876
+ visible: boolean;
877
+ within: ReadonlySet<Element>;
878
+ }
879
+
880
+ export const NO_FOCUS: FocusState = {
881
+ element: null,
882
+ visible: false,
883
+ within: new Set(),
884
+ };
885
+
886
+ /**
887
+ * What the selectors that test one state — the pointer, or the focus — say
888
+ * of the elements a change of it can restyle: the compounds that test it,
889
+ * each without it and with whether a sibling combinator follows it, and
890
+ * the anchors of the `:has()`s that test it (`pointerCompounds`).
891
+ */
892
+ class StateRules {
893
+ /** The compounds, each without the state, and their matchers, compiled
894
+ * when a change first asks (`touches`). */
895
+ private _compounds = new Map<string, boolean>();
896
+ private _matchers:
897
+ { match: (el: Element) => boolean; followed: boolean }[] | null = null;
898
+ /** Whether a change can be restyled where it happened
899
+ * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
900
+ * state inside a functional pseudo-class, which reaches elements no
901
+ * compound names. */
902
+ local = true;
903
+ /** The compounds whose `:has()` tests the state, each without it,
904
+ * whether its argument starts at a sibling, and whether a sibling
905
+ * combinator follows it (`anchors`). */
906
+ private _anchors = new Map<
907
+ string,
908
+ { siblings: boolean; followed: boolean }
909
+ >();
910
+ private _anchorMatchers:
911
+ | {
912
+ match: (el: Element) => boolean;
913
+ siblings: boolean;
914
+ followed: boolean;
915
+ }[]
916
+ | null = null;
917
+
918
+ constructor(private readonly _kind: StateKind) {}
919
+
920
+ note(selector: string): void {
921
+ if (!this._kind.mentions.test(selector)) return;
922
+ const found = pointerCompounds(selector, this._kind);
923
+ if (found.nested) this.local = false;
924
+ const followed = new Set(found.followed);
925
+ for (const c of found.compounds) {
926
+ this._compounds.set(
927
+ c,
928
+ followed.has(c) || (this._compounds.get(c) ?? false),
929
+ );
930
+ }
931
+ for (const { anchor, siblings } of found.has) {
932
+ const was = this._anchors.get(anchor);
933
+ this._anchors.set(anchor, {
934
+ siblings: siblings || (was?.siblings ?? false),
935
+ followed: followed.has(anchor) || (was?.followed ?? false),
936
+ });
937
+ }
938
+ }
939
+
940
+ /** `Cascade.hoverTouches`, for this state. */
941
+ touches(
942
+ el: Element,
943
+ compile: (selector: string) => ((el: Element) => boolean) | null,
944
+ ): HoverTouch {
945
+ // the followed ones first, so the first that matches is the answer
946
+ this._matchers ??= [...this._compounds]
947
+ .sort(([, a], [, b]) => Number(b) - Number(a))
948
+ .flatMap(([compound, followed]) => {
949
+ const match = compile(compound);
950
+ return match ? [{ match, followed }] : [];
951
+ });
952
+ for (const { match, followed } of this._matchers) {
953
+ if (match(el)) return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
954
+ }
955
+ return HOVER_UNTOUCHED;
956
+ }
957
+
958
+ /** `Cascade.hoverAnchors`, for this state. */
959
+ anchors(
960
+ el: Element,
961
+ into: Map<Element, HoverTouch>,
962
+ compile: (selector: string) => ((el: Element) => boolean) | null,
963
+ ): void {
964
+ if (!this._anchors.size) return;
965
+ this._anchorMatchers ??= [...this._anchors]
966
+ .sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
967
+ .flatMap(([anchor, { siblings, followed }]) => {
968
+ const match = compile(anchor);
969
+ return match ? [{ match, siblings, followed }] : [];
970
+ });
971
+ const note = (at: Element, followed: boolean): void => {
972
+ if (followed) into.set(at, HOVER_FOLLOWED);
973
+ else if (!into.has(at)) into.set(at, HOVER_TOUCHED);
974
+ };
975
+ const siblings = this._anchorMatchers.some((m) => m.siblings);
976
+ for (let at: Element | null = el; at;) {
977
+ if (at !== el) {
978
+ for (const { match, followed } of this._anchorMatchers) {
979
+ if (match(at)) {
980
+ note(at, followed);
981
+ break;
982
+ }
983
+ }
984
+ }
985
+ if (siblings) {
986
+ for (let s = at.prev; s; s = s.prev) {
987
+ if (!isTag(s as Element)) continue;
988
+ for (const m of this._anchorMatchers) {
989
+ if (m.siblings && m.match(s as Element)) {
990
+ note(s as Element, m.followed);
991
+ break;
992
+ }
993
+ }
994
+ }
995
+ }
996
+ const parent: Element['parent'] = at.parent;
997
+ at = parent && isTag(parent as Element) ? (parent as Element) : null;
998
+ }
999
+ }
1000
+ }
1001
+
710
1002
  /**
711
1003
  * The face a style's font-relative units are measured in: its family list,
712
1004
  * size, weight and slant, the four that pick one. A family's faces can be
@@ -763,33 +1055,31 @@ export class Cascade {
763
1055
  marker: new RuleIndex(),
764
1056
  selection: new RuleIndex(),
765
1057
  };
1058
+ /** The author's rules that could style a shape in a drawing: the ones
1059
+ * that declare a property a shape has (`shapes.ts`) and whose subject
1060
+ * is no element of HTML's alone. They are in `_index` too, for the
1061
+ * elements that have boxes; these are asked of the ones in an inline
1062
+ * `<svg>`, which have none (`shapeStyles`). */
1063
+ private _shapes = new RuleIndex();
1064
+ private _shapeNeeds: ShapeNeeds | null = null;
766
1065
  private _adapter: CssSelectAdapter;
767
- /** The compounds of the selectors that test the pointer, each without
768
- * its `:hover` or `:active` (`pointerCompounds`), and their matchers,
769
- * compiled when a pointer move first asks (`hoverTouches`). */
770
- private _hoverCompounds = new Map<string, boolean>();
771
- private _hoverMatchers:
772
- { match: (el: Element) => boolean; followed: boolean }[] | null = null;
773
- /** Whether a pointer move can be restyled where it happened
774
- * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
775
- * pointer inside a functional pseudo-class, which reaches elements no
776
- * compound names. */
777
- hoverLocal = true;
778
- /** The compounds whose `:has()` tests the pointer, each without it,
779
- * whether its argument starts at a sibling, and whether a sibling
780
- * combinator follows it (`hoverAnchors`). */
781
- private _hoverAnchors = new Map<
782
- string,
783
- { siblings: boolean; followed: boolean }
784
- >();
785
- private _anchorMatchers:
786
- | {
787
- match: (el: Element) => boolean;
788
- siblings: boolean;
789
- followed: boolean;
790
- }[]
791
- | null = null;
1066
+ /**
1067
+ * The tree a `<use>`'s copy is in, for `_copyAdapter`: the element it is
1068
+ * a copy of, which is its top (`ShapeStyles.used`). A copy is its own
1069
+ * tree, a shadow tree of the `<use>`'s (SVG 2, 5.5.1), so a selector is
1070
+ * matched against its original with nothing above that element and
1071
+ * nothing beside it — the top of a copy is its tree's first child and
1072
+ * its last, whichever of its parent's it is. And nothing in it is under
1073
+ * the pointer, as nothing in a drawing is: it has no box to be.
1074
+ */
1075
+ private _copyTop: Element | null = null;
1076
+ private _copyAdapter: CssSelectAdapter;
1077
+ /** What the selectors that test the pointer, and the ones that test the
1078
+ * focus, say of the elements a change of it reaches. */
1079
+ private _hoverRules = new StateRules(HOVER_STATE);
1080
+ private _focusRules = new StateRules(FOCUS_STATE);
792
1081
  private _pointer: PointerState = NO_POINTER;
1082
+ private _focus: FocusState = NO_FOCUS;
793
1083
  readonly initial: ComputedStyle;
794
1084
  readonly look: RootLook;
795
1085
  /** Viewport width the media queries were evaluated at, in device pixels
@@ -837,6 +1127,18 @@ export class Cascade {
837
1127
  /** Whether any declaration has a length in `lh` or `rlh`: only then is
838
1128
  * the line height settled ahead of the declarations that read it. */
839
1129
  private _lh = false;
1130
+ /** The pseudo-classes css-select is handed: `:root` this cascade's, and
1131
+ * the focus's three answered from `_focus`. */
1132
+ private _pseudos: typeof PSEUDOS;
1133
+ /** The colour schemes the page says it supports with a `<meta
1134
+ * name="color-scheme">` (HTML 4.2.5.4), which its root element takes
1135
+ * as it takes a presentational attribute: under any rule that sets its
1136
+ * `color-scheme`. Set by the box builder from the document. */
1137
+ pageColorScheme: string | null = null;
1138
+ /** Each system colour as the `light-dark()` it is: the palette's where
1139
+ * the element's scheme is the palette's, and Chrome's in an SVG image,
1140
+ * which has none. */
1141
+ private _systemColors: Map<string, string>;
840
1142
 
841
1143
  constructor(
842
1144
  sheets: Stylesheet[],
@@ -848,7 +1150,26 @@ export class Cascade {
848
1150
  zeroWidth: FaceMetric | null = null,
849
1151
  normalLine: FaceMetric | null = null,
850
1152
  families: FontFamilies | null = null,
1153
+ /** The document's element where it is not an `<html>`: an SVG image's
1154
+ * `<svg>`, which is `:root` there (Selectors 4, 14.1). */
1155
+ documentElement: Element | null = null,
1156
+ /** The element the document's URL names by its fragment, which is
1157
+ * `:target` (Selectors 4, 9.1): an SVG image's, `image.svg#icon`. */
1158
+ target: Element | null = null,
851
1159
  ) {
1160
+ // css-select has none of the focus's three, and a pseudo-class is
1161
+ // handed only the element, so they are this cascade's own closures
1162
+ this._pseudos = {
1163
+ ...PSEUDOS,
1164
+ ...(documentElement && {
1165
+ root: (el: Element) => el === documentElement,
1166
+ }),
1167
+ ...(target && { target: (el: Element) => el === target }),
1168
+ focus: (el: Element) => this._focus.element === el,
1169
+ 'focus-visible': (el: Element) =>
1170
+ this._focus.visible && this._focus.element === el,
1171
+ 'focus-within': (el: Element) => this._focus.within.has(el),
1172
+ };
852
1173
  this._xHeightOf = xHeight;
853
1174
  this._zeroWidthOf = zeroWidth;
854
1175
  this._normalLineOf = normalLine;
@@ -857,6 +1178,7 @@ export class Cascade {
857
1178
  ? (list: string) => families.map(list)
858
1179
  : undefined;
859
1180
  this.look = look;
1181
+ this._systemColors = systemColorTable(documentElement ? null : look);
860
1182
  this.initial = initialStyle(look, scale);
861
1183
  this.viewportWidth = viewportWidth;
862
1184
  this.viewportHeight = viewportHeight;
@@ -875,8 +1197,20 @@ export class Cascade {
875
1197
  if (!this._lh && usesLh(rule.declarations)) this._lh = true;
876
1198
  const pseudo = splitPseudoElement(rule);
877
1199
  if (pseudo) this._pseudo[pseudo.which].add(pseudo.rule);
878
- else this._index.add(rule);
879
- this._noteHover((pseudo?.rule ?? rule).selector);
1200
+ else {
1201
+ const key = rightmostKey(rule.selector);
1202
+ this._index.add(rule, key);
1203
+ if (
1204
+ rule.order >= 0 &&
1205
+ (key.kind !== 'tag' || SHAPE_TAGS.has(key.name)) &&
1206
+ declaresShape(rule.declarations)
1207
+ ) {
1208
+ this._shapes.add(rule, key);
1209
+ }
1210
+ }
1211
+ const selector = (pseudo?.rule ?? rule).selector;
1212
+ this._hoverRules.note(selector);
1213
+ this._focusRules.note(selector);
880
1214
  }
881
1215
  for (const bp of sheet.breakpoints) breakpoints.add(bp);
882
1216
  // a query on the viewport's height reads it as a `vh` does
@@ -904,6 +1238,16 @@ export class Cascade {
904
1238
  isActive: (el: Element) => this._pointer.active.has(el),
905
1239
  isVisited: () => false,
906
1240
  };
1241
+ this._copyAdapter = {
1242
+ ...this._adapter,
1243
+ getParent: (el) => (el === this._copyTop ? null : DomUtils.getParent(el)),
1244
+ getSiblings: (el) =>
1245
+ el === this._copyTop ? [el] : DomUtils.getSiblings(el),
1246
+ prevElementSibling: (el) =>
1247
+ el === this._copyTop ? null : DomUtils.prevElementSibling(el),
1248
+ isHovered: () => false,
1249
+ isActive: () => false,
1250
+ };
907
1251
  }
908
1252
 
909
1253
  /** Whether declarations read the viewport's width or height (`vw`,
@@ -936,6 +1280,36 @@ export class Cascade {
936
1280
  this._pointer = pointer;
937
1281
  }
938
1282
 
1283
+ /** Whether a widget taking the focus or giving it up can change what this
1284
+ * cascade produces. */
1285
+ get focusSensitive(): boolean {
1286
+ return (
1287
+ this._index.focusSensitive ||
1288
+ this._pseudo.before.focusSensitive ||
1289
+ this._pseudo.after.focusSensitive ||
1290
+ this._pseudo['first-letter'].focusSensitive ||
1291
+ this._pseudo['first-line'].focusSensitive ||
1292
+ this._pseudo.marker.focusSensitive
1293
+ );
1294
+ }
1295
+
1296
+ setFocus(focus: FocusState): void {
1297
+ this._focus = focus;
1298
+ }
1299
+
1300
+ /** Whether a pointer move can be restyled where it happened
1301
+ * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
1302
+ * pointer inside a functional pseudo-class, which reaches elements no
1303
+ * compound names. */
1304
+ get hoverLocal(): boolean {
1305
+ return this._hoverRules.local;
1306
+ }
1307
+
1308
+ /** …and a focus change. */
1309
+ get focusLocal(): boolean {
1310
+ return this._focusRules.local;
1311
+ }
1312
+
939
1313
  /**
940
1314
  * Which pseudo-elements' rules test the pointer: none's
941
1315
  * (`HOVER_PSEUDO_NONE`); a `::before`'s or an `::after`'s only
@@ -945,15 +1319,23 @@ export class Cascade {
945
1319
  * move cannot.
946
1320
  */
947
1321
  get hoverPseudo(): 0 | 1 | 2 {
1322
+ return this._pseudoRules('hoverSensitive');
1323
+ }
1324
+
1325
+ /** …and which test the focus. */
1326
+ get focusPseudo(): 0 | 1 | 2 {
1327
+ return this._pseudoRules('focusSensitive');
1328
+ }
1329
+
1330
+ private _pseudoRules(state: 'hoverSensitive' | 'focusSensitive'): 0 | 1 | 2 {
948
1331
  if (
949
- this._pseudo['first-letter'].hoverSensitive ||
950
- this._pseudo['first-line'].hoverSensitive ||
951
- this._pseudo.marker.hoverSensitive
1332
+ this._pseudo['first-letter'][state] ||
1333
+ this._pseudo['first-line'][state] ||
1334
+ this._pseudo.marker[state]
952
1335
  ) {
953
1336
  return HOVER_PSEUDO_OTHER;
954
1337
  }
955
- return this._pseudo.before.hoverSensitive ||
956
- this._pseudo.after.hoverSensitive
1338
+ return this._pseudo.before[state] || this._pseudo.after[state]
957
1339
  ? HOVER_PSEUDO_GENERATED
958
1340
  : HOVER_PSEUDO_NONE;
959
1341
  }
@@ -969,17 +1351,13 @@ export class Cascade {
969
1351
  * table restyle the rows after it.
970
1352
  */
971
1353
  hoverTouches(el: Element): HoverTouch {
972
- // the followed ones first, so the first that matches is the answer
973
- this._hoverMatchers ??= [...this._hoverCompounds]
974
- .sort(([, a], [, b]) => Number(b) - Number(a))
975
- .flatMap(([compound, followed]) => {
976
- const match = this._compile(compound);
977
- return match ? [{ match, followed }] : [];
978
- });
979
- for (const { match, followed } of this._hoverMatchers) {
980
- if (match(el)) return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
981
- }
982
- return HOVER_UNTOUCHED;
1354
+ return this._hoverRules.touches(el, (selector) => this._compile(selector));
1355
+ }
1356
+
1357
+ /** `hoverTouches` for the focus: whether `el` taking the focus or giving
1358
+ * it up, or its `:focus-within` changing, can change a style. */
1359
+ focusTouches(el: Element): HoverTouch {
1360
+ return this._focusRules.touches(el, (selector) => this._compile(selector));
983
1361
  }
984
1362
 
985
1363
  /**
@@ -991,68 +1369,19 @@ export class Cascade {
991
1369
  * siblings can change with it (`hoverTouches`).
992
1370
  */
993
1371
  hoverAnchors(el: Element, into: Map<Element, HoverTouch>): void {
994
- if (!this._hoverAnchors.size) return;
995
- this._anchorMatchers ??= [...this._hoverAnchors]
996
- .sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
997
- .flatMap(([anchor, { siblings, followed }]) => {
998
- const match = this._compile(anchor);
999
- return match ? [{ match, siblings, followed }] : [];
1000
- });
1001
- const note = (at: Element, followed: boolean): void => {
1002
- if (followed) into.set(at, HOVER_FOLLOWED);
1003
- else if (!into.has(at)) into.set(at, HOVER_TOUCHED);
1004
- };
1005
- const siblings = this._anchorMatchers.some((m) => m.siblings);
1006
- for (let at: Element | null = el; at;) {
1007
- if (at !== el) {
1008
- for (const { match, followed } of this._anchorMatchers) {
1009
- if (match(at)) {
1010
- note(at, followed);
1011
- break;
1012
- }
1013
- }
1014
- }
1015
- if (siblings) {
1016
- for (let s = at.prev; s; s = s.prev) {
1017
- if (!isTag(s as Element)) continue;
1018
- for (const m of this._anchorMatchers) {
1019
- if (m.siblings && m.match(s as Element)) {
1020
- note(s as Element, m.followed);
1021
- break;
1022
- }
1023
- }
1024
- }
1025
- }
1026
- const parent: Element['parent'] = at.parent;
1027
- at = parent && isTag(parent as Element) ? (parent as Element) : null;
1028
- }
1372
+ this._hoverRules.anchors(el, into, (selector) => this._compile(selector));
1029
1373
  }
1030
1374
 
1031
- private _noteHover(selector: string): void {
1032
- if (!/:(?:hover|active)/i.test(selector)) return;
1033
- const found = pointerCompounds(selector);
1034
- if (found.nested) this.hoverLocal = false;
1035
- const followed = new Set(found.followed);
1036
- for (const c of found.compounds) {
1037
- this._hoverCompounds.set(
1038
- c,
1039
- followed.has(c) || (this._hoverCompounds.get(c) ?? false),
1040
- );
1041
- }
1042
- for (const { anchor, siblings } of found.has) {
1043
- const was = this._hoverAnchors.get(anchor);
1044
- this._hoverAnchors.set(anchor, {
1045
- siblings: siblings || (was?.siblings ?? false),
1046
- followed: followed.has(anchor) || (was?.followed ?? false),
1047
- });
1048
- }
1375
+ /** `hoverAnchors` for the focus: `form:has(input:focus)`'s form. */
1376
+ focusAnchors(el: Element, into: Map<Element, HoverTouch>): void {
1377
+ this._focusRules.anchors(el, into, (selector) => this._compile(selector));
1049
1378
  }
1050
1379
 
1051
1380
  /** A selector compiled as a rule's is, or null where css-select refuses
1052
1381
  * it, as a rule it refuses drops out of the cascade. */
1053
1382
  private _compile(selector: string): ((el: Element) => boolean) | null {
1054
1383
  try {
1055
- return compileSelector(selector, this._adapter);
1384
+ return compileSelector(selector, this._adapter, this._pseudos);
1056
1385
  } catch {
1057
1386
  return null;
1058
1387
  }
@@ -1221,6 +1550,11 @@ export class Cascade {
1221
1550
  if (this._pointer.hovered.has(el)) key += '\u0001:hover';
1222
1551
  if (this._pointer.active.has(el)) key += '\u0001:active';
1223
1552
  }
1553
+ // `:focus-within` is not shared under (`UNSHAREABLE`), and the focus
1554
+ // is on one element
1555
+ if (this._focus.element === el && this._index.focusSensitive) {
1556
+ key += this._focus.visible ? '\u0001:focus-visible' : '\u0001:focus';
1557
+ }
1224
1558
  const attribs = el.attribs;
1225
1559
  for (const name in attribs) {
1226
1560
  const value = attribs[name];
@@ -1543,6 +1877,8 @@ export class Cascade {
1543
1877
  families: this._mapFamilies,
1544
1878
  lh: () => this._lineHeightOf(parentStyle),
1545
1879
  rlh: () => this._lineHeightOf(rootStyle),
1880
+ focusRing: this.look.focusRing,
1881
+ systemColors: this._systemColors,
1546
1882
  };
1547
1883
  // The family, the weight and the slant go with the size: together they
1548
1884
  // pick the face an `ex`, a `ch` or an `lh` in any declaration is
@@ -1658,7 +1994,9 @@ export class Cascade {
1658
1994
  style.alignBlocks = null;
1659
1995
  }
1660
1996
  settleClamp(style, parentStyle);
1997
+ settleAlign(style, parentStyle);
1661
1998
  settleOverflow(style);
1999
+ settleOutline(style, this.look, this.scale);
1662
2000
  settleContentVisibility(style);
1663
2001
  blockify(style, inFlexContainer);
1664
2002
  // a button is laid out as one, whatever `display` it was given
@@ -1743,6 +2081,306 @@ export class Cascade {
1743
2081
  return { style: asRoot(this.styleFor(synthetic, parent, false)), html };
1744
2082
  }
1745
2083
 
2084
+ /** Whether any rule could style a shape in a drawing: a document with
2085
+ * none asks nothing of its drawings (`shapeStyles`). */
2086
+ get stylesShapes(): boolean {
2087
+ return this._shapes.size > 0;
2088
+ }
2089
+
2090
+ /** Whether a pointer move can change what a rule gives a shape in a
2091
+ * drawing. */
2092
+ get shapesFollowPointer(): boolean {
2093
+ return this._shapes.hoverSensitive;
2094
+ }
2095
+
2096
+ /**
2097
+ * What the rules give the elements of an inline drawing, `root` and what
2098
+ * is in it, or null where none reaches one: the narrow cascade of
2099
+ * `shapes.ts`. `style` is the root's computed style — its `color`, its
2100
+ * custom properties and its font size are what a value in the drawing is
2101
+ * read against, as though every element in it had inherited them, which
2102
+ * all but one a rule gives its own has.
2103
+ *
2104
+ * An element's rules and its `style` attribute are put in cascade order,
2105
+ * and each property is the last valid declaration's. The presentation
2106
+ * attributes are below them all (SVG 2, 6.2), and `SvgView` reads those
2107
+ * itself. The root's box has its paint, its opacity and whether it is
2108
+ * shown from its computed style, so those are left out for it.
2109
+ *
2110
+ * What a `<use>` draws is a copy, styled in a tree of its own
2111
+ * (`ShapeStyles.used`): the element it names is looked for as `copyTree`
2112
+ * looks — the drawing's own first, then the document's — and what is in
2113
+ * it asked of where it stands in that tree, once a build for all the
2114
+ * `<use>`s that name it (`ShapeCopies`). An icon sprite's `<symbol>`,
2115
+ * outside the drawing in a hidden `<svg>` of them, is styled that way,
2116
+ * and so is one of the drawing's own.
2117
+ *
2118
+ * Asked as a drawing is first painted (`BoxTree.shapeStyler`), of the
2119
+ * rules in `_shapes` alone: a page of hundreds of icons pays for the ones
2120
+ * it shows, a look in the buckets for each of their elements where no
2121
+ * rule reaches them, and a document with no such rule nothing.
2122
+ */
2123
+ shapeStyles(
2124
+ root: Element,
2125
+ style: ComputedStyle,
2126
+ copies: ShapeCopies | null = null,
2127
+ ): ShapeStyles | null {
2128
+ const index = this._shapes;
2129
+ if (!index.size) return null;
2130
+ // The rules kept by a class or an id are asked of the elements with
2131
+ // that name. The rest — `.dark .logo path`, `.menu [aria-hidden]` — are
2132
+ // asked of every `<path>`, or of every element, and nearly all of them
2133
+ // are for some other part of the page: each says so by the class or the
2134
+ // id it asks of an ancestor, which is looked for once for the drawing,
2135
+ // around it and in it, where matching the rule would look for it from
2136
+ // each of its elements. On a page with none of them around a drawing,
2137
+ // nothing is matched at all. A copy's tree has nothing around it.
2138
+ const needs = (this._shapeNeeds ??= this._needsOfShapes());
2139
+ let found = new Set<string>();
2140
+ const asks = needs.classes.size > 0 || needs.ids.size > 0;
2141
+ const note = (el: Element): void => {
2142
+ if (needs.ids.size) {
2143
+ const id = attr(el, 'id');
2144
+ if (id && needs.ids.has(id)) found.add(`#${id}`);
2145
+ }
2146
+ if (needs.classes.size) {
2147
+ const className = attr(el, 'class');
2148
+ if (className) {
2149
+ for (const name of className.split(/\s+/)) {
2150
+ if (needs.classes.has(name)) found.add(`.${name}`);
2151
+ }
2152
+ }
2153
+ }
2154
+ };
2155
+ if (asks) {
2156
+ let at = root.parent;
2157
+ while (at && at.type === 'tag') {
2158
+ note(at as Element);
2159
+ at = at.parent;
2160
+ }
2161
+ }
2162
+ const live = (indexed: IndexedRule): boolean => {
2163
+ const keys = indexed.needs;
2164
+ if (keys) for (const key of keys) if (!found.has(key)) return false;
2165
+ return true;
2166
+ };
2167
+ // The root's box has its paint from its computed style, so most of the
2168
+ // rules that name a drawing by its class — an icon set's — have
2169
+ // nothing else to say of it.
2170
+ const liveForRoot = (indexed: IndexedRule): boolean =>
2171
+ (indexed.root ??= declaresRootShape(indexed.rule.declarations)) &&
2172
+ live(indexed);
2173
+
2174
+ /** The elements under `top` and it, in document order, the top first:
2175
+ * its ancestors are noted before an element is asked of. Each with
2176
+ * the rules it matched, in cascade order, where it matched any. */
2177
+ const walk = (
2178
+ top: Element,
2179
+ inCopy: boolean,
2180
+ each: (el: Element, candidates: Candidate[] | null) => void,
2181
+ ): void => {
2182
+ const stack: Element[] = [top];
2183
+ while (stack.length) {
2184
+ const el = stack.pop()!;
2185
+ // what is in a `<foreignObject>` is not the drawing's to draw
2186
+ if (el.name !== 'foreignobject' && el.name !== 'foreignObject') {
2187
+ const children = el.children;
2188
+ for (let i = children.length - 1; i >= 0; i -= 1) {
2189
+ const child = children[i];
2190
+ if (child.type === 'tag') stack.push(child as Element);
2191
+ }
2192
+ }
2193
+ if (asks) note(el);
2194
+ const tag = tagOf(el);
2195
+ const asked = found.size
2196
+ ? index.universal.length > 0 || index.byTag.has(tag)
2197
+ : needs.universal || needs.tags.has(tag);
2198
+ if (!asked && !index.names(el)) {
2199
+ each(el, null);
2200
+ continue;
2201
+ }
2202
+ const candidates: Candidate[] = [];
2203
+ this._matchInto(
2204
+ index,
2205
+ el,
2206
+ candidates,
2207
+ undefined,
2208
+ el === root && !inCopy ? liveForRoot : live,
2209
+ inCopy,
2210
+ );
2211
+ if (candidates.length) {
2212
+ const inline = attr(el, 'style');
2213
+ if (inline) pushInlineShapes(candidates, inline);
2214
+ candidates.sort(byCascade);
2215
+ }
2216
+ each(el, candidates.length ? candidates : null);
2217
+ }
2218
+ };
2219
+
2220
+ let ctx: ShapeContext | null = null;
2221
+ /** What an element's rules come to, against the drawing's style. */
2222
+ const resolve = (
2223
+ candidates: Candidate[],
2224
+ isRoot: boolean,
2225
+ ): Record<string, string> | null => {
2226
+ ctx ??= {
2227
+ color: style.color,
2228
+ scheme: style.colorScheme,
2229
+ units: {
2230
+ em: style.fontSize,
2231
+ rem: (this._root ?? this.initial).fontSize,
2232
+ vw: this.viewportWidth,
2233
+ vh: this.viewportHeight,
2234
+ scale: this.scale,
2235
+ initial: this.initial,
2236
+ systemColors: this._systemColors,
2237
+ },
2238
+ };
2239
+ let own: Record<string, string> | null = null;
2240
+ for (const c of candidates) {
2241
+ for (const d of pick(c)) {
2242
+ if (!isShapeProp(d.prop)) continue;
2243
+ if (isRoot && ROOT_BOX_PROPS.has(d.prop)) continue;
2244
+ // a `var()` with nothing to stand for it is invalid at
2245
+ // computed-value time, and the property as though `unset`
2246
+ const value = shapeValue(
2247
+ d.prop,
2248
+ d.vars ? (substituteIn(d.value, style.custom) ?? 'unset') : d.value,
2249
+ ctx,
2250
+ );
2251
+ if (value !== null) (own ??= {})[d.prop] = value;
2252
+ }
2253
+ }
2254
+ return own;
2255
+ };
2256
+ // an element's place in the walks is what the key names it by
2257
+ let key = '';
2258
+ let place = -1;
2259
+ const named = (own: Record<string, string>, at: number): string => {
2260
+ let part = `${at}{`;
2261
+ for (const prop in own) part += `${prop}:${own[prop]};`;
2262
+ return `${part}}`;
2263
+ };
2264
+
2265
+ // (made as they are first needed: most drawings need none of them)
2266
+ let of = null as Map<Element, ShapeStyle> | null;
2267
+ // the `<use>`s met, and the drawing's own elements by their ids, which
2268
+ // one looks in first
2269
+ let uses = null as Element[] | null;
2270
+ let ids = null as Map<string, Element> | null;
2271
+ walk(root, false, (el, candidates) => {
2272
+ place += 1;
2273
+ if (isUse(el)) (uses ??= []).push(el);
2274
+ if (el !== root) {
2275
+ const id = el.attribs.id;
2276
+ if (id && !ids?.has(id)) (ids ??= new Map()).set(id, el);
2277
+ }
2278
+ const own = candidates && resolve(candidates, el === root);
2279
+ if (!own) return;
2280
+ (of ??= new Map()).set(el, own);
2281
+ key += named(own, place);
2282
+ });
2283
+ if (!uses) return of ? { of, used: null, key } : null;
2284
+
2285
+ // Each element a `<use>` names, in the tree of its copy: once however
2286
+ // many name it, and one a copy's `<use>` names in turn, however deep —
2287
+ // `copyTree` stops at a depth, and a `<use>` that names what it is in
2288
+ // finds it being walked. What is matched there is the same for every
2289
+ // drawing, and kept for the build (`ShapeCopies`); what it comes to is
2290
+ // the drawing's, since every property the copy inherits is the
2291
+ // `<use>`'s, and its custom properties and its colour are the
2292
+ // drawing's, as they are for the rest of it — kept too, for each
2293
+ // style a drawing has, which the icons of a page mostly share.
2294
+ const byId = copies?.byId ?? ((id: string) => elementById(root, id));
2295
+ const matched = copies?.matched ?? new Map<Element, CopyMatch>();
2296
+ let styled = copies?.resolved.get(style);
2297
+ if (!styled) {
2298
+ styled = new Map();
2299
+ copies?.resolved.set(style, styled);
2300
+ }
2301
+ const match = (target: Element): CopyMatch => {
2302
+ let copy = matched.get(target);
2303
+ if (copy) return copy;
2304
+ const rules: CopyMatch['rules'] = [];
2305
+ const inner: Element[] = [];
2306
+ let size = 0;
2307
+ found = new Set();
2308
+ this._copyTop = target;
2309
+ try {
2310
+ walk(target, true, (el, candidates) => {
2311
+ if (isUse(el)) inner.push(el);
2312
+ if (candidates) rules.push({ el, at: size, candidates });
2313
+ size += 1;
2314
+ });
2315
+ } finally {
2316
+ this._copyTop = null;
2317
+ }
2318
+ copy = { rules, size, uses: inner };
2319
+ matched.set(target, copy);
2320
+ return copy;
2321
+ };
2322
+ let used: Map<Element, ReadonlyMap<Element, ShapeStyle>> | null = null;
2323
+ let made: Set<Element> | null = null;
2324
+ for (let i = 0; i < uses.length; i += 1) {
2325
+ const use = uses[i];
2326
+ const id = useHref(use);
2327
+ if (id === null) continue;
2328
+ const target = ids?.get(id) ?? byId(id);
2329
+ if (!target) continue;
2330
+ const copy = match(target);
2331
+ let done = styled.get(target);
2332
+ if (!done) {
2333
+ let into: Map<Element, ShapeStyle> | null = null;
2334
+ let part = '';
2335
+ for (const { el, at, candidates } of copy.rules) {
2336
+ const own = resolve(candidates, false);
2337
+ if (!own) continue;
2338
+ (into ??= new Map()).set(el, own);
2339
+ part += named(own, at);
2340
+ }
2341
+ done = { into, key: part };
2342
+ styled.set(target, done);
2343
+ }
2344
+ if (!made?.has(target)) {
2345
+ (made ??= new Set()).add(target);
2346
+ for (const next of copy.uses) uses.push(next);
2347
+ // its place in the key is after what came before it
2348
+ if (done.key) key += `@${place + 1}:${done.key}`;
2349
+ place += copy.size;
2350
+ }
2351
+ if (done.into) (used ??= new Map()).set(use, done.into);
2352
+ }
2353
+ return of || used ? { of: of ?? NO_SHAPES, used, key } : null;
2354
+ }
2355
+
2356
+ /** `ShapeNeeds`, and each such rule's own (`IndexedRule.needs`). */
2357
+ private _needsOfShapes(): ShapeNeeds {
2358
+ const index = this._shapes;
2359
+ const needs: ShapeNeeds = {
2360
+ classes: new Set(),
2361
+ ids: new Set(),
2362
+ universal: false,
2363
+ tags: new Set(),
2364
+ };
2365
+ const read = (bucket: IndexedRule[]): boolean => {
2366
+ let free = false;
2367
+ for (const indexed of bucket) {
2368
+ const keys = ancestorKeys(indexed.rule.selector);
2369
+ indexed.needs = keys;
2370
+ if (!keys.length) free = true;
2371
+ for (const key of keys) {
2372
+ (key[0] === '#' ? needs.ids : needs.classes).add(key.slice(1));
2373
+ }
2374
+ }
2375
+ return free;
2376
+ };
2377
+ needs.universal = read(index.universal);
2378
+ for (const [tag, bucket] of index.byTag) {
2379
+ if (read(bucket)) needs.tags.add(tag);
2380
+ }
2381
+ return needs;
2382
+ }
2383
+
1746
2384
  /**
1747
2385
  * Whether the pointer's move from `was` to where it is changed which
1748
2386
  * rules match `el`, or its `::before`, `::after` or another of its
@@ -1750,19 +2388,27 @@ export class Cascade {
1750
2388
  * nothing else, since no other rule's answer is the pointer's. An element
1751
2389
  * they answer the same for, under a parent whose style is what it was, has
1752
2390
  * the style it had — which is most of what a move reaches: the rows of a
1753
- * hovered table body, what is in a hovered card.
2391
+ * hovered table body, what is in a hovered card. The focus's move from
2392
+ * `wasFocus` likewise, the rules that test it asked under both.
1754
2393
  */
1755
- pointerChanged(el: Element, was: PointerState): boolean {
2394
+ pointerChanged(
2395
+ el: Element,
2396
+ was: PointerState,
2397
+ wasFocus: FocusState = this._focus,
2398
+ ): boolean {
1756
2399
  const now = this._pointer;
2400
+ const nowFocus = this._focus;
1757
2401
  const after = this._pointerAnswers(el);
1758
2402
  this._pointer = was;
2403
+ this._focus = wasFocus;
1759
2404
  const before = this._pointerAnswers(el);
1760
2405
  this._pointer = now;
2406
+ this._focus = nowFocus;
1761
2407
  return before !== after;
1762
2408
  }
1763
2409
 
1764
- /** The ids of the rules testing the pointer that match `el` as things
1765
- * stand, the element's and each pseudo-element's. */
2410
+ /** The ids of the rules testing the pointer or the focus that match
2411
+ * `el` as things stand, the element's and each pseudo-element's. */
1766
2412
  private _pointerAnswers(el: Element): string {
1767
2413
  let answers = '';
1768
2414
  const ask = (index: RuleIndex, name: string): void => {
@@ -1785,6 +2431,11 @@ export class Cascade {
1785
2431
  el: Element,
1786
2432
  out: Candidate[] | null,
1787
2433
  matched?: number[],
2434
+ /** The rules to try, where some are known not to match without it. */
2435
+ live?: (indexed: IndexedRule) => boolean,
2436
+ /** Whether `el` is matched where a `<use>` draws a copy of it
2437
+ * (`_copyTop`). */
2438
+ inCopy = false,
1788
2439
  ): void {
1789
2440
  // A media query's width is CSS pixels; the viewport is kept in device.
1790
2441
  const width = this.viewportWidth / this.scale;
@@ -1793,6 +2444,7 @@ export class Cascade {
1793
2444
  const consider = (bucket: IndexedRule[] | undefined): void => {
1794
2445
  if (!bucket) return;
1795
2446
  for (const indexed of bucket) {
2447
+ if (live !== undefined && !live(indexed)) continue;
1796
2448
  const rule = indexed.rule;
1797
2449
  if (
1798
2450
  !mediaMatches(
@@ -1805,18 +2457,43 @@ export class Cascade {
1805
2457
  ) {
1806
2458
  continue;
1807
2459
  }
1808
- if (!indexed.compiled) {
1809
- indexed.compiled = true;
1810
- try {
1811
- indexed.match = compileSelector(rule.selector, this._adapter);
1812
- } catch {
1813
- // A selector this matcher does not know (`::-moz-…`, a CSS4 form
1814
- // it has not learnt) drops out of the cascade rather than out of
1815
- // the render.
1816
- indexed.match = null;
2460
+ let match: ((el: Element) => boolean) | null;
2461
+ if (inCopy) {
2462
+ if (!indexed.inCopyCompiled) {
2463
+ indexed.inCopyCompiled = true;
2464
+ try {
2465
+ // what it finds above an element depends on which copy it is
2466
+ // in, so none of it is kept
2467
+ indexed.inCopy = compileSelector(
2468
+ rule.selector,
2469
+ this._copyAdapter,
2470
+ this._pseudos,
2471
+ false,
2472
+ );
2473
+ } catch {
2474
+ indexed.inCopy = null;
2475
+ }
2476
+ }
2477
+ match = indexed.inCopy;
2478
+ } else {
2479
+ if (!indexed.compiled) {
2480
+ indexed.compiled = true;
2481
+ try {
2482
+ indexed.match = compileSelector(
2483
+ rule.selector,
2484
+ this._adapter,
2485
+ this._pseudos,
2486
+ );
2487
+ } catch {
2488
+ // A selector this matcher does not know (`::-moz-…`, a CSS4
2489
+ // form it has not learnt) drops out of the cascade rather than
2490
+ // out of the render.
2491
+ indexed.match = null;
2492
+ }
1817
2493
  }
2494
+ match = indexed.match;
1818
2495
  }
1819
- if (!indexed.match || !indexed.match(el)) continue;
2496
+ if (!match || !match(el)) continue;
1820
2497
  matched?.push(indexed.id);
1821
2498
  if (out === null) continue;
1822
2499
  const origin = rule.order < 0 ? Origin.UserAgent : Origin.Author;
@@ -1844,6 +2521,18 @@ export class Cascade {
1844
2521
  this._matchInto(this._index, el, out, matched);
1845
2522
 
1846
2523
  const hints = presentationHints(el);
2524
+ // the page's colour schemes are its root's where no rule sets them
2525
+ if (
2526
+ this.pageColorScheme &&
2527
+ el.name === 'html' &&
2528
+ !(el.parent && isTag(el.parent as Element))
2529
+ ) {
2530
+ hints.push({
2531
+ prop: 'color-scheme',
2532
+ value: this.pageColorScheme,
2533
+ important: false,
2534
+ });
2535
+ }
1847
2536
  if (hints.length) {
1848
2537
  out.push({
1849
2538
  origin: Origin.Presentation,
@@ -1908,9 +2597,18 @@ function dropPaletteChrome(out: Candidate[]): void {
1908
2597
  }
1909
2598
  if (!chrome) return;
1910
2599
  let styled = false;
2600
+ // `appearance` by the value that wins: `appearance: none; appearance:
2601
+ // auto` keeps the control's look, as a reset a page takes back does
2602
+ let appearance = '';
1911
2603
  for (let j = i; j < out.length && !styled; j += 1) {
1912
- styled = pick(out[j]).some(stylesChrome);
2604
+ for (const d of pick(out[j])) {
2605
+ if (d.prop === 'appearance' || d.prop === '-webkit-appearance') {
2606
+ const v = d.value.trim().toLowerCase();
2607
+ if (/^[a-z-]+$/.test(v)) appearance = v;
2608
+ } else if (stylesChrome(d)) styled = true;
2609
+ }
1913
2610
  }
2611
+ if (appearance === 'none') styled = true;
1914
2612
  if (!styled) return;
1915
2613
  let kept = 0;
1916
2614
  for (const c of out) {
@@ -1923,8 +2621,9 @@ function dropPaletteChrome(out: Candidate[]): void {
1923
2621
  }
1924
2622
 
1925
2623
  /** Whether a declaration styles what a control's native look draws: Blink's
1926
- * `is_background` and `is_border` properties and their shorthands, and an
1927
- * `appearance` of `none`. */
2624
+ * `is_background` and `is_border` properties and their shorthands. An
2625
+ * `appearance` of `none` does too, where it is the one that wins
2626
+ * (`dropPaletteChrome`). */
1928
2627
  function stylesChrome(d: Declaration): boolean {
1929
2628
  const prop = d.prop;
1930
2629
  if (prop.startsWith('background')) {
@@ -1933,10 +2632,7 @@ function stylesChrome(d: Declaration): boolean {
1933
2632
  if (prop.startsWith('border')) {
1934
2633
  return prop !== 'border-collapse' && prop !== 'border-spacing';
1935
2634
  }
1936
- return (
1937
- (prop === 'appearance' || prop === '-webkit-appearance') &&
1938
- d.value.trim().toLowerCase() === 'none'
1939
- );
2635
+ return false;
1940
2636
  }
1941
2637
 
1942
2638
  /** An implied element's style as the root box's: its look, not its role —
@@ -1952,18 +2648,22 @@ function asRoot(style: ComputedStyle): ComputedStyle {
1952
2648
  * fragment, whose style the root box takes (`rootStyle`), and never a
1953
2649
  * fragment's top-level elements, which css-select would take for it.
1954
2650
  *
1955
- * `:focus` and its two kin match nothing: no element of the document takes
1956
- * the focus — a control's widget does, beside it. css-select has none of
1957
- * the three and throws on them, which dropped every rule that named one,
1958
- * `:not(:focus)` among them: Wikipedia's skip link hides with
1959
- * `.mw-jump-link:not(:focus)`, and without it stood in the flow at the top
1960
- * of every page. */
2651
+ * `:focus` and its two kin are here as matching nothing, and each cascade
2652
+ * answers them from where the focus is (`Cascade.setFocus`): no element
2653
+ * of the document takes it itself — a control's widget does, beside it.
2654
+ * css-select has none of the three and throws on them, which dropped
2655
+ * every rule that named one, `:not(:focus)` among them: Wikipedia's skip
2656
+ * link hides with `.mw-jump-link:not(:focus)`, and without it stood in
2657
+ * the flow at the top of every page. */
1961
2658
  const PSEUDOS = {
1962
2659
  root: (el: Element) =>
1963
2660
  el.name === 'html' && !(el.parent && isTag(el.parent as Element)),
1964
- focus: () => false,
1965
- 'focus-visible': () => false,
1966
- 'focus-within': () => false,
2661
+ focus: (_el: Element) => false,
2662
+ 'focus-visible': (_el: Element) => false,
2663
+ 'focus-within': (_el: Element) => false,
2664
+ // css-select has none, and threw on one, so `:not(:target)` matched
2665
+ // nothing: a document is drawn at no fragment, an SVG image at its own
2666
+ target: (_el: Element) => false,
1967
2667
  // css-select's, but its ranges and tags lower-cased as ASCII has it, and
1968
2668
  // no other script (CSS 2.1 4.1.3): Unicode's took `:lang(\u212Al)`, a
1969
2669
  // Kelvin sign for the K, for `:lang(kl)`
@@ -1973,28 +2673,32 @@ const PSEUDOS = {
1973
2673
  .map((range) => range.trim().replace(/^['"]|['"]$/g, ''))
1974
2674
  .filter((range) => range.length > 0)
1975
2675
  .map((range) => asciiLower(range).split('-'));
1976
- let root = el;
1977
- for (let node: Element | null = el; node;) {
1978
- const value = node.attribs['xml:lang'] ?? node.attribs.lang;
1979
- if (value != null) {
1980
- if (!value) return ranges.some((range) => range[0] === '');
1981
- const tag = asciiLower(value).split('-');
1982
- return ranges.some((range) => langRangeMatches(tag, range));
1983
- }
1984
- root = node;
1985
- const parent: Element['parent'] = node.parent;
1986
- node = parent && isTag(parent as Element) ? (parent as Element) : null;
1987
- }
1988
- // with no element saying, the document's, as its `<meta>` sets it —
1989
- // searched from the document, where a fragment's `<meta>` is a
1990
- // sibling of what it covers
1991
- const pragma = pragmaLanguage(root.parent ?? root);
1992
- if (!pragma) return ranges.some((range) => range[0] === '');
1993
- const tag = asciiLower(pragma).split('-');
2676
+ const value = languageOf(el);
2677
+ if (!value) return ranges.some((range) => range[0] === '');
2678
+ const tag = asciiLower(value).split('-');
1994
2679
  return ranges.some((range) => langRangeMatches(tag, range));
1995
2680
  },
1996
2681
  };
1997
2682
 
2683
+ /**
2684
+ * An element's language: the `lang` or `xml:lang` of the nearest element
2685
+ * from it up that says one, or with none saying, the document's, as its
2686
+ * `<meta http-equiv="content-language">` sets it — searched from the
2687
+ * document, where a fragment's `<meta>` is a sibling of what it covers.
2688
+ * '' where none does, or the nearest says ''.
2689
+ */
2690
+ export function languageOf(el: Element): string {
2691
+ let root = el;
2692
+ for (let node: Element | null = el; node;) {
2693
+ const value = node.attribs['xml:lang'] ?? node.attribs.lang;
2694
+ if (value != null) return value;
2695
+ root = node;
2696
+ const parent: Element['parent'] = node.parent;
2697
+ node = parent && isTag(parent as Element) ? (parent as Element) : null;
2698
+ }
2699
+ return pragmaLanguage(root.parent ?? root);
2700
+ }
2701
+
1998
2702
  /** A length in a unit of the viewport: a number, then the unit, and no
1999
2703
  * more of a name after it. */
2000
2704
  const VIEWPORT_UNIT = /\d[sld]?v(?:w|h|i|b|min|max)(?![\w-])/i;
@@ -2050,6 +2754,33 @@ function pragmaLanguage(root: { children: unknown[] }): string {
2050
2754
 
2051
2755
  const PRAGMA_LANGUAGE = new WeakMap<object, string>();
2052
2756
 
2757
+ /**
2758
+ * The page's supported colour schemes: the content of the first `<meta
2759
+ * name="color-scheme">` that is a `color-scheme` value (HTML 4.2.5.4), or
2760
+ * null. Looked for where a `<meta>` is, at the top of a fragment and in
2761
+ * the `<html>` and its `<head>`, and not through the body.
2762
+ */
2763
+ export function metaColorScheme(root: { children: unknown[] }): string | null {
2764
+ const stack: { children: unknown[] }[] = [root];
2765
+ while (stack.length) {
2766
+ const node = stack.pop()!;
2767
+ for (const child of node.children) {
2768
+ if (!isTag(child as Element)) continue;
2769
+ const el = child as Element;
2770
+ if (el.name === 'meta') {
2771
+ const name = el.attribs.name?.trim().toLowerCase();
2772
+ const content = el.attribs.content?.trim();
2773
+ if (name === 'color-scheme' && content && usedColorScheme(content)) {
2774
+ return content;
2775
+ }
2776
+ } else if (el.name === 'html' || el.name === 'head') {
2777
+ stack.push(el);
2778
+ }
2779
+ }
2780
+ }
2781
+ return null;
2782
+ }
2783
+
2053
2784
  /** RFC 4647's extended filtering, of a tag's subtags by a range's, as
2054
2785
  * css-select does it. */
2055
2786
  function langRangeMatches(tag: string[], range: string[]): boolean {
@@ -2088,6 +2819,68 @@ function setsContent(c: Candidate): boolean {
2088
2819
  /** Whether a rule's declarations set a custom property, by the array. */
2089
2820
  const CUSTOM_IN = new WeakMap<Declaration[], boolean>();
2090
2821
 
2822
+ /** The document's first element with an id, looked for from one of its
2823
+ * elements: `shapeStyles`' answer where no index of them is handed in. */
2824
+ function elementById(from: Element, id: string): Element | null {
2825
+ let top: Element['parent'] | Element = from;
2826
+ while (top.parent) top = top.parent;
2827
+ const stack = [...top.children].reverse();
2828
+ for (let node = stack.pop(); node; node = stack.pop()) {
2829
+ if (node.type !== 'tag') continue;
2830
+ const el = node as Element;
2831
+ if (el.attribs.id === id) return el;
2832
+ for (let i = el.children.length - 1; i >= 0; i -= 1) {
2833
+ stack.push(el.children[i]);
2834
+ }
2835
+ }
2836
+ return null;
2837
+ }
2838
+
2839
+ /** Whether declarations set a property a shape in a drawing has. */
2840
+ function declaresShape(declarations: readonly Declaration[]): boolean {
2841
+ for (const d of declarations) if (isShapeProp(d.prop)) return true;
2842
+ return false;
2843
+ }
2844
+
2845
+ /** Whether declarations set one a drawing's root does not have from its
2846
+ * box's style (`ROOT_BOX_PROPS`). */
2847
+ function declaresRootShape(declarations: readonly Declaration[]): boolean {
2848
+ for (const d of declarations) {
2849
+ if (isShapeProp(d.prop) && !ROOT_BOX_PROPS.has(d.prop)) return true;
2850
+ }
2851
+ return false;
2852
+ }
2853
+
2854
+ /** A `style` attribute's declarations of a shape's properties, as
2855
+ * candidates: over every rule, and its `!important` ones over theirs. */
2856
+ function pushInlineShapes(out: Candidate[], inline: string): void {
2857
+ const declarations = parseDeclarations(inline).filter((d) =>
2858
+ isShapeProp(d.prop),
2859
+ );
2860
+ const normal = declarations.filter((d) => !d.important);
2861
+ const important = declarations.filter((d) => d.important);
2862
+ if (normal.length) {
2863
+ out.push({
2864
+ origin: Origin.Inline,
2865
+ layer: null,
2866
+ specificity: 0,
2867
+ order: 0,
2868
+ declarations: normal,
2869
+ only: -1,
2870
+ });
2871
+ }
2872
+ if (important.length) {
2873
+ out.push({
2874
+ origin: Origin.InlineImportant,
2875
+ layer: null,
2876
+ specificity: 0,
2877
+ order: 0,
2878
+ declarations: important,
2879
+ only: -1,
2880
+ });
2881
+ }
2882
+ }
2883
+
2091
2884
  function pick(c: Candidate): Declaration[] {
2092
2885
  return c.only < 0 ? c.declarations : [c.declarations[c.only]];
2093
2886
  }
@@ -2473,6 +3266,24 @@ function lengthAttr(value: string): string {
2473
3266
  return Number.isFinite(n) ? `${n}px` : v;
2474
3267
  }
2475
3268
 
3269
+ /** A number and a unit, as an SVG's `width` and `height` are written. */
3270
+ const SVG_LENGTH =
3271
+ /^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*([a-z]*)\s*$/i;
3272
+
3273
+ /**
3274
+ * The `width`/`height` of an inline `<svg>` as CSS: they are presentation
3275
+ * attributes for the properties of the same names (SVG 2, 5.1.1), so
3276
+ * `height="50%"` is a percentage of the containing block like a style's.
3277
+ * A bare number is pixels; null for what CSS would not parse.
3278
+ */
3279
+ function svgSizeHint(value: string): string | null {
3280
+ const v = value.trim();
3281
+ if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?%$/i.test(v)) return v;
3282
+ const m = SVG_LENGTH.exec(v);
3283
+ if (!m) return null;
3284
+ return m[2] ? v : `${parseFloat(m[1])}px`;
3285
+ }
3286
+
2476
3287
  function closestTable(el: Element): Element | null {
2477
3288
  let node = el.parent;
2478
3289
  while (node) {