@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
@@ -21,9 +21,10 @@ import { Element as DomElement, isTag } from 'domhandler';
21
21
  import { attr, tagOf } from '../dom.js';
22
22
  import { svgSizeHint } from '../svg.js';
23
23
  import { asciiLower, escapeEnd, mediaMatches, readIdent, startsIdent, } from './parse.js';
24
- import { applyDeclaration, blockify, settleContentVisibility, settleOverflow, settleClamp, copyStyle, decorate, inherit, initialOne, initialStyle, isInherited, } from './style.js';
24
+ import { applyDeclaration, blockify, settleButton, settleContentVisibility, settleOverflow, settleClamp, copyStyle, decorate, inherit, initialOne, initialStyle, isInherited, } from './style.js';
25
25
  import { parseDeclarations } from './parse.js';
26
26
  import { CounterStyles, counterStyleRule } from './counter-styles.js';
27
+ import { PALETTE_CHROME } from './ua.js';
27
28
  import { viewportUnit } from './values.js';
28
29
  import { customProperties, substituteIn } from './vars.js';
29
30
  /** Where a declaration came from. Higher wins before specificity is asked. */
@@ -36,6 +37,22 @@ var Origin;
36
37
  Origin[Origin["AuthorImportant"] = 4] = "AuthorImportant";
37
38
  Origin[Origin["InlineImportant"] = 5] = "InlineImportant";
38
39
  })(Origin || (Origin = {}));
40
+ export const HOVER_UNTOUCHED = 0;
41
+ export const HOVER_TOUCHED = 1;
42
+ export const HOVER_FOLLOWED = 2;
43
+ /** Which pseudo-elements' rules test the pointer (`Cascade.hoverPseudo`). */
44
+ export const HOVER_PSEUDO_NONE = 0;
45
+ export const HOVER_PSEUDO_GENERATED = 1;
46
+ export const HOVER_PSEUDO_OTHER = 2;
47
+ /** The pseudo-elements a pointer move can restyle: `::selection`'s style
48
+ * is worked out as a selection is painted. */
49
+ const POINTER_PSEUDO_ELEMENTS = [
50
+ 'before',
51
+ 'after',
52
+ 'first-letter',
53
+ 'first-line',
54
+ 'marker',
55
+ ];
39
56
  /**
40
57
  * What makes a selector's match depend on more than the element's own tag and
41
58
  * attributes and its ancestors' — its siblings, where it sits among them,
@@ -62,6 +79,11 @@ class RuleIndex {
62
79
  /** Whether any rule in here is pointer-sensitive, so the renderer knows
63
80
  * whether a pointer move can change the cascade at all. */
64
81
  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. */
86
+ pointer = null;
65
87
  /** The buckets holding a rule an element cannot share its style under
66
88
  * (`UNSHAREABLE`): the ids, classes and tags whose elements compute
67
89
  * their own, and whether the universal bucket makes every element do so. */
@@ -120,20 +142,27 @@ class RuleIndex {
120
142
  else
121
143
  this.ownStyleEverywhere = true;
122
144
  }
123
- const bucket = key.kind === 'id'
145
+ this._bucket(key).push(indexed);
146
+ if (HOVER.test(rule.selector)) {
147
+ this.pointer ??= new RuleIndex();
148
+ this.pointer.size += 1;
149
+ this.pointer._bucket(key).push(indexed);
150
+ }
151
+ }
152
+ _bucket(key) {
153
+ return key.kind === 'id'
124
154
  ? mapBucket(this.byId, key.name)
125
155
  : key.kind === 'class'
126
156
  ? mapBucket(this.byClass, key.name)
127
157
  : key.kind === 'tag'
128
158
  ? mapBucket(this.byTag, key.name)
129
159
  : this.universal;
130
- bucket.push(indexed);
131
160
  }
132
161
  }
