@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
@@ -19,14 +20,15 @@ import { compile } from 'css-select';
19
20
  import * as DomUtils from 'domutils';
20
21
  import { Element as DomElement, isTag } from 'domhandler';
21
22
  import { attr, tagOf } from '../dom.js';
22
- import { svgSizeHint } from '../svg.js';
23
+ import { systemColorTable, usedColorScheme } from './color.js';
23
24
  import { asciiLower, escapeEnd, mediaMatches, readIdent, startsIdent, } from './parse.js';
24
- import { applyDeclaration, blockify, settleButton, settleContentVisibility, settleOverflow, settleClamp, copyStyle, decorate, inherit, initialOne, initialStyle, isInherited, } from './style.js';
25
+ import { applyDeclaration, blockify, settleAlign, settleButton, settleContentVisibility, settleOverflow, settleOutline, settleClamp, copyStyle, decorate, inherit, initialOne, initialStyle, isInherited, } from './style.js';
25
26
  import { parseDeclarations } from './parse.js';
26
27
  import { CounterStyles, counterStyleRule } from './counter-styles.js';
27
28
  import { PALETTE_CHROME } from './ua.js';
28
29
  import { viewportUnit } from './values.js';
29
30
  import { customProperties, substituteIn } from './vars.js';
31
+ import { ROOT_BOX_PROPS, SHAPE_TAGS, isShapeProp, isUse, shapeValue, useHref, } from './shapes.js';
30
32
  /** Where a declaration came from. Higher wins before specificity is asked. */
31
33
  var Origin;
