@ng-native/fabric 0.4.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
  *
@@ -89,6 +124,80 @@ function drawNoBorder(own) {
89
124
  * The cost is one integer increment per comparison, against a Set lookup and several property
90
125
  * reads, so it does not distort what it measures.
91
126
  */
127
+ /**
128
+ * What a sheet asks about a pressed element. `own` is whether a rule styles one, `.button:active`,
129
+ * which matching the pressed elements again answers. `elsewhere` is each compound that asks about
130
+ * a pressed element from another one: `.card:active .title`, `.a:active + .b`, or `:active`
131
+ * inside `:not()`, `:is()` or `:has()`. An element such a compound could be is restyled with
132
+ * everything under it when it is pressed, as what it changes is not itself alone.
133
+ */
134
+ export function usesActive(sheet) {
135
+ const known = ACTIVE_USE.get(sheet);
136
+ if (known)
137
+ return known;
138
+ let own = false;
139
+ const elsewhere = [];
140
+ for (const rule of sheet.rules) {
141
+ rule.compounds.forEach((compound, at) => {
142
+ const subject = at === rule.compounds.length - 1;
143
+ // Asked of the element itself, on the compound or in its `:is()` and `:not()`: the
144
+ // subject's is its own, and any other compound's is of the element that compound is.
145
+ if (activeItself(compound, elsewhere)) {
146
+ if (subject)
147
+ own = true;
148
+ else
149
+ elsewhere.push(compound);
150
+ }
151
+ activeAround(compound, elsewhere);
152
+ });
153
+ }
154
+ const use = { own, elsewhere };
155
+ ACTIVE_USE.set(sheet, use);
156
+ return use;
157
+ }
158
+ const isActive = (compound) => compound.pseudo?.includes('active') === true;
159
+ /**
160
+ * Whether a compound asks about the pressed state of the element it matches: on itself, in a
161
+ * `:not()`, or as the last compound of an `:is()` argument. The compounds before the last in
162
+ * such an argument are other elements, and are collected as they are.
163
+ */
164
+ function activeItself(compound, elsewhere) {
165
+ let itself = isActive(compound);
166
+ for (const inner of compound.not ?? []) {
167
+ if (activeItself(inner, elsewhere))
168
+ itself = true;
169
+ activeAround(inner, elsewhere);
170
+ }
171
+ for (const chain of compound.is ?? []) {
172
+ chain.forEach((inner, at) => {
173
+ const asked = activeItself(inner, elsewhere);
174
+ if (asked && at === chain.length - 1)
175
+ itself = true;
176
+ else if (asked)
177
+ elsewhere.push(inner);
178
+ activeAround(inner, elsewhere);
179
+ });
180
+ }
181
+ return itself;
182
+ }
183
+ /** Each compound that asks about a pressed element around the one a compound matches. */
184
+ function activeAround(compound, elsewhere) {
185
+ const collect = (value) => {
186
+ if (!value || typeof value !== 'object')
187
+ return;
188
+ if (Array.isArray(value))
189
+ return value.forEach(collect);
190
+ if (isActive(value))
191
+ elsewhere.push(value);
192
+ for (const inside of Object.values(value))
193
+ collect(inside);
194
+ };
195
+ for (const [key, inside] of Object.entries(compound)) {
196
+ if (key !== 'is' && key !== 'not' && key !== 'pseudo')
197
+ collect(inside);
198
+ }
199
+ }
200
+ const ACTIVE_USE = new WeakMap();
92
201
  export const styleStats = {
93
202
  /** Calls to `matchesCompound`, the innermost unit of matching work. */
94
203
  compoundTests: 0,
@@ -96,11 +205,14 @@ export const styleStats = {
96
205
  ruleTests: 0,
97
206
  /** Nodes cascaded, as opposed to handed back from the memo. */
98
207
  nodesResolved: 0,
208
+ /** Rules filed by key selector: each sheet's once, however many sheets come after it. */
209
+ rulesFiled: 0,
99
210
  };
100
211
  export function resetStyleStats() {
101
212
  styleStats.compoundTests = 0;
102
213
  styleStats.ruleTests = 0;
103
214
  styleStats.nodesResolved = 0;
215
+ styleStats.rulesFiled = 0;
104
216
  }
105
217
  /** A prop's value as an attribute test compares it: in lower case, under the i flag. */
106
218
  const attributeText = (actual, test) => test.insensitive ? String(actual).toLowerCase() : String(actual);
@@ -168,10 +280,8 @@ function matchesCompound(node, compound, sheet) {
168
280
  return false;
169
281
  }
170
282
  }
171
- // An anchor is a comment on the web, and :empty does not see comments.
172
- if (compound.empty !== undefined && node.children?.some((child) => child.kind !== 'anchor')) {
283
+ if (compound.empty !== undefined && node.children?.some(fills))
173
284
  return false;
174
- }
175
285
  if (compound.nth !== undefined) {
176
286
  for (const test of compound.nth) {
177
287
  if (!matchesNth(node, test))
@@ -190,6 +300,21 @@ function matchesCompound(node, compound, sheet) {
190
300
  return false;
191
301
  }
192
302
  }
303
+ if (compound.has) {
304
+ for (const test of compound.has) {
305
+ if (!hasBeneath(node, test, sheet))
306
+ return false;
307
+ }
308
+ }
309
+ if (compound.parents) {
310
+ const parent = node.parent;
311
+ if (!parent)
312
+ return false;
313
+ for (const above of compound.parents) {
314
+ if (!matchesCompound(parent, above, sheet))
315
+ return false;
316
+ }
317
+ }
193
318
  if (compound.ancestors) {
194
319
  for (const ancestor of compound.ancestors) {
195
320
  let found = false;
@@ -212,6 +337,18 @@ function matchesCompound(node, compound, sheet) {
212
337
  }
213
338
  return true;
214
339
  }
340
+ /** Whether some element beneath `node`, or some child of it for `:has(> x)`, matches the test. */
341
+ function hasBeneath(node, test, sheet) {
342
+ for (const child of node.children ?? []) {
343
+ if (child.kind !== undefined && child.kind !== 'element')
344
+ continue;
345
+ if (test.any.some((option) => matchesCompound(child, option, sheet)))
346
+ return true;
347
+ if (!test.child && hasBeneath(child, test, sheet))
348
+ return true;
349
+ }
350
+ return false;
351
+ }
215
352
  /**
216
353
  * Right-to-left matching, as every CSS engine does it: the rightmost compound is the cheapest
217
354
  * filter and rejects almost everything immediately.
@@ -282,8 +419,152 @@ function matchAny(first, next, rule, index, compound, sheet) {
282
419
  }
283
420
  return false;
284
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
+ }
285
432
  /** Whether a node is one of the children CSS counts. Text and anchors are children, not elements. */
286
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
+ ];
287
568
  /** The element before this one, skipping everything that is not one. */
288
569
  function previousElement(node) {
289
570
  const siblings = node.parent?.children;
@@ -341,6 +622,133 @@ function overriddenBy(deferred, declarations, important) {
341
622
  }
342
623
  return kept.length ? kept : null;
343
624
  }
625
+ /**
626
+ * The logical property and the physical one for the same corner or edge, left to right. CSS
627
+ * settles the two as one property, the later declaration winning. A native view has a prop for
628
+ * each and takes one whatever their order: React Native the physical corner, Yoga the logical
629
+ * edge. So a declaration of either takes the other out of the cascade.
630
+ */
631
+ const SIDES_LTR = [
632
+ ['borderStartStartRadius', 'borderTopLeftRadius'],
633
+ ['borderStartEndRadius', 'borderTopRightRadius'],
634
+ ['borderEndStartRadius', 'borderBottomLeftRadius'],
635
+ ['borderEndEndRadius', 'borderBottomRightRadius'],
636
+ ...['margin', 'padding'].flatMap((box) => [
637
+ [`${box}Start`, `${box}Left`],
638
+ [`${box}End`, `${box}Right`],
639
+ ]),
640
+ ...['Width', 'Color'].flatMap((part) => [
641
+ [`borderStart${part}`, `borderLeft${part}`],
642
+ [`borderEnd${part}`, `borderRight${part}`],
643
+ ]),
644
+ ['start', 'left'],
645
+ ['end', 'right'],
646
+ ];
647
+ const mirrored = (physical) => physical.includes('Left') || physical === 'left'
648
+ ? physical.replace('Left', 'Right').replace('left', 'right')
649
+ : physical.replace('Right', 'Left').replace('right', 'left');
650
+ /** Each property's other form, both ways, in a layout of each direction. */
651
+ const twins = (pairs) => Object.fromEntries(pairs.flatMap(([logical, physical]) => [
652
+ [logical, physical],
653
+ [physical, logical],
654
+ ]));
655
+ const TWIN = {
656
+ ltr: twins(SIDES_LTR),
657
+ rtl: twins(SIDES_LTR.map(([logical, physical]) => [logical, mirrored(physical)])),
658
+ };
659
+ const sidedOf = new WeakMap();
660
+ function sided(rule) {
661
+ let found = sidedOf.get(rule);
662
+ if (found !== undefined)
663
+ return found;
664
+ const twin = TWIN.ltr;
665
+ const plain = Object.keys(rule.declarations).filter((key) => key in twin);
666
+ const important = rule.important ? Object.keys(rule.important).filter((key) => key in twin) : [];
667
+ for (const declaration of rule.deferred ?? []) {
668
+ for (const prop of declaration.props) {
669
+ if (prop in twin)
670
+ (declaration.important ? important : plain).push(prop);
671
+ }
672
+ }
673
+ found = plain.length || important.length ? { plain, important } : null;
674
+ sidedOf.set(rule, found);
675
+ return found;
676
+ }
677
+ /** Of `keys`, in the order a rule wrote them, each whose other form comes after it. */
678
+ function writtenEarlier(keys, twin) {
679
+ return keys.filter((key, at) => keys.indexOf(twin[key]) > at);
680
+ }
681
+ /** The other form of each of `keys`, as the record `overriddenBy` reads. */
682
+ function twinsOf(keys, twin) {
683
+ const out = {};
684
+ for (const key of keys)
685
+ out[twin[key]] = true;
686
+ return out;
687
+ }
688
+ /**
689
+ * One cascade's settling of the corners and edges set in both forms: each rule's are taken out of
690
+ * what the rules before it left, in the form the rule does not use.
691
+ */
692
+ class OtherForms {
693
+ /** Worked out for the first rule that sets a corner or an edge, which most elements have none of. */
694
+ twin = null;
695
+ overImportant = null;
696
+ direction;
697
+ constructor(direction) {
698
+ this.direction = direction;
699
+ }
700
+ /** Before `rule` is applied: the other form of what it sets goes, as the same property would. */
701
+ before(rule, normal, important, deferred) {
702
+ const sides = sided(rule);
703
+ if (!sides)
704
+ return deferred;
705
+ const twin = (this.twin ??= TWIN[this.direction()]);
706
+ const plain = twinsOf(sides.plain, twin);
707
+ for (const key in plain)
708
+ delete normal[key];
709
+ let kept = deferred && overriddenBy(deferred, plain, false);
710
+ if (!sides.important.length)
711
+ return kept;
712
+ const gone = twinsOf(sides.important, twin);
713
+ for (const key in gone)
714
+ delete important[key];
715
+ kept &&= overriddenBy(kept, gone, true);
716
+ Object.assign((this.overImportant ??= {}), gone);
717
+ return kept;
718
+ }
719
+ /**
720
+ * Once `rule` is applied, its deferred values among `deferred`: where it wrote both forms, the
721
+ * one it wrote later stands, among its plain declarations and among its important ones. The
722
+ * order is the rule's own, sent with it, since a value settled on device is in another list.
723
+ */
724
+ within(rule, normal, important, deferred) {
725
+ if (!rule.sides && !rule.importantSides)
726
+ return deferred;
727
+ const twin = (this.twin ??= TWIN[this.direction()]);
728
+ let kept = deferred;
729
+ const settle = (order, into, tier) => {
730
+ const lost = {};
731
+ for (const key of writtenEarlier(order, twin)) {
732
+ delete into[key];
733
+ lost[key] = true;
734
+ }
735
+ kept &&= overriddenBy(kept, lost, tier);
736
+ };
737
+ if (rule.sides)
738
+ settle(rule.sides, normal, false);
739
+ if (rule.importantSides)
740
+ settle(rule.importantSides, important, true);
741
+ return kept;
742
+ }
743
+ /** An important one stands over the other form wherever that was set, as over its own. */
744
+ after(normal, deferred) {
745
+ if (!this.overImportant)
746
+ return deferred;
747
+ for (const key in this.overImportant)
748
+ delete normal[key];
749
+ return deferred && overriddenBy(deferred, this.overImportant, false);
750
+ }
751
+ }
344
752
  /**
345
753
  * The deferred values still standing once `rule` has been applied, plus its own.
346
754
  *
@@ -474,69 +882,150 @@ const ELEMENT_ENTRIES = ELEMENT_STYLES.rules.map((rule) => ({
474
882
  * is offered to everything, because the alternative is a rule that silently stops applying.
475
883
  */
476
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) {
477
896
  const key = rule.compounds[rule.compounds.length - 1];
478
897
  if (!key)
479
- return UNIVERSAL;
898
+ return EVERY_NODE;
480
899
  if (key.classes.length)
481
- return `class:${key.classes[0]}`;
900
+ return [`class:${key.classes[0]}`];
482
901
  if (key.id !== undefined)
483
- return `id:${key.id}`;
902
+ return [`id:${key.id}`];
484
903
  if (key.type !== undefined)
485
- return `type:${key.type}`;
486
- 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;
487
936
  }
488
937
  const UNIVERSAL = '*';
489
- /** File a merged, sorted rule list by key selector. Done once per sheet pair, not per node. */
490
- export function indexRules(entries) {
491
- const buckets = new Map();
492
- const universal = [];
493
- entries.forEach((entry, at) => {
494
- const key = ruleKey(entry.rule);
495
- if (key === UNIVERSAL) {
496
- universal.push({ entry, at });
497
- 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]);
498
966
  }
499
- let bucket = buckets.get(key);
500
- if (!bucket)
501
- buckets.set(key, (bucket = []));
502
- bucket.push({ entry, at });
503
967
  });