133
162
  /** A selector's trailing `::before`, `::after`, `::first-letter`,
134
- * `::first-line` or `::marker`, or CSS 2's single-colon spelling of any of
135
- * its four. */
136
- const PSEUDO_ELEMENT = /::?(before|after|first-letter|first-line)$|::marker$/i;
163
+ * `::first-line`, `::marker` or `::selection`, or CSS 2's single-colon
164
+ * spelling of any of its four. */
165
+ const PSEUDO_ELEMENT = /::?(before|after|first-letter|first-line)$|::(marker|selection)$/i;
137
166
  /**
138
167
  * A rule for a pseudo-element, as the pseudo-element it styles and a rule
139
168
  * for the element it hangs off, which is what gets matched. The specificity
@@ -153,22 +182,35 @@ function splitPseudoElement(rule) {
153
182
  ? `${trimmed} *`
154
183
  : trimmed;
155
184
  return {
156
- which: (m[1] ?? 'marker').toLowerCase(),
185
+ which: (m[1] ?? m[2]).toLowerCase(),
157
186
  rule: { ...rule, selector },
158
187
  };
159
188
  }
160
189
  /**
161
190
  * The compounds of a selector that test `:hover`, each without it (and
162
191
  * without an `:active` beside it), and whether one is followed by a sibling
163
- * combinator, which reaches the element's later siblings. `nested` where a
164
- * `:hover` sits inside a functional pseudo-class — `:not(:hover)`,
165
- * `:is(:hover)` — and reaches elements no compound names.
192
+ * combinator, which reaches the element's later siblings.
166
193
  *
167
- * A `:hover` in a compound's `:has()` is the one exception, since what it
168
- * reaches is known: the compound, without its `:has()`, is an ancestor of
194
+ * A `:hover` inside `:is()`, `:where()` or `:not()` is found where it is:
195
+ * each takes a selector list and matches the element it is written on, so
196
+ * a list entry of one compound tests that element — `a:not(:hover)` is a
197
+ * compound of `a`'s that tests the pointer — and an entry with combinators
198
+ * in it names an ancestor or an earlier sibling, as it would outside.
199
+ * Tailwind 4 writes every `group-hover:` that way, `:is(:where(.group):hover
200
+ * *)`, and a page of them built its document again on every move. Whatever
201
+ * the nesting, the element whose hover changed is one a compound found here
202
+ * matches, and what changes with it is itself, what is in it, and what
203
+ * follows it where a sibling combinator is in play: a selector reads an
204
+ * element, its ancestors and the siblings before each of them, and nothing
205
+ * else. `nested` is left for a function this cannot read a selector list
206
+ * in — `:nth-child(… of :hover)`, `:host()` — which reaches elements no
207
+ * compound names.
208
+ *
209
+ * A `:hover` in a compound's `:has()` is the other way about, since what
210
+ * it tests is below: the compound, without its `:has()`, is an ancestor of
169
211
  * the element the pointer is over — or an earlier sibling of it or of an
170
212
  * ancestor, where the argument starts at a sibling (Selectors 4, 4.5). So
171
- * it is given as an anchor (`has`) rather than making the rule `nested`.
213
+ * it is given as an anchor (`has`).
172
214
  *
173
215
  * `:active` is not the pointer's here: nothing sets it (`setPointer` is
174
216
  * handed none), so a selector that tests only it never changes as the
@@ -181,8 +223,17 @@ export function pointerCompounds(selector) {
181
223
  siblings: false,
182
224
  nested: false,
183
225
  has: [],
226
+ followed: [],
184
227
  };
185
- // the compounds at the top level, each with the combinator after it
228
+ scanComplex(selector, out);
229
+ return out;
230
+ }
231
+ /** The functions that take a selector list and match the element they are
232
+ * written on (Selectors 4, 4.2 to 4.4), under the names css-select and
233
+ * older sheets know them by. */
234
+ const MATCHES_ANY = /^(?:is|where|not|matches|any|-webkit-any|-moz-any)$/i;
235
+ /** The compounds of a complex selector, each with the combinator after it. */
236
+ function compoundsOf(selector) {
186
237
  const parts = [];
187
238
  let start = 0;
188
239
  let depth = 0;
@@ -215,94 +266,203 @@ export function pointerCompounds(selector) {
215
266
  }
216
267
  }
217
268
  split(selector.length, '');
