@react-x11/components 0.12.0 → 0.14.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 (150) hide show
  1. package/dist/html/controls.d.ts +41 -5
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +85 -57
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +134 -20
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +545 -126
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +18 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +27 -0
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/parse.d.ts +47 -6
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +336 -30
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/style.d.ts +142 -15
  18. package/dist/html/css/style.d.ts.map +1 -1
  19. package/dist/html/css/style.js +563 -147
  20. package/dist/html/css/style.js.map +1 -1
  21. package/dist/html/css/transform.d.ts +85 -0
  22. package/dist/html/css/transform.d.ts.map +1 -0
  23. package/dist/html/css/transform.js +425 -0
  24. package/dist/html/css/transform.js.map +1 -0
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -9
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +4 -0
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js.map +1 -1
  32. package/dist/html/dom.d.ts.map +1 -1
  33. package/dist/html/dom.js +3 -1
  34. package/dist/html/dom.js.map +1 -1
  35. package/dist/html/fonts.d.ts +78 -4
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +373 -19
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/form.d.ts +200 -0
  40. package/dist/html/form.d.ts.map +1 -0
  41. package/dist/html/form.js +801 -0
  42. package/dist/html/form.js.map +1 -0
  43. package/dist/html/index.d.ts +12 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +30 -301
  46. package/dist/html/index.js.map +1 -1
  47. package/dist/html/layout/axes.d.ts +14 -0
  48. package/dist/html/layout/axes.d.ts.map +1 -0
  49. package/dist/html/layout/axes.js +28 -0
  50. package/dist/html/layout/axes.js.map +1 -0
  51. package/dist/html/layout/block.d.ts +93 -12
  52. package/dist/html/layout/block.d.ts.map +1 -1
  53. package/dist/html/layout/block.js +393 -39
  54. package/dist/html/layout/block.js.map +1 -1
  55. package/dist/html/layout/boxes.d.ts +90 -2
  56. package/dist/html/layout/boxes.d.ts.map +1 -1
  57. package/dist/html/layout/boxes.js +74 -10
  58. package/dist/html/layout/boxes.js.map +1 -1
  59. package/dist/html/layout/cache.d.ts +1 -1
  60. package/dist/html/layout/cache.d.ts.map +1 -1
  61. package/dist/html/layout/cache.js +6 -0
  62. package/dist/html/layout/cache.js.map +1 -1
  63. package/dist/html/layout/css-grid.d.ts.map +1 -1
  64. package/dist/html/layout/css-grid.js +90 -18
  65. package/dist/html/layout/css-grid.js.map +1 -1
  66. package/dist/html/layout/flex.d.ts.map +1 -1
  67. package/dist/html/layout/flex.js +574 -84
  68. package/dist/html/layout/flex.js.map +1 -1
  69. package/dist/html/layout/inline.d.ts +3 -0
  70. package/dist/html/layout/inline.d.ts.map +1 -1
  71. package/dist/html/layout/inline.js +154 -54
  72. package/dist/html/layout/inline.js.map +1 -1
  73. package/dist/html/layout/multicol.d.ts +42 -0
  74. package/dist/html/layout/multicol.d.ts.map +1 -0
  75. package/dist/html/layout/multicol.js +897 -0
  76. package/dist/html/layout/multicol.js.map +1 -0
  77. package/dist/html/layout/shaping.d.ts +7 -0
  78. package/dist/html/layout/shaping.d.ts.map +1 -1
  79. package/dist/html/layout/shaping.js +25 -20
  80. package/dist/html/layout/shaping.js.map +1 -1
  81. package/dist/html/layout/table.d.ts.map +1 -1
  82. package/dist/html/layout/table.js +62 -18
  83. package/dist/html/layout/table.js.map +1 -1
  84. package/dist/html/node.d.ts +90 -15
  85. package/dist/html/node.d.ts.map +1 -1
  86. package/dist/html/node.js +771 -186
  87. package/dist/html/node.js.map +1 -1
  88. package/dist/html/paint.d.ts +101 -5
  89. package/dist/html/paint.d.ts.map +1 -1
  90. package/dist/html/paint.js +1502 -160
  91. package/dist/html/paint.js.map +1 -1
  92. package/dist/html/svg.d.ts +15 -2
  93. package/dist/html/svg.d.ts.map +1 -1
  94. package/dist/html/svg.js +249 -17
  95. package/dist/html/svg.js.map +1 -1
  96. package/dist/html/widgets.d.ts +29 -0
  97. package/dist/html/widgets.d.ts.map +1 -0
  98. package/dist/html/widgets.js +700 -0
  99. package/dist/html/widgets.js.map +1 -0
  100. package/dist/html/woff2.d.ts +14 -0
  101. package/dist/html/woff2.d.ts.map +1 -0
  102. package/dist/html/woff2.js +608 -0
  103. package/dist/html/woff2.js.map +1 -0
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/maps/controller.d.ts +17 -1
  108. package/dist/maps/controller.d.ts.map +1 -1
  109. package/dist/maps/controller.js +27 -7
  110. package/dist/maps/controller.js.map +1 -1
  111. package/dist/richtext/node.d.ts +8 -1
  112. package/dist/richtext/node.d.ts.map +1 -1
  113. package/dist/richtext/node.js +5 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/richtext/runs.d.ts +25 -6
  116. package/dist/richtext/runs.d.ts.map +1 -1
  117. package/dist/richtext/runs.js +240 -24
  118. package/dist/richtext/runs.js.map +1 -1
  119. package/package.json +2 -2
  120. package/src/html/controls.ts +114 -51
  121. package/src/html/css/cascade.ts +614 -140
  122. package/src/html/css/color.ts +29 -0
  123. package/src/html/css/parse.ts +388 -33
  124. package/src/html/css/style.ts +688 -150
  125. package/src/html/css/transform.ts +459 -0
  126. package/src/html/css/ua.ts +77 -10
  127. package/src/html/css/values.ts +4 -0
  128. package/src/html/dom.ts +3 -1
  129. package/src/html/fonts.ts +417 -24
  130. package/src/html/form.ts +962 -0
  131. package/src/html/index.ts +49 -338
  132. package/src/html/layout/axes.ts +62 -0
  133. package/src/html/layout/block.ts +447 -37
  134. package/src/html/layout/boxes.ts +143 -14
  135. package/src/html/layout/cache.ts +6 -0
  136. package/src/html/layout/css-grid.ts +95 -22
  137. package/src/html/layout/flex.ts +675 -78
  138. package/src/html/layout/inline.ts +159 -55
  139. package/src/html/layout/multicol.ts +977 -0
  140. package/src/html/layout/shaping.ts +27 -18
  141. package/src/html/layout/table.ts +62 -19
  142. package/src/html/node.ts +798 -172
  143. package/src/html/paint.ts +1742 -241
  144. package/src/html/svg.ts +277 -19
  145. package/src/html/widgets.ts +821 -0
  146. package/src/html/woff2.ts +612 -0
  147. package/src/index.ts +1 -0
  148. package/src/maps/controller.ts +28 -7
  149. package/src/richtext/node.ts +13 -2
  150. package/src/richtext/runs.ts +313 -35