504
- 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}`);
505
993
  }
506
994
  /**
507
- * 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.
508
997
  *
509
- * Each bucket is already in order, so the collected list only needs sorting when more than one
510
- * contributed - which for a node with one class and a name is the common case, and cheap: the
511
- * 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.
512
1000
  */
513
- export function candidateRules(node, index) {
1001
+ export function candidateRules(node, indexes, order = byPlace) {
514
1002
  const found = [];
515
- if (index.universal.length)
516
- found.push(index.universal);
517
- const byName = index.buckets.get(`type:${node.name}`);
518
- if (byName)
519
- found.push(byName);
520
- const id = node.props['nativeID'];
521
- if (typeof id === 'string') {
522
- const byId = index.buckets.get(`id:${id}`);
523
- if (byId)
524
- found.push(byId);
525
- }
526
- if (node.classes) {
527
- for (const name of node.classes) {
528
- const byClass = index.buckets.get(`class:${name}`);
529
- if (byClass)
530
- found.push(byClass);
531
- }
532
- }
533
- if (!found.length)
534
- return [];
535
- if (found.length === 1)
536
- return found[0].map((indexed) => indexed.entry);
537
- const merged = found.flat();
538
- merged.sort((a, b) => a.at - b.at);
539
- 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);
540
1029
  }
541
1030
  /** What `em` falls back to when nothing in scope has set a font size. The web's default. */
542
1031
  const DEFAULT_FONT_SIZE = 16;
@@ -546,14 +1035,186 @@ const EMPTY = {};
546
1035
  const ROOT_CONTEXT = {};
547
1036
  /** Shared empty token map, for the overwhelmingly common case of a subtree defining none. */
548
1037
  const NO_TOKENS = {};
549
- /** Stands in for "created by a component with no sheet" as a cache key. */
550
- const NO_SHEET = { rules: [] };
551
1038
  /**
552
1039
  * Conditions generations are unique across every resolver in the process, not merely
553
1040
  * increasing within one, because the memo they stamp lives on the compiled rule and a sheet is
554
1041
  * shared by every engine that mounts the component.
555
1042
  */
556
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
+ }
557
1218
  function emptyCacheFor(epoch, generation) {
558
1219
  return {
559
1220
  epoch,
@@ -687,7 +1348,12 @@ export class StyleResolver {
687
1348
  * selector. Keyed by the creating sheet and then the hosted one, so a component's nodes share a
688
1349
  * single index.
689
1350
  */
690
- 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();
691
1357
  /**
692
1358
  * Whether any sheet merged so far has a rule in a cascade layer. Until one does, rules are
693
1359
  * ordered by weight alone and the cascade does not look for layers, which is nearly every app.
@@ -758,8 +1424,94 @@ export class StyleResolver {
758
1424
  this.addedSheets.push(sheet);
759
1425
  else
760
1426
  this.addedSheets[at] = sheet;
761
- 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;
762
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;
763
1515
  return true;
764
1516
  }
765
1517
  /** Stop matching a sheet `addGlobalSheet` added; false when it is not one. */
@@ -768,16 +1520,19 @@ export class StyleResolver {
768
1520
  if (at === -1)
769
1521
  return false;
770
1522
  this.addedSheets.splice(at, 1);
771
- this.merged = new WeakMap();
1523
+ this.filed = null;
1524
+ this.additions.length = 0;
772
1525
  this.generation = ++generations;
773
1526
  return true;
774
1527
  }
775
1528
  setConditions(next) {
776
1529
  this.conditions = next;
1530
+ this.additions.length = 0;
777
1531
  this.generation = ++generations;
778
1532
  }
779
1533
  setRootTokens(next) {
780
1534
  this.tokensOnRoot = next;
1535
+ this.additions.length = 0;
781
1536
  this.generation = ++generations;
782
1537
  }
783
1538
  /**
@@ -801,16 +1556,35 @@ export class StyleResolver {
801
1556
  cached.epoch = epoch;
802
1557
  return cached;
803
1558
  }
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;
1564
+ if (kept)
1565
+ return kept;
804
1566
  const parentInherited = parent ? parent.inherited : EMPTY;
805
1567
  // A box the engine made around loose text is in no template, so no rule matches it.
806
1568
  if (node.anonymous || this.hasNoRules(node)) {
807
1569
  return this.unstyled(node, epoch, parent, parentContext, parentInherited);
808
1570
  }
809
1571
  styleStats.nodesResolved++;
810
- const candidates = this.rulesFor(node);
811
- const matched = this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
1572
+ const matched = found ?? this.matchedBy(node);
812
1573
  const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
1574
+ this.heir(node, parent, matched);
813
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) {
814
1588
  const cache = {
815
1589
  epoch,
816
1590
  generation: this.generation,
@@ -818,10 +1592,75 @@ export class StyleResolver {
818
1592
  parentContext,
819
1593
  ...styled,
820
1594
  };
821
- node.styleCache = cache;
822
- node.styleDirty = false;
1595
+ if (this.tracksHas)
1596
+ cache.matched = matched;
823
1597
  return cache;
824
1598
  }
1599
+ /**
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.
1603
+ */
1604
+ tracksHas = false;
1605
+ /**
1606
+ * The cache of a node whose subtree changed, and nothing else, when it matches the rules it
1607
+ * did: keeping the object is what leaves everything under it alone. Nothing when it has to be
1608
+ * resolved, as it does with no record of what it matched, made before any sheet asked.
1609
+ */
1610
+ keptBeneath(node, parentContext, epoch) {
1611
+ const cached = node.styleCache;
1612
+ const stands = (node.hasDirty === true || node.stateDirty === true) &&
1613
+ cached?.matched !== undefined &&
1614
+ !node.styleDirty &&
1615
+ cached.parentContext === parentContext &&
1616
+ this.current(cached, node);
1617
+ if (!stands)
1618
+ return null;
1619
+ const now = this.matchedBy(node);
1620
+ const before = cached.matched;
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;
1624
+ return null;
1625
+ }
1626
+ node.hasDirty = false;
1627
+ node.stateDirty = false;
1628
+ cached.epoch = epoch;
1629
+ return cached;
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;
1641
+ /** A cache that still stands for a node marked for its subtree or its state alone. */
1642
+ kept(node, parentContext, epoch) {
1643
+ return (this.keptBeneath(node, parentContext, epoch) ?? this.keptUnmatched(node, parentContext, epoch));
1644
+ }
1645
+ /**
1646
+ * The cache of a node whose pressed state changed and nothing else, where no rule can match
1647
+ * it at all: it has no style of its own to change, so it and all under it stay as they are.
1648
+ */
1649
+ keptUnmatched(node, parentContext, epoch) {
1650
+ const cached = node.styleCache;
1651
+ const stands = node.stateDirty === true &&
1652
+ cached !== null &&
1653
+ !node.styleDirty &&
1654
+ !node.hasDirty &&
1655
+ cached.parentContext === parentContext &&
1656
+ this.current(cached, node) &&
1657
+ (node.anonymous === true || this.hasNoRules(node));
1658
+ if (!stands)
1659
+ return null;
1660
+ node.stateDirty = false;
1661
+ cached.epoch = epoch;
1662
+ return cached;
1663
+ }
825
1664
  /**
826
1665
  * Whether a node's last answer still stands under a parent that resolved to `parentContext`,
827
1666
  * without resolving it. What lets a commit step over a clean child in one comparison.
@@ -836,8 +1675,10 @@ export class StyleResolver {
836
1675
  reusable(cached, node, parentContext) {
837
1676
  return (cached !== null &&
838
1677
  !node.styleDirty &&
1678
+ !node.hasDirty &&
1679
+ !node.stateDirty &&
839
1680
  cached.parentContext === parentContext &&
840
- cached.generation === this.generation);
1681
+ this.current(cached, node));
841
1682
  }
842
1683
  /**
843
1684
  * Nothing anywhere can match this node, and it sets no custom property of its own, so its style
@@ -887,59 +1728,74 @@ export class StyleResolver {
887
1728
  };
888
1729
  node.styleCache = passthrough;
889
1730
  node.styleDirty = false;
1731
+ node.stateDirty = false;
890
1732
  return passthrough;
891
1733
  }
892
1734
  /**
893
- * The rules worth trying against a node.
1735
+ * The rules worth trying against a node, in cascade order.
894
1736
  *
895
- * The merged list is built once per (creating sheet, hosted sheet) pair and indexed by key
896
- * selector; what comes back here is only the buckets this node can reach. For a component's own
897
- * sheet that changes little, but a global utility sheet is hundreds of rules and a screen is a
898
- * thousand nodes, and the difference is between a million match attempts and a few thousand.
899
- */
900
- rulesFor(node) {
901
- const creator = node.sheet ?? NO_SHEET;
902
- let byHost = this.merged.get(creator);
903
- if (!byHost)
904
- this.merged.set(creator, (byHost = new Map()));
905
- let index = byHost.get(node.hostSheet);
906
- if (!index) {
907
- byHost.set(node.hostSheet, (index = indexRules(this.merge(node.sheet, node.hostSheet))));
908
- }
909
- return candidateRules(node, index);
910
- }
911
- /**
912
- * Weakest first. A host node is matched against two component sheets: the one belonging to the
913
- * component it hosts, whose `:host` rules exist to reach exactly this node, and the one that
914
- * created it. Both carry the bump; the creator comes later and so wins a tie, as the outer
915
- * document does over `:host` on the web. The sort is stable, so within one sheet source order
916
- * 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.
917
1749
  *
918
1750
  * A sheet added with `addGlobalSheet` comes last, with no bump: a browser adds a None
919
1751
  * component's CSS when the component first renders, after the styles of the components around
920
1752
  * it, so it loses to their extra attribute and wins a tie with them.
921
1753
  */
922
- merge(sheet, hostSheet) {
923
- const entries = [];
924
- const add = (from, bump) => {
925
- if (!from)
926
- return;
927
- this.placeLayers(from);
928
- for (const rule of from.rules) {
929
- entries.push({ rule, sheet: from, weight: rule.specificity + bump });
930
- }
931
- };
932
- add(this.globalSheet, 0);
933
- add(hostSheet, COMPONENT_SPECIFICITY_BUMP);
934
- add(sheet, COMPONENT_SPECIFICITY_BUMP);
935
- for (const added of this.addedSheets)
936
- add(added, 0);
937
- if (!entries.some((entry) => entry.rule.layer !== undefined)) {
938
- 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);
939
1786
  }
940
- this.layered = true;
941
- const places = this.layerPlaces;
942
- 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 }));
943
1799
  }
944
1800
  /**
945
1801
  * Gives each of a sheet's layers its place among every sheet's, the first time the sheet is
@@ -982,7 +1838,8 @@ export class StyleResolver {
982
1838
  styled(node, matched, parentTokens, parentInherited) {
983
1839
  // Shared between nodes that match alike, unless the node sets something of its own.
984
1840
  const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
985
- 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)) {
986
1843
  return this.style(node, matched, parentTokens, parentInherited, inline);
987
1844
  }
988
1845
  if (this.sharedGeneration !== this.generation) {
@@ -1022,21 +1879,23 @@ export class StyleResolver {
1022
1879
  ...matched,
1023
1880
  { declarations: inline ?? EMPTY, ...(bound && { deferred: bound }) },
1024
1881
  ]
1025
- : matched);
1882
+ : matched, parentInherited);
1026
1883
  // Tokens are in scope for this node's own declarations as well as its descendants', so they
1027
1884
  // are merged before any `var()` here is resolved.
1028
- const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
1885
+ const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties, result.importantTokens);
1029
1886
  const own = result.declarations;
1030
1887
  if (result.deferred) {
1031
1888
  this.applyDeferred(result.deferred, own, tokens, parentInherited, result.important);
1032
1889
  }
1033
- if (own['borderStyle'] === 'none')
1034
- drawNoBorder(own);
1890
+ settleBorders(own);
1035
1891
  // `pointer-events: inherit` won the cascade: what the parent hands down stands.
1036
1892
  if (own['pointerEvents'] === 'inherit')
1037
1893
  delete own['pointerEvents'];
1038
1894
  const style = { ...parentInherited, ...own };
1039
- 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 };
1040
1899
  }
1041
1900
  /** The rules that apply to a node, weakest first. */
1042
1901
  matched(node, entries) {
@@ -1048,14 +1907,16 @@ export class StyleResolver {
1048
1907
  return out;
1049
1908
  }
1050
1909
  /** The declarations of the rules a node matched. The list is sorted, so later simply wins. */
1051
- cascade(matched) {
1910
+ cascade(matched, parentInherited) {
1052
1911
  const normal = {};
1053
1912
  const important = {};
1054
1913
  let hasImportant = false;
1055
1914
  let tokens = null;
1056
1915
  let importantTokens = null;
1057
1916
  let deferred = null;
1917
+ const forms = new OtherForms(() => this.directionOf(matched, parentInherited));
1058
1918
  for (const rule of this.layered ? byImportance(matched, this.layerPlaces) : matched) {
1919
+ deferred = forms.before(rule, normal, important, deferred);
1059
1920
  Object.assign(normal, rule.declarations);
1060
1921
  if (rule.important) {
1061
1922
  Object.assign(important, rule.important);
@@ -1067,17 +1928,50 @@ export class StyleResolver {
1067
1928
  if (rule.importantTokens) {
1068
1929
  Object.assign((importantTokens ??= {}), pick(rule.tokens, rule.importantTokens));
1069
1930
  }
1070
- deferred = carryDeferred(deferred, rule);
1931
+ deferred = forms.within(rule, normal, important, carryDeferred(deferred, rule));
1071
1932
  }
1072
1933
  if (importantTokens)
1073
1934
  Object.assign(tokens, importantTokens);
1935
+ deferred = forms.after(normal, deferred);
1074
1936
  return {
1075
1937
  declarations: hasImportant ? { ...normal, ...important } : normal,
1076
1938
  important: hasImportant ? important : null,
1077
1939
  tokens,
1940
+ importantTokens,
1078
1941
  deferred,
1079
1942
  };
1080
1943
  }
1944
+ /**
1945
+ * An inline style over the style the cascade settled, both already in `style`: a corner or an
1946
+ * edge the inline style sets takes the other form of it out, as a later rule would.
1947
+ */
1948
+ overOtherForms(inline, style) {
1949
+ let twin = null;
1950
+ const written = Object.keys(inline);
1951
+ for (const [at, key] of written.entries()) {
1952
+ if (!(key in TWIN.ltr))
1953
+ continue;
1954
+ twin ??= TWIN[(style['direction'] ?? this.conditions.direction) === 'rtl' ? 'rtl' : 'ltr'];
1955
+ const other = twin[key];
1956
+ // Where the inline style sets both, the one it set later stands.
1957
+ if (written.indexOf(other) < at)
1958
+ delete style[other];
1959
+ }
1960
+ }
1961
+ /**
1962
+ * The direction an element is laid out in: the last `direction` its own rules give it, the one
1963
+ * it inherits, or the app's. A logical corner is a different physical one in each.
1964
+ */
1965
+ directionOf(matched, parentInherited) {
1966
+ let plain;
1967
+ let important;
1968
+ for (const rule of matched) {
1969
+ plain = rule.declarations['direction'] ?? plain;
1970
+ important = rule.important?.['direction'] ?? important;
1971
+ }
1972
+ const direction = important ?? plain ?? parentInherited['direction'] ?? this.conditions.direction;
1973
+ return direction === 'rtl' ? 'rtl' : 'ltr';
1974
+ }
1081
1975
  /**
1082
1976
  * Whether a rule's media query holds, memoised per rule.
1083
1977
  *
@@ -1115,7 +2009,9 @@ export class StyleResolver {
1115
2009
  }
1116
2010
  /** What the device answers for a feature that is a keyword rather than a measurement. */
1117
2011
  preference(feature) {
1118
- const { width, height, colorScheme, reducedMotion } = this.conditions;
2012
+ const { width, height, colorScheme, reducedMotion, direction } = this.conditions;
2013
+ if (feature === 'direction')
2014
+ return direction ?? 'ltr';
1119
2015
  if (feature === 'orientation')
1120
2016
  return width > height ? 'landscape' : 'portrait';
1121
2017
  if (feature === 'prefers-color-scheme')
@@ -1142,6 +2038,10 @@ export class StyleResolver {
1142
2038
  */
1143
2039
  applyDeferred(deferred, own, tokens, parentInherited, important) {
1144
2040
  for (const declaration of settlingOrder(deferred)) {
2041
+ if (declaration.inherit) {
2042
+ this.inherit(declaration, own, parentInherited, important);
2043
+ continue;
2044
+ }
1145
2045
  const settled = this.settled(declaration, own, parentInherited, tokens);
1146
2046
  if (settled === undefined && this.onUndefinedToken) {
1147
2047
  this.reportUndefined(declaration, tokens);
@@ -1160,6 +2060,42 @@ export class StyleResolver {
1160
2060
  }
1161
2061
  }
1162
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
+ }
1163
2099
  /** One deferred declaration's value, from the tokens in scope and the node's own style. */
1164
2100
  settled(declaration, own, parentInherited, tokens) {
1165
2101
  if (declaration.within !== undefined) {
@@ -1342,9 +2278,34 @@ function settlingOrder(deferred) {
1342
2278
  * The tokens in scope at a node: its parent's, overlaid with what its rules define, overlaid with
1343
2279
  * what it sets itself, which wins as inline style does.
1344
2280
  */
1345
- function tokensInScope(parentTokens, ruleTokens, custom) {
1346
- const own = custom ? { ...ruleTokens, ...custom } : ruleTokens;
1347
- 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;
1348
2309
  }
1349
2310
  function resolveAliases(own, merged, parentTokens) {
1350
2311
  const names = Object.keys(own);
@@ -1903,7 +2864,8 @@ const LINE_INITIAL = {
1903
2864
  color: 'currentcolor',
1904
2865
  };
1905
2866
  function fillRole(line, values, role, value) {
1906
- 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];
1907
2869
  for (const prop of props)
1908
2870
  values[prop] = value;
1909
2871
  }
@@ -2081,17 +3043,26 @@ function mixSides(mix, tokens, current) {
2081
3043
  const colour = (side) => 'mix' in side ? unroundedMix(side.mix, tokens, current) : innerColour(side, tokens, current);
2082
3044
  const first = colour(mix.a);
2083
3045
  const second = colour(mix.b);
2084
- 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))
2085
3049
  return undefined;
2086
3050
  return [
2087
- { colour: first, percentage: mix.aPercentage },
2088
- { colour: second, percentage: mix.bPercentage },
3051
+ { colour: first, percentage: shares[0] ?? undefined },
3052
+ { colour: second, percentage: shares[1] ?? undefined },
2089
3053
  ];
2090
3054
  }
2091
3055
  function unroundedMix(mix, tokens, current) {
2092
3056
  const sides = mixSides(mix, tokens, current);
2093
3057
  return sides && mixChannels(mix.space, ...sides, mix.hue);
2094
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
+ }
2095
3066
  /** A calc marker worked out, as its slot takes it; undefined when a token in it is not set. */
2096
3067
  function resolveCalc(marker, tokens) {
2097
3068
  const percentage = wholePercentage(marker, tokens);
@@ -2366,7 +3337,7 @@ const RGB = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)
2366
3337
  * mix. A value that is not one of ours is returned untouched: it may be a platform colour, and a
2367
3338
  * colour nobody can parse is better painted as it stands than dropped.
2368
3339
  */
2369
- function faded(value, fraction) {
3340
+ export function faded(value, fraction) {
2370
3341
  if (typeof value !== 'string')
2371
3342
  return value;
2372
3343
  const parts = RGB.exec(value);