@react-x11/components 0.14.1 → 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 +171 -26
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +616 -216
- 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 +18 -0
- package/dist/html/css/shapes.d.ts.map +1 -1
- package/dist/html/css/shapes.js +21 -1
- package/dist/html/css/shapes.js.map +1 -1
- 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 +74 -12
- 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 +29 -9
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +141 -52
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +16 -2
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +144 -23
- 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 +11 -9
- 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 +1631 -221
- 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 +886 -324
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +35 -4
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +118 -26
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +558 -178
- 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 +23 -7
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +264 -59
- 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 +697 -221
- 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 +35 -1
- package/src/html/css/style.ts +122 -17
- 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 +149 -58
- package/src/html/layout/boxes.ts +171 -27
- package/src/html/layout/css-grid.ts +12 -9
- package/src/html/layout/flex.ts +1843 -212
- package/src/html/layout/inline.ts +27 -7
- package/src/html/layout/table.ts +961 -332
- package/src/html/node.ts +151 -27
- package/src/html/paint.ts +692 -234
- package/src/html/resources.ts +13 -1
- package/src/html/svg.ts +299 -56
- 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,6 +22,7 @@ import { Element as DomElement, isTag } from 'domhandler';
|
|
|
21
22
|
import type { Element } from 'domhandler';
|
|
22
23
|
|
|
23
24
|
import { attr, tagOf } from '../dom.js';
|
|
25
|
+
import { systemColorTable, usedColorScheme } from './color.js';
|
|
24
26
|
import {
|
|
25
27
|
asciiLower,
|
|
26
28
|
escapeEnd,
|
|
@@ -32,9 +34,11 @@ import type { Declaration, StyleRule, Stylesheet } from './parse.js';
|
|
|
32
34
|
import {
|
|
33
35
|
applyDeclaration,
|
|
34
36
|
blockify,
|
|
37
|
+
settleAlign,
|
|
35
38
|
settleButton,
|
|
36
39
|
settleContentVisibility,
|
|
37
40
|
settleOverflow,
|
|
41
|
+
settleOutline,
|
|
38
42
|
settleClamp,
|
|
39
43
|
copyStyle,
|
|
40
44
|
decorate,
|
|
@@ -57,7 +61,9 @@ import {
|
|
|
57
61
|
ROOT_BOX_PROPS,
|
|
58
62
|
SHAPE_TAGS,
|
|
59
63
|
isShapeProp,
|
|
64
|
+
isUse,
|
|
60
65
|
shapeValue,
|
|
66
|
+
useHref,
|
|
61
67
|
} from './shapes.js';
|
|
62
68
|
import type { ShapeContext, ShapeStyle, ShapeStyles } from './shapes.js';
|
|
63
69
|
|
|
@@ -153,6 +159,10 @@ interface IndexedRule {
|
|
|
153
159
|
/** For a rule kept for drawings: whether it declares something a
|
|
154
160
|
* drawing's root does not have from its box's style. Null until asked. */
|
|
155
161
|
root: boolean | null;
|
|
162
|
+
/** The selector as it is matched in the tree a `<use>`'s copy is in
|
|
163
|
+
* (`Cascade.shapeStyles`), compiled once one is first asked. */
|
|
164
|
+
inCopy: ((el: Element) => boolean) | null;
|
|
165
|
+
inCopyCompiled: boolean;
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
/**
|
|
@@ -183,10 +193,12 @@ class RuleIndex {
|
|
|
183
193
|
/** Whether any rule in here is pointer-sensitive, so the renderer knows
|
|
184
194
|
* whether a pointer move can change the cascade at all. */
|
|
185
195
|
hoverSensitive = false;
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
*
|
|
196
|
+
/** …and focus-sensitive, for a widget taking the focus or giving it up. */
|
|
197
|
+
focusSensitive = false;
|
|
198
|
+
/** The rules that test `:hover` or the focus, in buckets of their own:
|
|
199
|
+
* the ones whose answers a pointer move or a focus change can change,
|
|
200
|
+
* which is all that is asked to know whether an element's did
|
|
201
|
+
* (`Cascade.pointerChanged`). Null until there is one. */
|
|
190
202
|
pointer: RuleIndex | null = null;
|
|
191
203
|
/** The buckets holding a rule an element cannot share its style under
|
|
192
204
|
* (`UNSHAREABLE`): the ids, classes and tags whose elements compute
|
|
@@ -237,10 +249,14 @@ class RuleIndex {
|
|
|
237
249
|
id: this._nextId++,
|
|
238
250
|
needs: null,
|
|
239
251
|
root: null,
|
|
252
|
+
inCopy: null,
|
|
253
|
+
inCopyCompiled: false,
|
|
240
254
|
};
|
|
241
255
|
if (rule.selector.includes(':hover') || rule.selector.includes(':active')) {
|
|
242
256
|
this.hoverSensitive = true;
|
|
243
257
|
}
|
|
258
|
+
const focus = FOCUS.test(rule.selector);
|
|
259
|
+
if (focus) this.focusSensitive = true;
|
|
244
260
|
if (UNSHAREABLE.test(rule.selector)) {
|
|
245
261
|
if (key.kind === 'id') this.ownStyleIds.add(key.name);
|
|
246
262
|
else if (key.kind === 'class') this.ownStyleClasses.add(key.name);
|
|
@@ -248,7 +264,7 @@ class RuleIndex {
|
|
|
248
264
|
else this.ownStyleEverywhere = true;
|
|
249
265
|
}
|
|
250
266
|
this._bucket(key).push(indexed);
|
|
251
|
-
if (HOVER.test(rule.selector)) {
|
|
267
|
+
if (focus || HOVER.test(rule.selector)) {
|
|
252
268
|
this.pointer ??= new RuleIndex();
|
|
253
269
|
this.pointer.size += 1;
|
|
254
270
|
this.pointer._bucket(key).push(indexed);
|
|
@@ -341,8 +357,18 @@ function splitPseudoElement(
|
|
|
341
357
|
* handed none), so a selector that tests only it never changes as the
|
|
342
358
|
* pointer moves — Wikipedia's buttons' `:focus:not(:active)` among them.
|
|
343
359
|
* A press that sets it would have to be counted here too.
|
|
360
|
+
*
|
|
361
|
+
* The focus is the other state a change of reaches only some elements, and
|
|
362
|
+
* `kind` says which is asked for: `FOCUS_STATE` finds the compounds that
|
|
363
|
+
* test `:focus`, `:focus-visible` or `:focus-within` the same way. Either
|
|
364
|
+
* kind leaves the other's pseudo-classes out of a compound as well as its
|
|
365
|
+
* own, so what is left matches wherever the compound could, in any state —
|
|
366
|
+
* `a:focus:hover` is a compound of `a`'s, for both.
|
|
344
367
|
*/
|
|
345
|
-
export function pointerCompounds(
|
|
368
|
+
export function pointerCompounds(
|
|
369
|
+
selector: string,
|
|
370
|
+
kind: StateKind = HOVER_STATE,
|
|
371
|
+
): PointerCompounds {
|
|
346
372
|
const out: PointerCompounds = {
|
|
347
373
|
compounds: [],
|
|
348
374
|
siblings: false,
|
|
@@ -350,10 +376,33 @@ export function pointerCompounds(selector: string): PointerCompounds {
|
|
|
350
376
|
has: [],
|
|
351
377
|
followed: [],
|
|
352
378
|
};
|
|
353
|
-
scanComplex(selector, out);
|
|
379
|
+
scanComplex(selector, out, kind);
|
|
354
380
|
return out;
|
|
355
381
|
}
|
|
356
382
|
|
|
383
|
+
/** A state a change of reaches only the elements some compound names: which
|
|
384
|
+
* selectors mention it at all, which tests it anywhere, and which of the
|
|
385
|
+
* pseudo-classes `STATE_PSEUDO_AT` takes out of a compound are its own. */
|
|
386
|
+
export interface StateKind {
|
|
387
|
+
mentions: RegExp;
|
|
388
|
+
any: RegExp;
|
|
389
|
+
own: RegExp;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** The pointer's: `:hover`, and `:active`, which nothing sets. */
|
|
393
|
+
export const HOVER_STATE: StateKind = {
|
|
394
|
+
mentions: /:(?:hover|active)/i,
|
|
395
|
+
any: /:hover(?![\w-])/i,
|
|
396
|
+
own: /^:hover$/i,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/** The focus's: `:focus`, `:focus-visible` and `:focus-within`. */
|
|
400
|
+
export const FOCUS_STATE: StateKind = {
|
|
401
|
+
mentions: /:focus/i,
|
|
402
|
+
any: /:focus(?:-visible|-within)?(?![\w-])/i,
|
|
403
|
+
own: /^:focus/i,
|
|
404
|
+
};
|
|
405
|
+
|
|
357
406
|
export interface PointerCompounds {
|
|
358
407
|
compounds: string[];
|
|
359
408
|
siblings: boolean;
|
|
@@ -442,11 +491,15 @@ function closeOf(text: string, open: number): number {
|
|
|
442
491
|
}
|
|
443
492
|
|
|
444
493
|
/** `pointerCompounds` for one complex selector, into `out`. */
|
|
445
|
-
function scanComplex(
|
|
494
|
+
function scanComplex(
|
|
495
|
+
selector: string,
|
|
496
|
+
out: PointerCompounds,
|
|
497
|
+
kind: StateKind,
|
|
498
|
+
): void {
|
|
446
499
|
for (const { text, next } of compoundsOf(selector)) {
|
|
447
500
|
const compoundsFrom = out.compounds.length;
|
|
448
501
|
const hasFrom = out.has.length;
|
|
449
|
-
const found = scanCompound(text, out);
|
|
502
|
+
const found = scanCompound(text, out, kind);
|
|
450
503
|
const sibling = next === '+' || next === '~';
|
|
451
504
|
if (found.args.length) {
|
|
452
505
|
out.has.push({
|
|
@@ -478,11 +531,13 @@ function scanComplex(selector: string, out: PointerCompounds): void {
|
|
|
478
531
|
* it (`bare`), whether it tests the element's own hover (`pointer`), the
|
|
479
532
|
* arguments of each `:has()` of the element's that tests it below (`args`),
|
|
480
533
|
* and whether a selector nested in it named another element's (`below`),
|
|
481
|
-
* whose compounds went into `out`.
|
|
534
|
+
* whose compounds went into `out`. Of the focus, likewise, for
|
|
535
|
+
* `FOCUS_STATE`.
|
|
482
536
|
*/
|
|
483
537
|
function scanCompound(
|
|
484
538
|
text: string,
|
|
485
539
|
out: PointerCompounds,
|
|
540
|
+
kind: StateKind,
|
|
486
541
|
): { bare: string; pointer: boolean; below: boolean; args: string[] } {
|
|
487
542
|
let bare = '';
|
|
488
543
|
let pointer = false;
|
|
@@ -507,9 +562,9 @@ function scanCompound(
|
|
|
507
562
|
else if (c === '[') bracket += 1;
|
|
508
563
|
else if (c === ']') bracket = Math.max(0, bracket - 1);
|
|
509
564
|
else if (c === ':' && bracket === 0 && text[i + 1] !== ':') {
|
|
510
|
-
const m =
|
|
565
|
+
const m = STATE_PSEUDO_AT.exec(text.slice(i));
|
|
511
566
|
if (m) {
|
|
512
|
-
if (m[0]
|
|
567
|
+
if (kind.own.test(m[0])) pointer = true;
|
|
513
568
|
i += m[0].length - 1;
|
|
514
569
|
continue;
|
|
515
570
|
}
|
|
@@ -520,7 +575,7 @@ function scanCompound(
|
|
|
520
575
|
const arg = text.slice(open + 1, close);
|
|
521
576
|
const whole = text.slice(i, close + 1);
|
|
522
577
|
i = close;
|
|
523
|
-
if (!
|
|
578
|
+
if (!kind.any.test(arg)) {
|
|
524
579
|
bare += whole;
|
|
525
580
|
continue;
|
|
526
581
|
}
|
|
@@ -541,15 +596,15 @@ function scanCompound(
|
|
|
541
596
|
const own: string[] = [];
|
|
542
597
|
let any = false;
|
|
543
598
|
for (const entry of selectorsOf(arg)) {
|
|
544
|
-
if (!
|
|
599
|
+
if (!kind.any.test(entry)) continue;
|
|
545
600
|
const parts = compoundsOf(entry);
|
|
546
601
|
if (parts.length !== 1) {
|
|
547
|
-
scanComplex(entry, out);
|
|
602
|
+
scanComplex(entry, out, kind);
|
|
548
603
|
below = true;
|
|
549
604
|
continue;
|
|
550
605
|
}
|
|
551
606
|
// of this element itself
|
|
552
|
-
const one = scanCompound(parts[0].text, out);
|
|
607
|
+
const one = scanCompound(parts[0].text, out, kind);
|
|
553
608
|
if (one.below) below = true;
|
|
554
609
|
if (!one.pointer && !one.args.length) continue;
|
|
555
610
|
if (one.pointer) pointer = true;
|
|
@@ -576,33 +631,42 @@ function scanCompound(
|
|
|
576
631
|
const FUNCTION_AT = /^:([\w-]+)\(/;
|
|
577
632
|
|
|
578
633
|
/** `:hover` anywhere in a selector. */
|
|
579
|
-
const HOVER =
|
|
634
|
+
const HOVER = HOVER_STATE.any;
|
|
635
|
+
|
|
636
|
+
/** `:focus`, `:focus-visible` or `:focus-within` anywhere in a selector. */
|
|
637
|
+
const FOCUS = FOCUS_STATE.any;
|
|
580
638
|
|
|
581
|
-
/**
|
|
582
|
-
|
|
639
|
+
/** A pseudo-class of a state this renderer keeps — `:hover`, `:active`,
|
|
640
|
+
* `:focus` and its two kin — at the start of a string, and nothing
|
|
641
|
+
* longer. */
|
|
642
|
+
const STATE_PSEUDO_AT =
|
|
643
|
+
/^:(?:hover|active|focus(?:-visible|-within)?)(?![\w-])/i;
|
|
583
644
|
|
|
584
|
-
/**
|
|
585
|
-
const
|
|
645
|
+
/** Any of them anywhere in a selector. */
|
|
646
|
+
const STATE_PSEUDO = /:(?:hover|active|focus(?:-visible|-within)?)(?![\w-])/i;
|
|
586
647
|
|
|
587
648
|
/**
|
|
588
649
|
* A selector compiled to a matcher over this adapter. css-select keeps the
|
|
589
650
|
* answers of a `:has()` above the subject, or of the ancestors a
|
|
590
651
|
* descendant combinator tried under one, for as long as the matcher lives
|
|
591
652
|
* (`cacheResults`) — which is the cascade's life, and a pointer move is no
|
|
592
|
-
* new cascade. So a selector that tests the pointer
|
|
593
|
-
* `#box:has(a:hover) .m` answered what it did before the
|
|
594
|
-
* good.
|
|
653
|
+
* new cascade. So a selector that tests the pointer or the focus keeps
|
|
654
|
+
* none: with them, `#box:has(a:hover) .m` answered what it did before the
|
|
655
|
+
* first move, for good.
|
|
595
656
|
*/
|
|
596
657
|
function compileSelector(
|
|
597
658
|
selector: string,
|
|
598
659
|
adapter: CssSelectAdapter,
|
|
599
660
|
pseudos: typeof PSEUDOS = PSEUDOS,
|
|
661
|
+
/** Whether what it answers may be kept: not where the adapter's tree
|
|
662
|
+
* moves under it, as a `<use>`'s copy's does (`Cascade.shapeStyles`). */
|
|
663
|
+
cache = true,
|
|
600
664
|
): (el: Element) => boolean {
|
|
601
665
|
return compile(noEmptyWords(selector), {
|
|
602
666
|
adapter,
|
|
603
667
|
xmlMode: false,
|
|
604
668
|
pseudos,
|
|
605
|
-
cacheResults: !
|
|
669
|
+
cacheResults: cache && !STATE_PSEUDO.test(selector),
|
|
606
670
|
} as unknown as Parameters<typeof compile>[1]) as unknown as (
|
|
607
671
|
node: Element,
|
|
608
672
|
) => boolean;
|
|
@@ -724,6 +788,41 @@ function ancestorKeys(selector: string): string[] {
|
|
|
724
788
|
return keys;
|
|
725
789
|
}
|
|
726
790
|
|
|
791
|
+
/** A drawing none of whose own elements a rule reaches. */
|
|
792
|
+
const NO_SHAPES: ReadonlyMap<Element, ShapeStyle> = new Map();
|
|
793
|
+
|
|
794
|
+
/** What the rules matched in the copy a `<use>` makes of an element: each
|
|
795
|
+
* element they reach, with its place in the copy and its rules in cascade
|
|
796
|
+
* order; how many elements the copy has; and the `<use>`s in it. */
|
|
797
|
+
interface CopyMatch {
|
|
798
|
+
rules: { el: Element; at: number; candidates: Candidate[] }[];
|
|
799
|
+
size: number;
|
|
800
|
+
uses: Element[];
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* What `Cascade.shapeStyles` keeps for the drawings of one build of the
|
|
805
|
+
* boxes (`BoxTree.shapeCopies`): the document's elements by their ids, and
|
|
806
|
+
* what the rules matched in the copy of each element a `<use>` names —
|
|
807
|
+
* the same whichever `<use>` makes it, so a page of hundreds of icons from
|
|
808
|
+
* one sprite matches each symbol once — and what that comes to for a
|
|
809
|
+
* drawing of each style. A build is made again whenever the document, its
|
|
810
|
+
* style sheets or a viewport a value reads change, and a pointer move
|
|
811
|
+
* changes nothing a copy is matched by: nothing in one is under the
|
|
812
|
+
* pointer, and a drawing whose style it changes has another style.
|
|
813
|
+
*/
|
|
814
|
+
export class ShapeCopies {
|
|
815
|
+
/** @internal */
|
|
816
|
+
readonly matched = new Map<Element, CopyMatch>();
|
|
817
|
+
/** @internal What each copy comes to for a drawing of a style, and the
|
|
818
|
+
* key that names it. */
|
|
819
|
+
readonly resolved = new WeakMap<
|
|
820
|
+
ComputedStyle,
|
|
821
|
+
Map<Element, { into: Map<Element, ShapeStyle> | null; key: string }>
|
|
822
|
+
>();
|
|
823
|
+
constructor(readonly byId: (id: string) => Element | null) {}
|
|
824
|
+
}
|
|
825
|
+
|
|
727
826
|
/** What the rules asked of every element of a drawing, or of every one of
|
|
728
827
|
* a type, ask of its ancestors (`Cascade.shapeStyles`). */
|
|
729
828
|
interface ShapeNeeds {
|
|
@@ -768,6 +867,138 @@ export interface PointerState {
|
|
|
768
867
|
|
|
769
868
|
const NO_POINTER: PointerState = { hovered: new Set(), active: new Set() };
|
|
770
869
|
|
|
870
|
+
/** Where the focus is, for `:focus` and its kin: the element whose widget
|
|
871
|
+
* holds it, whether that shows (`:focus-visible`), and the element with
|
|
872
|
+
* every element around it (`:focus-within`). No element of a document
|
|
873
|
+
* takes the focus itself; a control's widget, mounted beside it, does. */
|
|
874
|
+
export interface FocusState {
|
|
875
|
+
element: Element | null;
|
|
876
|
+
visible: boolean;
|
|
877
|
+
within: ReadonlySet<Element>;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export const NO_FOCUS: FocusState = {
|
|
881
|
+
element: null,
|
|
882
|
+
visible: false,
|
|
883
|
+
within: new Set(),
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* What the selectors that test one state — the pointer, or the focus — say
|
|
888
|
+
* of the elements a change of it can restyle: the compounds that test it,
|
|
889
|
+
* each without it and with whether a sibling combinator follows it, and
|
|
890
|
+
* the anchors of the `:has()`s that test it (`pointerCompounds`).
|
|
891
|
+
*/
|
|
892
|
+
class StateRules {
|
|
893
|
+
/** The compounds, each without the state, and their matchers, compiled
|
|
894
|
+
* when a change first asks (`touches`). */
|
|
895
|
+
private _compounds = new Map<string, boolean>();
|
|
896
|
+
private _matchers:
|
|
897
|
+
{ match: (el: Element) => boolean; followed: boolean }[] | null = null;
|
|
898
|
+
/** Whether a change can be restyled where it happened
|
|
899
|
+
* (`HtmlViewNode._hoverInPlace`): false where a selector tests the
|
|
900
|
+
* state inside a functional pseudo-class, which reaches elements no
|
|
901
|
+
* compound names. */
|
|
902
|
+
local = true;
|
|
903
|
+
/** The compounds whose `:has()` tests the state, each without it,
|
|
904
|
+
* whether its argument starts at a sibling, and whether a sibling
|
|
905
|
+
* combinator follows it (`anchors`). */
|
|
906
|
+
private _anchors = new Map<
|
|
907
|
+
string,
|
|
908
|
+
{ siblings: boolean; followed: boolean }
|
|
909
|
+
>();
|
|
910
|
+
private _anchorMatchers:
|
|
911
|
+
| {
|
|
912
|
+
match: (el: Element) => boolean;
|
|
913
|
+
siblings: boolean;
|
|
914
|
+
followed: boolean;
|
|
915
|
+
}[]
|
|
916
|
+
| null = null;
|
|
917
|
+
|
|
918
|
+
constructor(private readonly _kind: StateKind) {}
|
|
919
|
+
|
|
920
|
+
note(selector: string): void {
|
|
921
|
+
if (!this._kind.mentions.test(selector)) return;
|
|
922
|
+
const found = pointerCompounds(selector, this._kind);
|
|
923
|
+
if (found.nested) this.local = false;
|
|
924
|
+
const followed = new Set(found.followed);
|
|
925
|
+
for (const c of found.compounds) {
|
|
926
|
+
this._compounds.set(
|
|
927
|
+
c,
|
|
928
|
+
followed.has(c) || (this._compounds.get(c) ?? false),
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
for (const { anchor, siblings } of found.has) {
|
|
932
|
+
const was = this._anchors.get(anchor);
|
|
933
|
+
this._anchors.set(anchor, {
|
|
934
|
+
siblings: siblings || (was?.siblings ?? false),
|
|
935
|
+
followed: followed.has(anchor) || (was?.followed ?? false),
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** `Cascade.hoverTouches`, for this state. */
|
|
941
|
+
touches(
|
|
942
|
+
el: Element,
|
|
943
|
+
compile: (selector: string) => ((el: Element) => boolean) | null,
|
|
944
|
+
): HoverTouch {
|
|
945
|
+
// the followed ones first, so the first that matches is the answer
|
|
946
|
+
this._matchers ??= [...this._compounds]
|
|
947
|
+
.sort(([, a], [, b]) => Number(b) - Number(a))
|
|
948
|
+
.flatMap(([compound, followed]) => {
|
|
949
|
+
const match = compile(compound);
|
|
950
|
+
return match ? [{ match, followed }] : [];
|
|
951
|
+
});
|
|
952
|
+
for (const { match, followed } of this._matchers) {
|
|
953
|
+
if (match(el)) return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
|
|
954
|
+
}
|
|
955
|
+
return HOVER_UNTOUCHED;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** `Cascade.hoverAnchors`, for this state. */
|
|
959
|
+
anchors(
|
|
960
|
+
el: Element,
|
|
961
|
+
into: Map<Element, HoverTouch>,
|
|
962
|
+
compile: (selector: string) => ((el: Element) => boolean) | null,
|
|
963
|
+
): void {
|
|
964
|
+
if (!this._anchors.size) return;
|
|
965
|
+
this._anchorMatchers ??= [...this._anchors]
|
|
966
|
+
.sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
|
|
967
|
+
.flatMap(([anchor, { siblings, followed }]) => {
|
|
968
|
+
const match = compile(anchor);
|
|
969
|
+
return match ? [{ match, siblings, followed }] : [];
|
|
970
|
+
});
|
|
971
|
+
const note = (at: Element, followed: boolean): void => {
|
|
972
|
+
if (followed) into.set(at, HOVER_FOLLOWED);
|
|
973
|
+
else if (!into.has(at)) into.set(at, HOVER_TOUCHED);
|
|
974
|
+
};
|
|
975
|
+
const siblings = this._anchorMatchers.some((m) => m.siblings);
|
|
976
|
+
for (let at: Element | null = el; at;) {
|
|
977
|
+
if (at !== el) {
|
|
978
|
+
for (const { match, followed } of this._anchorMatchers) {
|
|
979
|
+
if (match(at)) {
|
|
980
|
+
note(at, followed);
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (siblings) {
|
|
986
|
+
for (let s = at.prev; s; s = s.prev) {
|
|
987
|
+
if (!isTag(s as Element)) continue;
|
|
988
|
+
for (const m of this._anchorMatchers) {
|
|
989
|
+
if (m.siblings && m.match(s as Element)) {
|
|
990
|
+
note(s as Element, m.followed);
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const parent: Element['parent'] = at.parent;
|
|
997
|
+
at = parent && isTag(parent as Element) ? (parent as Element) : null;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
771
1002
|
/**
|
|
772
1003
|
* The face a style's font-relative units are measured in: its family list,
|
|
773
1004
|
* size, weight and slant, the four that pick one. A family's faces can be
|
|
@@ -832,32 +1063,23 @@ export class Cascade {
|
|
|
832
1063
|
private _shapes = new RuleIndex();
|
|
833
1064
|
private _shapeNeeds: ShapeNeeds | null = null;
|
|
834
1065
|
private _adapter: CssSelectAdapter;
|
|
835
|
-
/**
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
*
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
/**
|
|
847
|
-
*
|
|
848
|
-
|
|
849
|
-
private
|
|
850
|
-
string,
|
|
851
|
-
{ siblings: boolean; followed: boolean }
|
|
852
|
-
>();
|
|
853
|
-
private _anchorMatchers:
|
|
854
|
-
| {
|
|
855
|
-
match: (el: Element) => boolean;
|
|
856
|
-
siblings: boolean;
|
|
857
|
-
followed: boolean;
|
|
858
|
-
}[]
|
|
859
|
-
| null = null;
|
|
1066
|
+
/**
|
|
1067
|
+
* The tree a `<use>`'s copy is in, for `_copyAdapter`: the element it is
|
|
1068
|
+
* a copy of, which is its top (`ShapeStyles.used`). A copy is its own
|
|
1069
|
+
* tree, a shadow tree of the `<use>`'s (SVG 2, 5.5.1), so a selector is
|
|
1070
|
+
* matched against its original with nothing above that element and
|
|
1071
|
+
* nothing beside it — the top of a copy is its tree's first child and
|
|
1072
|
+
* its last, whichever of its parent's it is. And nothing in it is under
|
|
1073
|
+
* the pointer, as nothing in a drawing is: it has no box to be.
|
|
1074
|
+
*/
|
|
1075
|
+
private _copyTop: Element | null = null;
|
|
1076
|
+
private _copyAdapter: CssSelectAdapter;
|
|
1077
|
+
/** What the selectors that test the pointer, and the ones that test the
|
|
1078
|
+
* focus, say of the elements a change of it reaches. */
|
|
1079
|
+
private _hoverRules = new StateRules(HOVER_STATE);
|
|
1080
|
+
private _focusRules = new StateRules(FOCUS_STATE);
|
|
860
1081
|
private _pointer: PointerState = NO_POINTER;
|
|
1082
|
+
private _focus: FocusState = NO_FOCUS;
|
|
861
1083
|
readonly initial: ComputedStyle;
|
|
862
1084
|
readonly look: RootLook;
|
|
863
1085
|
/** Viewport width the media queries were evaluated at, in device pixels
|
|
@@ -905,8 +1127,18 @@ export class Cascade {
|
|
|
905
1127
|
/** Whether any declaration has a length in `lh` or `rlh`: only then is
|
|
906
1128
|
* the line height settled ahead of the declarations that read it. */
|
|
907
1129
|
private _lh = false;
|
|
908
|
-
/** The pseudo-classes css-select is handed
|
|
1130
|
+
/** The pseudo-classes css-select is handed: `:root` this cascade's, and
|
|
1131
|
+
* the focus's three answered from `_focus`. */
|
|
909
1132
|
private _pseudos: typeof PSEUDOS;
|
|
1133
|
+
/** The colour schemes the page says it supports with a `<meta
|
|
1134
|
+
* name="color-scheme">` (HTML 4.2.5.4), which its root element takes
|
|
1135
|
+
* as it takes a presentational attribute: under any rule that sets its
|
|
1136
|
+
* `color-scheme`. Set by the box builder from the document. */
|
|
1137
|
+
pageColorScheme: string | null = null;
|
|
1138
|
+
/** Each system colour as the `light-dark()` it is: the palette's where
|
|
1139
|
+
* the element's scheme is the palette's, and Chrome's in an SVG image,
|
|
1140
|
+
* which has none. */
|
|
1141
|
+
private _systemColors: Map<string, string>;
|
|
910
1142
|
|
|
911
1143
|
constructor(
|
|
912
1144
|
sheets: Stylesheet[],
|
|
@@ -921,10 +1153,23 @@ export class Cascade {
|
|
|
921
1153
|
/** The document's element where it is not an `<html>`: an SVG image's
|
|
922
1154
|
* `<svg>`, which is `:root` there (Selectors 4, 14.1). */
|
|
923
1155
|
documentElement: Element | null = null,
|
|
1156
|
+
/** The element the document's URL names by its fragment, which is
|
|
1157
|
+
* `:target` (Selectors 4, 9.1): an SVG image's, `image.svg#icon`. */
|
|
1158
|
+
target: Element | null = null,
|
|
924
1159
|
) {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1160
|
+
// css-select has none of the focus's three, and a pseudo-class is
|
|
1161
|
+
// handed only the element, so they are this cascade's own closures
|
|
1162
|
+
this._pseudos = {
|
|
1163
|
+
...PSEUDOS,
|
|
1164
|
+
...(documentElement && {
|
|
1165
|
+
root: (el: Element) => el === documentElement,
|
|
1166
|
+
}),
|
|
1167
|
+
...(target && { target: (el: Element) => el === target }),
|
|
1168
|
+
focus: (el: Element) => this._focus.element === el,
|
|
1169
|
+
'focus-visible': (el: Element) =>
|
|
1170
|
+
this._focus.visible && this._focus.element === el,
|
|
1171
|
+
'focus-within': (el: Element) => this._focus.within.has(el),
|
|
1172
|
+
};
|
|
928
1173
|
this._xHeightOf = xHeight;
|
|
929
1174
|
this._zeroWidthOf = zeroWidth;
|
|
930
1175
|
this._normalLineOf = normalLine;
|
|
@@ -933,6 +1178,7 @@ export class Cascade {
|
|
|
933
1178
|
? (list: string) => families.map(list)
|
|
934
1179
|
: undefined;
|
|
935
1180
|
this.look = look;
|
|
1181
|
+
this._systemColors = systemColorTable(documentElement ? null : look);
|
|
936
1182
|
this.initial = initialStyle(look, scale);
|
|
937
1183
|
this.viewportWidth = viewportWidth;
|
|
938
1184
|
this.viewportHeight = viewportHeight;
|
|
@@ -962,7 +1208,9 @@ export class Cascade {
|
|
|
962
1208
|
this._shapes.add(rule, key);
|
|
963
1209
|
}
|
|
964
1210
|
}
|
|
965
|
-
|
|
1211
|
+
const selector = (pseudo?.rule ?? rule).selector;
|
|
1212
|
+
this._hoverRules.note(selector);
|
|
1213
|
+
this._focusRules.note(selector);
|
|
966
1214
|
}
|
|
967
1215
|
for (const bp of sheet.breakpoints) breakpoints.add(bp);
|
|
968
1216
|
// a query on the viewport's height reads it as a `vh` does
|
|
@@ -990,6 +1238,16 @@ export class Cascade {
|
|
|
990
1238
|
isActive: (el: Element) => this._pointer.active.has(el),
|
|
991
1239
|
isVisited: () => false,
|
|
992
1240
|
};
|
|
1241
|
+
this._copyAdapter = {
|
|
1242
|
+
...this._adapter,
|
|
1243
|
+
getParent: (el) => (el === this._copyTop ? null : DomUtils.getParent(el)),
|
|
1244
|
+
getSiblings: (el) =>
|
|
1245
|
+
el === this._copyTop ? [el] : DomUtils.getSiblings(el),
|
|
1246
|
+
prevElementSibling: (el) =>
|
|
1247
|
+
el === this._copyTop ? null : DomUtils.prevElementSibling(el),
|
|
1248
|
+
isHovered: () => false,
|
|
1249
|
+
isActive: () => false,
|
|
1250
|
+
};
|
|
993
1251
|
}
|
|
994
1252
|
|
|
995
1253
|
/** Whether declarations read the viewport's width or height (`vw`,
|
|
@@ -1022,6 +1280,36 @@ export class Cascade {
|
|
|
1022
1280
|
this._pointer = pointer;
|
|
1023
1281
|
}
|
|
1024
1282
|
|
|
1283
|
+
/** Whether a widget taking the focus or giving it up can change what this
|
|
1284
|
+
* cascade produces. */
|
|
1285
|
+
get focusSensitive(): boolean {
|
|
1286
|
+
return (
|
|
1287
|
+
this._index.focusSensitive ||
|
|
1288
|
+
this._pseudo.before.focusSensitive ||
|
|
1289
|
+
this._pseudo.after.focusSensitive ||
|
|
1290
|
+
this._pseudo['first-letter'].focusSensitive ||
|
|
1291
|
+
this._pseudo['first-line'].focusSensitive ||
|
|
1292
|
+
this._pseudo.marker.focusSensitive
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
setFocus(focus: FocusState): void {
|
|
1297
|
+
this._focus = focus;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/** Whether a pointer move can be restyled where it happened
|
|
1301
|
+
* (`HtmlViewNode._hoverInPlace`): false where a selector tests the
|
|
1302
|
+
* pointer inside a functional pseudo-class, which reaches elements no
|
|
1303
|
+
* compound names. */
|
|
1304
|
+
get hoverLocal(): boolean {
|
|
1305
|
+
return this._hoverRules.local;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/** …and a focus change. */
|
|
1309
|
+
get focusLocal(): boolean {
|
|
1310
|
+
return this._focusRules.local;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1025
1313
|
/**
|
|
1026
1314
|
* Which pseudo-elements' rules test the pointer: none's
|
|
1027
1315
|
* (`HOVER_PSEUDO_NONE`); a `::before`'s or an `::after`'s only
|
|
@@ -1031,15 +1319,23 @@ export class Cascade {
|
|
|
1031
1319
|
* move cannot.
|
|
1032
1320
|
*/
|
|
1033
1321
|
get hoverPseudo(): 0 | 1 | 2 {
|
|
1322
|
+
return this._pseudoRules('hoverSensitive');
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/** …and which test the focus. */
|
|
1326
|
+
get focusPseudo(): 0 | 1 | 2 {
|
|
1327
|
+
return this._pseudoRules('focusSensitive');
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
private _pseudoRules(state: 'hoverSensitive' | 'focusSensitive'): 0 | 1 | 2 {
|
|
1034
1331
|
if (
|
|
1035
|
-
this._pseudo['first-letter']
|
|
1036
|
-
this._pseudo['first-line']
|
|
1037
|
-
this._pseudo.marker
|
|
1332
|
+
this._pseudo['first-letter'][state] ||
|
|
1333
|
+
this._pseudo['first-line'][state] ||
|
|
1334
|
+
this._pseudo.marker[state]
|
|
1038
1335
|
) {
|
|
1039
1336
|
return HOVER_PSEUDO_OTHER;
|
|
1040
1337
|
}
|
|
1041
|
-
return this._pseudo.before
|
|
1042
|
-
this._pseudo.after.hoverSensitive
|
|
1338
|
+
return this._pseudo.before[state] || this._pseudo.after[state]
|
|
1043
1339
|
? HOVER_PSEUDO_GENERATED
|
|
1044
1340
|
: HOVER_PSEUDO_NONE;
|
|
1045
1341
|
}
|
|
@@ -1055,17 +1351,13 @@ export class Cascade {
|
|
|
1055
1351
|
* table restyle the rows after it.
|
|
1056
1352
|
*/
|
|
1057
1353
|
hoverTouches(el: Element): HoverTouch {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
for (const { match, followed } of this._hoverMatchers) {
|
|
1066
|
-
if (match(el)) return followed ? HOVER_FOLLOWED : HOVER_TOUCHED;
|
|
1067
|
-
}
|
|
1068
|
-
return HOVER_UNTOUCHED;
|
|
1354
|
+
return this._hoverRules.touches(el, (selector) => this._compile(selector));
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/** `hoverTouches` for the focus: whether `el` taking the focus or giving
|
|
1358
|
+
* it up, or its `:focus-within` changing, can change a style. */
|
|
1359
|
+
focusTouches(el: Element): HoverTouch {
|
|
1360
|
+
return this._focusRules.touches(el, (selector) => this._compile(selector));
|
|
1069
1361
|
}
|
|
1070
1362
|
|
|
1071
1363
|
/**
|
|
@@ -1077,61 +1369,12 @@ export class Cascade {
|
|
|
1077
1369
|
* siblings can change with it (`hoverTouches`).
|
|
1078
1370
|
*/
|
|
1079
1371
|
hoverAnchors(el: Element, into: Map<Element, HoverTouch>): void {
|
|
1080
|
-
|
|
1081
|
-
this._anchorMatchers ??= [...this._hoverAnchors]
|
|
1082
|
-
.sort(([, a], [, b]) => Number(b.followed) - Number(a.followed))
|
|
1083
|
-
.flatMap(([anchor, { siblings, followed }]) => {
|
|
1084
|
-
const match = this._compile(anchor);
|
|
1085
|
-
return match ? [{ match, siblings, followed }] : [];
|
|
1086
|
-
});
|
|
1087
|
-
const note = (at: Element, followed: boolean): void => {
|
|
1088
|
-
if (followed) into.set(at, HOVER_FOLLOWED);
|
|
1089
|
-
else if (!into.has(at)) into.set(at, HOVER_TOUCHED);
|
|
1090
|
-
};
|
|
1091
|
-
const siblings = this._anchorMatchers.some((m) => m.siblings);
|
|
1092
|
-
for (let at: Element | null = el; at;) {
|
|
1093
|
-
if (at !== el) {
|
|
1094
|
-
for (const { match, followed } of this._anchorMatchers) {
|
|
1095
|
-
if (match(at)) {
|
|
1096
|
-
note(at, followed);
|
|
1097
|
-
break;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
if (siblings) {
|
|
1102
|
-
for (let s = at.prev; s; s = s.prev) {
|
|
1103
|
-
if (!isTag(s as Element)) continue;
|
|
1104
|
-
for (const m of this._anchorMatchers) {
|
|
1105
|
-
if (m.siblings && m.match(s as Element)) {
|
|
1106
|
-
note(s as Element, m.followed);
|
|
1107
|
-
break;
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
const parent: Element['parent'] = at.parent;
|
|
1113
|
-
at = parent && isTag(parent as Element) ? (parent as Element) : null;
|
|
1114
|
-
}
|
|
1372
|
+
this._hoverRules.anchors(el, into, (selector) => this._compile(selector));
|
|
1115
1373
|
}
|
|
1116
1374
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (found.nested) this.hoverLocal = false;
|
|
1121
|
-
const followed = new Set(found.followed);
|
|
1122
|
-
for (const c of found.compounds) {
|
|
1123
|
-
this._hoverCompounds.set(
|
|
1124
|
-
c,
|
|
1125
|
-
followed.has(c) || (this._hoverCompounds.get(c) ?? false),
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
for (const { anchor, siblings } of found.has) {
|
|
1129
|
-
const was = this._hoverAnchors.get(anchor);
|
|
1130
|
-
this._hoverAnchors.set(anchor, {
|
|
1131
|
-
siblings: siblings || (was?.siblings ?? false),
|
|
1132
|
-
followed: followed.has(anchor) || (was?.followed ?? false),
|
|
1133
|
-
});
|
|
1134
|
-
}
|
|
1375
|
+
/** `hoverAnchors` for the focus: `form:has(input:focus)`'s form. */
|
|
1376
|
+
focusAnchors(el: Element, into: Map<Element, HoverTouch>): void {
|
|
1377
|
+
this._focusRules.anchors(el, into, (selector) => this._compile(selector));
|
|
1135
1378
|
}
|
|
1136
1379
|
|
|
1137
1380
|
/** A selector compiled as a rule's is, or null where css-select refuses
|
|
@@ -1307,6 +1550,11 @@ export class Cascade {
|
|
|
1307
1550
|
if (this._pointer.hovered.has(el)) key += '\u0001:hover';
|
|
1308
1551
|
if (this._pointer.active.has(el)) key += '\u0001:active';
|
|
1309
1552
|
}
|
|
1553
|
+
// `:focus-within` is not shared under (`UNSHAREABLE`), and the focus
|
|
1554
|
+
// is on one element
|
|
1555
|
+
if (this._focus.element === el && this._index.focusSensitive) {
|
|
1556
|
+
key += this._focus.visible ? '\u0001:focus-visible' : '\u0001:focus';
|
|
1557
|
+
}
|
|
1310
1558
|
const attribs = el.attribs;
|
|
1311
1559
|
for (const name in attribs) {
|
|
1312
1560
|
const value = attribs[name];
|
|
@@ -1629,6 +1877,8 @@ export class Cascade {
|
|
|
1629
1877
|
families: this._mapFamilies,
|
|
1630
1878
|
lh: () => this._lineHeightOf(parentStyle),
|
|
1631
1879
|
rlh: () => this._lineHeightOf(rootStyle),
|
|
1880
|
+
focusRing: this.look.focusRing,
|
|
1881
|
+
systemColors: this._systemColors,
|
|
1632
1882
|
};
|
|
1633
1883
|
// The family, the weight and the slant go with the size: together they
|
|
1634
1884
|
// pick the face an `ex`, a `ch` or an `lh` in any declaration is
|
|
@@ -1744,7 +1994,9 @@ export class Cascade {
|
|
|
1744
1994
|
style.alignBlocks = null;
|
|
1745
1995
|
}
|
|
1746
1996
|
settleClamp(style, parentStyle);
|
|
1997
|
+
settleAlign(style, parentStyle);
|
|
1747
1998
|
settleOverflow(style);
|
|
1999
|
+
settleOutline(style, this.look, this.scale);
|
|
1748
2000
|
settleContentVisibility(style);
|
|
1749
2001
|
blockify(style, inFlexContainer);
|
|
1750
2002
|
// a button is laid out as one, whatever `display` it was given
|
|
@@ -1855,12 +2107,24 @@ export class Cascade {
|
|
|
1855
2107
|
* itself. The root's box has its paint, its opacity and whether it is
|
|
1856
2108
|
* shown from its computed style, so those are left out for it.
|
|
1857
2109
|
*
|
|
2110
|
+
* What a `<use>` draws is a copy, styled in a tree of its own
|
|
2111
|
+
* (`ShapeStyles.used`): the element it names is looked for as `copyTree`
|
|
2112
|
+
* looks — the drawing's own first, then the document's — and what is in
|
|
2113
|
+
* it asked of where it stands in that tree, once a build for all the
|
|
2114
|
+
* `<use>`s that name it (`ShapeCopies`). An icon sprite's `<symbol>`,
|
|
2115
|
+
* outside the drawing in a hidden `<svg>` of them, is styled that way,
|
|
2116
|
+
* and so is one of the drawing's own.
|
|
2117
|
+
*
|
|
1858
2118
|
* Asked as a drawing is first painted (`BoxTree.shapeStyler`), of the
|
|
1859
2119
|
* rules in `_shapes` alone: a page of hundreds of icons pays for the ones
|
|
1860
2120
|
* it shows, a look in the buckets for each of their elements where no
|
|
1861
2121
|
* rule reaches them, and a document with no such rule nothing.
|
|
1862
2122
|
*/
|
|
1863
|
-
shapeStyles(
|
|
2123
|
+
shapeStyles(
|
|
2124
|
+
root: Element,
|
|
2125
|
+
style: ComputedStyle,
|
|
2126
|
+
copies: ShapeCopies | null = null,
|
|
2127
|
+
): ShapeStyles | null {
|
|
1864
2128
|
const index = this._shapes;
|
|
1865
2129
|
if (!index.size) return null;
|
|
1866
2130
|
// The rules kept by a class or an id are asked of the elements with
|
|
@@ -1870,9 +2134,9 @@ export class Cascade {
|
|
|
1870
2134
|
// id it asks of an ancestor, which is looked for once for the drawing,
|
|
1871
2135
|
// around it and in it, where matching the rule would look for it from
|
|
1872
2136
|
// each of its elements. On a page with none of them around a drawing,
|
|
1873
|
-
// nothing is matched at all.
|
|
2137
|
+
// nothing is matched at all. A copy's tree has nothing around it.
|
|
1874
2138
|
const needs = (this._shapeNeeds ??= this._needsOfShapes());
|
|
1875
|
-
|
|
2139
|
+
let found = new Set<string>();
|
|
1876
2140
|
const asks = needs.classes.size > 0 || needs.ids.size > 0;
|
|
1877
2141
|
const note = (el: Element): void => {
|
|
1878
2142
|
if (needs.ids.size) {
|
|
@@ -1907,43 +2171,58 @@ export class Cascade {
|
|
|
1907
2171
|
(indexed.root ??= declaresRootShape(indexed.rule.declarations)) &&
|
|
1908
2172
|
live(indexed);
|
|
1909
2173
|
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2174
|
+
/** The elements under `top` and it, in document order, the top first:
|
|
2175
|
+
* its ancestors are noted before an element is asked of. Each with
|
|
2176
|
+
* the rules it matched, in cascade order, where it matched any. */
|
|
2177
|
+
const walk = (
|
|
2178
|
+
top: Element,
|
|
2179
|
+
inCopy: boolean,
|
|
2180
|
+
each: (el: Element, candidates: Candidate[] | null) => void,
|
|
2181
|
+
): void => {
|
|
2182
|
+
const stack: Element[] = [top];
|
|
2183
|
+
while (stack.length) {
|
|
2184
|
+
const el = stack.pop()!;
|
|
2185
|
+
// what is in a `<foreignObject>` is not the drawing's to draw
|
|
2186
|
+
if (el.name !== 'foreignobject' && el.name !== 'foreignObject') {
|
|
2187
|
+
const children = el.children;
|
|
2188
|
+
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
2189
|
+
const child = children[i];
|
|
2190
|
+
if (child.type === 'tag') stack.push(child as Element);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
if (asks) note(el);
|
|
2194
|
+
const tag = tagOf(el);
|
|
2195
|
+
const asked = found.size
|
|
2196
|
+
? index.universal.length > 0 || index.byTag.has(tag)
|
|
2197
|
+
: needs.universal || needs.tags.has(tag);
|
|
2198
|
+
if (!asked && !index.names(el)) {
|
|
2199
|
+
each(el, null);
|
|
2200
|
+
continue;
|
|
1927
2201
|
}
|
|
2202
|
+
const candidates: Candidate[] = [];
|
|
2203
|
+
this._matchInto(
|
|
2204
|
+
index,
|
|
2205
|
+
el,
|
|
2206
|
+
candidates,
|
|
2207
|
+
undefined,
|
|
2208
|
+
el === root && !inCopy ? liveForRoot : live,
|
|
2209
|
+
inCopy,
|
|
2210
|
+
);
|
|
2211
|
+
if (candidates.length) {
|
|
2212
|
+
const inline = attr(el, 'style');
|
|
2213
|
+
if (inline) pushInlineShapes(candidates, inline);
|
|
2214
|
+
candidates.sort(byCascade);
|
|
2215
|
+
}
|
|
2216
|
+
each(el, candidates.length ? candidates : null);
|
|
1928
2217
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
index,
|
|
1938
|
-
el,
|
|
1939
|
-
candidates,
|
|
1940
|
-
undefined,
|
|
1941
|
-
el === root ? liveForRoot : live,
|
|
1942
|
-
);
|
|
1943
|
-
if (!candidates.length) continue;
|
|
1944
|
-
const inline = attr(el, 'style');
|
|
1945
|
-
if (inline) pushInlineShapes(candidates, inline);
|
|
1946
|
-
candidates.sort(byCascade);
|
|
2218
|
+
};
|
|
2219
|
+
|
|
2220
|
+
let ctx: ShapeContext | null = null;
|
|
2221
|
+
/** What an element's rules come to, against the drawing's style. */
|
|
2222
|
+
const resolve = (
|
|
2223
|
+
candidates: Candidate[],
|
|
2224
|
+
isRoot: boolean,
|
|
2225
|
+
): Record<string, string> | null => {
|
|
1947
2226
|
ctx ??= {
|
|
1948
2227
|
color: style.color,
|
|
1949
2228
|
scheme: style.colorScheme,
|
|
@@ -1954,13 +2233,14 @@ export class Cascade {
|
|
|
1954
2233
|
vh: this.viewportHeight,
|
|
1955
2234
|
scale: this.scale,
|
|
1956
2235
|
initial: this.initial,
|
|
2236
|
+
systemColors: this._systemColors,
|
|
1957
2237
|
},
|
|
1958
2238
|
};
|
|
1959
2239
|
let own: Record<string, string> | null = null;
|
|
1960
2240
|
for (const c of candidates) {
|
|
1961
2241
|
for (const d of pick(c)) {
|
|
1962
2242
|
if (!isShapeProp(d.prop)) continue;
|
|
1963
|
-
if (
|
|
2243
|
+
if (isRoot && ROOT_BOX_PROPS.has(d.prop)) continue;
|
|
1964
2244
|
// a `var()` with nothing to stand for it is invalid at
|
|
1965
2245
|
// computed-value time, and the property as though `unset`
|
|
1966
2246
|
const value = shapeValue(
|
|
@@ -1971,13 +2251,106 @@ export class Cascade {
|
|
|
1971
2251
|
if (value !== null) (own ??= {})[d.prop] = value;
|
|
1972
2252
|
}
|
|
1973
2253
|
}
|
|
1974
|
-
|
|
2254
|
+
return own;
|
|
2255
|
+
};
|
|
2256
|
+
// an element's place in the walks is what the key names it by
|
|
2257
|
+
let key = '';
|
|
2258
|
+
let place = -1;
|
|
2259
|
+
const named = (own: Record<string, string>, at: number): string => {
|
|
2260
|
+
let part = `${at}{`;
|
|
2261
|
+
for (const prop in own) part += `${prop}:${own[prop]};`;
|
|
2262
|
+
return `${part}}`;
|
|
2263
|
+
};
|
|
2264
|
+
|
|
2265
|
+
// (made as they are first needed: most drawings need none of them)
|
|
2266
|
+
let of = null as Map<Element, ShapeStyle> | null;
|
|
2267
|
+
// the `<use>`s met, and the drawing's own elements by their ids, which
|
|
2268
|
+
// one looks in first
|
|
2269
|
+
let uses = null as Element[] | null;
|
|
2270
|
+
let ids = null as Map<string, Element> | null;
|
|
2271
|
+
walk(root, false, (el, candidates) => {
|
|
2272
|
+
place += 1;
|
|
2273
|
+
if (isUse(el)) (uses ??= []).push(el);
|
|
2274
|
+
if (el !== root) {
|
|
2275
|
+
const id = el.attribs.id;
|
|
2276
|
+
if (id && !ids?.has(id)) (ids ??= new Map()).set(id, el);
|
|
2277
|
+
}
|
|
2278
|
+
const own = candidates && resolve(candidates, el === root);
|
|
2279
|
+
if (!own) return;
|
|
1975
2280
|
(of ??= new Map()).set(el, own);
|
|
1976
|
-
key +=
|
|
1977
|
-
|
|
1978
|
-
|
|
2281
|
+
key += named(own, place);
|
|
2282
|
+
});
|
|
2283
|
+
if (!uses) return of ? { of, used: null, key } : null;
|
|
2284
|
+
|
|
2285
|
+
// Each element a `<use>` names, in the tree of its copy: once however
|
|
2286
|
+
// many name it, and one a copy's `<use>` names in turn, however deep —
|
|
2287
|
+
// `copyTree` stops at a depth, and a `<use>` that names what it is in
|
|
2288
|
+
// finds it being walked. What is matched there is the same for every
|
|
2289
|
+
// drawing, and kept for the build (`ShapeCopies`); what it comes to is
|
|
2290
|
+
// the drawing's, since every property the copy inherits is the
|
|
2291
|
+
// `<use>`'s, and its custom properties and its colour are the
|
|
2292
|
+
// drawing's, as they are for the rest of it — kept too, for each
|
|
2293
|
+
// style a drawing has, which the icons of a page mostly share.
|
|
2294
|
+
const byId = copies?.byId ?? ((id: string) => elementById(root, id));
|
|
2295
|
+
const matched = copies?.matched ?? new Map<Element, CopyMatch>();
|
|
2296
|
+
let styled = copies?.resolved.get(style);
|
|
2297
|
+
if (!styled) {
|
|
2298
|
+
styled = new Map();
|
|
2299
|
+
copies?.resolved.set(style, styled);
|
|
2300
|
+
}
|
|
2301
|
+
const match = (target: Element): CopyMatch => {
|
|
2302
|
+
let copy = matched.get(target);
|
|
2303
|
+
if (copy) return copy;
|
|
2304
|
+
const rules: CopyMatch['rules'] = [];
|
|
2305
|
+
const inner: Element[] = [];
|
|
2306
|
+
let size = 0;
|
|
2307
|
+
found = new Set();
|
|
2308
|
+
this._copyTop = target;
|
|
2309
|
+
try {
|
|
2310
|
+
walk(target, true, (el, candidates) => {
|
|
2311
|
+
if (isUse(el)) inner.push(el);
|
|
2312
|
+
if (candidates) rules.push({ el, at: size, candidates });
|
|
2313
|
+
size += 1;
|
|
2314
|
+
});
|
|
2315
|
+
} finally {
|
|
2316
|
+
this._copyTop = null;
|
|
2317
|
+
}
|
|
2318
|
+
copy = { rules, size, uses: inner };
|
|
2319
|
+
matched.set(target, copy);
|
|
2320
|
+
return copy;
|
|
2321
|
+
};
|
|
2322
|
+
let used: Map<Element, ReadonlyMap<Element, ShapeStyle>> | null = null;
|
|
2323
|
+
let made: Set<Element> | null = null;
|
|
2324
|
+
for (let i = 0; i < uses.length; i += 1) {
|
|
2325
|
+
const use = uses[i];
|
|
2326
|
+
const id = useHref(use);
|
|
2327
|
+
if (id === null) continue;
|
|
2328
|
+
const target = ids?.get(id) ?? byId(id);
|
|
2329
|
+
if (!target) continue;
|
|
2330
|
+
const copy = match(target);
|
|
2331
|
+
let done = styled.get(target);
|
|
2332
|
+
if (!done) {
|
|
2333
|
+
let into: Map<Element, ShapeStyle> | null = null;
|
|
2334
|
+
let part = '';
|
|
2335
|
+
for (const { el, at, candidates } of copy.rules) {
|
|
2336
|
+
const own = resolve(candidates, false);
|
|
2337
|
+
if (!own) continue;
|
|
2338
|
+
(into ??= new Map()).set(el, own);
|
|
2339
|
+
part += named(own, at);
|
|
2340
|
+
}
|
|
2341
|
+
done = { into, key: part };
|
|
2342
|
+
styled.set(target, done);
|
|
2343
|
+
}
|
|
2344
|
+
if (!made?.has(target)) {
|
|
2345
|
+
(made ??= new Set()).add(target);
|
|
2346
|
+
for (const next of copy.uses) uses.push(next);
|
|
2347
|
+
// its place in the key is after what came before it
|
|
2348
|
+
if (done.key) key += `@${place + 1}:${done.key}`;
|
|
2349
|
+
place += copy.size;
|
|
2350
|
+
}
|
|
2351
|
+
if (done.into) (used ??= new Map()).set(use, done.into);
|
|
1979
2352
|
}
|
|
1980
|
-
return of ? { of, key } : null;
|
|
2353
|
+
return of || used ? { of: of ?? NO_SHAPES, used, key } : null;
|
|
1981
2354
|
}
|
|
1982
2355
|
|
|
1983
2356
|
/** `ShapeNeeds`, and each such rule's own (`IndexedRule.needs`). */
|
|
@@ -2015,19 +2388,27 @@ export class Cascade {
|
|
|
2015
2388
|
* nothing else, since no other rule's answer is the pointer's. An element
|
|
2016
2389
|
* they answer the same for, under a parent whose style is what it was, has
|
|
2017
2390
|
* the style it had — which is most of what a move reaches: the rows of a
|
|
2018
|
-
* hovered table body, what is in a hovered card.
|
|
2391
|
+
* hovered table body, what is in a hovered card. The focus's move from
|
|
2392
|
+
* `wasFocus` likewise, the rules that test it asked under both.
|
|
2019
2393
|
*/
|
|
2020
|
-
pointerChanged(
|
|
2394
|
+
pointerChanged(
|
|
2395
|
+
el: Element,
|
|
2396
|
+
was: PointerState,
|
|
2397
|
+
wasFocus: FocusState = this._focus,
|
|
2398
|
+
): boolean {
|
|
2021
2399
|
const now = this._pointer;
|
|
2400
|
+
const nowFocus = this._focus;
|
|
2022
2401
|
const after = this._pointerAnswers(el);
|
|
2023
2402
|
this._pointer = was;
|
|
2403
|
+
this._focus = wasFocus;
|
|
2024
2404
|
const before = this._pointerAnswers(el);
|
|
2025
2405
|
this._pointer = now;
|
|
2406
|
+
this._focus = nowFocus;
|
|
2026
2407
|
return before !== after;
|
|
2027
2408
|
}
|
|
2028
2409
|
|
|
2029
|
-
/** The ids of the rules testing the pointer
|
|
2030
|
-
* stand, the element's and each pseudo-element's. */
|
|
2410
|
+
/** The ids of the rules testing the pointer or the focus that match
|
|
2411
|
+
* `el` as things stand, the element's and each pseudo-element's. */
|
|
2031
2412
|
private _pointerAnswers(el: Element): string {
|
|
2032
2413
|
let answers = '';
|
|
2033
2414
|
const ask = (index: RuleIndex, name: string): void => {
|
|
@@ -2052,6 +2433,9 @@ export class Cascade {
|
|
|
2052
2433
|
matched?: number[],
|
|
2053
2434
|
/** The rules to try, where some are known not to match without it. */
|
|
2054
2435
|
live?: (indexed: IndexedRule) => boolean,
|
|
2436
|
+
/** Whether `el` is matched where a `<use>` draws a copy of it
|
|
2437
|
+
* (`_copyTop`). */
|
|
2438
|
+
inCopy = false,
|
|
2055
2439
|
): void {
|
|
2056
2440
|
// A media query's width is CSS pixels; the viewport is kept in device.
|
|
2057
2441
|
const width = this.viewportWidth / this.scale;
|
|
@@ -2073,22 +2457,43 @@ export class Cascade {
|
|
|
2073
2457
|
) {
|
|
2074
2458
|
continue;
|
|
2075
2459
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
indexed.
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2460
|
+
let match: ((el: Element) => boolean) | null;
|
|
2461
|
+
if (inCopy) {
|
|
2462
|
+
if (!indexed.inCopyCompiled) {
|
|
2463
|
+
indexed.inCopyCompiled = true;
|
|
2464
|
+
try {
|
|
2465
|
+
// what it finds above an element depends on which copy it is
|
|
2466
|
+
// in, so none of it is kept
|
|
2467
|
+
indexed.inCopy = compileSelector(
|
|
2468
|
+
rule.selector,
|
|
2469
|
+
this._copyAdapter,
|
|
2470
|
+
this._pseudos,
|
|
2471
|
+
false,
|
|
2472
|
+
);
|
|
2473
|
+
} catch {
|
|
2474
|
+
indexed.inCopy = null;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
match = indexed.inCopy;
|
|
2478
|
+
} else {
|
|
2479
|
+
if (!indexed.compiled) {
|
|
2480
|
+
indexed.compiled = true;
|
|
2481
|
+
try {
|
|
2482
|
+
indexed.match = compileSelector(
|
|
2483
|
+
rule.selector,
|
|
2484
|
+
this._adapter,
|
|
2485
|
+
this._pseudos,
|
|
2486
|
+
);
|
|
2487
|
+
} catch {
|
|
2488
|
+
// A selector this matcher does not know (`::-moz-…`, a CSS4
|
|
2489
|
+
// form it has not learnt) drops out of the cascade rather than
|
|
2490
|
+
// out of the render.
|
|
2491
|
+
indexed.match = null;
|
|
2492
|
+
}
|
|
2089
2493
|
}
|
|
2494
|
+
match = indexed.match;
|
|
2090
2495
|
}
|
|
2091
|
-
if (!
|
|
2496
|
+
if (!match || !match(el)) continue;
|
|
2092
2497
|
matched?.push(indexed.id);
|
|
2093
2498
|
if (out === null) continue;
|
|
2094
2499
|
const origin = rule.order < 0 ? Origin.UserAgent : Origin.Author;
|
|
@@ -2116,6 +2521,18 @@ export class Cascade {
|
|
|
2116
2521
|
this._matchInto(this._index, el, out, matched);
|
|
2117
2522
|
|
|
2118
2523
|
const hints = presentationHints(el);
|
|
2524
|
+
// the page's colour schemes are its root's where no rule sets them
|
|
2525
|
+
if (
|
|
2526
|
+
this.pageColorScheme &&
|
|
2527
|
+
el.name === 'html' &&
|
|
2528
|
+
!(el.parent && isTag(el.parent as Element))
|
|
2529
|
+
) {
|
|
2530
|
+
hints.push({
|
|
2531
|
+
prop: 'color-scheme',
|
|
2532
|
+
value: this.pageColorScheme,
|
|
2533
|
+
important: false,
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2119
2536
|
if (hints.length) {
|
|
2120
2537
|
out.push({
|
|
2121
2538
|
origin: Origin.Presentation,
|
|
@@ -2180,9 +2597,18 @@ function dropPaletteChrome(out: Candidate[]): void {
|
|
|
2180
2597
|
}
|
|
2181
2598
|
if (!chrome) return;
|
|
2182
2599
|
let styled = false;
|
|
2600
|
+
// `appearance` by the value that wins: `appearance: none; appearance:
|
|
2601
|
+
// auto` keeps the control's look, as a reset a page takes back does
|
|
2602
|
+
let appearance = '';
|
|
2183
2603
|
for (let j = i; j < out.length && !styled; j += 1) {
|
|
2184
|
-
|
|
2604
|
+
for (const d of pick(out[j])) {
|
|
2605
|
+
if (d.prop === 'appearance' || d.prop === '-webkit-appearance') {
|
|
2606
|
+
const v = d.value.trim().toLowerCase();
|
|
2607
|
+
if (/^[a-z-]+$/.test(v)) appearance = v;
|
|
2608
|
+
} else if (stylesChrome(d)) styled = true;
|
|
2609
|
+
}
|
|
2185
2610
|
}
|
|
2611
|
+
if (appearance === 'none') styled = true;
|
|
2186
2612
|
if (!styled) return;
|
|
2187
2613
|
let kept = 0;
|
|
2188
2614
|
for (const c of out) {
|
|
@@ -2195,8 +2621,9 @@ function dropPaletteChrome(out: Candidate[]): void {
|
|
|
2195
2621
|
}
|
|
2196
2622
|
|
|
2197
2623
|
/** Whether a declaration styles what a control's native look draws: Blink's
|
|
2198
|
-
* `is_background` and `is_border` properties and their shorthands
|
|
2199
|
-
* `appearance` of `none
|
|
2624
|
+
* `is_background` and `is_border` properties and their shorthands. An
|
|
2625
|
+
* `appearance` of `none` does too, where it is the one that wins
|
|
2626
|
+
* (`dropPaletteChrome`). */
|
|
2200
2627
|
function stylesChrome(d: Declaration): boolean {
|
|
2201
2628
|
const prop = d.prop;
|
|
2202
2629
|
if (prop.startsWith('background')) {
|
|
@@ -2205,10 +2632,7 @@ function stylesChrome(d: Declaration): boolean {
|
|
|
2205
2632
|
if (prop.startsWith('border')) {
|
|
2206
2633
|
return prop !== 'border-collapse' && prop !== 'border-spacing';
|
|
2207
2634
|
}
|
|
2208
|
-
return
|
|
2209
|
-
(prop === 'appearance' || prop === '-webkit-appearance') &&
|
|
2210
|
-
d.value.trim().toLowerCase() === 'none'
|
|
2211
|
-
);
|
|
2635
|
+
return false;
|
|
2212
2636
|
}
|
|
2213
2637
|
|
|
2214
2638
|
/** An implied element's style as the root box's: its look, not its role —
|
|
@@ -2224,18 +2648,22 @@ function asRoot(style: ComputedStyle): ComputedStyle {
|
|
|
2224
2648
|
* fragment, whose style the root box takes (`rootStyle`), and never a
|
|
2225
2649
|
* fragment's top-level elements, which css-select would take for it.
|
|
2226
2650
|
*
|
|
2227
|
-
* `:focus` and its two kin
|
|
2228
|
-
*
|
|
2229
|
-
* the
|
|
2230
|
-
*
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
2651
|
+
* `:focus` and its two kin are here as matching nothing, and each cascade
|
|
2652
|
+
* answers them from where the focus is (`Cascade.setFocus`): no element
|
|
2653
|
+
* of the document takes it itself — a control's widget does, beside it.
|
|
2654
|
+
* css-select has none of the three and throws on them, which dropped
|
|
2655
|
+
* every rule that named one, `:not(:focus)` among them: Wikipedia's skip
|
|
2656
|
+
* link hides with `.mw-jump-link:not(:focus)`, and without it stood in
|
|
2657
|
+
* the flow at the top of every page. */
|
|
2233
2658
|
const PSEUDOS = {
|
|
2234
2659
|
root: (el: Element) =>
|
|
2235
2660
|
el.name === 'html' && !(el.parent && isTag(el.parent as Element)),
|
|
2236
|
-
focus: () => false,
|
|
2237
|
-
'focus-visible': () => false,
|
|
2238
|
-
'focus-within': () => false,
|
|
2661
|
+
focus: (_el: Element) => false,
|
|
2662
|
+
'focus-visible': (_el: Element) => false,
|
|
2663
|
+
'focus-within': (_el: Element) => false,
|
|
2664
|
+
// css-select has none, and threw on one, so `:not(:target)` matched
|
|
2665
|
+
// nothing: a document is drawn at no fragment, an SVG image at its own
|
|
2666
|
+
target: (_el: Element) => false,
|
|
2239
2667
|
// css-select's, but its ranges and tags lower-cased as ASCII has it, and
|
|
2240
2668
|
// no other script (CSS 2.1 4.1.3): Unicode's took `:lang(\u212Al)`, a
|
|
2241
2669
|
// Kelvin sign for the K, for `:lang(kl)`
|
|
@@ -2245,28 +2673,32 @@ const PSEUDOS = {
|
|
|
2245
2673
|
.map((range) => range.trim().replace(/^['"]|['"]$/g, ''))
|
|
2246
2674
|
.filter((range) => range.length > 0)
|
|
2247
2675
|
.map((range) => asciiLower(range).split('-'));
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
if (value != null) {
|
|
2252
|
-
if (!value) return ranges.some((range) => range[0] === '');
|
|
2253
|
-
const tag = asciiLower(value).split('-');
|
|
2254
|
-
return ranges.some((range) => langRangeMatches(tag, range));
|
|
2255
|
-
}
|
|
2256
|
-
root = node;
|
|
2257
|
-
const parent: Element['parent'] = node.parent;
|
|
2258
|
-
node = parent && isTag(parent as Element) ? (parent as Element) : null;
|
|
2259
|
-
}
|
|
2260
|
-
// with no element saying, the document's, as its `<meta>` sets it —
|
|
2261
|
-
// searched from the document, where a fragment's `<meta>` is a
|
|
2262
|
-
// sibling of what it covers
|
|
2263
|
-
const pragma = pragmaLanguage(root.parent ?? root);
|
|
2264
|
-
if (!pragma) return ranges.some((range) => range[0] === '');
|
|
2265
|
-
const tag = asciiLower(pragma).split('-');
|
|
2676
|
+
const value = languageOf(el);
|
|
2677
|
+
if (!value) return ranges.some((range) => range[0] === '');
|
|
2678
|
+
const tag = asciiLower(value).split('-');
|
|
2266
2679
|
return ranges.some((range) => langRangeMatches(tag, range));
|
|
2267
2680
|
},
|
|
2268
2681
|
};
|
|
2269
2682
|
|
|
2683
|
+
/**
|
|
2684
|
+
* An element's language: the `lang` or `xml:lang` of the nearest element
|
|
2685
|
+
* from it up that says one, or with none saying, the document's, as its
|
|
2686
|
+
* `<meta http-equiv="content-language">` sets it — searched from the
|
|
2687
|
+
* document, where a fragment's `<meta>` is a sibling of what it covers.
|
|
2688
|
+
* '' where none does, or the nearest says ''.
|
|
2689
|
+
*/
|
|
2690
|
+
export function languageOf(el: Element): string {
|
|
2691
|
+
let root = el;
|
|
2692
|
+
for (let node: Element | null = el; node;) {
|
|
2693
|
+
const value = node.attribs['xml:lang'] ?? node.attribs.lang;
|
|
2694
|
+
if (value != null) return value;
|
|
2695
|
+
root = node;
|
|
2696
|
+
const parent: Element['parent'] = node.parent;
|
|
2697
|
+
node = parent && isTag(parent as Element) ? (parent as Element) : null;
|
|
2698
|
+
}
|
|
2699
|
+
return pragmaLanguage(root.parent ?? root);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2270
2702
|
/** A length in a unit of the viewport: a number, then the unit, and no
|
|
2271
2703
|
* more of a name after it. */
|
|
2272
2704
|
const VIEWPORT_UNIT = /\d[sld]?v(?:w|h|i|b|min|max)(?![\w-])/i;
|
|
@@ -2322,6 +2754,33 @@ function pragmaLanguage(root: { children: unknown[] }): string {
|
|
|
2322
2754
|
|
|
2323
2755
|
const PRAGMA_LANGUAGE = new WeakMap<object, string>();
|
|
2324
2756
|
|
|
2757
|
+
/**
|
|
2758
|
+
* The page's supported colour schemes: the content of the first `<meta
|
|
2759
|
+
* name="color-scheme">` that is a `color-scheme` value (HTML 4.2.5.4), or
|
|
2760
|
+
* null. Looked for where a `<meta>` is, at the top of a fragment and in
|
|
2761
|
+
* the `<html>` and its `<head>`, and not through the body.
|
|
2762
|
+
*/
|
|
2763
|
+
export function metaColorScheme(root: { children: unknown[] }): string | null {
|
|
2764
|
+
const stack: { children: unknown[] }[] = [root];
|
|
2765
|
+
while (stack.length) {
|
|
2766
|
+
const node = stack.pop()!;
|
|
2767
|
+
for (const child of node.children) {
|
|
2768
|
+
if (!isTag(child as Element)) continue;
|
|
2769
|
+
const el = child as Element;
|
|
2770
|
+
if (el.name === 'meta') {
|
|
2771
|
+
const name = el.attribs.name?.trim().toLowerCase();
|
|
2772
|
+
const content = el.attribs.content?.trim();
|
|
2773
|
+
if (name === 'color-scheme' && content && usedColorScheme(content)) {
|
|
2774
|
+
return content;
|
|
2775
|
+
}
|
|
2776
|
+
} else if (el.name === 'html' || el.name === 'head') {
|
|
2777
|
+
stack.push(el);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
return null;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2325
2784
|
/** RFC 4647's extended filtering, of a tag's subtags by a range's, as
|
|
2326
2785
|
* css-select does it. */
|
|
2327
2786
|
function langRangeMatches(tag: string[], range: string[]): boolean {
|
|
@@ -2360,6 +2819,23 @@ function setsContent(c: Candidate): boolean {
|
|
|
2360
2819
|
/** Whether a rule's declarations set a custom property, by the array. */
|
|
2361
2820
|
const CUSTOM_IN = new WeakMap<Declaration[], boolean>();
|
|
2362
2821
|
|
|
2822
|
+
/** The document's first element with an id, looked for from one of its
|
|
2823
|
+
* elements: `shapeStyles`' answer where no index of them is handed in. */
|
|
2824
|
+
function elementById(from: Element, id: string): Element | null {
|
|
2825
|
+
let top: Element['parent'] | Element = from;
|
|
2826
|
+
while (top.parent) top = top.parent;
|
|
2827
|
+
const stack = [...top.children].reverse();
|
|
2828
|
+
for (let node = stack.pop(); node; node = stack.pop()) {
|
|
2829
|
+
if (node.type !== 'tag') continue;
|
|
2830
|
+
const el = node as Element;
|
|
2831
|
+
if (el.attribs.id === id) return el;
|
|
2832
|
+
for (let i = el.children.length - 1; i >= 0; i -= 1) {
|
|
2833
|
+
stack.push(el.children[i]);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return null;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2363
2839
|
/** Whether declarations set a property a shape in a drawing has. */
|
|
2364
2840
|
function declaresShape(declarations: readonly Declaration[]): boolean {
|
|
2365
2841
|
for (const d of declarations) if (isShapeProp(d.prop)) return true;
|