@react-x11/components 0.14.1 → 0.16.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.
Files changed (96) 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 +171 -26
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +616 -216
  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 +18 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -1
  22. package/dist/html/css/shapes.js +21 -1
  23. package/dist/html/css/shapes.js.map +1 -1
  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 +74 -12
  27. package/dist/html/css/style.js.map +1 -1
  28. package/dist/html/css/ua.js +27 -8
  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 +29 -9
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +141 -52
  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 +172 -23
  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 +11 -9
  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 +1759 -238
  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 +64 -19
  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 +886 -324
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +35 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +122 -26
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts.map +1 -1
  64. package/dist/html/paint.js +558 -178
  65. package/dist/html/paint.js.map +1 -1
  66. package/dist/html/resources.js +14 -1
  67. package/dist/html/resources.js.map +1 -1
  68. package/dist/html/svg.d.ts +23 -7
  69. package/dist/html/svg.d.ts.map +1 -1
  70. package/dist/html/svg.js +264 -59
  71. package/dist/html/svg.js.map +1 -1
  72. package/dist/html/widgets.d.ts.map +1 -1
  73. package/dist/html/widgets.js +57 -4
  74. package/dist/html/widgets.js.map +1 -1
  75. package/package.json +3 -3
  76. package/src/code-editor/node.ts +24 -5
  77. package/src/html/css/cascade.ts +697 -221
  78. package/src/html/css/color.ts +345 -18
  79. package/src/html/css/content.ts +173 -15
  80. package/src/html/css/parse.ts +60 -8
  81. package/src/html/css/shapes.ts +35 -1
  82. package/src/html/css/style.ts +122 -17
  83. package/src/html/css/ua.ts +27 -8
  84. package/src/html/css/values.ts +23 -9
  85. package/src/html/index.ts +7 -0
  86. package/src/html/layout/block.ts +149 -58
  87. package/src/html/layout/boxes.ts +211 -27
  88. package/src/html/layout/css-grid.ts +12 -9
  89. package/src/html/layout/flex.ts +1975 -231
  90. package/src/html/layout/inline.ts +70 -16
  91. package/src/html/layout/table.ts +961 -332
  92. package/src/html/node.ts +155 -27
  93. package/src/html/paint.ts +692 -234
  94. package/src/html/resources.ts +13 -1
  95. package/src/html/svg.ts +299 -56
  96. 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,6 +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';