32
34
  (function (Origin) {
@@ -79,10 +81,12 @@ class RuleIndex {
79
81
  /** Whether any rule in here is pointer-sensitive, so the renderer knows
80
82
  * whether a pointer move can change the cascade at all. */
81
83
  hoverSensitive = false;
82
- /** The rules that test `:hover`, in buckets of their own: the ones whose
83
- * answers a pointer move can change, which is all that is asked to know
84
- * whether an element's did (`Cascade.pointerChanged`). Null until there
85
- * is one. */
84
+ /** …and focus-sensitive, for a widget taking the focus or giving it up. */
85
+ focusSensitive = false;
86
+ /** The rules that test `:hover` or the focus, in buckets of their own:
87
+ * the ones whose answers a pointer move or a focus change can change,
88
+ * which is all that is asked to know whether an element's did
89
+ * (`Cascade.pointerChanged`). Null until there is one. */
86
90
  pointer = null;
87
91
  /** The buckets holding a rule an element cannot share its style under
88
92
  * (`UNSHAREABLE`): the ids, classes and tags whose elements compute
@@ -104,6 +108,10 @@ class RuleIndex {
104
108
  reaches(el) {
105
109
  if (this.universal.length || this.byTag.has(tagOf(el)))
106
110
  return true;
111
+ return this.names(el);
112
+ }
113
+ /** Whether a bucket here is for `el`'s id, or for a class of its. */
114
+ names(el) {
107
115
  if (this.byId.size) {
108
116
  const id = attr(el, 'id');
109
117
  if (id && this.byId.has(id))
@@ -120,18 +128,24 @@ class RuleIndex {
120
128
  }
121
129
  return false;
122
130
  }
123
- add(rule) {
131
+ add(rule, key = rightmostKey(rule.selector)) {
124
132
  this.size += 1;
125
133
  const indexed = {
126
134
  rule,
127
135
  match: null,
128
136
  compiled: false,
129
137
  id: this._nextId++,
138
+ needs: null,
139
+ root: null,
140
+ inCopy: null,
141
+ inCopyCompiled: false,
130
142
  };
131
143
  if (rule.selector.includes(':hover') || rule.selector.includes(':active')) {
132
144
  this.hoverSensitive = true;
133
145
  }
134
- const key = rightmostKey(rule.selector);
146
+ const focus = FOCUS.test(rule.selector);
147
+ if (focus)
148
+ this.focusSensitive = true;
135
149
  if (UNSHAREABLE.test(rule.selector)) {
136
150
  if (key.kind === 'id')
137
151
  this.ownStyleIds.add(key.name);
@@ -143,7 +157,7 @@ class RuleIndex {
143
157
  this.ownStyleEverywhere = true;
144
158
  }
145
159
  this._bucket(key).push(indexed);
146
- if (HOVER.test(rule.selector)) {
160
+ if (focus || HOVER.test(rule.selector)) {
147
161
  this.pointer ??= new RuleIndex();
148
162
  this.pointer.size += 1;
149
163
  this.pointer._bucket(key).push(indexed);
@@ -216,8 +230,15 @@ function splitPseudoElement(rule) {
216
230
  * handed none), so a selector that tests only it never changes as the
217
231
  * pointer moves — Wikipedia's buttons' `:focus:not(:active)` among them.
218
232
  * A press that sets it would have to be counted here too.
233
+ *
234
+ * The focus is the other state a change of reaches only some elements, and
235
+ * `kind` says which is asked for: `FOCUS_STATE` finds the compounds that
236
+ * test `:focus`, `:focus-visible` or `:focus-within` the same way. Either
237
+ * kind leaves the other's pseudo-classes out of a compound as well as its
238
+ * own, so what is left matches wherever the compound could, in any state —
239
+ * `a:focus:hover` is a compound of `a`'s, for both.
219
240
  */
220
- export function pointerCompounds(selector) {
241
+ export function pointerCompounds(selector, kind = HOVER_STATE) {
221
242
  const out = {
222
243
  compounds: [],
223
244
  siblings: false,
@@ -225,9 +246,21 @@ export function pointerCompounds(selector) {
225
246
  has: [],
226
247
  followed: [],
227
248
  };
228
- scanComplex(selector, out);
249
+ scanComplex(selector, out, kind);
229
250
  return out;
230
251
  }
252
+ /** The pointer's: `:hover`, and `:active`, which nothing sets. */
253
+ export const HOVER_STATE = {
254
+ mentions: /:(?:hover|active)/i,
255
+ any: /:hover(?![\w-])/i,
256
+ own: /^:hover$/i,
257
+ };
258
+ /** The focus's: `:focus`, `:focus-visible` and `:focus-within`. */
259
+ export const FOCUS_STATE = {
260
+ mentions: /:focus/i,
261
+ any: /:focus(?:-visible|-within)?(?![\w-])/i,
262
+ own: /^:focus/i,
263
+ };
231
264
  /** The functions that take a selector list and match the element they are
232
265
  * written on (Selectors 4, 4.2 to 4.4), under the names css-select and
233
266
  * older sheets know them by. */
@@ -321,11 +354,11 @@ function closeOf(text, open) {
321
354
  return text.length;
322
355
  }
323
356
  /** `pointerCompounds` for one complex selector, into `out`. */
324
- function scanComplex(selector, out) {
357
+ function scanComplex(selector, out, kind) {
325
358
  for (const { text, next } of compoundsOf(selector)) {
326
359
  const compoundsFrom = out.compounds.length;
327
360
  const hasFrom = out.has.length;
328
- const found = scanCompound(text, out);
361
+ const found = scanCompound(text, out, kind);
329
362
  const sibling = next === '+' || next === '~';
330
363
  if (found.args.length) {
331
364
  out.has.push({
@@ -355,9 +388,10 @@ function scanComplex(selector, out) {
355
388
  * it (`bare`), whether it tests the element's own hover (`pointer`), the
356
389
  * arguments of each `:has()` of the element's that tests it below (`args`),
357
390
  * and whether a selector nested in it named another element's (`below`),
358
- * whose compounds went into `out`.
391
+ * whose compounds went into `out`. Of the focus, likewise, for
392
+ * `FOCUS_STATE`.
359
393
  */
360
- function scanCompound(text, out) {
394
+ function scanCompound(text, out, kind) {
361
395
  let bare = '';
362
396
  let pointer = false;
363
397
  let below = false;
@@ -385,9 +419,9 @@ function scanCompound(text, out) {
385
419
  else if (c === ']')
386
420
  bracket = Math.max(0, bracket - 1);
387
421
  else if (c === ':' && bracket === 0 && text[i + 1] !== ':') {
388
- const m = POINTER_PSEUDO_AT.exec(text.slice(i));
422
+ const m = STATE_PSEUDO_AT.exec(text.slice(i));
389
423
  if (m) {
390
- if (m[0].length === 6)
424
+ if (kind.own.test(m[0]))
391
425
  pointer = true;
392
426
  i += m[0].length - 1;
393
427
  continue;
@@ -399,7 +433,7 @@ function scanCompound(text, out) {
399
433
  const arg = text.slice(open + 1, close);
400
434
  const whole = text.slice(i, close + 1);
401
435
  i = close;
402
- if (!HOVER.test(arg)) {
436
+ if (!kind.any.test(arg)) {
403
437
  bare += whole;
404
438
  continue;
405
439
  }
@@ -421,16 +455,16 @@ function scanCompound(text, out) {
421
455
  const own = [];
422
456
  let any = false;
423
457
  for (const entry of selectorsOf(arg)) {
424
- if (!HOVER.test(entry))
458
+ if (!kind.any.test(entry))
425
459
  continue;
426
460
  const parts = compoundsOf(entry);
427
461
  if (parts.length !== 1) {
428
- scanComplex(entry, out);
462
+ scanComplex(entry, out, kind);
429
463
  below = true;
430
464
  continue;
431
465
  }
432
466
  // of this element itself
433
- const one = scanCompound(parts[0].text, out);
467
+ const one = scanCompound(parts[0].text, out, kind);
434
468
  if (one.below)
435
469
  below = true;
436
470
  if (!one.pointer && !one.args.length)
@@ -462,26 +496,33 @@ function scanCompound(text, out) {
462
496
  * start of a string. */
463
497
  const FUNCTION_AT = /^:([\w-]+)\(/;
464
498
  /** `:hover` anywhere in a selector. */
465
- const HOVER = /:hover(?![\w-])/i;
466
- /** `:hover` or `:active` at the start of a string, and nothing longer. */
467
- const POINTER_PSEUDO_AT = /^:(?:hover|active)(?![\w-])/i;
468
- /** `:hover` or `:active` anywhere in a selector. */
469
- const POINTER_PSEUDO = /:(?:hover|active)(?![\w-])/i;
499
+ const HOVER = HOVER_STATE.any;
500
+ /** `:focus`, `:focus-visible` or `:focus-within` anywhere in a selector. */
501
+ const FOCUS = FOCUS_STATE.any;
502
+ /** A pseudo-class of a state this renderer keeps — `:hover`, `:active`,
503
+ * `:focus` and its two kin — at the start of a string, and nothing
504
+ * longer. */
505
+ const STATE_PSEUDO_AT = /^:(?:hover|active|focus(?:-visible|-within)?)(?![\w-])/i;
506
+ /** Any of them anywhere in a selector. */
507
+ const STATE_PSEUDO = /:(?:hover|active|focus(?:-visible|-within)?)(?![\w-])/i;
470
508
  /**
471
509
  * A selector compiled to a matcher over this adapter. css-select keeps the
472
510
  * answers of a `:has()` above the subject, or of the ancestors a
473
511
  * descendant combinator tried under one, for as long as the matcher lives
474
512
  * (`cacheResults`) — which is the cascade's life, and a pointer move is no
475
- * new cascade. So a selector that tests the pointer keeps none: with them,
476
- * `#box:has(a:hover) .m` answered what it did before the first move, for
477
- * good.
513
+ * new cascade. So a selector that tests the pointer or the focus keeps
514
+ * none: with them, `#box:has(a:hover) .m` answered what it did before the
515
+ * first move, for good.
478
516
  */
479
- function compileSelector(selector, adapter) {
517
+ function compileSelector(selector, adapter, pseudos = PSEUDOS,
518
+ /** Whether what it answers may be kept: not where the adapter's tree
519
+ * moves under it, as a `<use>`'s copy's does (`Cascade.shapeStyles`). */
520
+ cache = true) {
480
521
  return compile(noEmptyWords(selector), {
481
522
  adapter,
482
523
  xmlMode: false,
483
- pseudos: PSEUDOS,
484
- cacheResults: !POINTER_PSEUDO.test(selector),
524
+ pseudos,
525
+ cacheResults: cache && !STATE_PSEUDO.test(selector),
485
526
  });
486
527
  }
487
528
  function mapBucket(map, key) {
@@ -534,7 +575,10 @@ function rightmostKey(selector) {
534
575
  start = i + 1;
535
576
  }
536
577
  }
537
- const compound = selector.slice(start);
578
+ return compoundKey(selector.slice(start));
579
+ }
580
+ /** A compound selector's most selective key (`rightmostKey`). */
581
+ function compoundKey(compound) {
538
582
  let id = null;
539
583
  let cls = null;
540
584
  let tag = null;
@@ -577,6 +621,53 @@ function rightmostKey(selector) {
577
621
  return { kind: 'tag', name: tag };
578
622
  return { kind: 'any', name: '' };
579
623
  }
624
+ /**
625
+ * The ids and classes a selector asks of its subject's ancestors: for each
626
+ * compound a descendant or a child combinator follows, its id, or else its
627
+ * first class, as `#name` or `.name`. Such a compound is an ancestor of
628
+ * the subject — one a sibling combinator follows is a sibling of one — so
629
+ * an element with no ancestor of some such name is not the rule's, which
630
+ * is known without matching it. A compound with neither asks nothing.
631
+ */
632
+ function ancestorKeys(selector) {
633
+ const keys = [];
634
+ const compounds = compoundsOf(selector);
635
+ for (let i = 0; i < compounds.length - 1; i += 1) {
636
+ const { text, next } = compounds[i];
637
+ if (next !== ' ' && next !== '>')
638
+ continue;
639
+ const key = compoundKey(text);
640
+ if (key.kind === 'id')
641
+ keys.push(`#${key.name}`);
642
+ else if (key.kind === 'class')
643
+ keys.push(`.${key.name}`);
644
+ }
645
+ return keys;
646
+ }
647
+ /** A drawing none of whose own elements a rule reaches. */
648
+ const NO_SHAPES = new Map();
649
+ /**
650
+ * What `Cascade.shapeStyles` keeps for the drawings of one build of the
651
+ * boxes (`BoxTree.shapeCopies`): the document's elements by their ids, and
652
+ * what the rules matched in the copy of each element a `<use>` names —
653
+ * the same whichever `<use>` makes it, so a page of hundreds of icons from
654
+ * one sprite matches each symbol once — and what that comes to for a
655
+ * drawing of each style. A build is made again whenever the document, its
656
+ * style sheets or a viewport a value reads change, and a pointer move
657
+ * changes nothing a copy is matched by: nothing in one is under the
658
+ * pointer, and a drawing whose style it changes has another style.
659
+ */
660
+ export class ShapeCopies {
661
+ byId;
662
+ /** @internal */
663
+ matched = new Map();
664
+ /** @internal What each copy comes to for a drawing of a style, and the
665
+ * key that names it. */
666
+ resolved = new WeakMap();
667
+ constructor(byId) {
668
+ this.byId = byId;
669
+ }
670
+ }
580
671
  function balancedEnd(text, from, open, close) {
581
672
  let depth = 0;
582
673
  for (let i = from; i < text.length; i += 1) {
@@ -591,6 +682,112 @@ function balancedEnd(text, from, open, close) {
591
682
  return text.length;
592
683
  }
593
684
  const NO_POINTER = { hovered: new Set(), active: new Set() };
685
+ export const NO_FOCUS = {
686
+ element: null,
687
+ visible: false,
688
+ within: new Set(),
689
+ };
690
+ /**
691
+ * What the selectors that test one state — the pointer, or the focus — say
692
+ * of the elements a change of it can restyle: the compounds that test it,
693
+ * each without it and with whether a sibling combinator follows it, and
694
+ * the anchors of the `:has()`s that test it (`pointerCompounds`).
695
+ */
696
+ class StateRules {
697
+ _kind;
698
+ /** The compounds, each without the state, and their matchers, compiled
699
+ * when a change first asks (`touches`). */
700
+ _compounds = new Map();
701
+ _matchers = null;
702
+ /** Whether a change can be restyled where it happened
703
+ * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
704
+ * state inside a functional pseudo-class, which reaches elements no
705
+ * compound names. */
706
+ local = true;
707
+ /** The compounds whose `:has()` tests the state, each without it,
708
+ * whether its argument starts at a sibling, and whether a sibling
709
+ * combinator follows it (`anchors`). */
710
+ _anchors = new Map();
711
+ _anchorMatchers = null;
712
+ constructor(_kind) {
713
+ this._kind = _kind;
714
+ }
715
+ note(selector) {
716
+ if (!this._kind.mentions.test(selector))
717
+ return;
718
+ const found = pointerCompounds(selector, this._kind);
719
+ if (found.nested)
720
+ this.local = false;
721
+ const followed = new Set(found.followed);
722
+ for (const c of found.compounds) {
723
+ this._compounds.set(c, followed.has(c) || (this._compounds.get(c) ?? false));
724
+ }
725
+ for (const { anchor, siblings } of found.has) {
726
+ const was = this._anchors.get(anchor);
727
+ this._anchors.set(anchor, {
728
+ siblings: siblings || (was?.siblings ?? false),
729
+ followed: followed.has(anchor) || (was?.followed ?? false),
730
+ });
731
+ }
732
+ }
733
+ /** `Cascade.hoverTouches`, for this state. */
734
+ touches(el, compile) {
735
+ // the followed ones first, so the first that matches is the answer
736
+ this._matchers ??= [...this._compounds]
737
+ .sort(([, a], [, b]) => Number(b) - Number(a))
738
+ .flatMap(([compound, followed]) => {
739
+ const match = compile(compound);
740
+ return match ? [{ match, followed }] : [];
741
+ });
742
+ for (const { match, followed } of this._matchers) {
743
+ if (match(el))
744
+ return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
745
+ }
746
+ return HOVER_UNTOUCHED;
747
+ }
748
+ /** `Cascade.hoverAnchors`, for this state. */
749
+ anchors(el, into, compile) {
750
+ if (!this._anchors.size)
751
+ return;
752
+ this._anchorMatchers ??= [...this._anchors]
753
+ .sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
754
+ .flatMap(([anchor, { siblings, followed }]) => {
755
+ const match = compile(anchor);
756
+ return match ? [{ match, siblings, followed }] : [];
757
+ });
758
+ const note = (at, followed) => {
759
+ if (followed)
760
+ into.set(at, HOVER_FOLLOWED);
761
+ else if (!into.has(at))
762
+ into.set(at, HOVER_TOUCHED);
763
+ };
764
+ const siblings = this._anchorMatchers.some((m) => m.siblings);
765
+ for (let at = el; at;) {
766
+ if (at !== el) {
767
+ for (const { match, followed } of this._anchorMatchers) {
768
+ if (match(at)) {
769
+ note(at, followed);
770
+ break;
771
+ }
772
+ }
773
+ }
774
+ if (siblings) {
775
+ for (let s = at.prev; s; s = s.prev) {
776
+ if (!isTag(s))
777
+ continue;
778
+ for (const m of this._anchorMatchers) {
779
+ if (m.siblings && m.match(s)) {
780
+ note(s, m.followed);
781
+ break;
782
+ }
783
+ }
784
+ }
785
+ }
786
+ const parent = at.parent;
787
+ at = parent && isTag(parent) ? parent : null;
788
+ }
789
+ }
790
+ }
594
791
  function faceOf(style) {
595
792
  return {
596
793
  family: style.fontFamily,
@@ -618,23 +815,31 @@ export class Cascade {
618
815
  marker: new RuleIndex(),
619
816
  selection: new RuleIndex(),
620
817
  };
818
+ /** The author's rules that could style a shape in a drawing: the ones
819
+ * that declare a property a shape has (`shapes.ts`) and whose subject
820
+ * is no element of HTML's alone. They are in `_index` too, for the
821
+ * elements that have boxes; these are asked of the ones in an inline
822
+ * `<svg>`, which have none (`shapeStyles`). */
823
+ _shapes = new RuleIndex();
824
+ _shapeNeeds = null;
621
825
  _adapter;
622
- /** The compounds of the selectors that test the pointer, each without
623
- * its `:hover` or `:active` (`pointerCompounds`), and their matchers,
624
- * compiled when a pointer move first asks (`hoverTouches`). */
625
- _hoverCompounds = new Map();
626
- _hoverMatchers = null;
627
- /** Whether a pointer move can be restyled where it happened
628
- * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
629
- * pointer inside a functional pseudo-class, which reaches elements no
630
- * compound names. */
631
- hoverLocal = true;
632
- /** The compounds whose `:has()` tests the pointer, each without it,
633
- * whether its argument starts at a sibling, and whether a sibling
634
- * combinator follows it (`hoverAnchors`). */
635
- _hoverAnchors = new Map();
636
- _anchorMatchers = null;
826
+ /**
827
+ * The tree a `<use>`'s copy is in, for `_copyAdapter`: the element it is
828
+ * a copy of, which is its top (`ShapeStyles.used`). A copy is its own
829
+ * tree, a shadow tree of the `<use>`'s (SVG 2, 5.5.1), so a selector is
830
+ * matched against its original with nothing above that element and
831
+ * nothing beside it — the top of a copy is its tree's first child and
832
+ * its last, whichever of its parent's it is. And nothing in it is under
833
+ * the pointer, as nothing in a drawing is: it has no box to be.
834
+ */
835
+ _copyTop = null;
836
+ _copyAdapter;
837
+ /** What the selectors that test the pointer, and the ones that test the
838
+ * focus, say of the elements a change of it reaches. */
839
+ _hoverRules = new StateRules(HOVER_STATE);
840
+ _focusRules = new StateRules(FOCUS_STATE);
637
841
  _pointer = NO_POINTER;
842
+ _focus = NO_FOCUS;
638
843
  initial;
639
844
  look;
640
845
  /** Viewport width the media queries were evaluated at, in device pixels
@@ -681,7 +886,37 @@ export class Cascade {
681
886
  /** Whether any declaration has a length in `lh` or `rlh`: only then is
682
887
  * the line height settled ahead of the declarations that read it. */
683
888
  _lh = false;
684
- constructor(sheets, look, viewportWidth, viewportHeight, scale = 1, xHeight = null, zeroWidth = null, normalLine = null, families = null) {
889
+ /** The pseudo-classes css-select is handed: `:root` this cascade's, and
890
+ * the focus's three answered from `_focus`. */
891
+ _pseudos;
892
+ /** The colour schemes the page says it supports with a `<meta
893
+ * name="color-scheme">` (HTML 4.2.5.4), which its root element takes
894
+ * as it takes a presentational attribute: under any rule that sets its
895
+ * `color-scheme`. Set by the box builder from the document. */
896
+ pageColorScheme = null;
897
+ /** Each system colour as the `light-dark()` it is: the palette's where
898
+ * the element's scheme is the palette's, and Chrome's in an SVG image,
899
+ * which has none. */
900
+ _systemColors;
901
+ constructor(sheets, look, viewportWidth, viewportHeight, scale = 1, xHeight = null, zeroWidth = null, normalLine = null, families = null,
902
+ /** The document's element where it is not an `<html>`: an SVG image's
903
+ * `<svg>`, which is `:root` there (Selectors 4, 14.1). */
904
+ documentElement = null,
905
+ /** The element the document's URL names by its fragment, which is
906
+ * `:target` (Selectors 4, 9.1): an SVG image's, `image.svg#icon`. */
907
+ target = null) {
908
+ // css-select has none of the focus's three, and a pseudo-class is
909
+ // handed only the element, so they are this cascade's own closures
910
+ this._pseudos = {
911
+ ...PSEUDOS,
912
+ ...(documentElement && {
913
+ root: (el) => el === documentElement,
914
+ }),
915
+ ...(target && { target: (el) => el === target }),
916
+ focus: (el) => this._focus.element === el,
917
+ 'focus-visible': (el) => this._focus.visible && this._focus.element === el,
918
+ 'focus-within': (el) => this._focus.within.has(el),
919
+ };
685
920
  this._xHeightOf = xHeight;
686
921
  this._zeroWidthOf = zeroWidth;
687
922
  this._normalLineOf = normalLine;
@@ -690,6 +925,7 @@ export class Cascade {
690
925
  ? (list) => families.map(list)
691
926
  : undefined;
692
927
  this.look = look;
928
+ this._systemColors = systemColorTable(documentElement ? null : look);
693
929
  this.initial = initialStyle(look, scale);
694
930
  this.viewportWidth = viewportWidth;
695
931
  this.viewportHeight = viewportHeight;
@@ -712,9 +948,18 @@ export class Cascade {
712
948
  const pseudo = splitPseudoElement(rule);
713
949
  if (pseudo)
714
950
  this._pseudo[pseudo.which].add(pseudo.rule);
715
- else
716
- this._index.add(rule);
717
- this._noteHover((pseudo?.rule ?? rule).selector);
951
+ else {
952
+ const key = rightmostKey(rule.selector);
953
+ this._index.add(rule, key);
954
+ if (rule.order >= 0 &&
955
+ (key.kind !== 'tag' || SHAPE_TAGS.has(key.name)) &&
956
+ declaresShape(rule.declarations)) {
957
+ this._shapes.add(rule, key);
958
+ }
959
+ }
960
+ const selector = (pseudo?.rule ?? rule).selector;
961
+ this._hoverRules.note(selector);
962
+ this._focusRules.note(selector);
718
963
  }
719
964
  for (const bp of sheet.breakpoints)
720
965
  breakpoints.add(bp);
@@ -744,6 +989,14 @@ export class Cascade {
744
989
  isActive: (el) => this._pointer.active.has(el),
745
990
  isVisited: () => false,
746
991
  };
992
+ this._copyAdapter = {
993
+ ...this._adapter,
994
+ getParent: (el) => (el === this._copyTop ? null : DomUtils.getParent(el)),
995
+ getSiblings: (el) => el === this._copyTop ? [el] : DomUtils.getSiblings(el),
996
+ prevElementSibling: (el) => el === this._copyTop ? null : DomUtils.prevElementSibling(el),
997
+ isHovered: () => false,
998
+ isActive: () => false,
999
+ };
747
1000
  }
748
1001
  /** Whether declarations read the viewport's width or height (`vw`,
749
1002
  * `vh`, `vmin`, `vmax`), noted on the cascade. */
@@ -774,6 +1027,30 @@ export class Cascade {
774
1027
  setPointer(pointer) {
775
1028
  this._pointer = pointer;
776
1029
  }
1030
+ /** Whether a widget taking the focus or giving it up can change what this
1031
+ * cascade produces. */
1032
+ get focusSensitive() {
1033
+ return (this._index.focusSensitive ||
1034
+ this._pseudo.before.focusSensitive ||
1035
+ this._pseudo.after.focusSensitive ||
1036
+ this._pseudo['first-letter'].focusSensitive ||
1037
+ this._pseudo['first-line'].focusSensitive ||
1038
+ this._pseudo.marker.focusSensitive);
1039
+ }
1040
+ setFocus(focus) {
1041
+ this._focus = focus;
1042
+ }
1043
+ /** Whether a pointer move can be restyled where it happened
1044
+ * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
1045
+ * pointer inside a functional pseudo-class, which reaches elements no
1046
+ * compound names. */
1047
+ get hoverLocal() {
1048
+ return this._hoverRules.local;
1049
+ }
1050
+ /** …and a focus change. */
1051
+ get focusLocal() {
1052
+ return this._focusRules.local;
1053
+ }
777
1054
  /**
778
1055
  * Which pseudo-elements' rules test the pointer: none's
779
1056
  * (`HOVER_PSEUDO_NONE`); a `::before`'s or an `::after`'s only
@@ -783,13 +1060,19 @@ export class Cascade {
783
1060
  * move cannot.
784
1061
  */
785
1062
  get hoverPseudo() {
786
- if (this._pseudo['first-letter'].hoverSensitive ||
787
- this._pseudo['first-line'].hoverSensitive ||
788
- this._pseudo.marker.hoverSensitive) {
1063
+ return this._pseudoRules('hoverSensitive');
1064
+ }
1065
+ /** …and which test the focus. */
1066
+ get focusPseudo() {
1067
+ return this._pseudoRules('focusSensitive');
1068
+ }
1069
+ _pseudoRules(state) {
1070
+ if (this._pseudo['first-letter'][state] ||
1071
+ this._pseudo['first-line'][state] ||
1072
+ this._pseudo.marker[state]) {
789
1073
  return HOVER_PSEUDO_OTHER;
790
1074
  }
791
- return this._pseudo.before.hoverSensitive ||
792
- this._pseudo.after.hoverSensitive
1075
+ return this._pseudo.before[state] || this._pseudo.after[state]
793
1076
  ? HOVER_PSEUDO_GENERATED
794
1077
  : HOVER_PSEUDO_NONE;
795
1078
  }
@@ -804,18 +1087,12 @@ export class Cascade {
804
1087
  * table restyle the rows after it.
805
1088
  */
806
1089
  hoverTouches(el) {
807
- // the followed ones first, so the first that matches is the answer
808
- this._hoverMatchers ??= [...this._hoverCompounds]
809
- .sort(([, a], [, b]) => Number(b) - Number(a))
810
- .flatMap(([compound, followed]) => {
811
- const match = this._compile(compound);
812
- return match ? [{ match, followed }] : [];
813
- });
814
- for (const { match, followed } of this._hoverMatchers) {
815
- if (match(el))
816
- return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
817
- }
818
- return HOVER_UNTOUCHED;
1090
+ return this._hoverRules.touches(el, (selector) => this._compile(selector));
1091
+ }
1092
+ /** `hoverTouches` for the focus: whether `el` taking the focus or giving
1093
+ * it up, or its `:focus-within` changing, can change a style. */
1094
+ focusTouches(el) {
1095
+ return this._focusRules.touches(el, (selector) => this._compile(selector));
819
1096
  }
820
1097
  /**
821
1098
  * The elements whose `:has()` may have changed its answer as `el` was
@@ -826,69 +1103,17 @@ export class Cascade {
826
1103
  * siblings can change with it (`hoverTouches`).
827
1104
  */
828
1105
  hoverAnchors(el, into) {
829
- if (!this._hoverAnchors.size)
830
- return;
831
- this._anchorMatchers ??= [...this._hoverAnchors]
832
- .sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
833
- .flatMap(([anchor, { siblings, followed }]) => {
834
- const match = this._compile(anchor);
835
- return match ? [{ match, siblings, followed }] : [];
836
- });
837
- const note = (at, followed) => {
838
- if (followed)
839
- into.set(at, HOVER_FOLLOWED);
840
- else if (!into.has(at))
841
- into.set(at, HOVER_TOUCHED);
842
- };
843
- const siblings = this._anchorMatchers.some((m) => m.siblings);
844
- for (let at = el; at;) {
845
- if (at !== el) {
846
- for (const { match, followed } of this._anchorMatchers) {
847
- if (match(at)) {
848
- note(at, followed);
849
- break;
850
- }
851
- }
852
- }
853
- if (siblings) {
854
- for (let s = at.prev; s; s = s.prev) {
855
- if (!isTag(s))
856
- continue;
857
- for (const m of this._anchorMatchers) {
858
- if (m.siblings && m.match(s)) {
859
- note(s, m.followed);
860
- break;
861
- }
862
- }
863
- }
864
- }
865
- const parent = at.parent;
866
- at = parent && isTag(parent) ? parent : null;
867
- }
1106
+ this._hoverRules.anchors(el, into, (selector) => this._compile(selector));
868
1107
  }
869
- _noteHover(selector) {
870
- if (!/:(?:hover|active)/i.test(selector))
871
- return;
872
- const found = pointerCompounds(selector);
873
- if (found.nested)
874
- this.hoverLocal = false;
875
- const followed = new Set(found.followed);
876
- for (const c of found.compounds) {
877
- this._hoverCompounds.set(c, followed.has(c) || (this._hoverCompounds.get(c) ?? false));
878
- }
879
- for (const { anchor, siblings } of found.has) {
880
- const was = this._hoverAnchors.get(anchor);
881
- this._hoverAnchors.set(anchor, {
882
- siblings: siblings || (was?.siblings ?? false),
883
- followed: followed.has(anchor) || (was?.followed ?? false),
884
- });
885
- }
1108
+ /** `hoverAnchors` for the focus: `form:has(input:focus)`'s form. */
1109
+ focusAnchors(el, into) {
1110
+ this._focusRules.anchors(el, into, (selector) => this._compile(selector));
886
1111
  }
887
1112
  /** A selector compiled as a rule's is, or null where css-select refuses
888
1113
  * it, as a rule it refuses drops out of the cascade. */
889
1114
  _compile(selector) {
890
1115
  try {
891
- return compileSelector(selector, this._adapter);
1116
+ return compileSelector(selector, this._adapter, this._pseudos);
892
1117
  }
893
1118
  catch {
894
1119
  return null;
@@ -1053,6 +1278,11 @@ export class Cascade {
1053
1278
  if (this._pointer.active.has(el))
1054
1279
  key += '\u0001:active';
1055
1280
  }
1281
+ // `:focus-within` is not shared under (`UNSHAREABLE`), and the focus
1282
+ // is on one element
1283
+ if (this._focus.element === el && this._index.focusSensitive) {
1284
+ key += this._focus.visible ? '\u0001:focus-visible' : '\u0001:focus';
1285
+ }
1056
1286
  const attribs = el.attribs;
1057
1287
  for (const name in attribs) {
1058
1288
  const value = attribs[name];
@@ -1336,6 +1566,8 @@ export class Cascade {
1336
1566
  families: this._mapFamilies,
1337
1567
  lh: () => this._lineHeightOf(parentStyle),
1338
1568
  rlh: () => this._lineHeightOf(rootStyle),
1569
+ focusRing: this.look.focusRing,
1570
+ systemColors: this._systemColors,
1339
1571
  };
1340
1572
  // The family, the weight and the slant go with the size: together they
1341
1573
  // pick the face an `ex`, a `ch` or an `lh` in any declaration is
@@ -1450,7 +1682,9 @@ export class Cascade {
1450
1682
  style.alignBlocks = null;
1451
1683
  }
1452
1684
  settleClamp(style, parentStyle);
1685
+ settleAlign(style, parentStyle);
1453
1686
  settleOverflow(style);
1687
+ settleOutline(style, this.look, this.scale);
1454
1688
  settleContentVisibility(style);
1455
1689
  blockify(style, inFlexContainer);
1456
1690
  // a button is laid out as one, whatever `display` it was given
@@ -1527,6 +1761,304 @@ export class Cascade {
1527
1761
  // the root is still the initial containing block.
1528
1762
  return { style: asRoot(this.styleFor(synthetic, parent, false)), html };
1529
1763
  }
1764
+ /** Whether any rule could style a shape in a drawing: a document with
1765
+ * none asks nothing of its drawings (`shapeStyles`). */
1766
+ get stylesShapes() {
1767
+ return this._shapes.size > 0;
1768
+ }
1769
+ /** Whether a pointer move can change what a rule gives a shape in a
1770
+ * drawing. */
1771
+ get shapesFollowPointer() {
1772
+ return this._shapes.hoverSensitive;
1773
+ }
1774
+ /**
1775
+ * What the rules give the elements of an inline drawing, `root` and what
1776
+ * is in it, or null where none reaches one: the narrow cascade of
1777
+ * `shapes.ts`. `style` is the root's computed style — its `color`, its
1778
+ * custom properties and its font size are what a value in the drawing is
1779
+ * read against, as though every element in it had inherited them, which
1780
+ * all but one a rule gives its own has.
1781
+ *
1782
+ * An element's rules and its `style` attribute are put in cascade order,
1783
+ * and each property is the last valid declaration's. The presentation
1784
+ * attributes are below them all (SVG 2, 6.2), and `SvgView` reads those
1785
+ * itself. The root's box has its paint, its opacity and whether it is
1786
+ * shown from its computed style, so those are left out for it.
1787
+ *
1788
+ * What a `<use>` draws is a copy, styled in a tree of its own
1789
+ * (`ShapeStyles.used`): the element it names is looked for as `copyTree`
1790
+ * looks — the drawing's own first, then the document's — and what is in
1791
+ * it asked of where it stands in that tree, once a build for all the
1792
+ * `<use>`s that name it (`ShapeCopies`). An icon sprite's `<symbol>`,
1793
+ * outside the drawing in a hidden `<svg>` of them, is styled that way,
1794
+ * and so is one of the drawing's own.
1795
+ *
1796
+ * Asked as a drawing is first painted (`BoxTree.shapeStyler`), of the
1797
+ * rules in `_shapes` alone: a page of hundreds of icons pays for the ones
1798
+ * it shows, a look in the buckets for each of their elements where no
1799
+ * rule reaches them, and a document with no such rule nothing.
1800
+ */
1801
+ shapeStyles(root, style, copies = null) {
1802
+ const index = this._shapes;
1803
+ if (!index.size)
1804
+ return null;
1805
+ // The rules kept by a class or an id are asked of the elements with
1806
+ // that name. The rest — `.dark .logo path`, `.menu [aria-hidden]` — are
1807
+ // asked of every `<path>`, or of every element, and nearly all of them
1808
+ // are for some other part of the page: each says so by the class or the
1809
+ // id it asks of an ancestor, which is looked for once for the drawing,
1810
+ // around it and in it, where matching the rule would look for it from
1811
+ // each of its elements. On a page with none of them around a drawing,
1812
+ // nothing is matched at all. A copy's tree has nothing around it.
1813
+ const needs = (this._shapeNeeds ??= this._needsOfShapes());
1814
+ let found = new Set();
1815
+ const asks = needs.classes.size > 0 || needs.ids.size > 0;
1816
+ const note = (el) => {
1817
+ if (needs.ids.size) {
1818
+ const id = attr(el, 'id');
1819
+ if (id && needs.ids.has(id))
1820
+ found.add(`#${id}`);
1821
+ }
1822
+ if (needs.classes.size) {
1823
+ const className = attr(el, 'class');
1824
+ if (className) {
1825
+ for (const name of className.split(/\s+/)) {
1826
+ if (needs.classes.has(name))
1827
+ found.add(`.${name}`);
1828
+ }
1829
+ }
1830
+ }
1831
+ };
1832
+ if (asks) {
1833
+ let at = root.parent;
1834
+ while (at && at.type === 'tag') {
1835
+ note(at);
1836
+ at = at.parent;
1837
+ }
1838
+ }
1839
+ const live = (indexed) => {
1840
+ const keys = indexed.needs;
1841
+ if (keys)
1842
+ for (const key of keys)
1843
+ if (!found.has(key))
1844
+ return false;
1845
+ return true;
1846
+ };
1847
+ // The root's box has its paint from its computed style, so most of the
1848
+ // rules that name a drawing by its class — an icon set's — have
1849
+ // nothing else to say of it.
1850
+ const liveForRoot = (indexed) => (indexed.root ??= declaresRootShape(indexed.rule.declarations)) &&
1851
+ live(indexed);
1852
+ /** The elements under `top` and it, in document order, the top first:
1853
+ * its ancestors are noted before an element is asked of. Each with
1854
+ * the rules it matched, in cascade order, where it matched any. */
1855
+ const walk = (top, inCopy, each) => {
1856
+ const stack = [top];
1857
+ while (stack.length) {
1858
+ const el = stack.pop();
1859
+ // what is in a `<foreignObject>` is not the drawing's to draw
1860
+ if (el.name !== 'foreignobject' && el.name !== 'foreignObject') {
1861
+ const children = el.children;
1862
+ for (let i = children.length - 1; i >= 0; i -= 1) {
1863
+ const child = children[i];
1864
+ if (child.type === 'tag')
1865
+ stack.push(child);
1866
+ }
1867
+ }
1868
+ if (asks)
1869
+ note(el);
1870
+ const tag = tagOf(el);
1871
+ const asked = found.size
1872
+ ? index.universal.length > 0 || index.byTag.has(tag)
1873
+ : needs.universal || needs.tags.has(tag);
1874
+ if (!asked && !index.names(el)) {
1875
+ each(el, null);
1876
+ continue;
1877
+ }
1878
+ const candidates = [];
1879
+ this._matchInto(index, el, candidates, undefined, el === root && !inCopy ? liveForRoot : live, inCopy);
1880
+ if (candidates.length) {
1881
+ const inline = attr(el, 'style');
1882
+ if (inline)
1883
+ pushInlineShapes(candidates, inline);
1884
+ candidates.sort(byCascade);
1885
+ }
1886
+ each(el, candidates.length ? candidates : null);
1887
+ }
1888
+ };
1889
+ let ctx = null;
1890
+ /** What an element's rules come to, against the drawing's style. */
1891
+ const resolve = (candidates, isRoot) => {
1892
+ ctx ??= {
1893
+ color: style.color,
1894
+ scheme: style.colorScheme,
1895
+ units: {
1896
+ em: style.fontSize,
1897
+ rem: (this._root ?? this.initial).fontSize,
1898
+ vw: this.viewportWidth,
1899
+ vh: this.viewportHeight,
1900
+ scale: this.scale,
1901
+ initial: this.initial,
1902
+ systemColors: this._systemColors,
1903
+ },
1904
+ };
1905
+ let own = null;
1906
+ for (const c of candidates) {
1907
+ for (const d of pick(c)) {
1908
+ if (!isShapeProp(d.prop))
1909
+ continue;
1910
+ if (isRoot && ROOT_BOX_PROPS.has(d.prop))
1911
+ continue;
1912
+ // a `var()` with nothing to stand for it is invalid at
1913
+ // computed-value time, and the property as though `unset`
1914
+ const value = shapeValue(d.prop, d.vars ? (substituteIn(d.value, style.custom) ?? 'unset') : d.value, ctx);
1915
+ if (value !== null)
1916
+ (own ??= {})[d.prop] = value;
1917
+ }
1918
+ }
1919
+ return own;
1920
+ };
1921
+ // an element's place in the walks is what the key names it by
1922
+ let key = '';
1923
+ let place = -1;
1924
+ const named = (own, at) => {
1925
+ let part = `${at}{`;
1926
+ for (const prop in own)
1927
+ part += `${prop}:${own[prop]};`;
1928
+ return `${part}}`;
1929
+ };
1930
+ // (made as they are first needed: most drawings need none of them)
1931
+ let of = null;
1932
+ // the `<use>`s met, and the drawing's own elements by their ids, which
1933
+ // one looks in first
1934
+ let uses = null;
1935
+ let ids = null;
1936
+ walk(root, false, (el, candidates) => {
1937
+ place += 1;
1938
+ if (isUse(el))
1939
+ (uses ??= []).push(el);
1940
+ if (el !== root) {
1941
+ const id = el.attribs.id;
1942
+ if (id && !ids?.has(id))
1943
+ (ids ??= new Map()).set(id, el);
1944
+ }
1945
+ const own = candidates && resolve(candidates, el === root);
1946
+ if (!own)
1947
+ return;
1948
+ (of ??= new Map()).set(el, own);
1949
+ key += named(own, place);
1950
+ });
1951
+ if (!uses)
1952
+ return of ? { of, used: null, key } : null;
1953
+ // Each element a `<use>` names, in the tree of its copy: once however
1954
+ // many name it, and one a copy's `<use>` names in turn, however deep —
1955
+ // `copyTree` stops at a depth, and a `<use>` that names what it is in
1956
+ // finds it being walked. What is matched there is the same for every
1957
+ // drawing, and kept for the build (`ShapeCopies`); what it comes to is
1958
+ // the drawing's, since every property the copy inherits is the
1959
+ // `<use>`'s, and its custom properties and its colour are the
1960
+ // drawing's, as they are for the rest of it — kept too, for each
1961
+ // style a drawing has, which the icons of a page mostly share.
1962
+ const byId = copies?.byId ?? ((id) => elementById(root, id));
1963
+ const matched = copies?.matched ?? new Map();
1964
+ let styled = copies?.resolved.get(style);
1965
+ if (!styled) {
1966
+ styled = new Map();
1967
+ copies?.resolved.set(style, styled);
1968
+ }
1969
+ const match = (target) => {
1970
+ let copy = matched.get(target);
1971
+ if (copy)
1972
+ return copy;
1973
+ const rules = [];
1974
+ const inner = [];
1975
+ let size = 0;
1976
+ found = new Set();
1977
+ this._copyTop = target;
1978
+ try {
1979
+ walk(target, true, (el, candidates) => {
1980
+ if (isUse(el))
1981
+ inner.push(el);
1982
+ if (candidates)
1983
+ rules.push({ el, at: size, candidates });
1984
+ size += 1;
1985
+ });
1986
+ }
1987
+ finally {
1988
+ this._copyTop = null;
1989
+ }
1990
+ copy = { rules, size, uses: inner };
1991
+ matched.set(target, copy);
1992
+ return copy;
1993
+ };
1994
+ let used = null;
1995
+ let made = null;
1996
+ for (let i = 0; i < uses.length; i += 1) {
1997
+ const use = uses[i];
1998
+ const id = useHref(use);
1999
+ if (id === null)
2000
+ continue;
2001
+ const target = ids?.get(id) ?? byId(id);
2002
+ if (!target)
2003
+ continue;
2004
+ const copy = match(target);
2005
+ let done = styled.get(target);
2006
+ if (!done) {
2007
+ let into = null;
2008
+ let part = '';
2009
+ for (const { el, at, candidates } of copy.rules) {
2010
+ const own = resolve(candidates, false);
2011
+ if (!own)
2012
+ continue;
2013
+ (into ??= new Map()).set(el, own);
2014
+ part += named(own, at);
2015
+ }
2016
+ done = { into, key: part };
2017
+ styled.set(target, done);
2018
+ }
2019
+ if (!made?.has(target)) {
2020
+ (made ??= new Set()).add(target);
2021
+ for (const next of copy.uses)
2022
+ uses.push(next);
2023
+ // its place in the key is after what came before it
2024
+ if (done.key)
2025
+ key += `@${place + 1}:${done.key}`;
2026
+ place += copy.size;
2027
+ }
2028
+ if (done.into)
2029
+ (used ??= new Map()).set(use, done.into);
2030
+ }
2031
+ return of || used ? { of: of ?? NO_SHAPES, used, key } : null;
2032
+ }
2033
+ /** `ShapeNeeds`, and each such rule's own (`IndexedRule.needs`). */
2034
+ _needsOfShapes() {
2035
+ const index = this._shapes;
2036
+ const needs = {
2037
+ classes: new Set(),
2038
+ ids: new Set(),
2039
+ universal: false,
2040
+ tags: new Set(),
2041
+ };
2042
+ const read = (bucket) => {
2043
+ let free = false;
2044
+ for (const indexed of bucket) {
2045
+ const keys = ancestorKeys(indexed.rule.selector);
2046
+ indexed.needs = keys;
2047
+ if (!keys.length)
2048
+ free = true;
2049
+ for (const key of keys) {
2050
+ (key[0] === '#' ? needs.ids : needs.classes).add(key.slice(1));
2051
+ }
2052
+ }
2053
+ return free;
2054
+ };
2055
+ needs.universal = read(index.universal);
2056
+ for (const [tag, bucket] of index.byTag) {
2057
+ if (read(bucket))
2058
+ needs.tags.add(tag);
2059
+ }
2060
+ return needs;
2061
+ }
1530
2062
  /**
1531
2063
  * Whether the pointer's move from `was` to where it is changed which
1532
2064
  * rules match `el`, or its `::before`, `::after` or another of its
@@ -1534,18 +2066,22 @@ export class Cascade {
1534
2066
  * nothing else, since no other rule's answer is the pointer's. An element
1535
2067
  * they answer the same for, under a parent whose style is what it was, has
1536
2068
  * the style it had — which is most of what a move reaches: the rows of a
1537
- * hovered table body, what is in a hovered card.
2069
+ * hovered table body, what is in a hovered card. The focus's move from
2070
+ * `wasFocus` likewise, the rules that test it asked under both.
1538
2071
  */
1539
- pointerChanged(el, was) {
2072
+ pointerChanged(el, was, wasFocus = this._focus) {
1540
2073
  const now = this._pointer;
2074
+ const nowFocus = this._focus;
1541
2075
  const after = this._pointerAnswers(el);
1542
2076
  this._pointer = was;
2077
+ this._focus = wasFocus;
1543
2078
  const before = this._pointerAnswers(el);
1544
2079
  this._pointer = now;
2080
+ this._focus = nowFocus;
1545
2081
  return before !== after;
1546
2082
  }
1547
- /** The ids of the rules testing the pointer that match `el` as things
1548
- * stand, the element's and each pseudo-element's. */
2083
+ /** The ids of the rules testing the pointer or the focus that match
2084
+ * `el` as things stand, the element's and each pseudo-element's. */
1549
2085
  _pointerAnswers(el) {
1550
2086
  let answers = '';
1551
2087
  const ask = (index, name) => {
@@ -1564,7 +2100,12 @@ export class Cascade {
1564
2100
  }
1565
2101
  /** The rules of `index` that match `el`, pushed onto `out` as candidates,
1566
2102
  * and their ids onto `matched` in the order they were tried. */
1567
- _matchInto(index, el, out, matched) {
2103
+ _matchInto(index, el, out, matched,
2104
+ /** The rules to try, where some are known not to match without it. */
2105
+ live,
2106
+ /** Whether `el` is matched where a `<use>` draws a copy of it
2107
+ * (`_copyTop`). */
2108
+ inCopy = false) {
1568
2109
  // A media query's width is CSS pixels; the viewport is kept in device.
1569
2110
  const width = this.viewportWidth / this.scale;
1570
2111
  const height = this.viewportHeight / this.scale;
@@ -1572,23 +2113,43 @@ export class Cascade {
1572
2113
  if (!bucket)
1573
2114
  return;
1574
2115
  for (const indexed of bucket) {
2116
+ if (live !== undefined && !live(indexed))
2117
+ continue;
1575
2118
  const rule = indexed.rule;
1576
2119
  if (!mediaMatches(rule.media, width, this.look.colorScheme, height, this.scale)) {
1577
2120
  continue;
1578
2121
  }
1579
- if (!indexed.compiled) {
1580
- indexed.compiled = true;
1581
- try {
1582
- indexed.match = compileSelector(rule.selector, this._adapter);
2122
+ let match;
2123
+ if (inCopy) {
2124
+ if (!indexed.inCopyCompiled) {
2125
+ indexed.inCopyCompiled = true;
2126
+ try {
2127
+ // what it finds above an element depends on which copy it is
2128
+ // in, so none of it is kept
2129
+ indexed.inCopy = compileSelector(rule.selector, this._copyAdapter, this._pseudos, false);
2130
+ }
2131
+ catch {
2132
+ indexed.inCopy = null;
2133
+ }
1583
2134
  }
1584
- catch {
1585
- // A selector this matcher does not know (`::-moz-…`, a CSS4 form
1586
- // it has not learnt) drops out of the cascade rather than out of
1587
- // the render.
1588
- indexed.match = null;
2135
+ match = indexed.inCopy;
2136
+ }
2137
+ else {
2138
+ if (!indexed.compiled) {
2139
+ indexed.compiled = true;
2140
+ try {
2141
+ indexed.match = compileSelector(rule.selector, this._adapter, this._pseudos);
2142
+ }
2143
+ catch {
2144
+ // A selector this matcher does not know (`::-moz-…`, a CSS4
2145
+ // form it has not learnt) drops out of the cascade rather than
2146
+ // out of the render.
2147
+ indexed.match = null;
2148
+ }
1589
2149
  }
2150
+ match = indexed.match;
1590
2151
  }
1591
- if (!indexed.match || !indexed.match(el))
2152
+ if (!match || !match(el))
1592
2153
  continue;
1593
2154
  matched?.push(indexed.id);
1594
2155
  if (out === null)
@@ -1617,6 +2178,16 @@ export class Cascade {
1617
2178
  const out = [];
1618
2179
  this._matchInto(this._index, el, out, matched);
1619
2180
  const hints = presentationHints(el);
2181
+ // the page's colour schemes are its root's where no rule sets them
2182
+ if (this.pageColorScheme &&
2183
+ el.name === 'html' &&
2184
+ !(el.parent && isTag(el.parent))) {
2185
+ hints.push({
2186
+ prop: 'color-scheme',
2187
+ value: this.pageColorScheme,
2188
+ important: false,
2189
+ });
2190
+ }
1620
2191
  if (hints.length) {
1621
2192
  out.push({
1622
2193
  origin: Origin.Presentation,
@@ -1682,9 +2253,22 @@ function dropPaletteChrome(out) {
1682
2253
  if (!chrome)
1683
2254
  return;
1684
2255
  let styled = false;
2256
+ // `appearance` by the value that wins: `appearance: none; appearance:
2257
+ // auto` keeps the control's look, as a reset a page takes back does
2258
+ let appearance = '';
1685
2259
  for (let j = i; j < out.length && !styled; j += 1) {
1686
- styled = pick(out[j]).some(stylesChrome);
2260
+ for (const d of pick(out[j])) {
2261
+ if (d.prop === 'appearance' || d.prop === '-webkit-appearance') {
2262
+ const v = d.value.trim().toLowerCase();
2263
+ if (/^[a-z-]+$/.test(v))
2264
+ appearance = v;
2265
+ }
2266
+ else if (stylesChrome(d))
2267
+ styled = true;
2268
+ }
1687
2269
  }
2270
+ if (appearance === 'none')
2271
+ styled = true;
1688
2272
  if (!styled)
1689
2273
  return;
1690
2274
  let kept = 0;
@@ -1697,8 +2281,9 @@ function dropPaletteChrome(out) {
1697
2281
  out.length = kept;
1698
2282
  }
1699
2283
  /** Whether a declaration styles what a control's native look draws: Blink's
1700
- * `is_background` and `is_border` properties and their shorthands, and an
1701
- * `appearance` of `none`. */
2284
+ * `is_background` and `is_border` properties and their shorthands. An
2285
+ * `appearance` of `none` does too, where it is the one that wins
2286
+ * (`dropPaletteChrome`). */
1702
2287
  function stylesChrome(d) {
1703
2288
  const prop = d.prop;
1704
2289
  if (prop.startsWith('background')) {
@@ -1707,8 +2292,7 @@ function stylesChrome(d) {
1707
2292
  if (prop.startsWith('border')) {
1708
2293
  return prop !== 'border-collapse' && prop !== 'border-spacing';
1709
2294
  }
1710
- return ((prop === 'appearance' || prop === '-webkit-appearance') &&
1711
- d.value.trim().toLowerCase() === 'none');
2295
+ return false;
1712
2296
  }
1713
2297
  /** An implied element's style as the root box's: its look, not its role —
1714
2298
  * the root is still a block, in flow, and the initial containing block. */
@@ -1722,17 +2306,21 @@ function asRoot(style) {
1722
2306
  * fragment, whose style the root box takes (`rootStyle`), and never a
1723
2307
  * fragment's top-level elements, which css-select would take for it.
1724
2308
  *
1725
- * `:focus` and its two kin match nothing: no element of the document takes
1726
- * the focus — a control's widget does, beside it. css-select has none of
1727
- * the three and throws on them, which dropped every rule that named one,
1728
- * `:not(:focus)` among them: Wikipedia's skip link hides with
1729
- * `.mw-jump-link:not(:focus)`, and without it stood in the flow at the top
1730
- * of every page. */
2309
+ * `:focus` and its two kin are here as matching nothing, and each cascade
2310
+ * answers them from where the focus is (`Cascade.setFocus`): no element
2311
+ * of the document takes it itself — a control's widget does, beside it.
2312
+ * css-select has none of the three and throws on them, which dropped
2313
+ * every rule that named one, `:not(:focus)` among them: Wikipedia's skip
2314
+ * link hides with `.mw-jump-link:not(:focus)`, and without it stood in
2315
+ * the flow at the top of every page. */
1731
2316
  const PSEUDOS = {
1732
2317
  root: (el) => el.name === 'html' && !(el.parent && isTag(el.parent)),
1733
- focus: () => false,
1734
- 'focus-visible': () => false,
1735
- 'focus-within': () => false,
2318
+ focus: (_el) => false,
2319
+ 'focus-visible': (_el) => false,
2320
+ 'focus-within': (_el) => false,
2321
+ // css-select has none, and threw on one, so `:not(:target)` matched
2322
+ // nothing: a document is drawn at no fragment, an SVG image at its own
2323
+ target: (_el) => false,
1736
2324
  // css-select's, but its ranges and tags lower-cased as ASCII has it, and
1737
2325
  // no other script (CSS 2.1 4.1.3): Unicode's took `:lang(\u212Al)`, a
1738
2326
  // Kelvin sign for the K, for `:lang(kl)`
@@ -1742,29 +2330,32 @@ const PSEUDOS = {
1742
2330
  .map((range) => range.trim().replace(/^['"]|['"]$/g, ''))
1743
2331
  .filter((range) => range.length > 0)
1744
2332
  .map((range) => asciiLower(range).split('-'));
1745
- let root = el;
1746
- for (let node = el; node;) {
1747
- const value = node.attribs['xml:lang'] ?? node.attribs.lang;
1748
- if (value != null) {
1749
- if (!value)
1750
- return ranges.some((range) => range[0] === '');
1751
- const tag = asciiLower(value).split('-');
1752
- return ranges.some((range) => langRangeMatches(tag, range));
1753
- }
1754
- root = node;
1755
- const parent = node.parent;
1756
- node = parent && isTag(parent) ? parent : null;
1757
- }
1758
- // with no element saying, the document's, as its `<meta>` sets it —
1759
- // searched from the document, where a fragment's `<meta>` is a
1760
- // sibling of what it covers
1761
- const pragma = pragmaLanguage(root.parent ?? root);
1762
- if (!pragma)
2333
+ const value = languageOf(el);
2334
+ if (!value)
1763
2335
  return ranges.some((range) => range[0] === '');
1764
- const tag = asciiLower(pragma).split('-');
2336
+ const tag = asciiLower(value).split('-');
1765
2337
  return ranges.some((range) => langRangeMatches(tag, range));
1766
2338
  },
1767
2339
  };
2340
+ /**
2341
+ * An element's language: the `lang` or `xml:lang` of the nearest element
2342
+ * from it up that says one, or with none saying, the document's, as its
2343
+ * `<meta http-equiv="content-language">` sets it — searched from the
2344
+ * document, where a fragment's `<meta>` is a sibling of what it covers.
2345
+ * '' where none does, or the nearest says ''.
2346
+ */
2347
+ export function languageOf(el) {
2348
+ let root = el;
2349
+ for (let node = el; node;) {
2350
+ const value = node.attribs['xml:lang'] ?? node.attribs.lang;
2351
+ if (value != null)
2352
+ return value;
2353
+ root = node;
2354
+ const parent = node.parent;
2355
+ node = parent && isTag(parent) ? parent : null;
2356
+ }
2357
+ return pragmaLanguage(root.parent ?? root);
2358
+ }
1768
2359
  /** A length in a unit of the viewport: a number, then the unit, and no
1769
2360
  * more of a name after it. */
1770
2361
  const VIEWPORT_UNIT = /\d[sld]?v(?:w|h|i|b|min|max)(?![\w-])/i;
@@ -1815,6 +2406,34 @@ function pragmaLanguage(root) {
1815
2406
  return lang;
1816
2407
  }
1817
2408
  const PRAGMA_LANGUAGE = new WeakMap();
2409
+ /**
2410
+ * The page's supported colour schemes: the content of the first `<meta
2411
+ * name="color-scheme">` that is a `color-scheme` value (HTML 4.2.5.4), or
2412
+ * null. Looked for where a `<meta>` is, at the top of a fragment and in
2413
+ * the `<html>` and its `<head>`, and not through the body.
2414
+ */
2415
+ export function metaColorScheme(root) {
2416
+ const stack = [root];
2417
+ while (stack.length) {
2418
+ const node = stack.pop();
2419
+ for (const child of node.children) {
2420
+ if (!isTag(child))
2421
+ continue;
2422
+ const el = child;
2423
+ if (el.name === 'meta') {
2424
+ const name = el.attribs.name?.trim().toLowerCase();
2425
+ const content = el.attribs.content?.trim();
2426
+ if (name === 'color-scheme' && content && usedColorScheme(content)) {
2427
+ return content;
2428
+ }
2429
+ }
2430
+ else if (el.name === 'html' || el.name === 'head') {
2431
+ stack.push(el);
2432
+ }
2433
+ }
2434
+ }
2435
+ return null;
2436
+ }
1818
2437
  /** RFC 4647's extended filtering, of a tag's subtags by a range's, as
1819
2438
  * css-select does it. */
1820
2439
  function langRangeMatches(tag, range) {
@@ -1858,6 +2477,68 @@ function setsContent(c) {
1858
2477
  }
1859
2478
  /** Whether a rule's declarations set a custom property, by the array. */
1860
2479
  const CUSTOM_IN = new WeakMap();
2480
+ /** The document's first element with an id, looked for from one of its
2481
+ * elements: `shapeStyles`' answer where no index of them is handed in. */
2482
+ function elementById(from, id) {
2483
+ let top = from;
2484
+ while (top.parent)
2485
+ top = top.parent;
2486
+ const stack = [...top.children].reverse();
2487
+ for (let node = stack.pop(); node; node = stack.pop()) {
2488
+ if (node.type !== 'tag')
2489
+ continue;
2490
+ const el = node;
2491
+ if (el.attribs.id === id)
2492
+ return el;
2493
+ for (let i = el.children.length - 1; i >= 0; i -= 1) {
2494
+ stack.push(el.children[i]);
2495
+ }
2496
+ }
2497
+ return null;
2498
+ }
2499
+ /** Whether declarations set a property a shape in a drawing has. */
2500
+ function declaresShape(declarations) {
2501
+ for (const d of declarations)
2502
+ if (isShapeProp(d.prop))
2503
+ return true;
2504
+ return false;
2505
+ }
2506
+ /** Whether declarations set one a drawing's root does not have from its
2507
+ * box's style (`ROOT_BOX_PROPS`). */
2508
+ function declaresRootShape(declarations) {
2509
+ for (const d of declarations) {
2510
+ if (isShapeProp(d.prop) && !ROOT_BOX_PROPS.has(d.prop))
2511
+ return true;
2512
+ }
2513
+ return false;
2514
+ }
2515
+ /** A `style` attribute's declarations of a shape's properties, as
2516
+ * candidates: over every rule, and its `!important` ones over theirs. */
2517
+ function pushInlineShapes(out, inline) {
2518
+ const declarations = parseDeclarations(inline).filter((d) => isShapeProp(d.prop));
2519
+ const normal = declarations.filter((d) => !d.important);
2520
+ const important = declarations.filter((d) => d.important);
2521
+ if (normal.length) {
2522
+ out.push({
2523
+ origin: Origin.Inline,
2524
+ layer: null,
2525
+ specificity: 0,
2526
+ order: 0,
2527
+ declarations: normal,
2528
+ only: -1,
2529
+ });
2530
+ }
2531
+ if (important.length) {
2532
+ out.push({
2533
+ origin: Origin.InlineImportant,
2534
+ layer: null,
2535
+ specificity: 0,
2536
+ order: 0,
2537
+ declarations: important,
2538
+ only: -1,
2539
+ });
2540
+ }
2541
+ }
1861
2542
  function pick(c) {
1862
2543
  return c.only < 0 ? c.declarations : [c.declarations[c.only]];
1863
2544
  }
@@ -2252,6 +2933,23 @@ function lengthAttr(value) {
2252
2933
  const n = parseFloat(v);
2253
2934
  return Number.isFinite(n) ? `${n}px` : v;
2254
2935
  }
2936
+ /** A number and a unit, as an SVG's `width` and `height` are written. */
2937
+ const SVG_LENGTH = /^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*([a-z]*)\s*$/i;
2938
+ /**
2939
+ * The `width`/`height` of an inline `<svg>` as CSS: they are presentation
2940
+ * attributes for the properties of the same names (SVG 2, 5.1.1), so
2941
+ * `height="50%"` is a percentage of the containing block like a style's.
2942
+ * A bare number is pixels; null for what CSS would not parse.
2943
+ */
2944
+ function svgSizeHint(value) {
2945
+ const v = value.trim();
2946
+ if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?%$/i.test(v))
2947
+ return v;
2948
+ const m = SVG_LENGTH.exec(v);
2949
+ if (!m)
2950
+ return null;
2951
+ return m[2] ? v : `${parseFloat(m[1])}px`;
2952
+ }
2255
2953
  function closestTable(el) {
2256
2954
  let node = el.parent;
2257
2955
  while (node) {