218
- for (const { text, next } of parts) {
219
- let bare = '';
220
- let pointer = false;
221
- depth = 0;
222
- quote = '';
223
- /** Where a top-level `:has(` starts in `bare`, and its argument in
224
- * `text`, while one is open. */
225
- let hasAt = -1;
226
- let argAt = -1;
227
- let hasPointer = false;
228
- let anchored = false;
229
- const args = [];
230
- for (let i = 0; i < text.length; i += 1) {
231
- const c = text[i];
232
- if (quote) {
233
- if (c === quote && text[i - 1] !== '\\')
234
- quote = '';
235
- bare += c;
236
- continue;
269
+ return parts;
270
+ }
271
+ /** A selector list's entries: split at its top-level commas. */
272
+ function selectorsOf(list) {
273
+ const entries = [];
274
+ let start = 0;
275
+ let depth = 0;
276
+ let quote = '';
277
+ for (let i = 0; i < list.length; i += 1) {
278
+ const c = list[i];
279
+ if (quote) {
280
+ if (c === quote && list[i - 1] !== '\\')
281
+ quote = '';
282
+ }
283
+ else if (c === '\\')
284
+ i = escapeEnd(list, i) - 1;
285
+ else if (c === '"' || c === "'")
286
+ quote = c;
287
+ else if (c === '(' || c === '[')
288
+ depth += 1;
289
+ else if (c === ')' || c === ']')
290
+ depth = Math.max(0, depth - 1);
291
+ else if (c === ',' && depth === 0) {
292
+ entries.push(list.slice(start, i));
293
+ start = i + 1;
294
+ }
295
+ }
296
+ entries.push(list.slice(start));
297
+ return entries;
298
+ }
299
+ /** Where the parenthesis opened at `open` closes, or the text's end. */
300
+ function closeOf(text, open) {
301
+ let depth = 0;
302
+ let quote = '';
303
+ for (let i = open; i < text.length; i += 1) {
304
+ const c = text[i];
305
+ if (quote) {
306
+ if (c === quote && text[i - 1] !== '\\')
307
+ quote = '';
308
+ }
309
+ else if (c === '\\')
310
+ i = escapeEnd(text, i) - 1;
311
+ else if (c === '"' || c === "'")
312
+ quote = c;
313
+ else if (c === '(' || c === '[')
314
+ depth += 1;
315
+ else if (c === ')' || c === ']') {
316
+ depth -= 1;
317
+ if (depth === 0)
318
+ return i;
319
+ }
320
+ }
321
+ return text.length;
322
+ }
323
+ /** `pointerCompounds` for one complex selector, into `out`. */
324
+ function scanComplex(selector, out) {
325
+ for (const { text, next } of compoundsOf(selector)) {
326
+ const compoundsFrom = out.compounds.length;
327
+ const hasFrom = out.has.length;
328
+ const found = scanCompound(text, out);
329
+ const sibling = next === '+' || next === '~';
330
+ if (found.args.length) {
331
+ out.has.push({
332
+ anchor: found.bare.trim() || '*',
333
+ siblings: found.args.some((arg) => selectorsOf(arg).some((one) => /^\s*[+~]/.test(one))),
334
+ });
335
+ }
336
+ if (found.pointer)
337
+ out.compounds.push(found.bare.trim() || '*');
338
+ // what follows the element this compound matches follows one whose
339
+ // hover, or whose answer to a `:has()`, changed
340
+ // — the one this compound matches, or one a selector nested in it
341
+ // named, which is more than it reaches and never less
342
+ if (sibling && (found.pointer || found.below || found.args.length)) {
343
+ out.siblings = true;
344
+ for (let i = compoundsFrom; i < out.compounds.length; i += 1) {
345
+ out.followed.push(out.compounds[i]);
237
346
  }
238
- if (c === '\\') {
239
- const end = escapeEnd(text, i);
240
- bare += text.slice(i, end);
241
- i = end - 1;
347
+ for (let i = hasFrom; i < out.has.length; i += 1) {
348
+ out.followed.push(out.has[i].anchor);
349
+ }
350
+ }
351
+ }
352
+ }
353
+ /**
354
+ * What one compound says of the pointer: the compound without what tests
355
+ * it (`bare`), whether it tests the element's own hover (`pointer`), the
356
+ * arguments of each `:has()` of the element's that tests it below (`args`),
357
+ * and whether a selector nested in it named another element's (`below`),
358
+ * whose compounds went into `out`.
359
+ */
360
+ function scanCompound(text, out) {
361
+ let bare = '';
362
+ let pointer = false;
363
+ let below = false;
364
+ const args = [];
365
+ let bracket = 0;
366
+ let quote = '';
367
+ for (let i = 0; i < text.length; i += 1) {
368
+ const c = text[i];
369
+ if (quote) {
370
+ if (c === quote && text[i - 1] !== '\\')
371
+ quote = '';
372
+ bare += c;
373
+ continue;
374
+ }
375
+ if (c === '\\') {
376
+ const end = escapeEnd(text, i);
377
+ bare += text.slice(i, end);
378
+ i = end - 1;
379
+ continue;
380
+ }
381
+ if (c === '"' || c === "'")
382
+ quote = c;
383
+ else if (c === '[')
384
+ bracket += 1;
385
+ else if (c === ']')
386
+ bracket = Math.max(0, bracket - 1);
387
+ else if (c === ':' && bracket === 0 && text[i + 1] !== ':') {
388
+ const m = POINTER_PSEUDO_AT.exec(text.slice(i));
389
+ if (m) {
390
+ if (m[0].length === 6)
391
+ pointer = true;
392
+ i += m[0].length - 1;
242
393
  continue;
243
394
  }
244
- if (c === '"' || c === "'")
245
- quote = c;
246
- else if (c === '(' || c === '[') {
247
- if (depth === 0 && c === '(' && /:has$/i.test(bare)) {
248
- hasAt = bare.length - 4;
249
- argAt = i + 1;
250
- hasPointer = false;
395
+ const fn = FUNCTION_AT.exec(text.slice(i));
396
+ if (fn) {
397
+ const open = i + fn[0].length - 1;
398
+ const close = closeOf(text, open);
399
+ const arg = text.slice(open + 1, close);
400
+ const whole = text.slice(i, close + 1);
401
+ i = close;
402
+ if (!HOVER.test(arg)) {
403
+ bare += whole;
404
+ continue;
251
405
  }
252
- depth += 1;
253
- }
254
- else if (c === ')' || c === ']') {
255
- depth = Math.max(0, depth - 1);
256
- if (depth === 0 && hasAt >= 0 && c === ')') {
406
+ const name = fn[1].toLowerCase();
407
+ if (name === 'has') {
257
408
  // the compound is kept without it: its argument is what the
258
- // pointer changes
259
- if (hasPointer) {
260
- anchored = true;
261
- args.push(text.slice(argAt, i));
262
- bare = bare.slice(0, hasAt);
263
- }
264
- else
265
- bare += c;
266
- hasAt = -1;
267
- continue;
409
+ // pointer changes, and whatever holds the `:hover` in there, the
410
+ // element it tests is in the anchor's subtree or a later
411
+ // sibling's
412
+ args.push(arg);
268
413
  }
269
- }
270
- else if (c === ':' && text[i + 1] !== ':') {
271
- const m = POINTER_PSEUDO_AT.exec(text.slice(i));
272
- if (m) {
273
- const hover = m[0].length === 6;
274
- if (depth > 0) {
275
- // whatever holds it in the argument, the element it tests is
276
- // in the anchor's subtree or a later sibling's
277
- if (hover && hasAt >= 0)
278
- hasPointer = true;
279
- else if (hover)
280
- out.nested = true;
281
- }
282
- else {
283
- if (hover)
414
+ else if (MATCHES_ANY.test(name)) {
415
+ // Kept as what the entries that test this element's hover ask
416
+ // of it besides — `:where(a:hover)` is a compound of `a`'s, and
417
+ // so is `:not(a:hover)`, which only an `a` can change its answer
418
+ // to. Tailwind's typography writes its links `.prose
419
+ // :where(a:hover):not(:where(.not-prose, .not-prose *))`, and
420
+ // without the `a` that was a compound every element matched.
421
+ const own = [];
422
+ let any = false;
423
+ for (const entry of selectorsOf(arg)) {
424
+ if (!HOVER.test(entry))
425
+ continue;
426
+ const parts = compoundsOf(entry);
427
+ if (parts.length !== 1) {
428
+ scanComplex(entry, out);
429
+ below = true;
430
+ continue;
431
+ }
432
+ // of this element itself
433
+ const one = scanCompound(parts[0].text, out);
434
+ if (one.below)
435
+ below = true;
436
+ if (!one.pointer && !one.args.length)
437
+ continue;
438
+ if (one.pointer)
284
439
  pointer = true;
285
- i += m[0].length - 1;
286
- continue;
440
+ args.push(...one.args);
441
+ const rest = one.bare.trim();
442
+ if (rest && rest !== '*')
443
+ own.push(rest);
444
+ else
445
+ any = true;
287
446
  }
447
+ if (own.length && !any)
448
+ bare += `:is(${own.join(',')})`;
288
449
  }
450
+ else {
451
+ out.nested = true;
452
+ bare += whole;
453
+ }
454
+ continue;
289
455
  }
290
- bare += c;
291
- }
292
- if (anchored) {
293
- out.has.push({
294
- anchor: bare.trim() || '*',
295
- siblings: args.some((arg) => arg.split(',').some((one) => /^\s*[+~]/.test(one))),
296
- });
297
456
  }
298
- if (!pointer)
299
- continue;
300
- out.compounds.push(bare.trim() || '*');
301
- if (next === '+' || next === '~')
302
- out.siblings = true;
457
+ bare += c;
303
458
  }
304
- return out;
459
+ return { bare, pointer, below, args };
305
460
  }
461
+ /** A functional pseudo-class's name and its opening parenthesis, at the
462
+ * start of a string. */
463
+ const FUNCTION_AT = /^:([\w-]+)\(/;
464
+ /** `:hover` anywhere in a selector. */
465
+ const HOVER = /:hover(?![\w-])/i;
306
466
  /** `:hover` or `:active` at the start of a string, and nothing longer. */
307
467
  const POINTER_PSEUDO_AT = /^:(?:hover|active)(?![\w-])/i;
308
468
  /** `:hover` or `:active` anywhere in a selector. */
@@ -456,23 +616,22 @@ export class Cascade {
456
616
  'first-letter': new RuleIndex(),
457
617
  'first-line': new RuleIndex(),
458
618
  marker: new RuleIndex(),
619
+ selection: new RuleIndex(),
459
620
  };
460
621
  _adapter;
461
622
  /** The compounds of the selectors that test the pointer, each without
462
623
  * its `:hover` or `:active` (`pointerCompounds`), and their matchers,
463
624
  * compiled when a pointer move first asks (`hoverTouches`). */
464
- _hoverCompounds = new Set();
625
+ _hoverCompounds = new Map();
465
626
  _hoverMatchers = null;
466
627
  /** Whether a pointer move can be restyled where it happened
467
628
  * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
468
629
  * pointer inside a functional pseudo-class, which reaches elements no
469
630
  * compound names. */
470
631
  hoverLocal = true;
471
- /** Whether a compound that tests the pointer is followed by a sibling
472
- * combinator, so an element's later siblings restyle with it. */
473
- hoverSiblings = false;
474
- /** The compounds whose `:has()` tests the pointer, each without it, and
475
- * whether its argument starts at a sibling (`hoverAnchors`). */
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`). */
476
635
  _hoverAnchors = new Map();
477
636
  _anchorMatchers = null;
478
637
  _pointer = NO_POINTER;
@@ -512,6 +671,12 @@ export class Cascade {
512
671
  /** The families the document loads itself (`fonts.ts`), or null for a
513
672
  * document with no `@font-face`. */
514
673
  _families;
674
+ /** The root element's computed style: its `font-size` is what a `rem`
675
+ * is and its line height what an `rlh` is (CSS Values 4, 6.1.1), in
676
+ * every element but the root, whose own declarations measure them from
677
+ * the initial values. Set as the root's style is computed, which a build
678
+ * does ahead of every element under it. */
679
+ _root = null;
515
680
  _mapFamilies;
516
681
  /** Whether any declaration has a length in `lh` or `rlh`: only then is
517
682
  * the line height settled ahead of the declarations that read it. */
@@ -553,6 +718,11 @@ export class Cascade {
553
718
  }
554
719
  for (const bp of sheet.breakpoints)
555
720
  breakpoints.add(bp);
721
+ // a query on the viewport's height reads it as a `vh` does
722
+ if (sheet.readsHeight)
723
+ this.readsViewportHeight = true;
724
+ if (sheet.readsWidth)
725
+ this.readsViewportWidth = true;
556
726
  }
557
727
  this.breakpoints = [...breakpoints].sort((a, b) => a - b);
558
728
  this.counterStyles = new CounterStyles(counterStyles);
@@ -604,42 +774,78 @@ export class Cascade {
604
774
  setPointer(pointer) {
605
775
  this._pointer = pointer;
606
776
  }
777
+ /**
778
+ * Which pseudo-elements' rules test the pointer: none's
779
+ * (`HOVER_PSEUDO_NONE`); a `::before`'s or an `::after`'s only
780
+ * (`HOVER_PSEUDO_GENERATED`), whose boxes a move restyles where they are
781
+ * as it does an element's; or a `::first-line`'s, a `::first-letter`'s
782
+ * or a `::marker`'s (`HOVER_PSEUDO_OTHER`), which layout styles and a
783
+ * move cannot.
784
+ */
785
+ get hoverPseudo() {
786
+ if (this._pseudo['first-letter'].hoverSensitive ||
787
+ this._pseudo['first-line'].hoverSensitive ||
788
+ this._pseudo.marker.hoverSensitive) {
789
+ return HOVER_PSEUDO_OTHER;
790
+ }
791
+ return this._pseudo.before.hoverSensitive ||
792
+ this._pseudo.after.hoverSensitive
793
+ ? HOVER_PSEUDO_GENERATED
794
+ : HOVER_PSEUDO_NONE;
795
+ }
607
796
  /**
608
797
  * Whether the pointer entering or leaving `el` can change a style: one of
609
798
  * the compounds a selector tests the pointer in matches it, the pointer
610
799
  * aside. A move between two paragraphs of a page whose only such rule is
611
- * `a:hover` touches nothing, and restyles nothing.
800
+ * `a:hover` touches nothing, and restyles nothing. `HOVER_FOLLOWED`
801
+ * where a sibling combinator follows one that matches, so the element's
802
+ * later siblings can change with it — those rules' elements alone: a
803
+ * sheet with one `.peer:hover ~ *` in it does not make every row of a
804
+ * table restyle the rows after it.
612
805
  */
613
806
  hoverTouches(el) {
614
- this._hoverMatchers ??= [...this._hoverCompounds].flatMap((c) => {
615
- const match = this._compile(c);
616
- return match ? [match] : [];
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 }] : [];
617
813
  });
618
- for (const match of this._hoverMatchers)
814
+ for (const { match, followed } of this._hoverMatchers) {
619
815
  if (match(el))
620
- return true;
621
- return false;
816
+ return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
817
+ }
818
+ return HOVER_UNTOUCHED;
622
819
  }
623
820
  /**
624
821
  * The elements whose `:has()` may have changed its answer as `el` was
625
822
  * hovered or left: the ancestors of it a compound whose `:has()` tests
626
823
  * the pointer names, and their earlier siblings and its own where the
627
824
  * argument starts at a sibling — a superset, which is restyled and found
628
- * the same where it did not. Into `into`.
825
+ * the same where it did not. Into `into`, each with whether its later
826
+ * siblings can change with it (`hoverTouches`).
629
827
  */
630
828
  hoverAnchors(el, into) {
631
829
  if (!this._hoverAnchors.size)
632
830
  return;
633
- this._anchorMatchers ??= [...this._hoverAnchors].flatMap(([anchor, siblings]) => {
831
+ this._anchorMatchers ??= [...this._hoverAnchors]
832
+ .sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
833
+ .flatMap(([anchor, { siblings, followed }]) => {
634
834
  const match = this._compile(anchor);
635
- return match ? [{ match, siblings }] : [];
835
+ return match ? [{ match, siblings, followed }] : [];
636
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
+ };
637
843
  const siblings = this._anchorMatchers.some((m) => m.siblings);
638
844
  for (let at = el; at;) {
639
845
  if (at !== el) {
640
- for (const { match } of this._anchorMatchers) {
846
+ for (const { match, followed } of this._anchorMatchers) {
641
847
  if (match(at)) {
642
- into.push(at);
848
+ note(at, followed);
643
849
  break;
644
850
  }
645
851
  }
@@ -648,9 +854,9 @@ export class Cascade {
648
854
  for (let s = at.prev; s; s = s.prev) {
649
855
  if (!isTag(s))
650
856
  continue;
651
- for (const { match, siblings: apart } of this._anchorMatchers) {
652
- if (apart && match(s)) {
653
- into.push(s);
857
+ for (const m of this._anchorMatchers) {
858
+ if (m.siblings && m.match(s)) {
859
+ note(s, m.followed);
654
860
  break;
655
861
  }
656
862
  }
@@ -666,12 +872,16 @@ export class Cascade {
666
872
  const found = pointerCompounds(selector);
667
873
  if (found.nested)
668
874
  this.hoverLocal = false;
669
- if (found.siblings)
670
- this.hoverSiblings = true;
671
- for (const c of found.compounds)
672
- this._hoverCompounds.add(c);
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
+ }
673
879
  for (const { anchor, siblings } of found.has) {
674
- this._hoverAnchors.set(anchor, siblings || (this._hoverAnchors.get(anchor) ?? false));
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
+ });
675
885
  }
676
886
  }
677
887
  /** A selector compiled as a rule's is, or null where css-select refuses
@@ -708,12 +918,34 @@ export class Cascade {
708
918
  _customs = new Map();
709
919
  _ids = new WeakMap();
710
920
  _nextId = 1;
711
- /** A box tree is about to be built: the styles shared in the last build
712
- * were computed against a pointer and a viewport that may have moved. */
713
- beginSharing() {
921
+ /** The styles kept from the build before, while a build that keeps them
922
+ * runs, and what each kept element shares under in this one. */
923
+ _kept = null;
924
+ _keptShared = new Map();
925
+ /**
926
+ * A box tree is about to be built: the styles shared in the last build
927
+ * were computed against a pointer and a viewport that may have moved.
928
+ *
929
+ * With `kept`, only its `restyle` elements are matched: a build the
930
+ * pointer asked for — a hover that moved something — changes the styles
931
+ * of the elements the move reached, and every other element's is what it
932
+ * was. Matching them all again was most of such a build: a frame of 550 ms
933
+ * on a page with a Tailwind sheet, and 150 without it.
934
+ */
935
+ beginSharing(kept = null) {
714
936
  this._shared.clear();
715
937
  this._sharedByMatch.clear();
716
- this._customs.clear();
938
+ // the kept styles' custom properties are the sets in here, which the
939
+ // elements styled under them find again
940
+ if (!kept)
941
+ this._customs.clear();
942
+ this._kept = kept;
943
+ this._keptShared.clear();
944
+ }
945
+ /** The build is over: nothing later is answered from what it kept. */
946
+ endSharing() {
947
+ this._kept = null;
948
+ this._keptShared.clear();
717
949
  }
718
950
  /**
719
951
  * An element's custom properties: its parent's, or its own over them —
@@ -795,6 +1027,23 @@ export class Cascade {
795
1027
  * does after the cascade, and `rootStyle` does not go through here.
796
1028
  */
797
1029
  sharedStyleFor(el, parentStyle, parentKey, inFlexContainer) {
1030
+ const kept = this._kept;
1031
+ if (kept !== null && !kept.restyle.has(el)) {
1032
+ // An element the change did not reach keeps its style, under a key
1033
+ // of its own: a key stands for an element's ancestors, which the
1034
+ // elements styled again under it have in common, and two kept
1035
+ // elements with one style need not — a hover restyled in place
1036
+ // leaves a hovered card's children the styles its neighbours' have.
1037
+ let shared = this._keptShared.get(el);
1038
+ if (shared !== undefined)
1039
+ return shared;
1040
+ const was = kept.styles.get(el);
1041
+ if (was !== undefined && was.inFlex === inFlexContainer) {
1042
+ shared = { style: was.style, key: this._nextShareKey++ };
1043
+ this._keptShared.set(el, shared);
1044
+ return shared;
1045
+ }
1046
+ }
798
1047
  let key = `${parentKey}\u0001${inFlexContainer ? 1 : 0}`;
799
1048
  const tag = tagOf(el);
800
1049
  key += `\u0001${tag.length}:${tag}`;
@@ -816,7 +1065,7 @@ export class Cascade {
816
1065
  let shared = this._sharedByMatch.get(key);
817
1066
  if (shared === undefined) {
818
1067
  shared = {
819
- style: this._computeStyle(el, parentStyle, inFlexContainer, candidates),
1068
+ style: this._computeStyle(el, parentStyle, inFlexContainer, candidates, true),
820
1069
  key: this._nextShareKey++,
821
1070
  };
822
1071
  this._sharedByMatch.set(key, shared);
@@ -864,7 +1113,7 @@ export class Cascade {
864
1113
  * a style is computed only for the elements with none to share.
865
1114
  */
866
1115
  styleFor(el, parentStyle, inFlexContainer) {
867
- return this._computeStyle(el, parentStyle, inFlexContainer, this._candidates(el));
1116
+ return this._computeStyle(el, parentStyle, inFlexContainer, this._candidates(el), true);
868
1117
  }
869
1118
  /**
870
1119
  * The style of an element's `::before` or `::after`, or null when it has
@@ -939,6 +1188,53 @@ export class Cascade {
939
1188
  out.unicodeBidi = 'isolate';
940
1189
  return out;
941
1190
  }
1191
+ /** Whether any rule styles a `::selection`: a document with none draws
1192
+ * a selection in the palette's colour, and asks nothing. */
1193
+ get hasSelection() {
1194
+ return this._pseudo.selection.size > 0;
1195
+ }
1196
+ /**
1197
+ * An element's `::selection`, over its parent's (`parent`, null for the
1198
+ * palette's own): the colour and the background the rules that reach it
1199
+ * set, and its parent's for either they leave alone. Both inherit along
1200
+ * the chain of highlights rather than the elements' (CSS Pseudo 4, 3.5,
1201
+ * "highlight inheritance"), which is what Chrome draws: a `<span>` in a
1202
+ * `div::selection { background: red }` is selected in red, and a `<p>`
1203
+ * whose own rule sets only a colour keeps the red under it.
1204
+ *
1205
+ * The palette's highlight is taken only where no rule has set either
1206
+ * (3.6, "paired defaults"): one that sets a colour and no background is
1207
+ * selected over none, as Chrome has it. A rule that sets neither — a
1208
+ * `text-shadow` only — leaves the element where its parent was.
1209
+ */
1210
+ selectionStyle(el, style, parent) {
1211
+ const index = this._pseudo.selection;
1212
+ if (!index.size || !index.reaches(el))
1213
+ return parent;
1214
+ const candidates = [];
1215
+ this._matchInto(index, el, candidates);
1216
+ let color = false;
1217
+ let background = false;
1218
+ for (const candidate of candidates) {
1219
+ for (const d of candidate.declarations) {
1220
+ if (d.prop === 'color')
1221
+ color = true;
1222
+ else if (d.prop === 'background' || d.prop === 'background-color') {
1223
+ background = true;
1224
+ }
1225
+ }
1226
+ }
1227
+ if (!color && !background)
1228
+ return parent;
1229
+ candidates.sort(byCascade);
1230
+ const own = this._computeStyle(el, style, false, candidates);
1231
+ return {
1232
+ color: color ? own.color : (parent?.color ?? null),
1233
+ background: background
1234
+ ? own.backgroundColor
1235
+ : (parent?.background ?? null),
1236
+ };
1237
+ }
942
1238
  /**
943
1239
  * An element's `::first-line` style, inheriting from its own, or null
944
1240
  * when no rule reaches it (CSS 2.1 5.12.1).
@@ -1009,7 +1305,10 @@ export class Cascade {
1009
1305
  return line;
1010
1306
  }
1011
1307
  /** `styleFor`, from the rules and hints already gathered for `el`. */
1012
- _computeStyle(el, parentStyle, inFlexContainer, candidates) {
1308
+ _computeStyle(el, parentStyle, inFlexContainer, candidates,
1309
+ /** Whether the style is the element's own, and not that of one of its
1310
+ * pseudo-elements. */
1311
+ own = false) {
1013
1312
  const style = inherit(parentStyle, this.initial);
1014
1313
  // custom properties first, in cascade order, so every `var()` in the
1015
1314
  // declarations below finds the one that wins
@@ -1019,10 +1318,16 @@ export class Cascade {
1019
1318
  // size, so a `font-size: 1.2em` in the cascade resolves against the
1020
1319
  // right em, and again after the font size is settled so every *other*
1021
1320
  // em-relative length in the same rule resolves against this element's.
1321
+ const root = isRootElement(el);
1322
+ const rootStyle = root ? this.initial : (this._root ?? this.initial);
1323
+ // whose declaration is being applied: the UA sheet's families are the
1324
+ // host's own faces, and its lists end where they end
1325
+ let authored = true;
1022
1326
  const ctxParent = {
1023
1327
  em: parentStyle.fontSize,
1024
- rem: this.initial.fontSize,
1328
+ rem: rootStyle.fontSize,
1025
1329
  initial: this.initial,
1330
+ fallbackFamily: () => (authored ? this.initial.fontFamily : null),
1026
1331
  vw: this.viewportWidth,
1027
1332
  vh: this.viewportHeight,
1028
1333
  scale: this.scale,
@@ -1030,7 +1335,7 @@ export class Cascade {
1030
1335
  ch: () => this._chOf(parentStyle),
1031
1336
  families: this._mapFamilies,
1032
1337
  lh: () => this._lineHeightOf(parentStyle),
1033
- rlh: () => this._lineHeightOf(this.initial),
1338
+ rlh: () => this._lineHeightOf(rootStyle),
1034
1339
  };
1035
1340
  // The family, the weight and the slant go with the size: together they
1036
1341
  // pick the face an `ex`, a `ch` or an `lh` in any declaration is
@@ -1041,6 +1346,7 @@ export class Cascade {
1041
1346
  let sized = false;
1042
1347
  let keyword = false;
1043
1348
  for (const c of candidates) {
1349
+ authored = c.origin !== Origin.UserAgent;
1044
1350
  for (const d of pick(c)) {
1045
1351
  if (d.prop === 'font-family' ||
1046
1352
  d.prop === 'font-weight' ||
@@ -1086,9 +1392,13 @@ export class Cascade {
1086
1392
  fontStyle: style.fontStyle,
1087
1393
  };
1088
1394
  let line = null;
1395
+ // the root's other declarations measure a `rem` from its own size
1396
+ if (root)
1397
+ this._root = style;
1089
1398
  const ctx = {
1090
1399
  ...ctxParent,
1091
1400
  em: style.fontSize,
1401
+ rem: root ? style.fontSize : ctxParent.rem,
1092
1402
  ex: () => this._exOf(face),
1093
1403
  ch: () => this._chOf(face),
1094
1404
  lh: () => this._lineHeightOf(line ?? style),
@@ -1099,6 +1409,7 @@ export class Cascade {
1099
1409
  // them. A `font` sets the size as well, which the first pass settled.
1100
1410
  if (this._lh) {
1101
1411
  for (const c of candidates) {
1412
+ authored = c.origin !== Origin.UserAgent;
1102
1413
  for (const d of pick(c)) {
1103
1414
  if (d.prop === 'line-height' || d.prop === 'font') {
1104
1415
  this._apply(style, parentStyle, d, ctx);
@@ -1114,6 +1425,7 @@ export class Cascade {
1114
1425
  }
1115
1426
  const settled = style.fontSize;
1116
1427
  for (const c of candidates) {
1428
+ authored = c.origin !== Origin.UserAgent;
1117
1429
  for (const d of pick(c)) {
1118
1430
  if (d.prop === 'font-size' || d.custom)
1119
1431
  continue;
@@ -1141,6 +1453,10 @@ export class Cascade {
1141
1453
  settleOverflow(style);
1142
1454
  settleContentVisibility(style);
1143
1455
  blockify(style, inFlexContainer);
1456
+ // a button is laid out as one, whatever `display` it was given
1457
+ if (own && el.name.length === 6 && tagOf(el) === 'button') {
1458
+ settleButton(style);
1459
+ }
1144
1460
  decorate(style);
1145
1461
  // which faces of the document's own families this family, weight and
1146
1462
  // slant ask for — known only now, with all three computed
@@ -1211,18 +1527,55 @@ export class Cascade {
1211
1527
  // the root is still the initial containing block.
1212
1528
  return { style: asRoot(this.styleFor(synthetic, parent, false)), html };
1213
1529
  }
1530
+ /**
1531
+ * Whether the pointer's move from `was` to where it is changed which
1532
+ * rules match `el`, or its `::before`, `::after` or another of its
1533
+ * pseudo-elements: the rules that test `:hover` asked under both, and
1534
+ * nothing else, since no other rule's answer is the pointer's. An element
1535
+ * they answer the same for, under a parent whose style is what it was, has
1536
+ * 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.
1538
+ */
1539
+ pointerChanged(el, was) {
1540
+ const now = this._pointer;
1541
+ const after = this._pointerAnswers(el);
1542
+ this._pointer = was;
1543
+ const before = this._pointerAnswers(el);
1544
+ this._pointer = now;
1545
+ return before !== after;
1546
+ }
1547
+ /** The ids of the rules testing the pointer that match `el` as things
1548
+ * stand, the element's and each pseudo-element's. */
1549
+ _pointerAnswers(el) {
1550
+ let answers = '';
1551
+ const ask = (index, name) => {
1552
+ const pointer = index.pointer;
1553
+ if (!pointer || !pointer.reaches(el))
1554
+ return;
1555
+ const matched = [];
1556
+ this._matchInto(pointer, el, null, matched);
1557
+ if (matched.length)
1558
+ answers += `${name}${matched.join(',')};`;
1559
+ };
1560
+ ask(this._index, '');
1561
+ for (const which of POINTER_PSEUDO_ELEMENTS)
1562
+ ask(this._pseudo[which], which);
1563
+ return answers;
1564
+ }
1214
1565
  /** The rules of `index` that match `el`, pushed onto `out` as candidates,
1215
1566
  * and their ids onto `matched` in the order they were tried. */
1216
1567
  _matchInto(index, el, out, matched) {
1217
1568
  // A media query's width is CSS pixels; the viewport is kept in device.
1218
1569
  const width = this.viewportWidth / this.scale;
1570
+ const height = this.viewportHeight / this.scale;
1219
1571
  const consider = (bucket) => {
1220
1572
  if (!bucket)
1221
1573
  return;
1222
1574
  for (const indexed of bucket) {
1223
1575
  const rule = indexed.rule;
1224
- if (!mediaMatches(rule.media, width, this.look.colorScheme))
1576
+ if (!mediaMatches(rule.media, width, this.look.colorScheme, height, this.scale)) {
1225
1577
  continue;
1578
+ }
1226
1579
  if (!indexed.compiled) {
1227
1580
  indexed.compiled = true;
1228
1581
  try {
@@ -1238,6 +1591,8 @@ export class Cascade {
1238
1591
  if (!indexed.match || !indexed.match(el))
1239
1592
  continue;
1240
1593
  matched?.push(indexed.id);
1594
+ if (out === null)
1595
+ continue;
1241
1596
  const origin = rule.order < 0 ? Origin.UserAgent : Origin.Author;
1242
1597
  pushRule(out, rule, origin);
1243
1598
  }
@@ -1304,9 +1659,57 @@ export class Cascade {
1304
1659
  }
1305
1660
  }
1306
1661
  out.sort(byCascade);
1662
+ dropPaletteChrome(out);
1307
1663
  return out;
1308
1664
  }
1309
1665
  }
1666
+ /**
1667
+ * Takes the palette's control chrome (`PALETTE_CHROME`) out of an element's
1668
+ * candidates where the page styles its background or border, so what shows
1669
+ * through is the web's UA values the page was written against. The rule is
1670
+ * Blink's (`LayoutTheme::IsControlStyled`): a background or border longhand
1671
+ * set by the page, radius included, drops the control's native appearance;
1672
+ * so does `appearance: none`. `out` is in cascade order, so the UA's
1673
+ * candidates are the ones before the first of any other origin.
1674
+ */
1675
+ function dropPaletteChrome(out) {
1676
+ let chrome = false;
1677
+ let i = 0;
1678
+ for (; i < out.length && out[i].origin === Origin.UserAgent; i += 1) {
1679
+ if (PALETTE_CHROME.has(out[i].declarations))
1680
+ chrome = true;
1681
+ }
1682
+ if (!chrome)
1683
+ return;
1684
+ let styled = false;
1685
+ for (let j = i; j < out.length && !styled; j += 1) {
1686
+ styled = pick(out[j]).some(stylesChrome);
1687
+ }
1688
+ if (!styled)
1689
+ return;
1690
+ let kept = 0;
1691
+ for (const c of out) {
1692
+ if (c.origin === Origin.UserAgent && PALETTE_CHROME.has(c.declarations)) {
1693
+ continue;
1694
+ }
1695
+ out[kept++] = c;
1696
+ }
1697
+ out.length = kept;
1698
+ }
1699
+ /** 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`. */
1702
+ function stylesChrome(d) {
1703
+ const prop = d.prop;
1704
+ if (prop.startsWith('background')) {
1705
+ return prop !== 'background-repeat' && prop !== 'background-blend-mode';
1706
+ }
1707
+ if (prop.startsWith('border')) {
1708
+ return prop !== 'border-collapse' && prop !== 'border-spacing';
1709
+ }
1710
+ return ((prop === 'appearance' || prop === '-webkit-appearance') &&
1711
+ d.value.trim().toLowerCase() === 'none');
1712
+ }
1310
1713
  /** An implied element's style as the root box's: its look, not its role —
1311
1714
  * the root is still a block, in flow, and the initial containing block. */
1312
1715
  function asRoot(style) {
@@ -1638,7 +2041,10 @@ function presentationHints(el) {
1638
2041
  // `width`/`height` are lengths on the replaced and table elements and mean
1639
2042
  // nothing anywhere else, which is what stops a `<input width>` from
1640
2043
  // becoming a CSS width the widget then disagrees with.
1641
- if (SIZED.has(tag)) {
2044
+ if (SIZED.has(tag) ||
2045
+ // an image button's are its image's, as an `<img>`'s are (HTML 15.4.3)
2046
+ (tag === 'input' &&
2047
+ (attr(el, 'type') ?? '').trim().toLowerCase() === 'image')) {
1642
2048
  const width = attr(el, 'width');
1643
2049
  if (width)
1644
2050
  push('width', lengthAttr(width));
@@ -1666,6 +2072,14 @@ function presentationHints(el) {
1666
2072
  const h = height ? svgSizeHint(height) : null;
1667
2073
  if (h)
1668
2074
  push('height', h);
2075
+ // and its paint is its `fill` and `stroke` properties' (13.2), which a
2076
+ // rule of the document's then sets over the attribute
2077
+ const fill = attr(el, 'fill');
2078
+ if (fill)
2079
+ push('fill', fill);
2080
+ const stroke = attr(el, 'stroke');
2081
+ if (stroke)
2082
+ push('stroke', stroke);
1669
2083
  }
1670
2084
  if (tag === 'table') {
1671
2085
  const border = attr(el, 'border');
@@ -1878,4 +2292,9 @@ const ABSOLUTE_SIZES = new Set([
1878
2292
  'xxx-large',
1879
2293
  'initial',
1880
2294
  ]);
2295
+ /** Whether an element is the document's root: its parent the document, or
2296
+ * none, as the `<html>` the cascade supplies where the markup has none. */
2297
+ function isRootElement(el) {
2298
+ return !el.parent || el.parent.type === 'root';
2299
+ }
1881
2300
  //# sourceMappingURL=cascade.js.map