25
+ import { systemColorTable, usedColorScheme } from './color.js';
24
26
  import {
25
27
  asciiLower,
26
28
  escapeEnd,
@@ -32,9 +34,11 @@ import type { Declaration, StyleRule, Stylesheet } from './parse.js';
32
34
  import {
33
35
  applyDeclaration,
34
36
  blockify,
37
+ settleAlign,
35
38
  settleButton,
36
39
  settleContentVisibility,
37
40
  settleOverflow,
41
+ settleOutline,
38
42
  settleClamp,
39
43
  copyStyle,
40
44
  decorate,
@@ -57,7 +61,9 @@ import {
57
61
  ROOT_BOX_PROPS,
58
62
  SHAPE_TAGS,
59
63
  isShapeProp,
64
+ isUse,
60
65
  shapeValue,
66
+ useHref,
61
67
  } from './shapes.js';
62
68
  import type { ShapeContext, ShapeStyle, ShapeStyles } from './shapes.js';
63
69
 
@@ -153,6 +159,10 @@ interface IndexedRule {
153
159
  /** For a rule kept for drawings: whether it declares something a
154
160
  * drawing's root does not have from its box's style. Null until asked. */
155
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;
156
166
  }
157
167
 
158
168
  /**
@@ -183,10 +193,12 @@ class RuleIndex {
183
193
  /** Whether any rule in here is pointer-sensitive, so the renderer knows
184
194
  * whether a pointer move can change the cascade at all. */
185
195
  hoverSensitive = false;
186
- /** The rules that test `:hover`, in buckets of their own: the ones whose
187
- * answers a pointer move can change, which is all that is asked to know
188
- * whether an element's did (`Cascade.pointerChanged`). Null until there
189
- * 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. */
190
202
  pointer: RuleIndex | null = null;
191
203
  /** The buckets holding a rule an element cannot share its style under
192
204
  * (`UNSHAREABLE`): the ids, classes and tags whose elements compute
@@ -237,10 +249,14 @@ class RuleIndex {
237
249
  id: this._nextId++,
238
250
  needs: null,
239
251
  root: null,
252
+ inCopy: null,
253
+ inCopyCompiled: false,
240
254
  };
241
255
  if (rule.selector.includes(':hover') || rule.selector.includes(':active')) {
242
256
  this.hoverSensitive = true;
243
257
  }
258
+ const focus = FOCUS.test(rule.selector);
259
+ if (focus) this.focusSensitive = true;
244
260
  if (UNSHAREABLE.test(rule.selector)) {
245
261
  if (key.kind === 'id') this.ownStyleIds.add(key.name);
246
262
  else if (key.kind === 'class') this.ownStyleClasses.add(key.name);
@@ -248,7 +264,7 @@ class RuleIndex {
248
264
  else this.ownStyleEverywhere = true;
249
265
  }
250
266
  this._bucket(key).push(indexed);
251
- if (HOVER.test(rule.selector)) {
267
+ if (focus || HOVER.test(rule.selector)) {
252
268
  this.pointer ??= new RuleIndex();
253
269
  this.pointer.size += 1;
254
270
  this.pointer._bucket(key).push(indexed);
@@ -341,8 +357,18 @@ function splitPseudoElement(
341
357
  * handed none), so a selector that tests only it never changes as the
342
358
  * pointer moves — Wikipedia's buttons' `:focus:not(:active)` among them.
343
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.
344
367
  */
345
- export function pointerCompounds(selector: string): PointerCompounds {
368
+ export function pointerCompounds(
369
+ selector: string,
370
+ kind: StateKind = HOVER_STATE,
371
+ ): PointerCompounds {
346
372
  const out: PointerCompounds = {
347
373
  compounds: [],
348
374
  siblings: false,
@@ -350,10 +376,33 @@ export function pointerCompounds(selector: string): PointerCompounds {
350
376
  has: [],
351
377
  followed: [],
352
378
  };
353
- scanComplex(selector, out);
379
+ scanComplex(selector, out, kind);
354
380
  return out;
355
381
  }
356
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
+
357
406
  export interface PointerCompounds {
358
407
  compounds: string[];
359
408
  siblings: boolean;
@@ -442,11 +491,15 @@ function closeOf(text: string, open: number): number {
442
491
  }
443
492
 
444
493
  /** `pointerCompounds` for one complex selector, into `out`. */
445
- function scanComplex(selector: string, out: PointerCompounds): void {
494
+ function scanComplex(
495
+ selector: string,
496
+ out: PointerCompounds,
497
+ kind: StateKind,
498
+ ): void {
446
499
  for (const { text, next } of compoundsOf(selector)) {
447
500
  const compoundsFrom = out.compounds.length;
448
501
  const hasFrom = out.has.length;
449
- const found = scanCompound(text, out);
502
+ const found = scanCompound(text, out, kind);
450
503
  const sibling = next === '+' || next === '~';
451
504
  if (found.args.length) {
452
505
  out.has.push({
@@ -478,11 +531,13 @@ function scanComplex(selector: string, out: PointerCompounds): void {
478
531
  * it (`bare`), whether it tests the element's own hover (`pointer`), the
479
532
  * arguments of each `:has()` of the element's that tests it below (`args`),
480
533
  * and whether a selector nested in it named another element's (`below`),
481
- * whose compounds went into `out`.
534
+ * whose compounds went into `out`. Of the focus, likewise, for
535
+ * `FOCUS_STATE`.
482
536
  */
483
537
  function scanCompound(
484
538
  text: string,
485
539
  out: PointerCompounds,
540
+ kind: StateKind,
486
541
  ): { bare: string; pointer: boolean; below: boolean; args: string[] } {
487
542
  let bare = '';
488
543
  let pointer = false;
@@ -507,9 +562,9 @@ function scanCompound(
507
562
  else if (c === '[') bracket += 1;
508
563
  else if (c === ']') bracket = Math.max(0, bracket - 1);
509
564
  else if (c === ':' && bracket === 0 && text[i + 1] !== ':') {
510
- const m = POINTER_PSEUDO_AT.exec(text.slice(i));
565
+ const m = STATE_PSEUDO_AT.exec(text.slice(i));
511
566
  if (m) {
512
- if (m[0].length === 6) pointer = true;
567
+ if (kind.own.test(m[0])) pointer = true;
513
568
  i += m[0].length - 1;
514
569
  continue;
515
570
  }
@@ -520,7 +575,7 @@ function scanCompound(
520
575
  const arg = text.slice(open + 1, close);
521
576
  const whole = text.slice(i, close + 1);
522
577
  i = close;
523
- if (!HOVER.test(arg)) {
578
+ if (!kind.any.test(arg)) {
524
579
  bare += whole;
525
580
  continue;
526
581
  }
@@ -541,15 +596,15 @@ function scanCompound(
541
596
  const own: string[] = [];
542
597
  let any = false;
543
598
  for (const entry of selectorsOf(arg)) {
544
- if (!HOVER.test(entry)) continue;
599
+ if (!kind.any.test(entry)) continue;
545
600
  const parts = compoundsOf(entry);
546
601
  if (parts.length !== 1) {
547
- scanComplex(entry, out);
602
+ scanComplex(entry, out, kind);
548
603
  below = true;
549
604
  continue;
550
605
  }
551
606
  // of this element itself
552
- const one = scanCompound(parts[0].text, out);
607
+ const one = scanCompound(parts[0].text, out, kind);
553
608
  if (one.below) below = true;
554
609
  if (!one.pointer && !one.args.length) continue;
555
610
  if (one.pointer) pointer = true;
@@ -576,33 +631,42 @@ function scanCompound(
576
631
  const FUNCTION_AT = /^:([\w-]+)\(/;
577
632
 
578
633
  /** `:hover` anywhere in a selector. */
579
- 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;
580
638
 
581
- /** `:hover` or `:active` at the start of a string, and nothing longer. */
582
- 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;
583
644
 
584
- /** `:hover` or `:active` anywhere in a selector. */
585
- 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;
586
647
 
587
648
  /**
588
649
  * A selector compiled to a matcher over this adapter. css-select keeps the
589
650
  * answers of a `:has()` above the subject, or of the ancestors a
590
651
  * descendant combinator tried under one, for as long as the matcher lives
591
652
  * (`cacheResults`) — which is the cascade's life, and a pointer move is no
592
- * new cascade. So a selector that tests the pointer keeps none: with them,
593
- * `#box:has(a:hover) .m` answered what it did before the first move, for
594
- * 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.
595
656
  */
596
657
  function compileSelector(
597
658
  selector: string,
598
659
  adapter: CssSelectAdapter,
599
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,
600
664
  ): (el: Element) => boolean {
601
665
  return compile(noEmptyWords(selector), {
602
666
  adapter,
603
667
  xmlMode: false,
604
668
  pseudos,
605
- cacheResults: !POINTER_PSEUDO.test(selector),
669
+ cacheResults: cache && !STATE_PSEUDO.test(selector),
606
670
  } as unknown as Parameters<typeof compile>[1]) as unknown as (
607
671
  node: Element,
608
672
  ) => boolean;
@@ -724,6 +788,41 @@ function ancestorKeys(selector: string): string[] {
724
788
  return keys;
725
789
  }
726
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
+
727
826
  /** What the rules asked of every element of a drawing, or of every one of
728
827
  * a type, ask of its ancestors (`Cascade.shapeStyles`). */
729
828
  interface ShapeNeeds {
@@ -768,6 +867,138 @@ export interface PointerState {
768
867
 
769
868
  const NO_POINTER: PointerState = { hovered: new Set(), active: new Set() };
770
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
+
771
1002
  /**
772
1003
  * The face a style's font-relative units are measured in: its family list,
773
1004
  * size, weight and slant, the four that pick one. A family's faces can be
@@ -832,32 +1063,23 @@ export class Cascade {
832
1063
  private _shapes = new RuleIndex();
833
1064
  private _shapeNeeds: ShapeNeeds | null = null;
834
1065
  private _adapter: CssSelectAdapter;
835
- /** The compounds of the selectors that test the pointer, each without
836
- * its `:hover` or `:active` (`pointerCompounds`), and their matchers,
837
- * compiled when a pointer move first asks (`hoverTouches`). */
838
- private _hoverCompounds = new Map<string, boolean>();
839
- private _hoverMatchers:
840
- { match: (el: Element) => boolean; followed: boolean }[] | null = null;
841
- /** Whether a pointer move can be restyled where it happened
842
- * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
843
- * pointer inside a functional pseudo-class, which reaches elements no
844
- * compound names. */
845
- hoverLocal = true;
846
- /** The compounds whose `:has()` tests the pointer, each without it,
847
- * whether its argument starts at a sibling, and whether a sibling
848
- * combinator follows it (`hoverAnchors`). */
849
- private _hoverAnchors = new Map<
850
- string,
851
- { siblings: boolean; followed: boolean }
852
- >();
853
- private _anchorMatchers:
854
- | {
855
- match: (el: Element) => boolean;
856
- siblings: boolean;
857
- followed: boolean;
858
- }[]
859
- | 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);
860
1081
  private _pointer: PointerState = NO_POINTER;
1082
+ private _focus: FocusState = NO_FOCUS;
861
1083
  readonly initial: ComputedStyle;
862
1084
  readonly look: RootLook;
863
1085
  /** Viewport width the media queries were evaluated at, in device pixels
@@ -905,8 +1127,18 @@ export class Cascade {
905
1127
  /** Whether any declaration has a length in `lh` or `rlh`: only then is
906
1128
  * the line height settled ahead of the declarations that read it. */
907
1129
  private _lh = false;
908
- /** The pseudo-classes css-select is handed, `:root` this cascade's. */
1130
+ /** The pseudo-classes css-select is handed: `:root` this cascade's, and
1131
+ * the focus's three answered from `_focus`. */
909
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>;
910
1142
 
911
1143
  constructor(
912
1144
  sheets: Stylesheet[],
@@ -921,10 +1153,23 @@ export class Cascade {
921
1153
  /** The document's element where it is not an `<html>`: an SVG image's
922
1154
  * `<svg>`, which is `:root` there (Selectors 4, 14.1). */
923
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,
924
1159
  ) {
925
- this._pseudos = documentElement
926
- ? { ...PSEUDOS, root: (el: Element) => el === documentElement }
927
- : PSEUDOS;
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
+ };
928
1173
  this._xHeightOf = xHeight;
929
1174
  this._zeroWidthOf = zeroWidth;
930
1175
  this._normalLineOf = normalLine;
@@ -933,6 +1178,7 @@ export class Cascade {
933
1178
  ? (list: string) => families.map(list)
934
1179
  : undefined;
935
1180
  this.look = look;
1181
+ this._systemColors = systemColorTable(documentElement ? null : look);
936
1182
  this.initial = initialStyle(look, scale);
937
1183
  this.viewportWidth = viewportWidth;
938
1184
  this.viewportHeight = viewportHeight;
@@ -962,7 +1208,9 @@ export class Cascade {
962
1208
  this._shapes.add(rule, key);
963
1209
  }
964
1210
  }
965
- this._noteHover((pseudo?.rule ?? rule).selector);
1211
+ const selector = (pseudo?.rule ?? rule).selector;
1212
+ this._hoverRules.note(selector);
1213
+ this._focusRules.note(selector);
966
1214
  }
967
1215
  for (const bp of sheet.breakpoints) breakpoints.add(bp);
968
1216
  // a query on the viewport's height reads it as a `vh` does
@@ -990,6 +1238,16 @@ export class Cascade {
990
1238
  isActive: (el: Element) => this._pointer.active.has(el),
991
1239
  isVisited: () => false,
992
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
+ };
993
1251
  }
994
1252
 
995
1253
  /** Whether declarations read the viewport's width or height (`vw`,
@@ -1022,6 +1280,36 @@ export class Cascade {
1022
1280
  this._pointer = pointer;
1023
1281
  }
1024
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
+
1025
1313
  /**
1026
1314
  * Which pseudo-elements' rules test the pointer: none's
1027
1315
  * (`HOVER_PSEUDO_NONE`); a `::before`'s or an `::after`'s only
@@ -1031,15 +1319,23 @@ export class Cascade {
1031
1319
  * move cannot.
1032
1320
  */
1033
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 {
1034
1331
  if (
1035
- this._pseudo['first-letter'].hoverSensitive ||
1036
- this._pseudo['first-line'].hoverSensitive ||
1037
- this._pseudo.marker.hoverSensitive
1332
+ this._pseudo['first-letter'][state] ||
1333
+ this._pseudo['first-line'][state] ||
1334
+ this._pseudo.marker[state]
1038
1335
  ) {
1039
1336
  return HOVER_PSEUDO_OTHER;
1040
1337
  }
1041
- return this._pseudo.before.hoverSensitive ||
1042
- this._pseudo.after.hoverSensitive
1338
+ return this._pseudo.before[state] || this._pseudo.after[state]
1043
1339
  ? HOVER_PSEUDO_GENERATED
1044
1340
  : HOVER_PSEUDO_NONE;
1045
1341
  }
@@ -1055,17 +1351,13 @@ export class Cascade {
1055
1351
  * table restyle the rows after it.
1056
1352
  */
1057
1353
  hoverTouches(el: Element): HoverTouch {
1058
- // the followed ones first, so the first that matches is the answer
1059
- this._hoverMatchers ??= [...this._hoverCompounds]
1060
- .sort(([, a], [, b]) => Number(b) - Number(a))
1061
- .flatMap(([compound, followed]) => {
1062
- const match = this._compile(compound);
1063
- return match ? [{ match, followed }] : [];
1064
- });
1065
- for (const { match, followed } of this._hoverMatchers) {
1066
- if (match(el)) return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
1067
- }
1068
- 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));
1069
1361
  }
1070
1362
 
1071
1363
  /**
@@ -1077,61 +1369,12 @@ export class Cascade {
1077
1369
  * siblings can change with it (`hoverTouches`).
1078
1370
  */
1079
1371
  hoverAnchors(el: Element, into: Map<Element, HoverTouch>): void {
1080
- if (!this._hoverAnchors.size) return;
1081
- this._anchorMatchers ??= [...this._hoverAnchors]
1082
- .sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
1083
- .flatMap(([anchor, { siblings, followed }]) => {
1084
- const match = this._compile(anchor);
1085
- return match ? [{ match, siblings, followed }] : [];
1086
- });
1087
- const note = (at: Element, followed: boolean): void => {
1088
- if (followed) into.set(at, HOVER_FOLLOWED);
1089
- else if (!into.has(at)) into.set(at, HOVER_TOUCHED);
1090
- };
1091
- const siblings = this._anchorMatchers.some((m) => m.siblings);
1092
- for (let at: Element | null = el; at;) {
1093
- if (at !== el) {
1094
- for (const { match, followed } of this._anchorMatchers) {
1095
- if (match(at)) {
1096
- note(at, followed);
1097
- break;
1098
- }
1099
- }
1100
- }
1101
- if (siblings) {
1102
- for (let s = at.prev; s; s = s.prev) {
1103
- if (!isTag(s as Element)) continue;
1104
- for (const m of this._anchorMatchers) {
1105
- if (m.siblings && m.match(s as Element)) {
1106
- note(s as Element, m.followed);
1107
- break;
1108
- }
1109
- }
1110
- }
1111
- }
1112
- const parent: Element['parent'] = at.parent;
1113
- at = parent && isTag(parent as Element) ? (parent as Element) : null;
1114
- }
1372
+ this._hoverRules.anchors(el, into, (selector) => this._compile(selector));
1115
1373
  }
1116
1374
 
1117
- private _noteHover(selector: string): void {
1118
- if (!/:(?:hover|active)/i.test(selector)) return;
1119
- const found = pointerCompounds(selector);
1120
- if (found.nested) this.hoverLocal = false;
1121
- const followed = new Set(found.followed);
1122
- for (const c of found.compounds) {
1123
- this._hoverCompounds.set(
1124
- c,
1125
- followed.has(c) || (this._hoverCompounds.get(c) ?? false),
1126
- );
1127
- }
1128
- for (const { anchor, siblings } of found.has) {
1129
- const was = this._hoverAnchors.get(anchor);
1130
- this._hoverAnchors.set(anchor, {
1131
- siblings: siblings || (was?.siblings ?? false),
1132
- followed: followed.has(anchor) || (was?.followed ?? false),
1133
- });
1134
- }
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));
1135
1378
  }
1136
1379
 
1137
1380
  /** A selector compiled as a rule's is, or null where css-select refuses
@@ -1307,6 +1550,11 @@ export class Cascade {
1307
1550
  if (this._pointer.hovered.has(el)) key += '\u0001:hover';
1308
1551
  if (this._pointer.active.has(el)) key += '\u0001:active';
1309
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
+ }
1310
1558
  const attribs = el.attribs;
1311
1559
  for (const name in attribs) {
1312
1560
  const value = attribs[name];
@@ -1629,6 +1877,8 @@ export class Cascade {
1629
1877
  families: this._mapFamilies,
1630
1878
  lh: () => this._lineHeightOf(parentStyle),
1631
1879
  rlh: () => this._lineHeightOf(rootStyle),
1880
+ focusRing: this.look.focusRing,
1881
+ systemColors: this._systemColors,
1632
1882
  };
1633
1883
  // The family, the weight and the slant go with the size: together they
1634
1884
  // pick the face an `ex`, a `ch` or an `lh` in any declaration is
@@ -1744,7 +1994,9 @@ export class Cascade {
1744
1994
  style.alignBlocks = null;
1745
1995
  }
1746
1996
  settleClamp(style, parentStyle);
1997
+ settleAlign(style, parentStyle);
1747
1998
  settleOverflow(style);
1999
+ settleOutline(style, this.look, this.scale);
1748
2000
  settleContentVisibility(style);
1749
2001
  blockify(style, inFlexContainer);
1750
2002
  // a button is laid out as one, whatever `display` it was given
@@ -1855,12 +2107,24 @@ export class Cascade {
1855
2107
  * itself. The root's box has its paint, its opacity and whether it is
1856
2108
  * shown from its computed style, so those are left out for it.
1857
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
+ *
1858
2118
  * Asked as a drawing is first painted (`BoxTree.shapeStyler`), of the
1859
2119
  * rules in `_shapes` alone: a page of hundreds of icons pays for the ones
1860
2120
  * it shows, a look in the buckets for each of their elements where no
1861
2121
  * rule reaches them, and a document with no such rule nothing.
1862
2122
  */
1863
- shapeStyles(root: Element, style: ComputedStyle): ShapeStyles | null {
2123
+ shapeStyles(
2124
+ root: Element,
2125
+ style: ComputedStyle,
2126
+ copies: ShapeCopies | null = null,
2127
+ ): ShapeStyles | null {
1864
2128
  const index = this._shapes;
1865
2129
  if (!index.size) return null;
1866
2130
  // The rules kept by a class or an id are asked of the elements with
@@ -1870,9 +2134,9 @@ export class Cascade {
1870
2134
  // id it asks of an ancestor, which is looked for once for the drawing,
1871
2135
  // around it and in it, where matching the rule would look for it from
1872
2136
  // each of its elements. On a page with none of them around a drawing,
1873
- // nothing is matched at all.
2137
+ // nothing is matched at all. A copy's tree has nothing around it.
1874
2138
  const needs = (this._shapeNeeds ??= this._needsOfShapes());
1875
- const found = new Set<string>();
2139
+ let found = new Set<string>();
1876
2140
  const asks = needs.classes.size > 0 || needs.ids.size > 0;
1877
2141
  const note = (el: Element): void => {
1878
2142
  if (needs.ids.size) {
@@ -1907,43 +2171,58 @@ export class Cascade {
1907
2171
  (indexed.root ??= declaresRootShape(indexed.rule.declarations)) &&
1908
2172
  live(indexed);
1909
2173
 
1910
- let of: Map<Element, ShapeStyle> | null = null;
1911
- let key = '';
1912
- let ctx: ShapeContext | null = null;
1913
- // in document order, the root first: an element's place in it is what
1914
- // the key names it by, and its ancestors in the drawing are noted
1915
- // before it is asked of
1916
- const stack: Element[] = [root];
1917
- let place = -1;
1918
- while (stack.length) {
1919
- const el = stack.pop()!;
1920
- place += 1;
1921
- // what is in a `<foreignObject>` is not the drawing's to draw
1922
- if (el.name !== 'foreignobject' && el.name !== 'foreignObject') {
1923
- const children = el.children;
1924
- for (let i = children.length - 1; i >= 0; i -= 1) {
1925
- const child = children[i];
1926
- if (child.type === 'tag') stack.push(child as Element);
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;
1927
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);
1928
2217
  }
1929
- if (asks) note(el);
1930
- const tag = tagOf(el);
1931
- const asked = found.size
1932
- ? index.universal.length > 0 || index.byTag.has(tag)
1933
- : needs.universal || needs.tags.has(tag);
1934
- if (!asked && !index.names(el)) continue;
1935
- const candidates: Candidate[] = [];
1936
- this._matchInto(
1937
- index,
1938
- el,
1939
- candidates,
1940
- undefined,
1941
- el === root ? liveForRoot : live,
1942
- );
1943
- if (!candidates.length) continue;
1944
- const inline = attr(el, 'style');
1945
- if (inline) pushInlineShapes(candidates, inline);
1946
- candidates.sort(byCascade);
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 => {
1947
2226
  ctx ??= {
1948
2227
  color: style.color,
1949
2228
  scheme: style.colorScheme,
@@ -1954,13 +2233,14 @@ export class Cascade {
1954
2233
  vh: this.viewportHeight,
1955
2234
  scale: this.scale,
1956
2235
  initial: this.initial,
2236
+ systemColors: this._systemColors,
1957
2237
  },
1958
2238
  };
1959
2239
  let own: Record<string, string> | null = null;
1960
2240
  for (const c of candidates) {
1961
2241
  for (const d of pick(c)) {
1962
2242
  if (!isShapeProp(d.prop)) continue;
1963
- if (el === root && ROOT_BOX_PROPS.has(d.prop)) continue;
2243
+ if (isRoot && ROOT_BOX_PROPS.has(d.prop)) continue;
1964
2244
  // a `var()` with nothing to stand for it is invalid at
1965
2245
  // computed-value time, and the property as though `unset`
1966
2246
  const value = shapeValue(
@@ -1971,13 +2251,106 @@ export class Cascade {
1971
2251
  if (value !== null) (own ??= {})[d.prop] = value;
1972
2252
  }
1973
2253
  }
1974
- if (!own) continue;
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;
1975
2280
  (of ??= new Map()).set(el, own);
1976
- key += `${place}{`;
1977
- for (const prop in own) key += `${prop}:${own[prop]};`;
1978
- key += '}';
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);
1979
2352
  }
1980
- return of ? { of, key } : null;
2353
+ return of || used ? { of: of ?? NO_SHAPES, used, key } : null;
1981
2354
  }
1982
2355
 
1983
2356
  /** `ShapeNeeds`, and each such rule's own (`IndexedRule.needs`). */
@@ -2015,19 +2388,27 @@ export class Cascade {
2015
2388
  * nothing else, since no other rule's answer is the pointer's. An element
2016
2389
  * they answer the same for, under a parent whose style is what it was, has
2017
2390
  * the style it had — which is most of what a move reaches: the rows of a
2018
- * 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.
2019
2393
  */
2020
- pointerChanged(el: Element, was: PointerState): boolean {
2394
+ pointerChanged(
2395
+ el: Element,
2396
+ was: PointerState,
2397
+ wasFocus: FocusState = this._focus,
2398
+ ): boolean {
2021
2399
  const now = this._pointer;
2400
+ const nowFocus = this._focus;
2022
2401
  const after = this._pointerAnswers(el);
2023
2402
  this._pointer = was;
2403
+ this._focus = wasFocus;
2024
2404
  const before = this._pointerAnswers(el);
2025
2405
  this._pointer = now;
2406
+ this._focus = nowFocus;
2026
2407
  return before !== after;
2027
2408
  }
2028
2409
 
2029
- /** The ids of the rules testing the pointer that match `el` as things
2030
- * 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. */
2031
2412
  private _pointerAnswers(el: Element): string {
2032
2413
  let answers = '';
2033
2414
  const ask = (index: RuleIndex, name: string): void => {
@@ -2052,6 +2433,9 @@ export class Cascade {
2052
2433
  matched?: number[],
2053
2434
  /** The rules to try, where some are known not to match without it. */
2054
2435
  live?: (indexed: IndexedRule) => boolean,
2436
+ /** Whether `el` is matched where a `<use>` draws a copy of it
2437
+ * (`_copyTop`). */
2438
+ inCopy = false,
2055
2439
  ): void {
2056
2440
  // A media query's width is CSS pixels; the viewport is kept in device.
2057
2441
  const width = this.viewportWidth / this.scale;
@@ -2073,22 +2457,43 @@ export class Cascade {
2073
2457
  ) {
2074
2458
  continue;
2075
2459
  }
2076
- if (!indexed.compiled) {
2077
- indexed.compiled = true;
2078
- try {
2079
- indexed.match = compileSelector(
2080
- rule.selector,
2081
- this._adapter,
2082
- this._pseudos,
2083
- );
2084
- } catch {
2085
- // A selector this matcher does not know (`::-moz-…`, a CSS4 form
2086
- // it has not learnt) drops out of the cascade rather than out of
2087
- // the render.
2088
- 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
+ }
2089
2493
  }
2494
+ match = indexed.match;
2090
2495
  }
2091
- if (!indexed.match || !indexed.match(el)) continue;
2496
+ if (!match || !match(el)) continue;
2092
2497
  matched?.push(indexed.id);
2093
2498
  if (out === null) continue;
2094
2499
  const origin = rule.order < 0 ? Origin.UserAgent : Origin.Author;
@@ -2116,6 +2521,18 @@ export class Cascade {
2116
2521
  this._matchInto(this._index, el, out, matched);
2117
2522
 
2118
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
+ }
2119
2536
  if (hints.length) {
2120
2537
  out.push({
2121
2538
  origin: Origin.Presentation,
@@ -2180,9 +2597,18 @@ function dropPaletteChrome(out: Candidate[]): void {
2180
2597
  }
2181
2598
  if (!chrome) return;
2182
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 = '';
2183
2603
  for (let j = i; j < out.length && !styled; j += 1) {
2184
- 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
+ }
2185
2610
  }
2611
+ if (appearance === 'none') styled = true;
2186
2612
  if (!styled) return;
2187
2613
  let kept = 0;
2188
2614
  for (const c of out) {
@@ -2195,8 +2621,9 @@ function dropPaletteChrome(out: Candidate[]): void {
2195
2621
  }
2196
2622
 
2197
2623
  /** Whether a declaration styles what a control's native look draws: Blink's
2198
- * `is_background` and `is_border` properties and their shorthands, and an
2199
- * `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`). */
2200
2627
  function stylesChrome(d: Declaration): boolean {
2201
2628
  const prop = d.prop;
2202
2629
  if (prop.startsWith('background')) {
@@ -2205,10 +2632,7 @@ function stylesChrome(d: Declaration): boolean {
2205
2632
  if (prop.startsWith('border')) {
2206
2633
  return prop !== 'border-collapse' && prop !== 'border-spacing';
2207
2634
  }
2208
- return (
2209
- (prop === 'appearance' || prop === '-webkit-appearance') &&
2210
- d.value.trim().toLowerCase() === 'none'
2211
- );
2635
+ return false;
2212
2636
  }
2213
2637
 
2214
2638
  /** An implied element's style as the root box's: its look, not its role —
@@ -2224,18 +2648,22 @@ function asRoot(style: ComputedStyle): ComputedStyle {
2224
2648
  * fragment, whose style the root box takes (`rootStyle`), and never a
2225
2649
  * fragment's top-level elements, which css-select would take for it.
2226
2650
  *
2227
- * `:focus` and its two kin match nothing: no element of the document takes
2228
- * the focus — a control's widget does, beside it. css-select has none of
2229
- * the three and throws on them, which dropped every rule that named one,
2230
- * `:not(:focus)` among them: Wikipedia's skip link hides with
2231
- * `.mw-jump-link:not(:focus)`, and without it stood in the flow at the top
2232
- * 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. */
2233
2658
  const PSEUDOS = {
2234
2659
  root: (el: Element) =>
2235
2660
  el.name === 'html' && !(el.parent && isTag(el.parent as Element)),
2236
- focus: () => false,
2237
- 'focus-visible': () => false,
2238
- '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,
2239
2667
  // css-select's, but its ranges and tags lower-cased as ASCII has it, and
2240
2668
  // no other script (CSS 2.1 4.1.3): Unicode's took `:lang(\u212Al)`, a
2241
2669
  // Kelvin sign for the K, for `:lang(kl)`
@@ -2245,28 +2673,32 @@ const PSEUDOS = {
2245
2673
  .map((range) => range.trim().replace(/^['"]|['"]$/g, ''))
2246
2674
  .filter((range) => range.length > 0)
2247
2675
  .map((range) => asciiLower(range).split('-'));
2248
- let root = el;
2249
- for (let node: Element | null = el; node;) {
2250
- const value = node.attribs['xml:lang'] ?? node.attribs.lang;
2251
- if (value != null) {
2252
- if (!value) return ranges.some((range) => range[0] === '');
2253
- const tag = asciiLower(value).split('-');
2254
- return ranges.some((range) => langRangeMatches(tag, range));
2255
- }
2256
- root = node;
2257
- const parent: Element['parent'] = node.parent;
2258
- node = parent && isTag(parent as Element) ? (parent as Element) : null;
2259
- }
2260
- // with no element saying, the document's, as its `<meta>` sets it —
2261
- // searched from the document, where a fragment's `<meta>` is a
2262
- // sibling of what it covers
2263
- const pragma = pragmaLanguage(root.parent ?? root);
2264
- if (!pragma) return ranges.some((range) => range[0] === '');
2265
- 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('-');
2266
2679
  return ranges.some((range) => langRangeMatches(tag, range));
2267
2680
  },
2268
2681
  };
2269
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
+
2270
2702
  /** A length in a unit of the viewport: a number, then the unit, and no
2271
2703
  * more of a name after it. */
2272
2704
  const VIEWPORT_UNIT = /\d[sld]?v(?:w|h|i|b|min|max)(?![\w-])/i;
@@ -2322,6 +2754,33 @@ function pragmaLanguage(root: { children: unknown[] }): string {
2322
2754
 
2323
2755
  const PRAGMA_LANGUAGE = new WeakMap<object, string>();
2324
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
+
2325
2784
  /** RFC 4647's extended filtering, of a tag's subtags by a range's, as
2326
2785
  * css-select does it. */
2327
2786
  function langRangeMatches(tag: string[], range: string[]): boolean {
@@ -2360,6 +2819,23 @@ function setsContent(c: Candidate): boolean {
2360
2819
  /** Whether a rule's declarations set a custom property, by the array. */
2361
2820
  const CUSTOM_IN = new WeakMap<Declaration[], boolean>();
2362
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
+
2363
2839
  /** Whether declarations set a property a shape in a drawing has. */
2364
2840
  function declaresShape(declarations: readonly Declaration[]): boolean {
2365
2841
  for (const d of declarations) if (isShapeProp(d.prop)) return true;