@@ -33,6 +33,7 @@ import type { Declaration, StyleRule, Stylesheet } from './parse.js';
33
33
  import {
34
34
  applyDeclaration,
35
35
  blockify,
36
+ settleButton,
36
37
  settleContentVisibility,
37
38
  settleOverflow,
38
39
  settleClamp,
@@ -48,6 +49,7 @@ import type { FontFamilies } from '../fonts.js';
48
49
  import { parseDeclarations } from './parse.js';
49
50
  import { CounterStyles, counterStyleRule } from './counter-styles.js';
50
51
  import type { CounterStyleRule } from './counter-styles.js';
52
+ import { PALETTE_CHROME } from './ua.js';
51
53
  import type { UnitContext } from './values.js';
52
54
  import { viewportUnit } from './values.js';
53
55
  import { customProperties, substituteIn } from './vars.js';
@@ -93,6 +95,41 @@ export interface SharedStyle {
93
95
  key: number;
94
96
  }
95
97
 
98
+ /**
99
+ * The styles of the build before, for a build that only some elements'
100
+ * styles can differ in (`Cascade.beginSharing`): what each element's came
101
+ * to, and the elements to style again — `restyle`, each with everything in
102
+ * it, since a style is its parent's and the rules', and a rule reads an
103
+ * element's ancestors.
104
+ */
105
+ export interface KeptStyles {
106
+ styles: ReadonlyMap<Element, { style: ComputedStyle; inFlex: boolean }>;
107
+ restyle: ReadonlySet<Element>;
108
+ }
109
+
110
+ /** What the pointer entering or leaving an element can change
111
+ * (`Cascade.hoverTouches`): nothing, the element and what is in it, or
112
+ * those and the siblings after it. */
113
+ export type HoverTouch = 0 | 1 | 2;
114
+ export const HOVER_UNTOUCHED = 0;
115
+ export const HOVER_TOUCHED = 1;
116
+ export const HOVER_FOLLOWED = 2;
117
+
118
+ /** Which pseudo-elements' rules test the pointer (`Cascade.hoverPseudo`). */
119
+ export const HOVER_PSEUDO_NONE = 0;
120
+ export const HOVER_PSEUDO_GENERATED = 1;
121
+ export const HOVER_PSEUDO_OTHER = 2;
122
+
123
+ /** The pseudo-elements a pointer move can restyle: `::selection`'s style
124
+ * is worked out as a selection is painted. */
125
+ const POINTER_PSEUDO_ELEMENTS = [
126
+ 'before',
127
+ 'after',
128
+ 'first-letter',
129
+ 'first-line',
130
+ 'marker',
131
+ ] as const;
132
+
96
133
  /** A compiled matcher, kept beside the rule it came from. */
97
134
  interface IndexedRule {
98
135
  rule: StyleRule;
@@ -132,6 +169,11 @@ class RuleIndex {
132
169
  /** Whether any rule in here is pointer-sensitive, so the renderer knows
133
170
  * whether a pointer move can change the cascade at all. */
134
171
  hoverSensitive = false;
172
+ /** The rules that test `:hover`, in buckets of their own: the ones whose
173
+ * answers a pointer move can change, which is all that is asked to know
174
+ * whether an element's did (`Cascade.pointerChanged`). Null until there
175
+ * is one. */
176
+ pointer: RuleIndex | null = null;
135
177
  /** The buckets holding a rule an element cannot share its style under
136
178
  * (`UNSHAREABLE`): the ids, classes and tags whose elements compute
137
179
  * their own, and whether the universal bucket makes every element do so. */
@@ -185,26 +227,44 @@ class RuleIndex {
185
227
  else if (key.kind === 'tag') this.ownStyleTags.add(key.name);
186
228
  else this.ownStyleEverywhere = true;
187
229
  }
188
- const bucket =
189
- key.kind === 'id'
190
- ? mapBucket(this.byId, key.name)
191
- : key.kind === 'class'
192
- ? mapBucket(this.byClass, key.name)
193
- : key.kind === 'tag'
194
- ? mapBucket(this.byTag, key.name)
195
- : this.universal;
196
- bucket.push(indexed);
230
+ this._bucket(key).push(indexed);
231
+ if (HOVER.test(rule.selector)) {
232
+ this.pointer ??= new RuleIndex();
233
+ this.pointer.size += 1;
234
+ this.pointer._bucket(key).push(indexed);
235
+ }
236
+ }
237
+
238
+ private _bucket(key: ReturnType<typeof rightmostKey>): IndexedRule[] {
239
+ return key.kind === 'id'
240
+ ? mapBucket(this.byId, key.name)
241
+ : key.kind === 'class'
242
+ ? mapBucket(this.byClass, key.name)
243
+ : key.kind === 'tag'
244
+ ? mapBucket(this.byTag, key.name)
245
+ : this.universal;
197
246
  }
198
247
  }
199
248
 
200
249
  /** The pseudo-elements a rule can style here. */
201
250
  type PseudoElement =
202
- 'before' | 'after' | 'first-letter' | 'first-line' | 'marker';
251
+ 'before' | 'after' | 'first-letter' | 'first-line' | 'marker' | 'selection';
203
252
 
204
253
  /** A selector's trailing `::before`, `::after`, `::first-letter`,
205
- * `::first-line` or `::marker`, or CSS 2's single-colon spelling of any of
206
- * its four. */
207
- const PSEUDO_ELEMENT = /::?(before|after|first-letter|first-line)$|::marker$/i;
254
+ * `::first-line`, `::marker` or `::selection`, or CSS 2's single-colon
255
+ * spelling of any of its four. */
256
+ const PSEUDO_ELEMENT =
257
+ /::?(before|after|first-letter|first-line)$|::(marker|selection)$/i;
258
+
259
+ /**
260
+ * What a `::selection` makes of the text it covers (CSS Pseudo 4, 3.2): the
261
+ * colour the text is drawn in, null for its own, and the band under it,
262
+ * null for none.
263
+ */
264
+ export interface SelectionStyle {
265
+ color: string | null;
266
+ background: string | null;
267
+ }
208
268
 
209
269
  /**
210
270
  * A rule for a pseudo-element, as the pseudo-element it styles and a rule
@@ -226,7 +286,7 @@ function splitPseudoElement(
226
286
  ? `${trimmed} *`
227
287
  : trimmed;
228
288
  return {
229
- which: (m[1] ?? 'marker').toLowerCase() as PseudoElement,
289
+ which: (m[1] ?? m[2]).toLowerCase() as PseudoElement,
230
290
  rule: { ...rule, selector },
231
291
  };
232
292
  }
@@ -234,34 +294,63 @@ function splitPseudoElement(
234
294
  /**
235
295
  * The compounds of a selector that test `:hover`, each without it (and
236
296
  * without an `:active` beside it), and whether one is followed by a sibling
237
- * combinator, which reaches the element's later siblings. `nested` where a
238
- * `:hover` sits inside a functional pseudo-class — `:not(:hover)`,
239
- * `:is(:hover)` — and reaches elements no compound names.
297
+ * combinator, which reaches the element's later siblings.
298
+ *
299
+ * A `:hover` inside `:is()`, `:where()` or `:not()` is found where it is:
300
+ * each takes a selector list and matches the element it is written on, so
301
+ * a list entry of one compound tests that element — `a:not(:hover)` is a
302
+ * compound of `a`'s that tests the pointer — and an entry with combinators
303
+ * in it names an ancestor or an earlier sibling, as it would outside.
304
+ * Tailwind 4 writes every `group-hover:` that way, `:is(:where(.group):hover
305
+ * *)`, and a page of them built its document again on every move. Whatever
306
+ * the nesting, the element whose hover changed is one a compound found here
307
+ * matches, and what changes with it is itself, what is in it, and what
308
+ * follows it where a sibling combinator is in play: a selector reads an
309
+ * element, its ancestors and the siblings before each of them, and nothing
310
+ * else. `nested` is left for a function this cannot read a selector list
311
+ * in — `:nth-child(… of :hover)`, `:host()` — which reaches elements no
312
+ * compound names.
240
313
  *
241
- * A `:hover` in a compound's `:has()` is the one exception, since what it
242
- * reaches is known: the compound, without its `:has()`, is an ancestor of
314
+ * A `:hover` in a compound's `:has()` is the other way about, since what
315
+ * it tests is below: the compound, without its `:has()`, is an ancestor of
243
316
  * the element the pointer is over — or an earlier sibling of it or of an
244
317
  * ancestor, where the argument starts at a sibling (Selectors 4, 4.5). So
245
- * it is given as an anchor (`has`) rather than making the rule `nested`.
318
+ * it is given as an anchor (`has`).
246
319
  *
247
320
  * `:active` is not the pointer's here: nothing sets it (`setPointer` is
248
321
  * handed none), so a selector that tests only it never changes as the
249
322
  * pointer moves — Wikipedia's buttons' `:focus:not(:active)` among them.
250
323
  * A press that sets it would have to be counted here too.
251
324
  */
252
- export function pointerCompounds(selector: string): {
325
+ export function pointerCompounds(selector: string): PointerCompounds {
326
+ const out: PointerCompounds = {
327
+ compounds: [],
328
+ siblings: false,
329
+ nested: false,
330
+ has: [],
331
+ followed: [],
332
+ };
333
+ scanComplex(selector, out);
334
+ return out;
335
+ }
336
+
337
+ export interface PointerCompounds {
253
338
  compounds: string[];
254
339
  siblings: boolean;
255
340
  nested: boolean;
256
341
  has: { anchor: string; siblings: boolean }[];
257
- } {
258
- const out = {
259
- compounds: [] as string[],
260
- siblings: false,
261
- nested: false,
262
- has: [] as { anchor: string; siblings: boolean }[],
263
- };
264
- // the compounds at the top level, each with the combinator after it
342
+ /** The compounds and anchors a sibling combinator follows: the ones
343
+ * whose elements' later siblings change with them. */
344
+ followed: string[];
345
+ }
346
+
347
+ /** The functions that take a selector list and match the element they are
348
+ * written on (Selectors 4, 4.2 to 4.4), under the names css-select and
349
+ * older sheets know them by. */
350
+ const MATCHES_ANY = /^(?:is|where|not|matches|any|-webkit-any|-moz-any)$/i;
351
+
352
+ /** The compounds of a complex selector, each with the combinator after it. */
353
+ function compoundsOf(selector: string): { text: string; next: string }[] {
265
354
  const parts: { text: string; next: string }[] = [];
266
355
  let start = 0;
267
356
  let depth = 0;
@@ -287,85 +376,188 @@ export function pointerCompounds(selector: string): {
287
376
  }
288
377
  }
289
378
  split(selector.length, '');
290
- for (const { text, next } of parts) {
291
- let bare = '';
292
- let pointer = false;
293
- depth = 0;
294
- quote = '';
295
- /** Where a top-level `:has(` starts in `bare`, and its argument in
296
- * `text`, while one is open. */
297
- let hasAt = -1;
298
- let argAt = -1;
299
- let hasPointer = false;
300
- let anchored = false;
301
- const args: string[] = [];
302
- for (let i = 0; i < text.length; i += 1) {
303
- const c = text[i];
304
- if (quote) {
305
- if (c === quote && text[i - 1] !== '\\') quote = '';
306
- bare += c;
307
- continue;
379
+ return parts;
380
+ }
381
+
382
+ /** A selector list's entries: split at its top-level commas. */
383
+ function selectorsOf(list: string): string[] {
384
+ const entries: string[] = [];
385
+ let start = 0;
386
+ let depth = 0;
387
+ let quote = '';
388
+ for (let i = 0; i < list.length; i += 1) {
389
+ const c = list[i];
390
+ if (quote) {
391
+ if (c === quote && list[i - 1] !== '\\') quote = '';
392
+ } else if (c === '\\') i = escapeEnd(list, i) - 1;
393
+ else if (c === '"' || c === "'") quote = c;
394
+ else if (c === '(' || c === '[') depth += 1;
395
+ else if (c === ')' || c === ']') depth = Math.max(0, depth - 1);
396
+ else if (c === ',' && depth === 0) {
397
+ entries.push(list.slice(start, i));
398
+ start = i + 1;
399
+ }
400
+ }
401
+ entries.push(list.slice(start));
402
+ return entries;
403
+ }
404
+
405
+ /** Where the parenthesis opened at `open` closes, or the text's end. */
406
+ function closeOf(text: string, open: number): number {
407
+ let depth = 0;
408
+ let quote = '';
409
+ for (let i = open; i < text.length; i += 1) {
410
+ const c = text[i];
411
+ if (quote) {
412
+ if (c === quote && text[i - 1] !== '\\') quote = '';
413
+ } else if (c === '\\') i = escapeEnd(text, i) - 1;
414
+ else if (c === '"' || c === "'") quote = c;
415
+ else if (c === '(' || c === '[') depth += 1;
416
+ else if (c === ')' || c === ']') {
417
+ depth -= 1;
418
+ if (depth === 0) return i;
419
+ }
420
+ }
421
+ return text.length;
422
+ }
423
+
424
+ /** `pointerCompounds` for one complex selector, into `out`. */
425
+ function scanComplex(selector: string, out: PointerCompounds): void {
426
+ for (const { text, next } of compoundsOf(selector)) {
427
+ const compoundsFrom = out.compounds.length;
428
+ const hasFrom = out.has.length;
429
+ const found = scanCompound(text, out);
430
+ const sibling = next === '+' || next === '~';
431
+ if (found.args.length) {
432
+ out.has.push({
433
+ anchor: found.bare.trim() || '*',
434
+ siblings: found.args.some((arg) =>
435
+ selectorsOf(arg).some((one) => /^\s*[+~]/.test(one)),
436
+ ),
437
+ });
438
+ }
439
+ if (found.pointer) out.compounds.push(found.bare.trim() || '*');
440
+ // what follows the element this compound matches follows one whose
441
+ // hover, or whose answer to a `:has()`, changed
442
+ // — the one this compound matches, or one a selector nested in it
443
+ // named, which is more than it reaches and never less
444
+ if (sibling && (found.pointer || found.below || found.args.length)) {
445
+ out.siblings = true;
446
+ for (let i = compoundsFrom; i < out.compounds.length; i += 1) {
447
+ out.followed.push(out.compounds[i]);
308
448
  }
309
- if (c === '\\') {
310
- const end = escapeEnd(text, i);
311
- bare += text.slice(i, end);
312
- i = end - 1;
449
+ for (let i = hasFrom; i < out.has.length; i += 1) {
450
+ out.followed.push(out.has[i].anchor);
451
+ }
452
+ }
453
+ }
454
+ }
455
+
456
+ /**
457
+ * What one compound says of the pointer: the compound without what tests
458
+ * it (`bare`), whether it tests the element's own hover (`pointer`), the
459
+ * arguments of each `:has()` of the element's that tests it below (`args`),
460
+ * and whether a selector nested in it named another element's (`below`),
461
+ * whose compounds went into `out`.
462
+ */
463
+ function scanCompound(
464
+ text: string,
465
+ out: PointerCompounds,
466
+ ): { bare: string; pointer: boolean; below: boolean; args: string[] } {
467
+ let bare = '';
468
+ let pointer = false;
469
+ let below = false;
470
+ const args: string[] = [];
471
+ let bracket = 0;
472
+ let quote = '';
473
+ for (let i = 0; i < text.length; i += 1) {
474
+ const c = text[i];
475
+ if (quote) {
476
+ if (c === quote && text[i - 1] !== '\\') quote = '';
477
+ bare += c;
478
+ continue;
479
+ }
480
+ if (c === '\\') {
481
+ const end = escapeEnd(text, i);
482
+ bare += text.slice(i, end);
483
+ i = end - 1;
484
+ continue;
485
+ }
486
+ if (c === '"' || c === "'") quote = c;
487
+ else if (c === '[') bracket += 1;
488
+ else if (c === ']') bracket = Math.max(0, bracket - 1);
489
+ else if (c === ':' && bracket === 0 && text[i + 1] !== ':') {
490
+ const m = POINTER_PSEUDO_AT.exec(text.slice(i));
491
+ if (m) {
492
+ if (m[0].length === 6) pointer = true;
493
+ i += m[0].length - 1;
313
494
  continue;
314
495
  }
315
- if (c === '"' || c === "'") quote = c;
316
- else if (c === '(' || c === '[') {
317
- if (depth === 0 && c === '(' && /:has$/i.test(bare)) {
318
- hasAt = bare.length - 4;
319
- argAt = i + 1;
320
- hasPointer = false;
321
- }
322
- depth += 1;
323
- } else if (c === ')' || c === ']') {
324
- depth = Math.max(0, depth - 1);
325
- if (depth === 0 && hasAt >= 0 && c === ')') {
326
- // the compound is kept without it: its argument is what the
327
- // pointer changes
328
- if (hasPointer) {
329
- anchored = true;
330
- args.push(text.slice(argAt, i));
331
- bare = bare.slice(0, hasAt);
332
- } else bare += c;
333
- hasAt = -1;
496
+ const fn = FUNCTION_AT.exec(text.slice(i));
497
+ if (fn) {
498
+ const open = i + fn[0].length - 1;
499
+ const close = closeOf(text, open);
500
+ const arg = text.slice(open + 1, close);
501
+ const whole = text.slice(i, close + 1);
502
+ i = close;
503
+ if (!HOVER.test(arg)) {
504
+ bare += whole;
334
505
  continue;
335
506
  }
336
- } else if (c === ':' && text[i + 1] !== ':') {
337
- const m = POINTER_PSEUDO_AT.exec(text.slice(i));
338
- if (m) {
339
- const hover = m[0].length === 6;
340
- if (depth > 0) {
341
- // whatever holds it in the argument, the element it tests is
342
- // in the anchor's subtree or a later sibling's
343
- if (hover && hasAt >= 0) hasPointer = true;
344
- else if (hover) out.nested = true;
345
- } else {
346
- if (hover) pointer = true;
347
- i += m[0].length - 1;
348
- continue;
507
+ const name = fn[1].toLowerCase();
508
+ if (name === 'has') {
509
+ // the compound is kept without it: its argument is what the
510
+ // pointer changes, and whatever holds the `:hover` in there, the
511
+ // element it tests is in the anchor's subtree or a later
512
+ // sibling's
513
+ args.push(arg);
514
+ } else if (MATCHES_ANY.test(name)) {
515
+ // Kept as what the entries that test this element's hover ask
516
+ // of it besides — `:where(a:hover)` is a compound of `a`'s, and
517
+ // so is `:not(a:hover)`, which only an `a` can change its answer
518
+ // to. Tailwind's typography writes its links `.prose
519
+ // :where(a:hover):not(:where(.not-prose, .not-prose *))`, and
520
+ // without the `a` that was a compound every element matched.
521
+ const own: string[] = [];
522
+ let any = false;
523
+ for (const entry of selectorsOf(arg)) {
524
+ if (!HOVER.test(entry)) continue;
525
+ const parts = compoundsOf(entry);
526
+ if (parts.length !== 1) {
527
+ scanComplex(entry, out);
528
+ below = true;
529
+ continue;
530
+ }
531
+ // of this element itself
532
+ const one = scanCompound(parts[0].text, out);
533
+ if (one.below) below = true;
534
+ if (!one.pointer && !one.args.length) continue;
535
+ if (one.pointer) pointer = true;
536
+ args.push(...one.args);
537
+ const rest = one.bare.trim();
538
+ if (rest && rest !== '*') own.push(rest);
539
+ else any = true;
349
540
  }
541
+ if (own.length && !any) bare += `:is(${own.join(',')})`;
542
+ } else {
543
+ out.nested = true;
544
+ bare += whole;
350
545
  }
546
+ continue;
351
547
  }
352
- bare += c;
353
548
  }
354
- if (anchored) {
355
- out.has.push({
356
- anchor: bare.trim() || '*',
357
- siblings: args.some((arg) =>
358
- arg.split(',').some((one) => /^\s*[+~]/.test(one)),
359
- ),
360
- });
361
- }
362
- if (!pointer) continue;
363
- out.compounds.push(bare.trim() || '*');
364
- if (next === '+' || next === '~') out.siblings = true;
549
+ bare += c;
365
550
  }
366
- return out;
551
+ return { bare, pointer, below, args };
367
552
  }
368
553
 
554
+ /** A functional pseudo-class's name and its opening parenthesis, at the
555
+ * start of a string. */
556
+ const FUNCTION_AT = /^:([\w-]+)\(/;
557
+
558
+ /** `:hover` anywhere in a selector. */
559
+ const HOVER = /:hover(?![\w-])/i;
560
+
369
561
  /** `:hover` or `:active` at the start of a string, and nothing longer. */
370
562
  const POINTER_PSEUDO_AT = /^:(?:hover|active)(?![\w-])/i;
371
563
 
@@ -569,26 +761,34 @@ export class Cascade {
569
761
  'first-letter': new RuleIndex(),
570
762
  'first-line': new RuleIndex(),
571
763
  marker: new RuleIndex(),
764
+ selection: new RuleIndex(),
572
765
  };
573
766
  private _adapter: CssSelectAdapter;
574
767
  /** The compounds of the selectors that test the pointer, each without
575
768
  * its `:hover` or `:active` (`pointerCompounds`), and their matchers,
576
769
  * compiled when a pointer move first asks (`hoverTouches`). */
577
- private _hoverCompounds = new Set<string>();
578
- private _hoverMatchers: ((el: Element) => boolean)[] | null = null;
770
+ private _hoverCompounds = new Map<string, boolean>();
771
+ private _hoverMatchers:
772
+ { match: (el: Element) => boolean; followed: boolean }[] | null = null;
579
773
  /** Whether a pointer move can be restyled where it happened
580
774
  * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
581
775
  * pointer inside a functional pseudo-class, which reaches elements no
582
776
  * compound names. */
583
777
  hoverLocal = true;
584
- /** Whether a compound that tests the pointer is followed by a sibling
585
- * combinator, so an element's later siblings restyle with it. */
586
- hoverSiblings = false;
587
- /** The compounds whose `:has()` tests the pointer, each without it, and
588
- * whether its argument starts at a sibling (`hoverAnchors`). */
589
- private _hoverAnchors = new Map<string, boolean>();
778
+ /** The compounds whose `:has()` tests the pointer, each without it,
779
+ * whether its argument starts at a sibling, and whether a sibling
780
+ * combinator follows it (`hoverAnchors`). */
781
+ private _hoverAnchors = new Map<
782
+ string,
783
+ { siblings: boolean; followed: boolean }
784
+ >();
590
785
  private _anchorMatchers:
591
- { match: (el: Element) => boolean; siblings: boolean }[] | null = null;
786
+ | {
787
+ match: (el: Element) => boolean;
788
+ siblings: boolean;
789
+ followed: boolean;
790
+ }[]
791
+ | null = null;
592
792
  private _pointer: PointerState = NO_POINTER;
593
793
  readonly initial: ComputedStyle;
594
794
  readonly look: RootLook;
@@ -627,6 +827,12 @@ export class Cascade {
627
827
  /** The families the document loads itself (`fonts.ts`), or null for a
628
828
  * document with no `@font-face`. */
629
829
  private _families: FontFamilies | null;
830
+ /** The root element's computed style: its `font-size` is what a `rem`
831
+ * is and its line height what an `rlh` is (CSS Values 4, 6.1.1), in
832
+ * every element but the root, whose own declarations measure them from
833
+ * the initial values. Set as the root's style is computed, which a build
834
+ * does ahead of every element under it. */
835
+ private _root: ComputedStyle | null = null;
630
836
  private _mapFamilies: ((list: string) => string) | undefined;
631
837
  /** Whether any declaration has a length in `lh` or `rlh`: only then is
632
838
  * the line height settled ahead of the declarations that read it. */
@@ -673,6 +879,9 @@ export class Cascade {
673
879
  this._noteHover((pseudo?.rule ?? rule).selector);
674
880
  }
675
881
  for (const bp of sheet.breakpoints) breakpoints.add(bp);
882
+ // a query on the viewport's height reads it as a `vh` does
883
+ if (sheet.readsHeight) this.readsViewportHeight = true;
884
+ if (sheet.readsWidth) this.readsViewportWidth = true;
676
885
  }
677
886
  this.breakpoints = [...breakpoints].sort((a, b) => a - b);
678
887
  this.counterStyles = new CounterStyles(counterStyles);
@@ -727,19 +936,50 @@ export class Cascade {
727
936
  this._pointer = pointer;
728
937
  }
729
938
 
939
+ /**
940
+ * Which pseudo-elements' rules test the pointer: none's
941
+ * (`HOVER_PSEUDO_NONE`); a `::before`'s or an `::after`'s only
942
+ * (`HOVER_PSEUDO_GENERATED`), whose boxes a move restyles where they are
943
+ * as it does an element's; or a `::first-line`'s, a `::first-letter`'s
944
+ * or a `::marker`'s (`HOVER_PSEUDO_OTHER`), which layout styles and a
945
+ * move cannot.
946
+ */
947
+ get hoverPseudo(): 0 | 1 | 2 {
948
+ if (
949
+ this._pseudo['first-letter'].hoverSensitive ||
950
+ this._pseudo['first-line'].hoverSensitive ||
951
+ this._pseudo.marker.hoverSensitive
952
+ ) {
953
+ return HOVER_PSEUDO_OTHER;
954
+ }
955
+ return this._pseudo.before.hoverSensitive ||
956
+ this._pseudo.after.hoverSensitive
957
+ ? HOVER_PSEUDO_GENERATED
958
+ : HOVER_PSEUDO_NONE;
959
+ }
960
+
730
961
  /**
731
962
  * Whether the pointer entering or leaving `el` can change a style: one of
732
963
  * the compounds a selector tests the pointer in matches it, the pointer
733
964
  * aside. A move between two paragraphs of a page whose only such rule is
734
- * `a:hover` touches nothing, and restyles nothing.
965
+ * `a:hover` touches nothing, and restyles nothing. `HOVER_FOLLOWED`
966
+ * where a sibling combinator follows one that matches, so the element's
967
+ * later siblings can change with it — those rules' elements alone: a
968
+ * sheet with one `.peer:hover ~ *` in it does not make every row of a
969
+ * table restyle the rows after it.
735
970
  */
736
- hoverTouches(el: Element): boolean {
737
- this._hoverMatchers ??= [...this._hoverCompounds].flatMap((c) => {
738
- const match = this._compile(c);
739
- return match ? [match] : [];
740
- });
741
- for (const match of this._hoverMatchers) if (match(el)) return true;
742
- return false;
971
+ hoverTouches(el: Element): HoverTouch {
972
+ // the followed ones first, so the first that matches is the answer
973
+ this._hoverMatchers ??= [...this._hoverCompounds]
974
+ .sort(([, a], [, b]) => Number(b) - Number(a))
975
+ .flatMap(([compound, followed]) => {
976
+ const match = this._compile(compound);
977
+ return match ? [{ match, followed }] : [];
978
+ });
979
+ for (const { match, followed } of this._hoverMatchers) {
980
+ if (match(el)) return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
981
+ }
982
+ return HOVER_UNTOUCHED;
743
983
  }
744
984
 
745
985
  /**
@@ -747,22 +987,27 @@ export class Cascade {
747
987
  * hovered or left: the ancestors of it a compound whose `:has()` tests
748
988
  * the pointer names, and their earlier siblings and its own where the
749
989
  * argument starts at a sibling — a superset, which is restyled and found
750
- * the same where it did not. Into `into`.
990
+ * the same where it did not. Into `into`, each with whether its later
991
+ * siblings can change with it (`hoverTouches`).
751
992
  */
752
- hoverAnchors(el: Element, into: Element[]): void {
993
+ hoverAnchors(el: Element, into: Map<Element, HoverTouch>): void {
753
994
  if (!this._hoverAnchors.size) return;
754
- this._anchorMatchers ??= [...this._hoverAnchors].flatMap(
755
- ([anchor, siblings]) => {
995
+ this._anchorMatchers ??= [...this._hoverAnchors]
996
+ .sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
997
+ .flatMap(([anchor, { siblings, followed }]) => {
756
998
  const match = this._compile(anchor);
757
- return match ? [{ match, siblings }] : [];
758
- },
759
- );
999
+ return match ? [{ match, siblings, followed }] : [];
1000
+ });
1001
+ const note = (at: Element, followed: boolean): void => {
1002
+ if (followed) into.set(at, HOVER_FOLLOWED);
1003
+ else if (!into.has(at)) into.set(at, HOVER_TOUCHED);
1004
+ };
760
1005
  const siblings = this._anchorMatchers.some((m) => m.siblings);
761
1006
  for (let at: Element | null = el; at;) {
762
1007
  if (at !== el) {
763
- for (const { match } of this._anchorMatchers) {
1008
+ for (const { match, followed } of this._anchorMatchers) {
764
1009
  if (match(at)) {
765
- into.push(at);
1010
+ note(at, followed);
766
1011
  break;
767
1012
  }
768
1013
  }
@@ -770,9 +1015,9 @@ export class Cascade {
770
1015
  if (siblings) {
771
1016
  for (let s = at.prev; s; s = s.prev) {
772
1017
  if (!isTag(s as Element)) continue;
773
- for (const { match, siblings: apart } of this._anchorMatchers) {
774
- if (apart && match(s as Element)) {
775
- into.push(s as Element);
1018
+ for (const m of this._anchorMatchers) {
1019
+ if (m.siblings && m.match(s as Element)) {
1020
+ note(s as Element, m.followed);
776
1021
  break;
777
1022
  }
778
1023
  }
@@ -787,14 +1032,20 @@ export class Cascade {
787
1032
  if (!/:(?:hover|active)/i.test(selector)) return;
788
1033
  const found = pointerCompounds(selector);
789
1034
  if (found.nested) this.hoverLocal = false;
790
- if (found.siblings) this.hoverSiblings = true;
791
- for (const c of found.compounds) this._hoverCompounds.add(c);
792
- for (const { anchor, siblings } of found.has) {
793
- this._hoverAnchors.set(
794
- anchor,
795
- siblings || (this._hoverAnchors.get(anchor) ?? false),
1035
+ const followed = new Set(found.followed);
1036
+ for (const c of found.compounds) {
1037
+ this._hoverCompounds.set(
1038
+ c,
1039
+ followed.has(c) || (this._hoverCompounds.get(c) ?? false),
796
1040
  );
797
1041
  }
1042
+ for (const { anchor, siblings } of found.has) {
1043
+ const was = this._hoverAnchors.get(anchor);
1044
+ this._hoverAnchors.set(anchor, {
1045
+ siblings: siblings || (was?.siblings ?? false),
1046
+ followed: followed.has(anchor) || (was?.followed ?? false),
1047
+ });
1048
+ }
798
1049
  }
799
1050
 
800
1051
  /** A selector compiled as a rule's is, or null where css-select refuses
@@ -831,12 +1082,35 @@ export class Cascade {
831
1082
  private _ids = new WeakMap<object, number>();
832
1083
  private _nextId = 1;
833
1084
 
834
- /** A box tree is about to be built: the styles shared in the last build
835
- * were computed against a pointer and a viewport that may have moved. */
836
- beginSharing(): void {
1085
+ /** The styles kept from the build before, while a build that keeps them
1086
+ * runs, and what each kept element shares under in this one. */
1087
+ private _kept: KeptStyles | null = null;
1088
+ private _keptShared = new Map<Element, SharedStyle>();
1089
+
1090
+ /**
1091
+ * A box tree is about to be built: the styles shared in the last build
1092
+ * were computed against a pointer and a viewport that may have moved.
1093
+ *
1094
+ * With `kept`, only its `restyle` elements are matched: a build the
1095
+ * pointer asked for — a hover that moved something — changes the styles
1096
+ * of the elements the move reached, and every other element's is what it
1097
+ * was. Matching them all again was most of such a build: a frame of 550 ms
1098
+ * on a page with a Tailwind sheet, and 150 without it.
1099
+ */
1100
+ beginSharing(kept: KeptStyles | null = null): void {
837
1101
  this._shared.clear();
838
1102
  this._sharedByMatch.clear();
839
- this._customs.clear();
1103
+ // the kept styles' custom properties are the sets in here, which the
1104
+ // elements styled under them find again
1105
+ if (!kept) this._customs.clear();
1106
+ this._kept = kept;
1107
+ this._keptShared.clear();
1108
+ }
1109
+
1110
+ /** The build is over: nothing later is answered from what it kept. */
1111
+ endSharing(): void {
1112
+ this._kept = null;
1113
+ this._keptShared.clear();
840
1114
  }
841
1115
 
842
1116
  /**
@@ -924,6 +1198,22 @@ export class Cascade {
924
1198
  parentKey: number,
925
1199
  inFlexContainer: boolean,
926
1200
  ): SharedStyle {
1201
+ const kept = this._kept;
1202
+ if (kept !== null && !kept.restyle.has(el)) {
1203
+ // An element the change did not reach keeps its style, under a key
1204
+ // of its own: a key stands for an element's ancestors, which the
1205
+ // elements styled again under it have in common, and two kept
1206
+ // elements with one style need not — a hover restyled in place
1207
+ // leaves a hovered card's children the styles its neighbours' have.
1208
+ let shared = this._keptShared.get(el);
1209
+ if (shared !== undefined) return shared;
1210
+ const was = kept.styles.get(el);
1211
+ if (was !== undefined && was.inFlex === inFlexContainer) {
1212
+ shared = { style: was.style, key: this._nextShareKey++ };
1213
+ this._keptShared.set(el, shared);
1214
+ return shared;
1215
+ }
1216
+ }
927
1217
  let key = `${parentKey}\u0001${inFlexContainer ? 1 : 0}`;
928
1218
  const tag = tagOf(el);
929
1219
  key += `\u0001${tag.length}:${tag}`;
@@ -948,6 +1238,7 @@ export class Cascade {
948
1238
  parentStyle,
949
1239
  inFlexContainer,
950
1240
  candidates,
1241
+ true,
951
1242
  ),
952
1243
  key: this._nextShareKey++,
953
1244
  };
@@ -1004,6 +1295,7 @@ export class Cascade {
1004
1295
  parentStyle,
1005
1296
  inFlexContainer,
1006
1297
  this._candidates(el),
1298
+ true,
1007
1299
  );
1008
1300
  }
1009
1301
 
@@ -1087,6 +1379,56 @@ export class Cascade {
1087
1379
  return out;
1088
1380
  }
1089
1381
 
1382
+ /** Whether any rule styles a `::selection`: a document with none draws
1383
+ * a selection in the palette's colour, and asks nothing. */
1384
+ get hasSelection(): boolean {
1385
+ return this._pseudo.selection.size > 0;
1386
+ }
1387
+
1388
+ /**
1389
+ * An element's `::selection`, over its parent's (`parent`, null for the
1390
+ * palette's own): the colour and the background the rules that reach it
1391
+ * set, and its parent's for either they leave alone. Both inherit along
1392
+ * the chain of highlights rather than the elements' (CSS Pseudo 4, 3.5,
1393
+ * "highlight inheritance"), which is what Chrome draws: a `<span>` in a
1394
+ * `div::selection { background: red }` is selected in red, and a `<p>`
1395
+ * whose own rule sets only a colour keeps the red under it.
1396
+ *
1397
+ * The palette's highlight is taken only where no rule has set either
1398
+ * (3.6, "paired defaults"): one that sets a colour and no background is
1399
+ * selected over none, as Chrome has it. A rule that sets neither — a
1400
+ * `text-shadow` only — leaves the element where its parent was.
1401
+ */
1402
+ selectionStyle(
1403
+ el: Element,
1404
+ style: ComputedStyle,
1405
+ parent: SelectionStyle | null,
1406
+ ): SelectionStyle | null {
1407
+ const index = this._pseudo.selection;
1408
+ if (!index.size || !index.reaches(el)) return parent;
1409
+ const candidates: Candidate[] = [];
1410
+ this._matchInto(index, el, candidates);
1411
+ let color = false;
1412
+ let background = false;
1413
+ for (const candidate of candidates) {
1414
+ for (const d of candidate.declarations) {
1415
+ if (d.prop === 'color') color = true;
1416
+ else if (d.prop === 'background' || d.prop === 'background-color') {
1417
+ background = true;
1418
+ }
1419
+ }
1420
+ }
1421
+ if (!color && !background) return parent;
1422
+ candidates.sort(byCascade);
1423
+ const own = this._computeStyle(el, style, false, candidates);
1424
+ return {
1425
+ color: color ? own.color : (parent?.color ?? null),
1426
+ background: background
1427
+ ? own.backgroundColor
1428
+ : (parent?.background ?? null),
1429
+ };
1430
+ }
1431
+
1090
1432
  /**
1091
1433
  * An element's `::first-line` style, inheriting from its own, or null
1092
1434
  * when no rule reaches it (CSS 2.1 5.12.1).
@@ -1170,6 +1512,9 @@ export class Cascade {
1170
1512
  parentStyle: ComputedStyle,
1171
1513
  inFlexContainer: boolean,
1172
1514
  candidates: Candidate[],
1515
+ /** Whether the style is the element's own, and not that of one of its
1516
+ * pseudo-elements. */
1517
+ own = false,
1173
1518
  ): ComputedStyle {
1174
1519
  const style = inherit(parentStyle, this.initial);
1175
1520
  // custom properties first, in cascade order, so every `var()` in the
@@ -1180,10 +1525,16 @@ export class Cascade {
1180
1525
  // size, so a `font-size: 1.2em` in the cascade resolves against the
1181
1526
  // right em, and again after the font size is settled so every *other*
1182
1527
  // em-relative length in the same rule resolves against this element's.
1528
+ const root = isRootElement(el);
1529
+ const rootStyle = root ? this.initial : (this._root ?? this.initial);
1530
+ // whose declaration is being applied: the UA sheet's families are the
1531
+ // host's own faces, and its lists end where they end
1532
+ let authored = true;
1183
1533
  const ctxParent: UnitContext = {
1184
1534
  em: parentStyle.fontSize,
1185
- rem: this.initial.fontSize,
1535
+ rem: rootStyle.fontSize,
1186
1536
  initial: this.initial,
1537
+ fallbackFamily: () => (authored ? this.initial.fontFamily : null),
1187
1538
  vw: this.viewportWidth,
1188
1539
  vh: this.viewportHeight,
1189
1540
  scale: this.scale,
@@ -1191,7 +1542,7 @@ export class Cascade {
1191
1542
  ch: () => this._chOf(parentStyle),
1192
1543
  families: this._mapFamilies,
1193
1544
  lh: () => this._lineHeightOf(parentStyle),
1194
- rlh: () => this._lineHeightOf(this.initial),
1545
+ rlh: () => this._lineHeightOf(rootStyle),
1195
1546
  };
1196
1547
  // The family, the weight and the slant go with the size: together they
1197
1548
  // pick the face an `ex`, a `ch` or an `lh` in any declaration is
@@ -1202,6 +1553,7 @@ export class Cascade {
1202
1553
  let sized = false;
1203
1554
  let keyword = false;
1204
1555
  for (const c of candidates) {
1556
+ authored = c.origin !== Origin.UserAgent;
1205
1557
  for (const d of pick(c)) {
1206
1558
  if (
1207
1559
  d.prop === 'font-family' ||
@@ -1249,9 +1601,12 @@ export class Cascade {
1249
1601
  fontStyle: style.fontStyle,
1250
1602
  };
1251
1603
  let line: LineSource | null = null;
1604
+ // the root's other declarations measure a `rem` from its own size
1605
+ if (root) this._root = style;
1252
1606
  const ctx: UnitContext = {
1253
1607
  ...ctxParent,
1254
1608
  em: style.fontSize,
1609
+ rem: root ? style.fontSize : ctxParent.rem,
1255
1610
  ex: () => this._exOf(face),
1256
1611
  ch: () => this._chOf(face),
1257
1612
  lh: () => this._lineHeightOf(line ?? style),
@@ -1262,6 +1617,7 @@ export class Cascade {
1262
1617
  // them. A `font` sets the size as well, which the first pass settled.
1263
1618
  if (this._lh) {
1264
1619
  for (const c of candidates) {
1620
+ authored = c.origin !== Origin.UserAgent;
1265
1621
  for (const d of pick(c)) {
1266
1622
  if (d.prop === 'line-height' || d.prop === 'font') {
1267
1623
  this._apply(style, parentStyle, d, ctx);
@@ -1277,6 +1633,7 @@ export class Cascade {
1277
1633
  }
1278
1634
  const settled = style.fontSize;
1279
1635
  for (const c of candidates) {
1636
+ authored = c.origin !== Origin.UserAgent;
1280
1637
  for (const d of pick(c)) {
1281
1638
  if (d.prop === 'font-size' || d.custom) continue;
1282
1639
  this._apply(style, parentStyle, d, ctx);
@@ -1304,6 +1661,10 @@ export class Cascade {
1304
1661
  settleOverflow(style);
1305
1662
  settleContentVisibility(style);
1306
1663
  blockify(style, inFlexContainer);
1664
+ // a button is laid out as one, whatever `display` it was given
1665
+ if (own && el.name.length === 6 && tagOf(el) === 'button') {
1666
+ settleButton(style);
1667
+ }
1307
1668
  decorate(style);
1308
1669
  // which faces of the document's own families this family, weight and
1309
1670
  // slant ask for — known only now, with all three computed
@@ -1382,22 +1743,68 @@ export class Cascade {
1382
1743
  return { style: asRoot(this.styleFor(synthetic, parent, false)), html };
1383
1744
  }
1384
1745
 
1746
+ /**
1747
+ * Whether the pointer's move from `was` to where it is changed which
1748
+ * rules match `el`, or its `::before`, `::after` or another of its
1749
+ * pseudo-elements: the rules that test `:hover` asked under both, and
1750
+ * nothing else, since no other rule's answer is the pointer's. An element
1751
+ * they answer the same for, under a parent whose style is what it was, has
1752
+ * the style it had — which is most of what a move reaches: the rows of a
1753
+ * hovered table body, what is in a hovered card.
1754
+ */
1755
+ pointerChanged(el: Element, was: PointerState): boolean {
1756
+ const now = this._pointer;
1757
+ const after = this._pointerAnswers(el);
1758
+ this._pointer = was;
1759
+ const before = this._pointerAnswers(el);
1760
+ this._pointer = now;
1761
+ return before !== after;
1762
+ }
1763
+
1764
+ /** The ids of the rules testing the pointer that match `el` as things
1765
+ * stand, the element's and each pseudo-element's. */
1766
+ private _pointerAnswers(el: Element): string {
1767
+ let answers = '';
1768
+ const ask = (index: RuleIndex, name: string): void => {
1769
+ const pointer = index.pointer;
1770
+ if (!pointer || !pointer.reaches(el)) return;
1771
+ const matched: number[] = [];
1772
+ this._matchInto(pointer, el, null, matched);
1773
+ if (matched.length) answers += `${name}${matched.join(',')};`;
1774
+ };
1775
+ ask(this._index, '');
1776
+ for (const which of POINTER_PSEUDO_ELEMENTS)
1777
+ ask(this._pseudo[which], which);
1778
+ return answers;
1779
+ }
1780
+
1385
1781
  /** The rules of `index` that match `el`, pushed onto `out` as candidates,
1386
1782
  * and their ids onto `matched` in the order they were tried. */
1387
1783
  private _matchInto(
1388
1784
  index: RuleIndex,
1389
1785
  el: Element,
1390
- out: Candidate[],
1786
+ out: Candidate[] | null,
1391
1787
  matched?: number[],
1392
1788
  ): void {
1393
1789
  // A media query's width is CSS pixels; the viewport is kept in device.
1394
1790
  const width = this.viewportWidth / this.scale;
1791
+ const height = this.viewportHeight / this.scale;
1395
1792
 
1396
1793
  const consider = (bucket: IndexedRule[] | undefined): void => {
1397
1794
  if (!bucket) return;
1398
1795
  for (const indexed of bucket) {
1399
1796
  const rule = indexed.rule;
1400
- if (!mediaMatches(rule.media, width, this.look.colorScheme)) continue;
1797
+ if (
1798
+ !mediaMatches(
1799
+ rule.media,
1800
+ width,
1801
+ this.look.colorScheme,
1802
+ height,
1803
+ this.scale,
1804
+ )
1805
+ ) {
1806
+ continue;
1807
+ }
1401
1808
  if (!indexed.compiled) {
1402
1809
  indexed.compiled = true;
1403
1810
  try {
@@ -1411,6 +1818,7 @@ export class Cascade {
1411
1818
  }
1412
1819
  if (!indexed.match || !indexed.match(el)) continue;
1413
1820
  matched?.push(indexed.id);
1821
+ if (out === null) continue;
1414
1822
  const origin = rule.order < 0 ? Origin.UserAgent : Origin.Author;
1415
1823
  pushRule(out, rule, origin);
1416
1824
  }
@@ -1478,10 +1886,59 @@ export class Cascade {
1478
1886
  }
1479
1887
 
1480
1888
  out.sort(byCascade);
1889
+ dropPaletteChrome(out);
1481
1890
  return out;
1482
1891
  }
1483
1892
  }
1484
1893
 
1894
+ /**
1895
+ * Takes the palette's control chrome (`PALETTE_CHROME`) out of an element's
1896
+ * candidates where the page styles its background or border, so what shows
1897
+ * through is the web's UA values the page was written against. The rule is
1898
+ * Blink's (`LayoutTheme::IsControlStyled`): a background or border longhand
1899
+ * set by the page, radius included, drops the control's native appearance;
1900
+ * so does `appearance: none`. `out` is in cascade order, so the UA's
1901
+ * candidates are the ones before the first of any other origin.
1902
+ */
1903
+ function dropPaletteChrome(out: Candidate[]): void {
1904
+ let chrome = false;
1905
+ let i = 0;
1906
+ for (; i < out.length && out[i].origin === Origin.UserAgent; i += 1) {
1907
+ if (PALETTE_CHROME.has(out[i].declarations)) chrome = true;
1908
+ }
1909
+ if (!chrome) return;
1910
+ let styled = false;
1911
+ for (let j = i; j < out.length && !styled; j += 1) {
1912
+ styled = pick(out[j]).some(stylesChrome);
1913
+ }
1914
+ if (!styled) return;
1915
+ let kept = 0;
1916
+ for (const c of out) {
1917
+ if (c.origin === Origin.UserAgent && PALETTE_CHROME.has(c.declarations)) {
1918
+ continue;
1919
+ }
1920
+ out[kept++] = c;
1921
+ }
1922
+ out.length = kept;
1923
+ }
1924
+
1925
+ /** Whether a declaration styles what a control's native look draws: Blink's
1926
+ * `is_background` and `is_border` properties and their shorthands, and an
1927
+ * `appearance` of `none`. */
1928
+ function stylesChrome(d: Declaration): boolean {
1929
+ const prop = d.prop;
1930
+ if (prop.startsWith('background')) {
1931
+ return prop !== 'background-repeat' && prop !== 'background-blend-mode';
1932
+ }
1933
+ if (prop.startsWith('border')) {
1934
+ return prop !== 'border-collapse' && prop !== 'border-spacing';
1935
+ }
1936
+ return (
1937
+ (prop === 'appearance' || prop === '-webkit-appearance') &&
1938
+ d.value.trim().toLowerCase() === 'none'
1939
+ );
1940
+ }
1941
+
1485
1942
  /** An implied element's style as the root box's: its look, not its role —
1486
1943
  * the root is still a block, in flow, and the initial containing block. */
1487
1944
  function asRoot(style: ComputedStyle): ComputedStyle {
@@ -1807,7 +2264,12 @@ function presentationHints(el: Element): Declaration[] {
1807
2264
  // `width`/`height` are lengths on the replaced and table elements and mean
1808
2265
  // nothing anywhere else, which is what stops a `<input width>` from
1809
2266
  // becoming a CSS width the widget then disagrees with.
1810
- if (SIZED.has(tag)) {
2267
+ if (
2268
+ SIZED.has(tag) ||
2269
+ // an image button's are its image's, as an `<img>`'s are (HTML 15.4.3)
2270
+ (tag === 'input' &&
2271
+ (attr(el, 'type') ?? '').trim().toLowerCase() === 'image')
2272
+ ) {
1811
2273
  const width = attr(el, 'width');
1812
2274
  if (width) push('width', lengthAttr(width));
1813
2275
  const height = attr(el, 'height');
@@ -1830,6 +2292,12 @@ function presentationHints(el: Element): Declaration[] {
1830
2292
  const height = attr(el, 'height');
1831
2293
  const h = height ? svgSizeHint(height) : null;
1832
2294
  if (h) push('height', h);
2295
+ // and its paint is its `fill` and `stroke` properties' (13.2), which a
2296
+ // rule of the document's then sets over the attribute
2297
+ const fill = attr(el, 'fill');
2298
+ if (fill) push('fill', fill);
2299
+ const stroke = attr(el, 'stroke');
2300
+ if (stroke) push('stroke', stroke);
1833
2301
  }
1834
2302
 
1835
2303
  if (tag === 'table') {
@@ -2047,3 +2515,9 @@ const ABSOLUTE_SIZES = new Set([
2047
2515
  'xxx-large',
2048
2516
  'initial',
2049
2517
  ]);
2518
+
2519
+ /** Whether an element is the document's root: its parent the document, or
2520
+ * none, as the `<html>` the cascade supplies where the markup has none. */
2521
+ function isRootElement(el: Element): boolean {
2522
+ return !el.parent || el.parent.type === 'root';
2523
+ }