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