@ng-native/fabric 0.5.0 → 0.7.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/css.d.ts +212 -31
- package/dist/css.js +814 -134
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +187 -2
- package/dist/engine.js +930 -88
- package/dist/engine.js.map +1 -1
- package/dist/inline-token.js +25 -12
- package/dist/inline-token.js.map +1 -1
- package/dist/inline-transform.js +21 -3
- package/dist/inline-transform.js.map +1 -1
- package/dist/native-drive.js +5 -0
- package/dist/native-drive.js.map +1 -1
- package/dist/scroll-animation.d.ts +13 -0
- package/dist/scroll-animation.js +53 -4
- package/dist/scroll-animation.js.map +1 -1
- package/dist/transition.d.ts +25 -1
- package/dist/transition.js +179 -30
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
package/dist/css.js
CHANGED
|
@@ -29,6 +29,15 @@ function formOf(token, kind) {
|
|
|
29
29
|
const stand = STAND_IN[kind];
|
|
30
30
|
return token[kind] ?? (stand ? token[stand] : undefined);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether a child is one `:empty` sees. An anchor is a comment on the web, which it does not;
|
|
34
|
+
* nor text of no length, which a binding to nothing leaves behind.
|
|
35
|
+
*/
|
|
36
|
+
export function fills(child) {
|
|
37
|
+
if (child.kind === 'anchor')
|
|
38
|
+
return false;
|
|
39
|
+
return child.kind !== 'text' || child.text !== '';
|
|
40
|
+
}
|
|
32
41
|
/**
|
|
33
42
|
* Properties CSS inherits that mean something on native.
|
|
34
43
|
*
|
|
@@ -79,6 +88,32 @@ function drawNoBorder(own) {
|
|
|
79
88
|
own[key] = 0;
|
|
80
89
|
own['borderWidth'] = 0;
|
|
81
90
|
}
|
|
91
|
+
/** Each side's own style, as the compiler keeps it, and the width it is the style of. */
|
|
92
|
+
const SIDE_STYLES = ['Top', 'Right', 'Bottom', 'Left', 'Start', 'End'].map((side) => [`border${side}Style`, `border${side}Width`]);
|
|
93
|
+
/**
|
|
94
|
+
* Settle a border side by side, once the cascade has picked each side's style: a side styled
|
|
95
|
+
* `none` has no width, whichever rule set one, and the one style native has is that of a side
|
|
96
|
+
* that is drawn. With no side drawn and no style at all, there is no border. A side's own style
|
|
97
|
+
* is not sent on: native has none.
|
|
98
|
+
*/
|
|
99
|
+
function settleBorders(own) {
|
|
100
|
+
let drawn;
|
|
101
|
+
for (const [style, width] of SIDE_STYLES) {
|
|
102
|
+
if (!(style in own))
|
|
103
|
+
continue;
|
|
104
|
+
if (own[style] === 'none')
|
|
105
|
+
own[width] = 0;
|
|
106
|
+
else
|
|
107
|
+
drawn ??= own[style];
|
|
108
|
+
delete own[style];
|
|
109
|
+
}
|
|
110
|
+
if (own['borderStyle'] !== 'none')
|
|
111
|
+
return;
|
|
112
|
+
if (drawn === undefined)
|
|
113
|
+
drawNoBorder(own);
|
|
114
|
+
else
|
|
115
|
+
own['borderStyle'] = drawn;
|
|
116
|
+
}
|
|
82
117
|
/**
|
|
83
118
|
* Deterministic work counters for the matcher.
|
|
84
119
|
*
|
|
@@ -170,11 +205,14 @@ export const styleStats = {
|
|
|
170
205
|
ruleTests: 0,
|
|
171
206
|
/** Nodes cascaded, as opposed to handed back from the memo. */
|
|
172
207
|
nodesResolved: 0,
|
|
208
|
+
/** Rules filed by key selector: each sheet's once, however many sheets come after it. */
|
|
209
|
+
rulesFiled: 0,
|
|
173
210
|
};
|
|
174
211
|
export function resetStyleStats() {
|
|
175
212
|
styleStats.compoundTests = 0;
|
|
176
213
|
styleStats.ruleTests = 0;
|
|
177
214
|
styleStats.nodesResolved = 0;
|
|
215
|
+
styleStats.rulesFiled = 0;
|
|
178
216
|
}
|
|
179
217
|
/** A prop's value as an attribute test compares it: in lower case, under the i flag. */
|
|
180
218
|
const attributeText = (actual, test) => test.insensitive ? String(actual).toLowerCase() : String(actual);
|
|
@@ -242,10 +280,8 @@ function matchesCompound(node, compound, sheet) {
|
|
|
242
280
|
return false;
|
|
243
281
|
}
|
|
244
282
|
}
|
|
245
|
-
|
|
246
|
-
if (compound.empty !== undefined && node.children?.some((child) => child.kind !== 'anchor')) {
|
|
283
|
+
if (compound.empty !== undefined && node.children?.some(fills))
|
|
247
284
|
return false;
|
|
248
|
-
}
|
|
249
285
|
if (compound.nth !== undefined) {
|
|
250
286
|
for (const test of compound.nth) {
|
|
251
287
|
if (!matchesNth(node, test))
|
|
@@ -383,8 +419,152 @@ function matchAny(first, next, rule, index, compound, sheet) {
|
|
|
383
419
|
}
|
|
384
420
|
return false;
|
|
385
421
|
}
|
|
422
|
+
const TAKES_PARENTS = new WeakMap();
|
|
423
|
+
/** Whether a rule has an `inherit` in it, which is settled from the parent's own style. */
|
|
424
|
+
function takesParents(rule) {
|
|
425
|
+
let takes = TAKES_PARENTS.get(rule);
|
|
426
|
+
if (takes === undefined) {
|
|
427
|
+
takes = rule.deferred?.some((declaration) => declaration.inherit) === true;
|
|
428
|
+
TAKES_PARENTS.set(rule, takes);
|
|
429
|
+
}
|
|
430
|
+
return takes;
|
|
431
|
+
}
|
|
386
432
|
/** Whether a node is one of the children CSS counts. Text and anchors are children, not elements. */
|
|
387
433
|
const isElement = (node) => node.kind === undefined || node.kind === 'element';
|
|
434
|
+
/** A node's first `count` elements, or its last, skipping everything that is not one. */
|
|
435
|
+
export function elementsAtEnd(siblings, count, fromEnd) {
|
|
436
|
+
const found = [];
|
|
437
|
+
for (let step = 0; step < siblings.length && found.length < count; step++) {
|
|
438
|
+
const sibling = siblings[fromEnd ? siblings.length - 1 - step : step];
|
|
439
|
+
if (isElement(sibling))
|
|
440
|
+
found.push(sibling);
|
|
441
|
+
}
|
|
442
|
+
return found;
|
|
443
|
+
}
|
|
444
|
+
const REACH = new WeakMap();
|
|
445
|
+
/**
|
|
446
|
+
* How far a sheet asks about a child list beyond which element is first and which is last.
|
|
447
|
+
* Where no sheet reaches either way, only the elements at the two ends can match differently.
|
|
448
|
+
*/
|
|
449
|
+
export function siblingReach(sheet) {
|
|
450
|
+
let known = REACH.get(sheet);
|
|
451
|
+
if (known === undefined) {
|
|
452
|
+
const reach = { after: 0, before: false };
|
|
453
|
+
for (const rule of sheet.rules) {
|
|
454
|
+
reach.after = Math.max(reach.after, stepsAlong(rule.combinators));
|
|
455
|
+
for (const compound of rule.compounds)
|
|
456
|
+
countSiblings(compound, reach, false);
|
|
457
|
+
}
|
|
458
|
+
known = reach;
|
|
459
|
+
REACH.set(sheet, known);
|
|
460
|
+
}
|
|
461
|
+
return known;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* How many elements on from one a rule's combinators read: every later one for a `~`, and
|
|
465
|
+
* otherwise its longest run of `+`. A run ends at a descendant or child combinator, which steps
|
|
466
|
+
* out of the list.
|
|
467
|
+
*/
|
|
468
|
+
function stepsAlong(combinators) {
|
|
469
|
+
let longest = 0;
|
|
470
|
+
let run = 0;
|
|
471
|
+
for (const one of combinators) {
|
|
472
|
+
if (one === 'later-sibling')
|
|
473
|
+
return Infinity;
|
|
474
|
+
run = one === 'next-sibling' ? run + 1 : 0;
|
|
475
|
+
longest = Math.max(longest, run);
|
|
476
|
+
}
|
|
477
|
+
return longest;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Note in `reach` which end a compound, or one nested in it, counts its siblings from, further
|
|
481
|
+
* than the first or last. A count inside a `:has()` is read as both: what it is asked of is
|
|
482
|
+
* matched again by other means, and which way it reaches is not worked out.
|
|
483
|
+
*/
|
|
484
|
+
function countSiblings(compound, reach, withinHas) {
|
|
485
|
+
// Which is first and which is last is the ends, and no count.
|
|
486
|
+
const counts = (compound.nth ?? []).filter((test) => test.a !== 0 || test.b !== 1);
|
|
487
|
+
if (counts.some((test) => withinHas || !test.fromEnd))
|
|
488
|
+
reach.after = Infinity;
|
|
489
|
+
if (counts.some((test) => withinHas || test.fromEnd))
|
|
490
|
+
reach.before = true;
|
|
491
|
+
for (const inner of nestedCompounds(compound))
|
|
492
|
+
countSiblings(inner, reach, withinHas);
|
|
493
|
+
for (const test of compound.has ?? []) {
|
|
494
|
+
for (const inner of test.any)
|
|
495
|
+
countSiblings(inner, reach, true);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const PLACE_ELSEWHERE = new WeakMap();
|
|
499
|
+
/**
|
|
500
|
+
* Each compound in a sheet whose element's place among its siblings is read from another
|
|
501
|
+
* element: one with something under it in the rule, `.row:nth-child(odd) .label` and
|
|
502
|
+
* `.a + .b .c`, or one an `:is()` or `:host-context()` looks for above the element styled.
|
|
503
|
+
*
|
|
504
|
+
* An element whose place changed is matched again, and where it matches the rules it did, it
|
|
505
|
+
* and everything under it are left as they were: its own rules are all that its place can
|
|
506
|
+
* change. Not an element one of these could be. What is under that one can match differently
|
|
507
|
+
* though it matches the same, so it is styled again with everything under it.
|
|
508
|
+
*
|
|
509
|
+
* A place read by the element a rule styles, or by the elements before it that the rule steps
|
|
510
|
+
* from (`.a:first-child + .b`), is that element's own: the rules it matches say.
|
|
511
|
+
*/
|
|
512
|
+
export function placeReadElsewhere(sheet) {
|
|
513
|
+
let known = PLACE_ELSEWHERE.get(sheet);
|
|
514
|
+
if (known === undefined) {
|
|
515
|
+
const elsewhere = [];
|
|
516
|
+
for (const rule of sheet.rules) {
|
|
517
|
+
// A run of compounds joined by `+` and `~` is one child list. Every run but the last is
|
|
518
|
+
// above the element styled, and its last compound is the element the rest hangs under.
|
|
519
|
+
let start = 0;
|
|
520
|
+
rule.compounds.forEach((compound, at) => {
|
|
521
|
+
placeReadAbove(compound, elsewhere);
|
|
522
|
+
const joined = rule.combinators[at];
|
|
523
|
+
if (joined === 'next-sibling' || joined === 'later-sibling')
|
|
524
|
+
return;
|
|
525
|
+
const run = rule.compounds.slice(start, at + 1);
|
|
526
|
+
start = at + 1;
|
|
527
|
+
const above = at < rule.compounds.length - 1;
|
|
528
|
+
if (above && (run.length > 1 || run.some(readsPlace)))
|
|
529
|
+
elsewhere.push(compound);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
known = elsewhere;
|
|
533
|
+
PLACE_ELSEWHERE.set(sheet, known);
|
|
534
|
+
}
|
|
535
|
+
return known;
|
|
536
|
+
}
|
|
537
|
+
/** Whether a compound, or one nested in it at any depth, asks about a place in a child list. */
|
|
538
|
+
function readsPlace(compound) {
|
|
539
|
+
if (compound.nth?.length)
|
|
540
|
+
return true;
|
|
541
|
+
const within = (compound.has ?? []).flatMap((test) => test.any);
|
|
542
|
+
return [...nestedCompounds(compound), ...within].some(readsPlace);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Collect the compounds nested in one that are matched against an element above the one it is
|
|
546
|
+
* matched against, and ask about that element's place: `:is(.group:first-child *)`.
|
|
547
|
+
*/
|
|
548
|
+
function placeReadAbove(compound, elsewhere) {
|
|
549
|
+
const above = [
|
|
550
|
+
...(compound.ancestors ?? []),
|
|
551
|
+
...(compound.parents ?? []),
|
|
552
|
+
...(compound.hostContext ?? []),
|
|
553
|
+
];
|
|
554
|
+
for (const inner of above)
|
|
555
|
+
if (readsPlace(inner))
|
|
556
|
+
elsewhere.push(inner);
|
|
557
|
+
for (const inner of nestedCompounds(compound))
|
|
558
|
+
placeReadAbove(inner, elsewhere);
|
|
559
|
+
}
|
|
560
|
+
/** The compounds nested in one, a `:has()`'s apart. */
|
|
561
|
+
const nestedCompounds = (compound) => [
|
|
562
|
+
...(compound.not ?? []),
|
|
563
|
+
...(compound.is ?? []).flat(),
|
|
564
|
+
...(compound.ancestors ?? []),
|
|
565
|
+
...(compound.parents ?? []),
|
|
566
|
+
...(compound.hostContext ?? []),
|
|
567
|
+
];
|
|
388
568
|
/** The element before this one, skipping everything that is not one. */
|
|
389
569
|
function previousElement(node) {
|
|
390
570
|
const siblings = node.parent?.children;
|
|
@@ -702,69 +882,150 @@ const ELEMENT_ENTRIES = ELEMENT_STYLES.rules.map((rule) => ({
|
|
|
702
882
|
* is offered to everything, because the alternative is a rule that silently stops applying.
|
|
703
883
|
*/
|
|
704
884
|
export function ruleKey(rule) {
|
|
885
|
+
return ruleKeys(rule)[0];
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Every bucket a rule is filed under. One, but for a rule whose rightmost compound is only an
|
|
889
|
+
* `:is()` of compounds that each have a key: it is filed under each of theirs, since a node it
|
|
890
|
+
* matches has one of them.
|
|
891
|
+
*
|
|
892
|
+
* A rule that says nothing of the node's own name can still say whose child it is, `.row > *`.
|
|
893
|
+
* It is filed under the parent's class, and offered to the children of an element that has it.
|
|
894
|
+
*/
|
|
895
|
+
export function ruleKeys(rule) {
|
|
705
896
|
const key = rule.compounds[rule.compounds.length - 1];
|
|
706
897
|
if (!key)
|
|
707
|
-
return
|
|
898
|
+
return EVERY_NODE;
|
|
708
899
|
if (key.classes.length)
|
|
709
|
-
return `class:${key.classes[0]}
|
|
900
|
+
return [`class:${key.classes[0]}`];
|
|
710
901
|
if (key.id !== undefined)
|
|
711
|
-
return `id:${key.id}
|
|
902
|
+
return [`id:${key.id}`];
|
|
712
903
|
if (key.type !== undefined)
|
|
713
|
-
return `type:${key.type}
|
|
714
|
-
|
|
904
|
+
return [`type:${key.type}`];
|
|
905
|
+
const held = parentClass(rule, key);
|
|
906
|
+
if (held !== undefined)
|
|
907
|
+
return [`in:${held}`];
|
|
908
|
+
return alternativeKeys(key) ?? EVERY_NODE;
|
|
909
|
+
}
|
|
910
|
+
/** A class the node's parent has to have for the rule to match, where its selector says one. */
|
|
911
|
+
function parentClass(rule, key) {
|
|
912
|
+
const written = key.parents?.find((compound) => compound.classes.length > 0);
|
|
913
|
+
if (written)
|
|
914
|
+
return written.classes[0];
|
|
915
|
+
if (rule.combinators[rule.combinators.length - 1] !== 'child')
|
|
916
|
+
return undefined;
|
|
917
|
+
return rule.compounds[rule.compounds.length - 2]?.classes[0];
|
|
918
|
+
}
|
|
919
|
+
/** The keys of each compound a bare `:is()` takes, or nothing where one of them has none. */
|
|
920
|
+
function alternativeKeys(key) {
|
|
921
|
+
const any = key.is?.[0];
|
|
922
|
+
if (!any?.length)
|
|
923
|
+
return undefined;
|
|
924
|
+
const keys = [];
|
|
925
|
+
for (const one of any) {
|
|
926
|
+
if (one.classes.length)
|
|
927
|
+
keys.push(`class:${one.classes[0]}`);
|
|
928
|
+
else if (one.id !== undefined)
|
|
929
|
+
keys.push(`id:${one.id}`);
|
|
930
|
+
else if (one.type !== undefined)
|
|
931
|
+
keys.push(`type:${one.type}`);
|
|
932
|
+
else
|
|
933
|
+
return undefined;
|
|
934
|
+
}
|
|
935
|
+
return keys;
|
|
715
936
|
}
|
|
716
937
|
const UNIVERSAL = '*';
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
938
|
+
const EVERY_NODE = [UNIVERSAL];
|
|
939
|
+
/**
|
|
940
|
+
* Where each kind of sheet is filed, as a multiple of the room one sheet's rules are given: the
|
|
941
|
+
* global sheet first, then a host's own, its creator's, and each added sheet after those.
|
|
942
|
+
*/
|
|
943
|
+
const SHEET_ROOM = 1_000_000;
|
|
944
|
+
const HOSTS = 1;
|
|
945
|
+
const CREATED = 2;
|
|
946
|
+
const ADDED = 3;
|
|
947
|
+
/**
|
|
948
|
+
* File rules by key selector, each at `from` and on: its place among every rule there is, which
|
|
949
|
+
* settles the order of two of the same weight. Into an index there already where one is given,
|
|
950
|
+
* so a sheet that arrives later is added to what is filed and nothing is filed again.
|
|
951
|
+
*/
|
|
952
|
+
export function indexRules(entries, into = { buckets: new Map(), universal: [] }, from = 0) {
|
|
953
|
+
styleStats.rulesFiled += entries.length;
|
|
954
|
+
entries.forEach((entry, n) => {
|
|
955
|
+
const indexed = { entry, at: from + n };
|
|
956
|
+
for (const key of ruleKeys(entry.rule)) {
|
|
957
|
+
if (key === UNIVERSAL) {
|
|
958
|
+
into.universal.push(indexed);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const bucket = into.buckets.get(key);
|
|
962
|
+
if (bucket)
|
|
963
|
+
bucket.push(indexed);
|
|
964
|
+
else
|
|
965
|
+
into.buckets.set(key, [indexed]);
|
|
726
966
|
}
|
|
727
|
-
let bucket = buckets.get(key);
|
|
728
|
-
if (!bucket)
|
|
729
|
-
buckets.set(key, (bucket = []));
|
|
730
|
-
bucket.push({ entry, at });
|
|
731
967
|
});
|
|
732
|
-
return
|
|
968
|
+
return into;
|
|
969
|
+
}
|
|
970
|
+
/** Rules in the order they were filed in, which is the order of a list sorted before it was. */
|
|
971
|
+
const byPlace = (a, b) => a.at - b.at;
|
|
972
|
+
/** Add to `found` what one index holds in the buckets a node's name, id and classes reach. */
|
|
973
|
+
function reach(node, index, found) {
|
|
974
|
+
const take = (key) => {
|
|
975
|
+
const bucket = index.buckets.get(key);
|
|
976
|
+
if (bucket)
|
|
977
|
+
found.push(...bucket);
|
|
978
|
+
};
|
|
979
|
+
if (index.universal.length)
|
|
980
|
+
found.push(...index.universal);
|
|
981
|
+
take(`type:${node.name}`);
|
|
982
|
+
const id = node.props['nativeID'];
|
|
983
|
+
if (typeof id === 'string')
|
|
984
|
+
take(`id:${id}`);
|
|
985
|
+
if (node.classes)
|
|
986
|
+
for (const name of node.classes)
|
|
987
|
+
take(`class:${name}`);
|
|
988
|
+
// What is written for any child of an element with one of the parent's classes.
|
|
989
|
+
const around = node.parent?.classes;
|
|
990
|
+
if (around)
|
|
991
|
+
for (const name of around)
|
|
992
|
+
take(`in:${name}`);
|
|
733
993
|
}
|
|
734
994
|
/**
|
|
735
|
-
* The rules worth trying against a node, in
|
|
995
|
+
* The rules worth trying against a node, in cascade order: `order` says which of two comes
|
|
996
|
+
* first, and with none it is the order they were filed in.
|
|
736
997
|
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
* lists are short by construction, which is the entire point of the index.
|
|
998
|
+
* From each index, only the buckets the node's name, id and classes reach, which is the entire
|
|
999
|
+
* point of the index: the lists are short by construction, so sorting them is cheap.
|
|
740
1000
|
*/
|
|
741
|
-
export function candidateRules(node,
|
|
1001
|
+
export function candidateRules(node, indexes, order = byPlace) {
|
|
742
1002
|
const found = [];
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (
|
|
747
|
-
found.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1003
|
+
const all = Array.isArray(indexes) ? indexes : [indexes];
|
|
1004
|
+
for (const index of all)
|
|
1005
|
+
reach(node, index, found);
|
|
1006
|
+
if (found.length < 2)
|
|
1007
|
+
return found.map((indexed) => indexed.entry);
|
|
1008
|
+
found.sort(order);
|
|
1009
|
+
// A rule filed under two keys the node has is offered once.
|
|
1010
|
+
const offered = [];
|
|
1011
|
+
for (let i = 0; i < found.length; i++) {
|
|
1012
|
+
if (found[i] !== found[i - 1])
|
|
1013
|
+
offered.push(found[i].entry);
|
|
1014
|
+
}
|
|
1015
|
+
return offered;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* The properties the rules matching a node declared `!important`, or nothing where none did:
|
|
1019
|
+
* each by its own name and by the other form of it, so `margin-left` stands over an inline
|
|
1020
|
+
* `marginStart` as it does over an inline `marginLeft`.
|
|
1021
|
+
*/
|
|
1022
|
+
function importantNames(result, twin) {
|
|
1023
|
+
const deferred = result.deferred?.filter((one) => one.important) ?? [];
|
|
1024
|
+
if (!result.important && !deferred.length)
|
|
1025
|
+
return undefined;
|
|
1026
|
+
const names = [...Object.keys(result.important ?? {}), ...deferred.flatMap((one) => one.props)];
|
|
1027
|
+
const sided = names.filter((name) => name in TWIN.ltr);
|
|
1028
|
+
return new Set(sided.length ? [...names, ...sided.map((name) => twin()[name])] : names);
|
|
768
1029
|
}
|
|
769
1030
|
/** What `em` falls back to when nothing in scope has set a font size. The web's default. */
|
|
770
1031
|
const DEFAULT_FONT_SIZE = 16;
|
|
@@ -774,14 +1035,186 @@ const EMPTY = {};
|
|
|
774
1035
|
const ROOT_CONTEXT = {};
|
|
775
1036
|
/** Shared empty token map, for the overwhelmingly common case of a subtree defining none. */
|
|
776
1037
|
const NO_TOKENS = {};
|
|
777
|
-
/** Stands in for "created by a component with no sheet" as a cache key. */
|
|
778
|
-
const NO_SHEET = { rules: [] };
|
|
779
1038
|
/**
|
|
780
1039
|
* Conditions generations are unique across every resolver in the process, not merely
|
|
781
1040
|
* increasing within one, because the memo they stamp lives on the compiled rule and a sheet is
|
|
782
1041
|
* shared by every engine that mounts the component.
|
|
783
1042
|
*/
|
|
784
1043
|
let generations = 0;
|
|
1044
|
+
const subjects = new WeakMap();
|
|
1045
|
+
function subjectsOf(sheet) {
|
|
1046
|
+
let found = subjects.get(sheet);
|
|
1047
|
+
if (found)
|
|
1048
|
+
return found;
|
|
1049
|
+
const classes = new Set();
|
|
1050
|
+
const types = new Set();
|
|
1051
|
+
const inside = new Set();
|
|
1052
|
+
const all = { classes, types, inside, any: false };
|
|
1053
|
+
for (const rule of sheet.rules)
|
|
1054
|
+
noteSubject(all, rule);
|
|
1055
|
+
subjects.set(sheet, (found = all));
|
|
1056
|
+
return found;
|
|
1057
|
+
}
|
|
1058
|
+
/** Note in `into` what a rule is for. */
|
|
1059
|
+
function noteSubject(into, rule) {
|
|
1060
|
+
const subject = rule.compounds.at(-1);
|
|
1061
|
+
// One class of several is enough to ask: a node without it matches none of them.
|
|
1062
|
+
if (subject?.classes.length)
|
|
1063
|
+
into.classes.add(subject.classes[0]);
|
|
1064
|
+
else if (boxAround(rule) !== undefined)
|
|
1065
|
+
into.inside.add(boxAround(rule));
|
|
1066
|
+
else if (subject?.type && subject.type !== '*')
|
|
1067
|
+
into.types.add(subject.type);
|
|
1068
|
+
else
|
|
1069
|
+
into.any = true;
|
|
1070
|
+
}
|
|
1071
|
+
const reaches = new WeakMap();
|
|
1072
|
+
/**
|
|
1073
|
+
* How far each class a sheet's selectors name reaches, and the tests of the `class` attribute
|
|
1074
|
+
* itself that are in them.
|
|
1075
|
+
*
|
|
1076
|
+
* The rules are walked whole for the classes in them, whatever a selector is made of: a part of
|
|
1077
|
+
* one added later counts as a class that reaches everything, without being named here.
|
|
1078
|
+
*/
|
|
1079
|
+
function classReach(sheet) {
|
|
1080
|
+
let found = reaches.get(sheet);
|
|
1081
|
+
if (found)
|
|
1082
|
+
return found;
|
|
1083
|
+
const classes = new Map();
|
|
1084
|
+
const reads = [];
|
|
1085
|
+
for (const rule of sheet.rules) {
|
|
1086
|
+
const every = new Set();
|
|
1087
|
+
collectClasses(rule, every, reads);
|
|
1088
|
+
const inside = classesInside(rule);
|
|
1089
|
+
for (const name of every) {
|
|
1090
|
+
if (!inside.has(name))
|
|
1091
|
+
classes.set(name, true);
|
|
1092
|
+
else
|
|
1093
|
+
noteInside(classes, name, rule);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
reaches.set(sheet, (found = { classes, reads }));
|
|
1097
|
+
return found;
|
|
1098
|
+
}
|
|
1099
|
+
/** Note a rule as one that names a class as what its element is inside. */
|
|
1100
|
+
function noteInside(found, name, rule) {
|
|
1101
|
+
let subjects = found.get(name);
|
|
1102
|
+
if (subjects === true)
|
|
1103
|
+
return;
|
|
1104
|
+
if (!subjects)
|
|
1105
|
+
found.set(name, (subjects = noSubjects()));
|
|
1106
|
+
const subject = rule.compounds.at(-1);
|
|
1107
|
+
if (subject?.classes.length)
|
|
1108
|
+
subjects.classes.add(subject.classes[0]);
|
|
1109
|
+
else if (subject?.type && subject.type !== '*')
|
|
1110
|
+
subjects.types.add(subject.type);
|
|
1111
|
+
// A rule for anything in the box, `.busy > *`: anything under the node the class changed
|
|
1112
|
+
// on. Asking which box has the class finds none once the class has gone from it.
|
|
1113
|
+
else
|
|
1114
|
+
subjects.any = true;
|
|
1115
|
+
}
|
|
1116
|
+
const noSubjects = () => ({
|
|
1117
|
+
classes: new Set(),
|
|
1118
|
+
types: new Set(),
|
|
1119
|
+
inside: new Set(),
|
|
1120
|
+
any: false,
|
|
1121
|
+
});
|
|
1122
|
+
/** Add to `into` what `from` is for. */
|
|
1123
|
+
function addSubjects(into, from) {
|
|
1124
|
+
for (const name of from.classes)
|
|
1125
|
+
into.classes.add(name);
|
|
1126
|
+
for (const name of from.types)
|
|
1127
|
+
into.types.add(name);
|
|
1128
|
+
for (const name of from.inside)
|
|
1129
|
+
into.inside.add(name);
|
|
1130
|
+
into.any ||= from.any;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* The classes a rule names only as what its element is inside: on a compound before the last,
|
|
1134
|
+
* with nothing but `>` and a space between there and the element, and nowhere else in the rule.
|
|
1135
|
+
*/
|
|
1136
|
+
function classesInside(rule) {
|
|
1137
|
+
const inside = new Set();
|
|
1138
|
+
const elsewhere = new Set();
|
|
1139
|
+
const last = rule.compounds.length - 1;
|
|
1140
|
+
let under = true;
|
|
1141
|
+
for (let index = last; index >= 0; index--) {
|
|
1142
|
+
const compound = rule.compounds[index];
|
|
1143
|
+
if (index < last) {
|
|
1144
|
+
const joined = rule.combinators[index];
|
|
1145
|
+
under &&= joined === 'child' || joined === 'descendant';
|
|
1146
|
+
}
|
|
1147
|
+
const { classes, ...rest } = compound;
|
|
1148
|
+
collectClasses(rest, elsewhere, []);
|
|
1149
|
+
for (const name of classes)
|
|
1150
|
+
(index < last && under ? inside : elsewhere).add(name);
|
|
1151
|
+
}
|
|
1152
|
+
for (const name of elsewhere)
|
|
1153
|
+
inside.delete(name);
|
|
1154
|
+
return inside;
|
|
1155
|
+
}
|
|
1156
|
+
/** What a rule declares, which names no element. */
|
|
1157
|
+
const DECLARED = new Set(['declarations', 'important', 'tokens']);
|
|
1158
|
+
/** Add each class under `value` to `into`, and each test of the `class` attribute to `reads`. */
|
|
1159
|
+
function collectClasses(value, into, reads) {
|
|
1160
|
+
if (value === null || typeof value !== 'object')
|
|
1161
|
+
return;
|
|
1162
|
+
if (Array.isArray(value))
|
|
1163
|
+
return value.forEach((inner) => collectClasses(inner, into, reads));
|
|
1164
|
+
const part = value;
|
|
1165
|
+
if (Array.isArray(part.classes))
|
|
1166
|
+
for (const name of part.classes)
|
|
1167
|
+
into.add(String(name));
|
|
1168
|
+
if (Array.isArray(part.attributes)) {
|
|
1169
|
+
for (const test of part.attributes)
|
|
1170
|
+
if (test.name === 'class')
|
|
1171
|
+
reads.push(test);
|
|
1172
|
+
}
|
|
1173
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
1174
|
+
if (!DECLARED.has(key))
|
|
1175
|
+
collectClasses(inner, into, reads);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Whether a class coming or going could change what a test of the `class` attribute answers.
|
|
1180
|
+
* One class of the list where the test is for a whole class or for part of one; any class
|
|
1181
|
+
* where it is for how the list starts or ends, or for a part with a space in it.
|
|
1182
|
+
*/
|
|
1183
|
+
function answers(test, name) {
|
|
1184
|
+
const value = test.value ?? '';
|
|
1185
|
+
const folded = test.insensitive ? name.toLowerCase() : name;
|
|
1186
|
+
if (test.operator === 'includes')
|
|
1187
|
+
return folded === value;
|
|
1188
|
+
if (test.operator === 'substring' && !/\s/.test(value))
|
|
1189
|
+
return folded.includes(value);
|
|
1190
|
+
return true;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* A class of the nearest box a rule's subject has to be in: the compound before it, where that
|
|
1194
|
+
* is joined as what holds it and names a class. Nothing where it is a sibling, or has no class.
|
|
1195
|
+
*/
|
|
1196
|
+
function boxAround(rule) {
|
|
1197
|
+
const joined = rule.combinators.at(-1);
|
|
1198
|
+
if (joined !== 'child' && joined !== 'descendant')
|
|
1199
|
+
return undefined;
|
|
1200
|
+
return rule.compounds.at(-2)?.classes[0];
|
|
1201
|
+
}
|
|
1202
|
+
/** Whether a rule among these could be for a node: never no where one is. */
|
|
1203
|
+
function writtenFor(of, node) {
|
|
1204
|
+
if (of.any || of.types.has(node.name))
|
|
1205
|
+
return true;
|
|
1206
|
+
for (const name of node.classes ?? [])
|
|
1207
|
+
if (of.classes.has(name))
|
|
1208
|
+
return true;
|
|
1209
|
+
if (!of.inside.size)
|
|
1210
|
+
return false;
|
|
1211
|
+
for (let over = node.parent; over; over = over.parent) {
|
|
1212
|
+
for (const name of over.classes ?? [])
|
|
1213
|
+
if (of.inside.has(name))
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
785
1218
|
function emptyCacheFor(epoch, generation) {
|
|
786
1219
|
return {
|
|
787
1220
|
epoch,
|
|
@@ -793,6 +1226,40 @@ function emptyCacheFor(epoch, generation) {
|
|
|
793
1226
|
tokens: NO_TOKENS,
|
|
794
1227
|
};
|
|
795
1228
|
}
|
|
1229
|
+
function isMultiple(value) {
|
|
1230
|
+
return typeof value === 'object' && value !== null && 'multiple' in value;
|
|
1231
|
+
}
|
|
1232
|
+
/** A line-height as points, for a node of font size `size`: what an em of it comes to. */
|
|
1233
|
+
function lineHeightPoints(line, size) {
|
|
1234
|
+
const points = line.multiple * (typeof size === 'number' ? size : DEFAULT_FONT_SIZE);
|
|
1235
|
+
return Math.round(points * 1000) / 1000;
|
|
1236
|
+
}
|
|
1237
|
+
/** A node's style with a unitless line-height it set or inherited worked out against its font size. */
|
|
1238
|
+
function settleLineHeight(style) {
|
|
1239
|
+
const line = style['lineHeight'];
|
|
1240
|
+
if (isMultiple(line))
|
|
1241
|
+
style['lineHeight'] = lineHeightPoints(line, style['fontSize']);
|
|
1242
|
+
}
|
|
1243
|
+
/** The styles of nodes with no rules, by the map they inherit, for one that hands down a multiple. */
|
|
1244
|
+
const unstyledStyles = new WeakMap();
|
|
1245
|
+
/**
|
|
1246
|
+
* The style of a node no rule matches: what it inherits, with a unitless line-height worked out
|
|
1247
|
+
* against its font size, which an inline style can set. The same object where there is none.
|
|
1248
|
+
*/
|
|
1249
|
+
function unstyledStyle(inherited, inline) {
|
|
1250
|
+
const line = inherited['lineHeight'];
|
|
1251
|
+
if (!isMultiple(line))
|
|
1252
|
+
return inherited;
|
|
1253
|
+
const size = inline?.['fontSize'];
|
|
1254
|
+
if (typeof size === 'number')
|
|
1255
|
+
return { ...inherited, lineHeight: lineHeightPoints(line, size) };
|
|
1256
|
+
let style = unstyledStyles.get(inherited);
|
|
1257
|
+
if (!style) {
|
|
1258
|
+
style = { ...inherited, lineHeight: lineHeightPoints(line, inherited['fontSize']) };
|
|
1259
|
+
unstyledStyles.set(inherited, style);
|
|
1260
|
+
}
|
|
1261
|
+
return style;
|
|
1262
|
+
}
|
|
796
1263
|
/**
|
|
797
1264
|
* What this node's children inherit: the parent's map, overlaid with any inheritable value the
|
|
798
1265
|
* node set itself. Copied on first write only, so a node that sets none hands the same object on
|
|
@@ -915,7 +1382,12 @@ export class StyleResolver {
|
|
|
915
1382
|
* selector. Keyed by the creating sheet and then the hosted one, so a component's nodes share a
|
|
916
1383
|
* single index.
|
|
917
1384
|
*/
|
|
918
|
-
|
|
1385
|
+
/** The global sheet and every sheet added to it, filed by key selector. See `rulesFor`. */
|
|
1386
|
+
filed = null;
|
|
1387
|
+
/** How many of the added sheets `filed` holds: one added since is filed at the next read. */
|
|
1388
|
+
filedAdded = 0;
|
|
1389
|
+
/** A component's own sheet, indexed once for each place it can take: a host's, or its creator's. */
|
|
1390
|
+
ownIndexes = new WeakMap();
|
|
919
1391
|
/**
|
|
920
1392
|
* Whether any sheet merged so far has a rule in a cascade layer. Until one does, rules are
|
|
921
1393
|
* ordered by weight alone and the cascade does not look for layers, which is nearly every app.
|
|
@@ -986,8 +1458,94 @@ export class StyleResolver {
|
|
|
986
1458
|
this.addedSheets.push(sheet);
|
|
987
1459
|
else
|
|
988
1460
|
this.addedSheets[at] = sheet;
|
|
989
|
-
|
|
1461
|
+
// One added at the end is filed after what is there. One in another's place files them all again.
|
|
1462
|
+
if (at !== -1)
|
|
1463
|
+
this.filed = null;
|
|
1464
|
+
const before = this.generation;
|
|
990
1465
|
this.generation = ++generations;
|
|
1466
|
+
// One that takes another's place can change what any node comes to. One that names layers
|
|
1467
|
+
// cannot: a layer has its place from the first sheet to name it, and a new name comes last.
|
|
1468
|
+
if (at !== -1)
|
|
1469
|
+
this.additions.length = 0;
|
|
1470
|
+
else
|
|
1471
|
+
this.additions.push({ before, after: this.generation, subjects: subjectsOf(sheet) });
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* The sheets added one after another with nothing else changed between them, each with the
|
|
1476
|
+
* generation it began and ended: a cache made before them still stands for a node none of
|
|
1477
|
+
* them could match. Emptied by anything else that moves the generation on.
|
|
1478
|
+
*/
|
|
1479
|
+
additions = [];
|
|
1480
|
+
/** Whether a sheet could match a node: one of its rules is written for it. */
|
|
1481
|
+
couldMatch(sheet, node) {
|
|
1482
|
+
return writtenFor(subjectsOf(sheet), node);
|
|
1483
|
+
}
|
|
1484
|
+
/** How far each class named by the sheets met so far reaches: see `classReach`. */
|
|
1485
|
+
reach = new Map();
|
|
1486
|
+
/** The tests of the `class` attribute itself in those sheets, which any class may answer. */
|
|
1487
|
+
classTests = [];
|
|
1488
|
+
noted = new WeakSet();
|
|
1489
|
+
/** Read the classes a sheet names, once, as soon as any node is given it. */
|
|
1490
|
+
noteSheet(sheet) {
|
|
1491
|
+
if (!sheet || this.noted.has(sheet))
|
|
1492
|
+
return;
|
|
1493
|
+
this.noted.add(sheet);
|
|
1494
|
+
const { classes, reads } = classReach(sheet);
|
|
1495
|
+
for (const [name, how] of classes)
|
|
1496
|
+
this.noteReach(name, how);
|
|
1497
|
+
this.classTests.push(...reads);
|
|
1498
|
+
}
|
|
1499
|
+
noteReach(name, how) {
|
|
1500
|
+
const had = this.reach.get(name);
|
|
1501
|
+
if (had === true)
|
|
1502
|
+
return;
|
|
1503
|
+
if (how === true)
|
|
1504
|
+
this.reach.set(name, true);
|
|
1505
|
+
else
|
|
1506
|
+
addSubjects(had ?? this.reach.set(name, noSubjects()).get(name), how);
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* What classes coming to an element or going from it can restyle: `true` for it and all
|
|
1510
|
+
* under it and after it, as any change to it does; the elements under it to restyle, by what
|
|
1511
|
+
* rules are for; or null, where no rule names any of them and nothing is styled again.
|
|
1512
|
+
*/
|
|
1513
|
+
reachOf(classes) {
|
|
1514
|
+
let under = null;
|
|
1515
|
+
for (const name of classes) {
|
|
1516
|
+
const how = this.reach.get(name);
|
|
1517
|
+
if (how === true || this.classTests.some((test) => answers(test, name)))
|
|
1518
|
+
return true;
|
|
1519
|
+
if (how)
|
|
1520
|
+
addSubjects((under ??= noSubjects()), how);
|
|
1521
|
+
}
|
|
1522
|
+
return under;
|
|
1523
|
+
}
|
|
1524
|
+
/** Whether one of the rules `subjects` stands for could be for a node. */
|
|
1525
|
+
isFor(subjects, node) {
|
|
1526
|
+
return writtenFor(subjects, node);
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Whether a cache is of this generation, or of one before sheets that could not match the
|
|
1530
|
+
* node were added, which leaves it as it was: it is then brought up to this one.
|
|
1531
|
+
*/
|
|
1532
|
+
current(cached, node) {
|
|
1533
|
+
if (cached.generation === this.generation)
|
|
1534
|
+
return true;
|
|
1535
|
+
// The one cache shared by every node with nothing to style is not one node's to move on.
|
|
1536
|
+
if (cached.context === ROOT_CONTEXT)
|
|
1537
|
+
return false;
|
|
1538
|
+
let at = cached.generation;
|
|
1539
|
+
for (const added of this.additions) {
|
|
1540
|
+
if (added.before !== at)
|
|
1541
|
+
continue;
|
|
1542
|
+
if (writtenFor(added.subjects, node))
|
|
1543
|
+
return false;
|
|
1544
|
+
at = added.after;
|
|
1545
|
+
}
|
|
1546
|
+
if (at !== this.generation)
|
|
1547
|
+
return false;
|
|
1548
|
+
cached.generation = at;
|
|
991
1549
|
return true;
|
|
992
1550
|
}
|
|
993
1551
|
/** Stop matching a sheet `addGlobalSheet` added; false when it is not one. */
|
|
@@ -996,16 +1554,19 @@ export class StyleResolver {
|
|
|
996
1554
|
if (at === -1)
|
|
997
1555
|
return false;
|
|
998
1556
|
this.addedSheets.splice(at, 1);
|
|
999
|
-
this.
|
|
1557
|
+
this.filed = null;
|
|
1558
|
+
this.additions.length = 0;
|
|
1000
1559
|
this.generation = ++generations;
|
|
1001
1560
|
return true;
|
|
1002
1561
|
}
|
|
1003
1562
|
setConditions(next) {
|
|
1004
1563
|
this.conditions = next;
|
|
1564
|
+
this.additions.length = 0;
|
|
1005
1565
|
this.generation = ++generations;
|
|
1006
1566
|
}
|
|
1007
1567
|
setRootTokens(next) {
|
|
1008
1568
|
this.tokensOnRoot = next;
|
|
1569
|
+
this.additions.length = 0;
|
|
1009
1570
|
this.generation = ++generations;
|
|
1010
1571
|
}
|
|
1011
1572
|
/**
|
|
@@ -1030,6 +1591,10 @@ export class StyleResolver {
|
|
|
1030
1591
|
return cached;
|
|
1031
1592
|
}
|
|
1032
1593
|
const kept = this.kept(node, parentContext, epoch);
|
|
1594
|
+
// The rules it was just found to match, where they are not the ones it did. Taken here
|
|
1595
|
+
// whether or not they are used, so that none is left for the next node resolved.
|
|
1596
|
+
const found = this.justMatched;
|
|
1597
|
+
this.justMatched = null;
|
|
1033
1598
|
if (kept)
|
|
1034
1599
|
return kept;
|
|
1035
1600
|
const parentInherited = parent ? parent.inherited : EMPTY;
|
|
@@ -1038,31 +1603,37 @@ export class StyleResolver {
|
|
|
1038
1603
|
return this.unstyled(node, epoch, parent, parentContext, parentInherited);
|
|
1039
1604
|
}
|
|
1040
1605
|
styleStats.nodesResolved++;
|
|
1041
|
-
const
|
|
1042
|
-
const matched = this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
|
|
1606
|
+
const matched = found ?? this.matchedBy(node);
|
|
1043
1607
|
const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
|
|
1608
|
+
this.heir(node, parent, matched);
|
|
1044
1609
|
const styled = this.styled(node, matched, parentTokens, parentInherited);
|
|
1610
|
+
const cache = this.cached(epoch, parentContext, styled, matched);
|
|
1611
|
+
node.styleCache = cache;
|
|
1612
|
+
node.styleDirty = false;
|
|
1613
|
+
node.hasDirty = false;
|
|
1614
|
+
node.stateDirty = false;
|
|
1615
|
+
return cache;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* A cache of a node just styled, with a context of its own. It keeps the rules the node
|
|
1619
|
+
* matched, for the node to be compared with, once a sheet has a use for them: see `tracksHas`.
|
|
1620
|
+
*/
|
|
1621
|
+
cached(epoch, parentContext, styled, matched) {
|
|
1045
1622
|
const cache = {
|
|
1046
1623
|
epoch,
|
|
1047
1624
|
generation: this.generation,
|
|
1048
1625
|
context: {},
|
|
1049
1626
|
parentContext,
|
|
1050
1627
|
...styled,
|
|
1051
|
-
...this.record(matched),
|
|
1052
1628
|
};
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
node.hasDirty = false;
|
|
1056
|
-
node.stateDirty = false;
|
|
1629
|
+
if (this.tracksHas)
|
|
1630
|
+
cache.matched = matched;
|
|
1057
1631
|
return cache;
|
|
1058
1632
|
}
|
|
1059
|
-
/** The rules a node matched, for its cache to keep, while a sheet uses `:has()`. */
|
|
1060
|
-
record(matched) {
|
|
1061
|
-
return this.tracksHas ? { matched } : {};
|
|
1062
|
-
}
|
|
1063
1633
|
/**
|
|
1064
|
-
* Set by the engine once a sheet uses `:has()
|
|
1065
|
-
* so an app that never asks
|
|
1634
|
+
* Set by the engine once a sheet uses `:has()`, styles a pressed element or asks about a place
|
|
1635
|
+
* in a child list. Until then no cache keeps the rules it matched, so an app that never asks
|
|
1636
|
+
* pays nothing for it.
|
|
1066
1637
|
*/
|
|
1067
1638
|
tracksHas = false;
|
|
1068
1639
|
/**
|
|
@@ -1076,19 +1647,31 @@ export class StyleResolver {
|
|
|
1076
1647
|
cached?.matched !== undefined &&
|
|
1077
1648
|
!node.styleDirty &&
|
|
1078
1649
|
cached.parentContext === parentContext &&
|
|
1079
|
-
cached
|
|
1650
|
+
this.current(cached, node);
|
|
1080
1651
|
if (!stands)
|
|
1081
1652
|
return null;
|
|
1082
|
-
const
|
|
1083
|
-
const now = this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
|
|
1653
|
+
const now = this.matchedBy(node);
|
|
1084
1654
|
const before = cached.matched;
|
|
1085
|
-
if (now.length !== before.length || now.some((rule, i) => rule !== before[i]))
|
|
1655
|
+
if (now.length !== before.length || now.some((rule, i) => rule !== before[i])) {
|
|
1656
|
+
// It is styled again next, from the rules just found rather than from finding them twice.
|
|
1657
|
+
this.justMatched = now;
|
|
1086
1658
|
return null;
|
|
1659
|
+
}
|
|
1087
1660
|
node.hasDirty = false;
|
|
1088
1661
|
node.stateDirty = false;
|
|
1089
1662
|
cached.epoch = epoch;
|
|
1090
1663
|
return cached;
|
|
1091
1664
|
}
|
|
1665
|
+
/** The rules a node matches now, weakest first. */
|
|
1666
|
+
matchedBy(node) {
|
|
1667
|
+
const candidates = this.rulesFor(node);
|
|
1668
|
+
return this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* The rules `keptBeneath` found the node being resolved to match, where they are other than
|
|
1672
|
+
* it matched before: it is styled from them next, rather than matched a second time.
|
|
1673
|
+
*/
|
|
1674
|
+
justMatched = null;
|
|
1092
1675
|
/** A cache that still stands for a node marked for its subtree or its state alone. */
|
|
1093
1676
|
kept(node, parentContext, epoch) {
|
|
1094
1677
|
return (this.keptBeneath(node, parentContext, epoch) ?? this.keptUnmatched(node, parentContext, epoch));
|
|
@@ -1104,7 +1687,7 @@ export class StyleResolver {
|
|
|
1104
1687
|
!node.styleDirty &&
|
|
1105
1688
|
!node.hasDirty &&
|
|
1106
1689
|
cached.parentContext === parentContext &&
|
|
1107
|
-
cached
|
|
1690
|
+
this.current(cached, node) &&
|
|
1108
1691
|
(node.anonymous === true || this.hasNoRules(node));
|
|
1109
1692
|
if (!stands)
|
|
1110
1693
|
return null;
|
|
@@ -1129,7 +1712,7 @@ export class StyleResolver {
|
|
|
1129
1712
|
!node.hasDirty &&
|
|
1130
1713
|
!node.stateDirty &&
|
|
1131
1714
|
cached.parentContext === parentContext &&
|
|
1132
|
-
cached
|
|
1715
|
+
this.current(cached, node));
|
|
1133
1716
|
}
|
|
1134
1717
|
/**
|
|
1135
1718
|
* Nothing anywhere can match this node, and it sets no custom property of its own, so its style
|
|
@@ -1173,65 +1756,80 @@ export class StyleResolver {
|
|
|
1173
1756
|
generation: this.generation,
|
|
1174
1757
|
context: {},
|
|
1175
1758
|
parentContext,
|
|
1176
|
-
style: parentInherited,
|
|
1759
|
+
style: unstyledStyle(parentInherited, inline),
|
|
1177
1760
|
inherited: inline ? inheritFrom(parentInherited, inline) : parentInherited,
|
|
1178
1761
|
tokens,
|
|
1179
1762
|
};
|
|
1180
1763
|
node.styleCache = passthrough;
|
|
1181
1764
|
node.styleDirty = false;
|
|
1765
|
+
node.stateDirty = false;
|
|
1182
1766
|
return passthrough;
|
|
1183
1767
|
}
|
|
1184
1768
|
/**
|
|
1185
|
-
* The rules worth trying against a node.
|
|
1769
|
+
* The rules worth trying against a node, in cascade order.
|
|
1186
1770
|
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
byHost.set(node.hostSheet, (index = indexRules(this.merge(node.sheet, node.hostSheet))));
|
|
1200
|
-
}
|
|
1201
|
-
return candidateRules(node, index);
|
|
1202
|
-
}
|
|
1203
|
-
/**
|
|
1204
|
-
* Weakest first. A host node is matched against two component sheets: the one belonging to the
|
|
1205
|
-
* component it hosts, whose `:host` rules exist to reach exactly this node, and the one that
|
|
1206
|
-
* created it. Both carry the bump; the creator comes later and so wins a tie, as the outer
|
|
1207
|
-
* document does over `:host` on the web. The sort is stable, so within one sheet source order
|
|
1208
|
-
* survives and a later sheet wins equal weight.
|
|
1771
|
+
* Every sheet is filed once, by key selector: the app's global sheet and each sheet added to it
|
|
1772
|
+
* in one index, and a component's own in one of its own. What comes back is only the buckets
|
|
1773
|
+
* this node can reach. A global utility sheet is hundreds of rules and a screen is a thousand
|
|
1774
|
+
* nodes, and the difference is between a million match attempts and a few thousand. A library
|
|
1775
|
+
* adds a sheet for each of its components as the component first renders, dozens on one
|
|
1776
|
+
* screen, so one more is added to what is filed and nothing is filed again.
|
|
1777
|
+
*
|
|
1778
|
+
* Sheets are filed in the order a document holds them, which is the order of two rules of the
|
|
1779
|
+
* same weight, and where a layer gets its place: the app's global sheet, the host's own, the
|
|
1780
|
+
* creating component's, then each added sheet as it came. The component layers get a small
|
|
1781
|
+
* specificity bump, so the encapsulation attribute Angular adds on the web does not have to
|
|
1782
|
+
* be written.
|
|
1209
1783
|
*
|
|
1210
1784
|
* A sheet added with `addGlobalSheet` comes last, with no bump: a browser adds a None
|
|
1211
1785
|
* component's CSS when the component first renders, after the styles of the components around
|
|
1212
1786
|
* it, so it loses to their extra attribute and wins a tie with them.
|
|
1213
1787
|
*/
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
this.
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1788
|
+
rulesFor(node) {
|
|
1789
|
+
if (this.globalSheet)
|
|
1790
|
+
this.placeLayers(this.globalSheet);
|
|
1791
|
+
const indexes = [];
|
|
1792
|
+
if (node.hostSheet)
|
|
1793
|
+
indexes.push(this.ownIndex(node.hostSheet, HOSTS));
|
|
1794
|
+
if (node.sheet)
|
|
1795
|
+
indexes.push(this.ownIndex(node.sheet, CREATED));
|
|
1796
|
+
indexes.push(this.sharedIndex());
|
|
1797
|
+
return candidateRules(node, indexes, this.cascadeOrder);
|
|
1798
|
+
}
|
|
1799
|
+
/** Weakest first: by layer where any sheet has one, then by weight, then as they were filed. */
|
|
1800
|
+
cascadeOrder = (a, b) => {
|
|
1801
|
+
const layers = this.layered
|
|
1802
|
+
? layerOrder(this.layerPlaces.get(a.entry.rule), this.layerPlaces.get(b.entry.rule))
|
|
1803
|
+
: 0;
|
|
1804
|
+
return layers || a.entry.weight - b.entry.weight || a.at - b.at;
|
|
1805
|
+
};
|
|
1806
|
+
/** A component's sheet as the rules of a host or of a creator, which differ only in their place. */
|
|
1807
|
+
ownIndex(sheet, role) {
|
|
1808
|
+
let roles = this.ownIndexes.get(sheet);
|
|
1809
|
+
if (!roles)
|
|
1810
|
+
this.ownIndexes.set(sheet, (roles = []));
|
|
1811
|
+
return (roles[role] ??= indexRules(this.entries(sheet, COMPONENT_SPECIFICITY_BUMP), undefined, role * SHEET_ROOM));
|
|
1812
|
+
}
|
|
1813
|
+
/** The app's global sheet and every sheet added to it, with any added since the last read. */
|
|
1814
|
+
sharedIndex() {
|
|
1815
|
+
if (!this.filed) {
|
|
1816
|
+
this.filed = { buckets: new Map(), universal: [] };
|
|
1817
|
+
this.filedAdded = 0;
|
|
1818
|
+
if (this.globalSheet)
|
|
1819
|
+
indexRules(this.entries(this.globalSheet, 0), this.filed, 0);
|
|
1820
|
+
}
|
|
1821
|
+
for (; this.filedAdded < this.addedSheets.length; this.filedAdded++) {
|
|
1822
|
+
const from = (ADDED + this.filedAdded) * SHEET_ROOM;
|
|
1823
|
+
indexRules(this.entries(this.addedSheets[this.filedAdded], 0), this.filed, from);
|
|
1231
1824
|
}
|
|
1232
|
-
this.
|
|
1233
|
-
|
|
1234
|
-
|
|
1825
|
+
return this.filed;
|
|
1826
|
+
}
|
|
1827
|
+
/** A sheet's rules at their weight here, with its layers given their places. */
|
|
1828
|
+
entries(sheet, bump) {
|
|
1829
|
+
this.placeLayers(sheet);
|
|
1830
|
+
if (sheet.rules.some((rule) => rule.layer !== undefined))
|
|
1831
|
+
this.layered = true;
|
|
1832
|
+
return sheet.rules.map((rule) => ({ rule, sheet, weight: rule.specificity + bump }));
|
|
1235
1833
|
}
|
|
1236
1834
|
/**
|
|
1237
1835
|
* Gives each of a sheet's layers its place among every sheet's, the first time the sheet is
|
|
@@ -1274,7 +1872,8 @@ export class StyleResolver {
|
|
|
1274
1872
|
styled(node, matched, parentTokens, parentInherited) {
|
|
1275
1873
|
// Shared between nodes that match alike, unless the node sets something of its own.
|
|
1276
1874
|
const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
|
|
1277
|
-
|
|
1875
|
+
// Nor one that takes a value from its parent's own style, which two parents differ in.
|
|
1876
|
+
if (node.customProperties || inline || node.boundStyle || matched.some(takesParents)) {
|
|
1278
1877
|
return this.style(node, matched, parentTokens, parentInherited, inline);
|
|
1279
1878
|
}
|
|
1280
1879
|
if (this.sharedGeneration !== this.generation) {
|
|
@@ -1317,18 +1916,21 @@ export class StyleResolver {
|
|
|
1317
1916
|
: matched, parentInherited);
|
|
1318
1917
|
// Tokens are in scope for this node's own declarations as well as its descendants', so they
|
|
1319
1918
|
// are merged before any `var()` here is resolved.
|
|
1320
|
-
const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
|
|
1919
|
+
const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties, result.importantTokens);
|
|
1321
1920
|
const own = result.declarations;
|
|
1322
1921
|
if (result.deferred) {
|
|
1323
1922
|
this.applyDeferred(result.deferred, own, tokens, parentInherited, result.important);
|
|
1324
1923
|
}
|
|
1325
|
-
|
|
1326
|
-
drawNoBorder(own);
|
|
1924
|
+
settleBorders(own);
|
|
1327
1925
|
// `pointer-events: inherit` won the cascade: what the parent hands down stands.
|
|
1328
1926
|
if (own['pointerEvents'] === 'inherit')
|
|
1329
1927
|
delete own['pointerEvents'];
|
|
1330
1928
|
const style = { ...parentInherited, ...own };
|
|
1331
|
-
|
|
1929
|
+
settleLineHeight(style);
|
|
1930
|
+
const inherited = decorate(style, inheritFrom(parentInherited, own), own);
|
|
1931
|
+
const rtl = (style['direction'] ?? this.conditions.direction) === 'rtl';
|
|
1932
|
+
const important = importantNames(result, () => TWIN[rtl ? 'rtl' : 'ltr']);
|
|
1933
|
+
return important ? { style, inherited, tokens, important } : { style, inherited, tokens };
|
|
1332
1934
|
}
|
|
1333
1935
|
/** The rules that apply to a node, weakest first. */
|
|
1334
1936
|
matched(node, entries) {
|
|
@@ -1370,6 +1972,7 @@ export class StyleResolver {
|
|
|
1370
1972
|
declarations: hasImportant ? { ...normal, ...important } : normal,
|
|
1371
1973
|
important: hasImportant ? important : null,
|
|
1372
1974
|
tokens,
|
|
1975
|
+
importantTokens,
|
|
1373
1976
|
deferred,
|
|
1374
1977
|
};
|
|
1375
1978
|
}
|
|
@@ -1470,6 +2073,10 @@ export class StyleResolver {
|
|
|
1470
2073
|
*/
|
|
1471
2074
|
applyDeferred(deferred, own, tokens, parentInherited, important) {
|
|
1472
2075
|
for (const declaration of settlingOrder(deferred)) {
|
|
2076
|
+
if (declaration.inherit) {
|
|
2077
|
+
this.inherit(declaration, own, parentInherited, important);
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
1473
2080
|
const settled = this.settled(declaration, own, parentInherited, tokens);
|
|
1474
2081
|
if (settled === undefined && this.onUndefinedToken) {
|
|
1475
2082
|
this.reportUndefined(declaration, tokens);
|
|
@@ -1488,6 +2095,42 @@ export class StyleResolver {
|
|
|
1488
2095
|
}
|
|
1489
2096
|
}
|
|
1490
2097
|
}
|
|
2098
|
+
/** Note what the node being resolved takes an `inherit` from, and that its parent has an heir. */
|
|
2099
|
+
heir(node, parent, matched) {
|
|
2100
|
+
this.parentOf = parent;
|
|
2101
|
+
this.parentInline = node.parent?.props['style'];
|
|
2102
|
+
if (parent && matched.some(takesParents))
|
|
2103
|
+
parent.heirs = true;
|
|
2104
|
+
}
|
|
2105
|
+
/** What the parent of the node being resolved came to: an `inherit` takes its values from it. */
|
|
2106
|
+
parentOf = null;
|
|
2107
|
+
/** That parent's inline style, which is over what its rules gave it. */
|
|
2108
|
+
parentInline;
|
|
2109
|
+
/** What the parent's inline style sets, under what its rules marked important, as on the parent. */
|
|
2110
|
+
parentSet() {
|
|
2111
|
+
if (!this.parentInline)
|
|
2112
|
+
return EMPTY;
|
|
2113
|
+
const inline = flattenInline(this.parentInline, {});
|
|
2114
|
+
for (const prop of this.parentOf?.important ?? [])
|
|
2115
|
+
delete inline[prop];
|
|
2116
|
+
return inline;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* `inherit`: each prop as the parent has it, handed down or its own, and gone where the parent
|
|
2120
|
+
* has none, which is what a weaker rule's value for it gives way to.
|
|
2121
|
+
*/
|
|
2122
|
+
inherit(declaration, own, parentInherited, important) {
|
|
2123
|
+
const inline = this.parentSet();
|
|
2124
|
+
for (const prop of declaration.props) {
|
|
2125
|
+
if (!declaration.important && important !== null && prop in important)
|
|
2126
|
+
continue;
|
|
2127
|
+
const value = inline[prop] ?? parentInherited[prop] ?? this.parentOf?.style[prop];
|
|
2128
|
+
if (value === undefined)
|
|
2129
|
+
delete own[prop];
|
|
2130
|
+
else
|
|
2131
|
+
own[prop] = value;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
1491
2134
|
/** One deferred declaration's value, from the tokens in scope and the node's own style. */
|
|
1492
2135
|
settled(declaration, own, parentInherited, tokens) {
|
|
1493
2136
|
if (declaration.within !== undefined) {
|
|
@@ -1625,9 +2268,11 @@ export class StyleResolver {
|
|
|
1625
2268
|
}
|
|
1626
2269
|
/** A length relative to the viewport or to the font size in scope. */
|
|
1627
2270
|
computed(declaration, own, parentInherited) {
|
|
1628
|
-
const { unit, factor, offset = 0 } = declaration.compute;
|
|
2271
|
+
const { unit, factor, offset = 0, multiple } = declaration.compute;
|
|
1629
2272
|
if (unit !== 'em')
|
|
1630
2273
|
return this.viewportLength(unit, factor) + offset;
|
|
2274
|
+
if (multiple && !offset)
|
|
2275
|
+
return { multiple: factor };
|
|
1631
2276
|
// The font size in scope. On `font-size` itself that is the inherited size rather than the
|
|
1632
2277
|
// one being computed, which is what makes a nested `1.5em` compound as on the web.
|
|
1633
2278
|
const base = declaration.props.includes('fontSize')
|
|
@@ -1670,9 +2315,34 @@ function settlingOrder(deferred) {
|
|
|
1670
2315
|
* The tokens in scope at a node: its parent's, overlaid with what its rules define, overlaid with
|
|
1671
2316
|
* what it sets itself, which wins as inline style does.
|
|
1672
2317
|
*/
|
|
1673
|
-
function tokensInScope(parentTokens, ruleTokens, custom) {
|
|
1674
|
-
|
|
1675
|
-
|
|
2318
|
+
function tokensInScope(parentTokens, ruleTokens, custom, important = null) {
|
|
2319
|
+
// What the element sets is over its rules' definitions, but for the ones marked important.
|
|
2320
|
+
const own = custom ? { ...ruleTokens, ...custom, ...important } : ruleTokens;
|
|
2321
|
+
if (!own || changesNothing(own, parentTokens))
|
|
2322
|
+
return parentTokens;
|
|
2323
|
+
return resolveAliases(own, { ...parentTokens, ...own }, parentTokens);
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Whether a node's own definitions leave every token as its parent has it: each is the value the
|
|
2327
|
+
* parent has already, from the same rule, or a CSS-wide keyword that comes to what is in scope.
|
|
2328
|
+
* Tailwind writes `--tw-translate-x: 0`, `--tw-blur: initial` and forty more like them on every
|
|
2329
|
+
* element, so on nearly every node this is all there is, and the parent's map is handed on as it
|
|
2330
|
+
* is where a copy of it, a few hundred tokens, would be made.
|
|
2331
|
+
*/
|
|
2332
|
+
function changesNothing(own, parentTokens) {
|
|
2333
|
+
for (const name in own) {
|
|
2334
|
+
const value = own[name];
|
|
2335
|
+
// The very value its parent has: the one rule gave it to both, as `*` gives every element.
|
|
2336
|
+
if (parentTokens[name] === value)
|
|
2337
|
+
continue;
|
|
2338
|
+
const word = value.keyword;
|
|
2339
|
+
if (word === undefined || !CSS_WIDE.has(word))
|
|
2340
|
+
return false;
|
|
2341
|
+
// `inherit` and `unset` are the parent's value whatever it is; the rest are no value at all.
|
|
2342
|
+
if (word !== 'inherit' && word !== 'unset' && parentTokens[name] !== undefined)
|
|
2343
|
+
return false;
|
|
2344
|
+
}
|
|
2345
|
+
return true;
|
|
1676
2346
|
}
|
|
1677
2347
|
function resolveAliases(own, merged, parentTokens) {
|
|
1678
2348
|
const names = Object.keys(own);
|
|
@@ -2231,7 +2901,8 @@ const LINE_INITIAL = {
|
|
|
2231
2901
|
color: 'currentcolor',
|
|
2232
2902
|
};
|
|
2233
2903
|
function fillRole(line, values, role, value) {
|
|
2234
|
-
const
|
|
2904
|
+
const styles = [line.style, ...(line.marks ?? [])];
|
|
2905
|
+
const props = { width: line.widths, color: line.colors, style: styles }[role];
|
|
2235
2906
|
for (const prop of props)
|
|
2236
2907
|
values[prop] = value;
|
|
2237
2908
|
}
|
|
@@ -2409,17 +3080,26 @@ function mixSides(mix, tokens, current) {
|
|
|
2409
3080
|
const colour = (side) => 'mix' in side ? unroundedMix(side.mix, tokens, current) : innerColour(side, tokens, current);
|
|
2410
3081
|
const first = colour(mix.a);
|
|
2411
3082
|
const second = colour(mix.b);
|
|
2412
|
-
|
|
3083
|
+
const shares = [mix.aPercentage, mix.bPercentage].map((share) => shareOf(share, tokens));
|
|
3084
|
+
// A share that is a token nothing sets: no mix, as CSS has no colour for one.
|
|
3085
|
+
if (first === undefined || second === undefined || shares.includes(null))
|
|
2413
3086
|
return undefined;
|
|
2414
3087
|
return [
|
|
2415
|
-
{ colour: first, percentage:
|
|
2416
|
-
{ colour: second, percentage:
|
|
3088
|
+
{ colour: first, percentage: shares[0] ?? undefined },
|
|
3089
|
+
{ colour: second, percentage: shares[1] ?? undefined },
|
|
2417
3090
|
];
|
|
2418
3091
|
}
|
|
2419
3092
|
function unroundedMix(mix, tokens, current) {
|
|
2420
3093
|
const sides = mixSides(mix, tokens, current);
|
|
2421
3094
|
return sides && mixChannels(mix.space, ...sides, mix.hue);
|
|
2422
3095
|
}
|
|
3096
|
+
/** A share as the percentage it comes to: undefined for none written, null for a token unset. */
|
|
3097
|
+
function shareOf(share, tokens) {
|
|
3098
|
+
if (share === undefined || typeof share === 'number')
|
|
3099
|
+
return share;
|
|
3100
|
+
const factor = tokens[share.reference]?.number ?? share.fallback;
|
|
3101
|
+
return factor === undefined ? null : factor * share.scale;
|
|
3102
|
+
}
|
|
2423
3103
|
/** A calc marker worked out, as its slot takes it; undefined when a token in it is not set. */
|
|
2424
3104
|
function resolveCalc(marker, tokens) {
|
|
2425
3105
|
const percentage = wholePercentage(marker, tokens);
|
|
@@ -2694,7 +3374,7 @@ const RGB = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)
|
|
|
2694
3374
|
* mix. A value that is not one of ours is returned untouched: it may be a platform colour, and a
|
|
2695
3375
|
* colour nobody can parse is better painted as it stands than dropped.
|
|
2696
3376
|
*/
|
|
2697
|
-
function faded(value, fraction) {
|
|
3377
|
+
export function faded(value, fraction) {
|
|
2698
3378
|
if (typeof value !== 'string')
|
|
2699
3379
|
return value;
|
|
2700
3380
|
const parts = RGB.exec(value);
|