@ng-native/fabric 0.5.0 → 0.6.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.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
- // An anchor is a comment on the web, and :empty does not see comments.
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 UNIVERSAL;
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
- return UNIVERSAL;
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
- /** File a merged, sorted rule list by key selector. Done once per sheet pair, not per node. */
718
- export function indexRules(entries) {
719
- const buckets = new Map();
720
- const universal = [];
721
- entries.forEach((entry, at) => {
722
- const key = ruleKey(entry.rule);
723
- if (key === UNIVERSAL) {
724
- universal.push({ entry, at });
725
- return;
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 { buckets, universal };
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 the order they were merged 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
- * Each bucket is already in order, so the collected list only needs sorting when more than one
738
- * contributed - which for a node with one class and a name is the common case, and cheap: the
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, index) {
1001
+ export function candidateRules(node, indexes, order = byPlace) {
742
1002
  const found = [];
743
- if (index.universal.length)
744
- found.push(index.universal);
745
- const byName = index.buckets.get(`type:${node.name}`);
746
- if (byName)
747
- found.push(byName);
748
- const id = node.props['nativeID'];
749
- if (typeof id === 'string') {
750
- const byId = index.buckets.get(`id:${id}`);
751
- if (byId)
752
- found.push(byId);
753
- }
754
- if (node.classes) {
755
- for (const name of node.classes) {
756
- const byClass = index.buckets.get(`class:${name}`);
757
- if (byClass)
758
- found.push(byClass);
759
- }
760
- }
761
- if (!found.length)
762
- return [];
763
- if (found.length === 1)
764
- return found[0].map((indexed) => indexed.entry);
765
- const merged = found.flat();
766
- merged.sort((a, b) => a.at - b.at);
767
- return merged.map((indexed) => indexed.entry);
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,
@@ -915,7 +1348,12 @@ export class StyleResolver {
915
1348
  * selector. Keyed by the creating sheet and then the hosted one, so a component's nodes share a
916
1349
  * single index.
917
1350
  */
918
- merged = new WeakMap();
1351
+ /** The global sheet and every sheet added to it, filed by key selector. See `rulesFor`. */
1352
+ filed = null;
1353
+ /** How many of the added sheets `filed` holds: one added since is filed at the next read. */
1354
+ filedAdded = 0;
1355
+ /** A component's own sheet, indexed once for each place it can take: a host's, or its creator's. */
1356
+ ownIndexes = new WeakMap();
919
1357
  /**
920
1358
  * Whether any sheet merged so far has a rule in a cascade layer. Until one does, rules are
921
1359
  * ordered by weight alone and the cascade does not look for layers, which is nearly every app.
@@ -986,8 +1424,94 @@ export class StyleResolver {
986
1424
  this.addedSheets.push(sheet);
987
1425
  else
988
1426
  this.addedSheets[at] = sheet;
989
- this.merged = new WeakMap();
1427
+ // One added at the end is filed after what is there. One in another's place files them all again.
1428
+ if (at !== -1)
1429
+ this.filed = null;
1430
+ const before = this.generation;
990
1431
  this.generation = ++generations;
1432
+ // One that takes another's place can change what any node comes to. One that names layers
1433
+ // cannot: a layer has its place from the first sheet to name it, and a new name comes last.
1434
+ if (at !== -1)
1435
+ this.additions.length = 0;
1436
+ else
1437
+ this.additions.push({ before, after: this.generation, subjects: subjectsOf(sheet) });
1438
+ return true;
1439
+ }
1440
+ /**
1441
+ * The sheets added one after another with nothing else changed between them, each with the
1442
+ * generation it began and ended: a cache made before them still stands for a node none of
1443
+ * them could match. Emptied by anything else that moves the generation on.
1444
+ */
1445
+ additions = [];
1446
+ /** Whether a sheet could match a node: one of its rules is written for it. */
1447
+ couldMatch(sheet, node) {
1448
+ return writtenFor(subjectsOf(sheet), node);
1449
+ }
1450
+ /** How far each class named by the sheets met so far reaches: see `classReach`. */
1451
+ reach = new Map();
1452
+ /** The tests of the `class` attribute itself in those sheets, which any class may answer. */
1453
+ classTests = [];
1454
+ noted = new WeakSet();
1455
+ /** Read the classes a sheet names, once, as soon as any node is given it. */
1456
+ noteSheet(sheet) {
1457
+ if (!sheet || this.noted.has(sheet))
1458
+ return;
1459
+ this.noted.add(sheet);
1460
+ const { classes, reads } = classReach(sheet);
1461
+ for (const [name, how] of classes)
1462
+ this.noteReach(name, how);
1463
+ this.classTests.push(...reads);
1464
+ }
1465
+ noteReach(name, how) {
1466
+ const had = this.reach.get(name);
1467
+ if (had === true)
1468
+ return;
1469
+ if (how === true)
1470
+ this.reach.set(name, true);
1471
+ else
1472
+ addSubjects(had ?? this.reach.set(name, noSubjects()).get(name), how);
1473
+ }
1474
+ /**
1475
+ * What classes coming to an element or going from it can restyle: `true` for it and all
1476
+ * under it and after it, as any change to it does; the elements under it to restyle, by what
1477
+ * rules are for; or null, where no rule names any of them and nothing is styled again.
1478
+ */
1479
+ reachOf(classes) {
1480
+ let under = null;
1481
+ for (const name of classes) {
1482
+ const how = this.reach.get(name);
1483
+ if (how === true || this.classTests.some((test) => answers(test, name)))
1484
+ return true;
1485
+ if (how)
1486
+ addSubjects((under ??= noSubjects()), how);
1487
+ }
1488
+ return under;
1489
+ }
1490
+ /** Whether one of the rules `subjects` stands for could be for a node. */
1491
+ isFor(subjects, node) {
1492
+ return writtenFor(subjects, node);
1493
+ }
1494
+ /**
1495
+ * Whether a cache is of this generation, or of one before sheets that could not match the
1496
+ * node were added, which leaves it as it was: it is then brought up to this one.
1497
+ */
1498
+ current(cached, node) {
1499
+ if (cached.generation === this.generation)
1500
+ return true;
1501
+ // The one cache shared by every node with nothing to style is not one node's to move on.
1502
+ if (cached.context === ROOT_CONTEXT)
1503
+ return false;
1504
+ let at = cached.generation;
1505
+ for (const added of this.additions) {
1506
+ if (added.before !== at)
1507
+ continue;
1508
+ if (writtenFor(added.subjects, node))
1509
+ return false;
1510
+ at = added.after;
1511
+ }
1512
+ if (at !== this.generation)
1513
+ return false;
1514
+ cached.generation = at;
991
1515
  return true;
992
1516
  }
993
1517
  /** Stop matching a sheet `addGlobalSheet` added; false when it is not one. */
@@ -996,16 +1520,19 @@ export class StyleResolver {
996
1520
  if (at === -1)
997
1521
  return false;
998
1522
  this.addedSheets.splice(at, 1);
999
- this.merged = new WeakMap();
1523
+ this.filed = null;
1524
+ this.additions.length = 0;
1000
1525
  this.generation = ++generations;
1001
1526
  return true;
1002
1527
  }
1003
1528
  setConditions(next) {
1004
1529
  this.conditions = next;
1530
+ this.additions.length = 0;
1005
1531
  this.generation = ++generations;
1006
1532
  }
1007
1533
  setRootTokens(next) {
1008
1534
  this.tokensOnRoot = next;
1535
+ this.additions.length = 0;
1009
1536
  this.generation = ++generations;
1010
1537
  }
1011
1538
  /**
@@ -1030,6 +1557,10 @@ export class StyleResolver {
1030
1557
  return cached;
1031
1558
  }
1032
1559
  const kept = this.kept(node, parentContext, epoch);
1560
+ // The rules it was just found to match, where they are not the ones it did. Taken here
1561
+ // whether or not they are used, so that none is left for the next node resolved.
1562
+ const found = this.justMatched;
1563
+ this.justMatched = null;
1033
1564
  if (kept)
1034
1565
  return kept;
1035
1566
  const parentInherited = parent ? parent.inherited : EMPTY;
@@ -1038,31 +1569,37 @@ export class StyleResolver {
1038
1569
  return this.unstyled(node, epoch, parent, parentContext, parentInherited);
1039
1570
  }
1040
1571
  styleStats.nodesResolved++;
1041
- const candidates = this.rulesFor(node);
1042
- const matched = this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
1572
+ const matched = found ?? this.matchedBy(node);
1043
1573
  const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
1574
+ this.heir(node, parent, matched);
1044
1575
  const styled = this.styled(node, matched, parentTokens, parentInherited);
1576
+ const cache = this.cached(epoch, parentContext, styled, matched);
1577
+ node.styleCache = cache;
1578
+ node.styleDirty = false;
1579
+ node.hasDirty = false;
1580
+ node.stateDirty = false;
1581
+ return cache;
1582
+ }
1583
+ /**
1584
+ * A cache of a node just styled, with a context of its own. It keeps the rules the node
1585
+ * matched, for the node to be compared with, once a sheet has a use for them: see `tracksHas`.
1586
+ */
1587
+ cached(epoch, parentContext, styled, matched) {
1045
1588
  const cache = {
1046
1589
  epoch,
1047
1590
  generation: this.generation,
1048
1591
  context: {},
1049
1592
  parentContext,
1050
1593
  ...styled,
1051
- ...this.record(matched),
1052
1594
  };
1053
- node.styleCache = cache;
1054
- node.styleDirty = false;
1055
- node.hasDirty = false;
1056
- node.stateDirty = false;
1595
+ if (this.tracksHas)
1596
+ cache.matched = matched;
1057
1597
  return cache;
1058
1598
  }
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
1599
  /**
1064
- * Set by the engine once a sheet uses `:has()`. Until then no cache keeps the rules it matched,
1065
- * so an app that never asks pays nothing for it.
1600
+ * Set by the engine once a sheet uses `:has()`, styles a pressed element or asks about a place
1601
+ * in a child list. Until then no cache keeps the rules it matched, so an app that never asks
1602
+ * pays nothing for it.
1066
1603
  */
1067
1604
  tracksHas = false;
1068
1605
  /**
@@ -1076,19 +1613,31 @@ export class StyleResolver {
1076
1613
  cached?.matched !== undefined &&
1077
1614
  !node.styleDirty &&
1078
1615
  cached.parentContext === parentContext &&
1079
- cached.generation === this.generation;
1616
+ this.current(cached, node);
1080
1617
  if (!stands)
1081
1618
  return null;
1082
- const candidates = this.rulesFor(node);
1083
- const now = this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
1619
+ const now = this.matchedBy(node);
1084
1620
  const before = cached.matched;
1085
- if (now.length !== before.length || now.some((rule, i) => rule !== before[i]))
1621
+ if (now.length !== before.length || now.some((rule, i) => rule !== before[i])) {
1622
+ // It is styled again next, from the rules just found rather than from finding them twice.
1623
+ this.justMatched = now;
1086
1624
  return null;
1625
+ }
1087
1626
  node.hasDirty = false;
1088
1627
  node.stateDirty = false;
1089
1628
  cached.epoch = epoch;
1090
1629
  return cached;
1091
1630
  }
1631
+ /** The rules a node matches now, weakest first. */
1632
+ matchedBy(node) {
1633
+ const candidates = this.rulesFor(node);
1634
+ return this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
1635
+ }
1636
+ /**
1637
+ * The rules `keptBeneath` found the node being resolved to match, where they are other than
1638
+ * it matched before: it is styled from them next, rather than matched a second time.
1639
+ */
1640
+ justMatched = null;
1092
1641
  /** A cache that still stands for a node marked for its subtree or its state alone. */
1093
1642
  kept(node, parentContext, epoch) {
1094
1643
  return (this.keptBeneath(node, parentContext, epoch) ?? this.keptUnmatched(node, parentContext, epoch));
@@ -1104,7 +1653,7 @@ export class StyleResolver {
1104
1653
  !node.styleDirty &&
1105
1654
  !node.hasDirty &&
1106
1655
  cached.parentContext === parentContext &&
1107
- cached.generation === this.generation &&
1656
+ this.current(cached, node) &&
1108
1657
  (node.anonymous === true || this.hasNoRules(node));
1109
1658
  if (!stands)
1110
1659
  return null;
@@ -1129,7 +1678,7 @@ export class StyleResolver {
1129
1678
  !node.hasDirty &&
1130
1679
  !node.stateDirty &&
1131
1680
  cached.parentContext === parentContext &&
1132
- cached.generation === this.generation);
1681
+ this.current(cached, node));
1133
1682
  }
1134
1683
  /**
1135
1684
  * Nothing anywhere can match this node, and it sets no custom property of its own, so its style
@@ -1179,59 +1728,74 @@ export class StyleResolver {
1179
1728
  };
1180
1729
  node.styleCache = passthrough;
1181
1730
  node.styleDirty = false;
1731
+ node.stateDirty = false;
1182
1732
  return passthrough;
1183
1733
  }
1184
1734
  /**
1185
- * The rules worth trying against a node.
1735
+ * The rules worth trying against a node, in cascade order.
1186
1736
  *
1187
- * The merged list is built once per (creating sheet, hosted sheet) pair and indexed by key
1188
- * selector; what comes back here is only the buckets this node can reach. For a component's own
1189
- * sheet that changes little, but a global utility sheet is hundreds of rules and a screen is a
1190
- * thousand nodes, and the difference is between a million match attempts and a few thousand.
1191
- */
1192
- rulesFor(node) {
1193
- const creator = node.sheet ?? NO_SHEET;
1194
- let byHost = this.merged.get(creator);
1195
- if (!byHost)
1196
- this.merged.set(creator, (byHost = new Map()));
1197
- let index = byHost.get(node.hostSheet);
1198
- if (!index) {
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.
1737
+ * Every sheet is filed once, by key selector: the app's global sheet and each sheet added to it
1738
+ * in one index, and a component's own in one of its own. What comes back is only the buckets
1739
+ * this node can reach. A global utility sheet is hundreds of rules and a screen is a thousand
1740
+ * nodes, and the difference is between a million match attempts and a few thousand. A library
1741
+ * adds a sheet for each of its components as the component first renders, dozens on one
1742
+ * screen, so one more is added to what is filed and nothing is filed again.
1743
+ *
1744
+ * Sheets are filed in the order a document holds them, which is the order of two rules of the
1745
+ * same weight, and where a layer gets its place: the app's global sheet, the host's own, the
1746
+ * creating component's, then each added sheet as it came. The component layers get a small
1747
+ * specificity bump, so the encapsulation attribute Angular adds on the web does not have to
1748
+ * be written.
1209
1749
  *
1210
1750
  * A sheet added with `addGlobalSheet` comes last, with no bump: a browser adds a None
1211
1751
  * component's CSS when the component first renders, after the styles of the components around
1212
1752
  * it, so it loses to their extra attribute and wins a tie with them.
1213
1753
  */
1214
- merge(sheet, hostSheet) {
1215
- const entries = [];
1216
- const add = (from, bump) => {
1217
- if (!from)
1218
- return;
1219
- this.placeLayers(from);
1220
- for (const rule of from.rules) {
1221
- entries.push({ rule, sheet: from, weight: rule.specificity + bump });
1222
- }
1223
- };
1224
- add(this.globalSheet, 0);
1225
- add(hostSheet, COMPONENT_SPECIFICITY_BUMP);
1226
- add(sheet, COMPONENT_SPECIFICITY_BUMP);
1227
- for (const added of this.addedSheets)
1228
- add(added, 0);
1229
- if (!entries.some((entry) => entry.rule.layer !== undefined)) {
1230
- return entries.sort((a, b) => a.weight - b.weight);
1754
+ rulesFor(node) {
1755
+ if (this.globalSheet)
1756
+ this.placeLayers(this.globalSheet);
1757
+ const indexes = [];
1758
+ if (node.hostSheet)
1759
+ indexes.push(this.ownIndex(node.hostSheet, HOSTS));
1760
+ if (node.sheet)
1761
+ indexes.push(this.ownIndex(node.sheet, CREATED));
1762
+ indexes.push(this.sharedIndex());
1763
+ return candidateRules(node, indexes, this.cascadeOrder);
1764
+ }
1765
+ /** Weakest first: by layer where any sheet has one, then by weight, then as they were filed. */
1766
+ cascadeOrder = (a, b) => {
1767
+ const layers = this.layered
1768
+ ? layerOrder(this.layerPlaces.get(a.entry.rule), this.layerPlaces.get(b.entry.rule))
1769
+ : 0;
1770
+ return layers || a.entry.weight - b.entry.weight || a.at - b.at;
1771
+ };
1772
+ /** A component's sheet as the rules of a host or of a creator, which differ only in their place. */
1773
+ ownIndex(sheet, role) {
1774
+ let roles = this.ownIndexes.get(sheet);
1775
+ if (!roles)
1776
+ this.ownIndexes.set(sheet, (roles = []));
1777
+ return (roles[role] ??= indexRules(this.entries(sheet, COMPONENT_SPECIFICITY_BUMP), undefined, role * SHEET_ROOM));
1778
+ }
1779
+ /** The app's global sheet and every sheet added to it, with any added since the last read. */
1780
+ sharedIndex() {
1781
+ if (!this.filed) {
1782
+ this.filed = { buckets: new Map(), universal: [] };
1783
+ this.filedAdded = 0;
1784
+ if (this.globalSheet)
1785
+ indexRules(this.entries(this.globalSheet, 0), this.filed, 0);
1231
1786
  }
1232
- this.layered = true;
1233
- const places = this.layerPlaces;
1234
- return entries.sort((a, b) => layerOrder(places.get(a.rule), places.get(b.rule)) || a.weight - b.weight);
1787
+ for (; this.filedAdded < this.addedSheets.length; this.filedAdded++) {
1788
+ const from = (ADDED + this.filedAdded) * SHEET_ROOM;
1789
+ indexRules(this.entries(this.addedSheets[this.filedAdded], 0), this.filed, from);
1790
+ }
1791
+ return this.filed;
1792
+ }
1793
+ /** A sheet's rules at their weight here, with its layers given their places. */
1794
+ entries(sheet, bump) {
1795
+ this.placeLayers(sheet);
1796
+ if (sheet.rules.some((rule) => rule.layer !== undefined))
1797
+ this.layered = true;
1798
+ return sheet.rules.map((rule) => ({ rule, sheet, weight: rule.specificity + bump }));
1235
1799
  }
1236
1800
  /**
1237
1801
  * Gives each of a sheet's layers its place among every sheet's, the first time the sheet is
@@ -1274,7 +1838,8 @@ export class StyleResolver {
1274
1838
  styled(node, matched, parentTokens, parentInherited) {
1275
1839
  // Shared between nodes that match alike, unless the node sets something of its own.
1276
1840
  const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
1277
- if (node.customProperties || inline || node.boundStyle) {
1841
+ // Nor one that takes a value from its parent's own style, which two parents differ in.
1842
+ if (node.customProperties || inline || node.boundStyle || matched.some(takesParents)) {
1278
1843
  return this.style(node, matched, parentTokens, parentInherited, inline);
1279
1844
  }
1280
1845
  if (this.sharedGeneration !== this.generation) {
@@ -1317,18 +1882,20 @@ export class StyleResolver {
1317
1882
  : matched, parentInherited);
1318
1883
  // Tokens are in scope for this node's own declarations as well as its descendants', so they
1319
1884
  // are merged before any `var()` here is resolved.
1320
- const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
1885
+ const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties, result.importantTokens);
1321
1886
  const own = result.declarations;
1322
1887
  if (result.deferred) {
1323
1888
  this.applyDeferred(result.deferred, own, tokens, parentInherited, result.important);
1324
1889
  }
1325
- if (own['borderStyle'] === 'none')
1326
- drawNoBorder(own);
1890
+ settleBorders(own);
1327
1891
  // `pointer-events: inherit` won the cascade: what the parent hands down stands.
1328
1892
  if (own['pointerEvents'] === 'inherit')
1329
1893
  delete own['pointerEvents'];
1330
1894
  const style = { ...parentInherited, ...own };
1331
- return { style, inherited: decorate(style, inheritFrom(parentInherited, own), own), tokens };
1895
+ const inherited = decorate(style, inheritFrom(parentInherited, own), own);
1896
+ const rtl = (style['direction'] ?? this.conditions.direction) === 'rtl';
1897
+ const important = importantNames(result, () => TWIN[rtl ? 'rtl' : 'ltr']);
1898
+ return important ? { style, inherited, tokens, important } : { style, inherited, tokens };
1332
1899
  }
1333
1900
  /** The rules that apply to a node, weakest first. */
1334
1901
  matched(node, entries) {
@@ -1370,6 +1937,7 @@ export class StyleResolver {
1370
1937
  declarations: hasImportant ? { ...normal, ...important } : normal,
1371
1938
  important: hasImportant ? important : null,
1372
1939
  tokens,
1940
+ importantTokens,
1373
1941
  deferred,
1374
1942
  };
1375
1943
  }
@@ -1470,6 +2038,10 @@ export class StyleResolver {
1470
2038
  */
1471
2039
  applyDeferred(deferred, own, tokens, parentInherited, important) {
1472
2040
  for (const declaration of settlingOrder(deferred)) {
2041
+ if (declaration.inherit) {
2042
+ this.inherit(declaration, own, parentInherited, important);
2043
+ continue;
2044
+ }
1473
2045
  const settled = this.settled(declaration, own, parentInherited, tokens);
1474
2046
  if (settled === undefined && this.onUndefinedToken) {
1475
2047
  this.reportUndefined(declaration, tokens);
@@ -1488,6 +2060,42 @@ export class StyleResolver {
1488
2060
  }
1489
2061
  }
1490
2062
  }
2063
+ /** Note what the node being resolved takes an `inherit` from, and that its parent has an heir. */
2064
+ heir(node, parent, matched) {
2065
+ this.parentOf = parent;
2066
+ this.parentInline = node.parent?.props['style'];
2067
+ if (parent && matched.some(takesParents))
2068
+ parent.heirs = true;
2069
+ }
2070
+ /** What the parent of the node being resolved came to: an `inherit` takes its values from it. */
2071
+ parentOf = null;
2072
+ /** That parent's inline style, which is over what its rules gave it. */
2073
+ parentInline;
2074
+ /** What the parent's inline style sets, under what its rules marked important, as on the parent. */
2075
+ parentSet() {
2076
+ if (!this.parentInline)
2077
+ return EMPTY;
2078
+ const inline = flattenInline(this.parentInline, {});
2079
+ for (const prop of this.parentOf?.important ?? [])
2080
+ delete inline[prop];
2081
+ return inline;
2082
+ }
2083
+ /**
2084
+ * `inherit`: each prop as the parent has it, handed down or its own, and gone where the parent
2085
+ * has none, which is what a weaker rule's value for it gives way to.
2086
+ */
2087
+ inherit(declaration, own, parentInherited, important) {
2088
+ const inline = this.parentSet();
2089
+ for (const prop of declaration.props) {
2090
+ if (!declaration.important && important !== null && prop in important)
2091
+ continue;
2092
+ const value = inline[prop] ?? parentInherited[prop] ?? this.parentOf?.style[prop];
2093
+ if (value === undefined)
2094
+ delete own[prop];
2095
+ else
2096
+ own[prop] = value;
2097
+ }
2098
+ }
1491
2099
  /** One deferred declaration's value, from the tokens in scope and the node's own style. */
1492
2100
  settled(declaration, own, parentInherited, tokens) {
1493
2101
  if (declaration.within !== undefined) {
@@ -1670,9 +2278,34 @@ function settlingOrder(deferred) {
1670
2278
  * The tokens in scope at a node: its parent's, overlaid with what its rules define, overlaid with
1671
2279
  * what it sets itself, which wins as inline style does.
1672
2280
  */
1673
- function tokensInScope(parentTokens, ruleTokens, custom) {
1674
- const own = custom ? { ...ruleTokens, ...custom } : ruleTokens;
1675
- return own ? resolveAliases(own, { ...parentTokens, ...own }, parentTokens) : parentTokens;
2281
+ function tokensInScope(parentTokens, ruleTokens, custom, important = null) {
2282
+ // What the element sets is over its rules' definitions, but for the ones marked important.
2283
+ const own = custom ? { ...ruleTokens, ...custom, ...important } : ruleTokens;
2284
+ if (!own || changesNothing(own, parentTokens))
2285
+ return parentTokens;
2286
+ return resolveAliases(own, { ...parentTokens, ...own }, parentTokens);
2287
+ }
2288
+ /**
2289
+ * Whether a node's own definitions leave every token as its parent has it: each is the value the
2290
+ * parent has already, from the same rule, or a CSS-wide keyword that comes to what is in scope.
2291
+ * Tailwind writes `--tw-translate-x: 0`, `--tw-blur: initial` and forty more like them on every
2292
+ * element, so on nearly every node this is all there is, and the parent's map is handed on as it
2293
+ * is where a copy of it, a few hundred tokens, would be made.
2294
+ */
2295
+ function changesNothing(own, parentTokens) {
2296
+ for (const name in own) {
2297
+ const value = own[name];
2298
+ // The very value its parent has: the one rule gave it to both, as `*` gives every element.
2299
+ if (parentTokens[name] === value)
2300
+ continue;
2301
+ const word = value.keyword;
2302
+ if (word === undefined || !CSS_WIDE.has(word))
2303
+ return false;
2304
+ // `inherit` and `unset` are the parent's value whatever it is; the rest are no value at all.
2305
+ if (word !== 'inherit' && word !== 'unset' && parentTokens[name] !== undefined)
2306
+ return false;
2307
+ }
2308
+ return true;
1676
2309
  }
1677
2310
  function resolveAliases(own, merged, parentTokens) {
1678
2311
  const names = Object.keys(own);
@@ -2231,7 +2864,8 @@ const LINE_INITIAL = {
2231
2864
  color: 'currentcolor',
2232
2865
  };
2233
2866
  function fillRole(line, values, role, value) {
2234
- const props = { width: line.widths, color: line.colors, style: [line.style] }[role];
2867
+ const styles = [line.style, ...(line.marks ?? [])];
2868
+ const props = { width: line.widths, color: line.colors, style: styles }[role];
2235
2869
  for (const prop of props)
2236
2870
  values[prop] = value;
2237
2871
  }
@@ -2409,17 +3043,26 @@ function mixSides(mix, tokens, current) {
2409
3043
  const colour = (side) => 'mix' in side ? unroundedMix(side.mix, tokens, current) : innerColour(side, tokens, current);
2410
3044
  const first = colour(mix.a);
2411
3045
  const second = colour(mix.b);
2412
- if (first === undefined || second === undefined)
3046
+ const shares = [mix.aPercentage, mix.bPercentage].map((share) => shareOf(share, tokens));
3047
+ // A share that is a token nothing sets: no mix, as CSS has no colour for one.
3048
+ if (first === undefined || second === undefined || shares.includes(null))
2413
3049
  return undefined;
2414
3050
  return [
2415
- { colour: first, percentage: mix.aPercentage },
2416
- { colour: second, percentage: mix.bPercentage },
3051
+ { colour: first, percentage: shares[0] ?? undefined },
3052
+ { colour: second, percentage: shares[1] ?? undefined },
2417
3053
  ];
2418
3054
  }
2419
3055
  function unroundedMix(mix, tokens, current) {
2420
3056
  const sides = mixSides(mix, tokens, current);
2421
3057
  return sides && mixChannels(mix.space, ...sides, mix.hue);
2422
3058
  }
3059
+ /** A share as the percentage it comes to: undefined for none written, null for a token unset. */
3060
+ function shareOf(share, tokens) {
3061
+ if (share === undefined || typeof share === 'number')
3062
+ return share;
3063
+ const factor = tokens[share.reference]?.number ?? share.fallback;
3064
+ return factor === undefined ? null : factor * share.scale;
3065
+ }
2423
3066
  /** A calc marker worked out, as its slot takes it; undefined when a token in it is not set. */
2424
3067
  function resolveCalc(marker, tokens) {
2425
3068
  const percentage = wholePercentage(marker, tokens);
@@ -2694,7 +3337,7 @@ const RGB = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)
2694
3337
  * mix. A value that is not one of ours is returned untouched: it may be a platform colour, and a
2695
3338
  * colour nobody can parse is better painted as it stands than dropped.
2696
3339
  */
2697
- function faded(value, fraction) {
3340
+ export function faded(value, fraction) {
2698
3341
  if (typeof value !== 'string')
2699
3342
  return value;
2700
3343
  const parts = RGB.exec(value);