@ng-native/fabric 0.3.0 → 0.5.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/aria-props.d.ts +16 -0
- package/dist/aria-props.js +78 -0
- package/dist/aria-props.js.map +1 -0
- package/dist/color-mix.js +1 -16
- package/dist/color-mix.js.map +1 -1
- package/dist/css.d.ts +169 -2
- package/dist/css.js +676 -34
- package/dist/css.js.map +1 -1
- package/dist/element-styles.d.ts +15 -0
- package/dist/element-styles.js +103 -0
- package/dist/element-styles.js.map +1 -0
- package/dist/engine.d.ts +252 -7
- package/dist/engine.js +1081 -80
- package/dist/engine.js.map +1 -1
- package/dist/font-faces.d.ts +5 -2
- package/dist/font-faces.js +13 -2
- package/dist/font-faces.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/inline-derived-token.js +11 -10
- package/dist/inline-derived-token.js.map +1 -1
- package/dist/inline-token.d.ts +7 -0
- package/dist/inline-token.js +53 -8
- package/dist/inline-token.js.map +1 -1
- package/dist/inline-transform.d.ts +5 -1
- package/dist/inline-transform.js +34 -6
- package/dist/inline-transform.js.map +1 -1
- package/dist/native-drive.d.ts +21 -0
- package/dist/native-drive.js +60 -0
- package/dist/native-drive.js.map +1 -1
- package/dist/premultiplied-stops.d.ts +7 -0
- package/dist/premultiplied-stops.js +131 -0
- package/dist/premultiplied-stops.js.map +1 -0
- package/dist/scroll-animation.d.ts +11 -0
- package/dist/scroll-animation.js +29 -0
- package/dist/scroll-animation.js.map +1 -1
- package/dist/transition.d.ts +15 -0
- package/dist/transition.js +42 -1
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
package/dist/css.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* framework-agnostic and knows nothing about Angular.
|
|
7
7
|
*/
|
|
8
8
|
import { mixChannels, mixColours, } from "./color-mix.js";
|
|
9
|
+
import { ELEMENT_STYLES } from "./element-styles.js";
|
|
9
10
|
import { relativeColour } from "./relative-colour.js";
|
|
10
11
|
/** A run of CSS whitespace: spaces, tabs, newlines, carriage returns and form feeds, and no other. */
|
|
11
12
|
export const CSS_SPACE = /[ \t\n\r\f]+/;
|
|
@@ -57,7 +58,15 @@ const INHERITED = new Set([
|
|
|
57
58
|
'textShadowRadius',
|
|
58
59
|
'textShadowColor',
|
|
59
60
|
'selectable',
|
|
61
|
+
// CSS inherits it, and its `none` is the element alone: a descendant's `auto` takes touches
|
|
62
|
+
// again. The engine commits a computed `none` as native's `box-none`, which means that.
|
|
63
|
+
'pointerEvents',
|
|
60
64
|
]);
|
|
65
|
+
/**
|
|
66
|
+
* The `pointer-events` values CSS has. Native's own `box-none` and `box-only` say what their
|
|
67
|
+
* children do already, so neither is handed down.
|
|
68
|
+
*/
|
|
69
|
+
const INHERITED_POINTER_EVENTS = new Set(['none', 'auto']);
|
|
61
70
|
/**
|
|
62
71
|
* `border-style: none`, which the compiler writes as a style native does not have. On the web it
|
|
63
72
|
* makes every border width 0, whichever rule set the width, so `border-hidden border-x` draws no
|
|
@@ -80,6 +89,80 @@ function drawNoBorder(own) {
|
|
|
80
89
|
* The cost is one integer increment per comparison, against a Set lookup and several property
|
|
81
90
|
* reads, so it does not distort what it measures.
|
|
82
91
|
*/
|
|
92
|
+
/**
|
|
93
|
+
* What a sheet asks about a pressed element. `own` is whether a rule styles one, `.button:active`,
|
|
94
|
+
* which matching the pressed elements again answers. `elsewhere` is each compound that asks about
|
|
95
|
+
* a pressed element from another one: `.card:active .title`, `.a:active + .b`, or `:active`
|
|
96
|
+
* inside `:not()`, `:is()` or `:has()`. An element such a compound could be is restyled with
|
|
97
|
+
* everything under it when it is pressed, as what it changes is not itself alone.
|
|
98
|
+
*/
|
|
99
|
+
export function usesActive(sheet) {
|
|
100
|
+
const known = ACTIVE_USE.get(sheet);
|
|
101
|
+
if (known)
|
|
102
|
+
return known;
|
|
103
|
+
let own = false;
|
|
104
|
+
const elsewhere = [];
|
|
105
|
+
for (const rule of sheet.rules) {
|
|
106
|
+
rule.compounds.forEach((compound, at) => {
|
|
107
|
+
const subject = at === rule.compounds.length - 1;
|
|
108
|
+
// Asked of the element itself, on the compound or in its `:is()` and `:not()`: the
|
|
109
|
+
// subject's is its own, and any other compound's is of the element that compound is.
|
|
110
|
+
if (activeItself(compound, elsewhere)) {
|
|
111
|
+
if (subject)
|
|
112
|
+
own = true;
|
|
113
|
+
else
|
|
114
|
+
elsewhere.push(compound);
|
|
115
|
+
}
|
|
116
|
+
activeAround(compound, elsewhere);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const use = { own, elsewhere };
|
|
120
|
+
ACTIVE_USE.set(sheet, use);
|
|
121
|
+
return use;
|
|
122
|
+
}
|
|
123
|
+
const isActive = (compound) => compound.pseudo?.includes('active') === true;
|
|
124
|
+
/**
|
|
125
|
+
* Whether a compound asks about the pressed state of the element it matches: on itself, in a
|
|
126
|
+
* `:not()`, or as the last compound of an `:is()` argument. The compounds before the last in
|
|
127
|
+
* such an argument are other elements, and are collected as they are.
|
|
128
|
+
*/
|
|
129
|
+
function activeItself(compound, elsewhere) {
|
|
130
|
+
let itself = isActive(compound);
|
|
131
|
+
for (const inner of compound.not ?? []) {
|
|
132
|
+
if (activeItself(inner, elsewhere))
|
|
133
|
+
itself = true;
|
|
134
|
+
activeAround(inner, elsewhere);
|
|
135
|
+
}
|
|
136
|
+
for (const chain of compound.is ?? []) {
|
|
137
|
+
chain.forEach((inner, at) => {
|
|
138
|
+
const asked = activeItself(inner, elsewhere);
|
|
139
|
+
if (asked && at === chain.length - 1)
|
|
140
|
+
itself = true;
|
|
141
|
+
else if (asked)
|
|
142
|
+
elsewhere.push(inner);
|
|
143
|
+
activeAround(inner, elsewhere);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return itself;
|
|
147
|
+
}
|
|
148
|
+
/** Each compound that asks about a pressed element around the one a compound matches. */
|
|
149
|
+
function activeAround(compound, elsewhere) {
|
|
150
|
+
const collect = (value) => {
|
|
151
|
+
if (!value || typeof value !== 'object')
|
|
152
|
+
return;
|
|
153
|
+
if (Array.isArray(value))
|
|
154
|
+
return value.forEach(collect);
|
|
155
|
+
if (isActive(value))
|
|
156
|
+
elsewhere.push(value);
|
|
157
|
+
for (const inside of Object.values(value))
|
|
158
|
+
collect(inside);
|
|
159
|
+
};
|
|
160
|
+
for (const [key, inside] of Object.entries(compound)) {
|
|
161
|
+
if (key !== 'is' && key !== 'not' && key !== 'pseudo')
|
|
162
|
+
collect(inside);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const ACTIVE_USE = new WeakMap();
|
|
83
166
|
export const styleStats = {
|
|
84
167
|
/** Calls to `matchesCompound`, the innermost unit of matching work. */
|
|
85
168
|
compoundTests: 0,
|
|
@@ -181,6 +264,21 @@ function matchesCompound(node, compound, sheet) {
|
|
|
181
264
|
return false;
|
|
182
265
|
}
|
|
183
266
|
}
|
|
267
|
+
if (compound.has) {
|
|
268
|
+
for (const test of compound.has) {
|
|
269
|
+
if (!hasBeneath(node, test, sheet))
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (compound.parents) {
|
|
274
|
+
const parent = node.parent;
|
|
275
|
+
if (!parent)
|
|
276
|
+
return false;
|
|
277
|
+
for (const above of compound.parents) {
|
|
278
|
+
if (!matchesCompound(parent, above, sheet))
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
184
282
|
if (compound.ancestors) {
|
|
185
283
|
for (const ancestor of compound.ancestors) {
|
|
186
284
|
let found = false;
|
|
@@ -203,6 +301,18 @@ function matchesCompound(node, compound, sheet) {
|
|
|
203
301
|
}
|
|
204
302
|
return true;
|
|
205
303
|
}
|
|
304
|
+
/** Whether some element beneath `node`, or some child of it for `:has(> x)`, matches the test. */
|
|
305
|
+
function hasBeneath(node, test, sheet) {
|
|
306
|
+
for (const child of node.children ?? []) {
|
|
307
|
+
if (child.kind !== undefined && child.kind !== 'element')
|
|
308
|
+
continue;
|
|
309
|
+
if (test.any.some((option) => matchesCompound(child, option, sheet)))
|
|
310
|
+
return true;
|
|
311
|
+
if (!test.child && hasBeneath(child, test, sheet))
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
206
316
|
/**
|
|
207
317
|
* Right-to-left matching, as every CSS engine does it: the rightmost compound is the cheapest
|
|
208
318
|
* filter and rejects almost everything immediately.
|
|
@@ -332,6 +442,133 @@ function overriddenBy(deferred, declarations, important) {
|
|
|
332
442
|
}
|
|
333
443
|
return kept.length ? kept : null;
|
|
334
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* The logical property and the physical one for the same corner or edge, left to right. CSS
|
|
447
|
+
* settles the two as one property, the later declaration winning. A native view has a prop for
|
|
448
|
+
* each and takes one whatever their order: React Native the physical corner, Yoga the logical
|
|
449
|
+
* edge. So a declaration of either takes the other out of the cascade.
|
|
450
|
+
*/
|
|
451
|
+
const SIDES_LTR = [
|
|
452
|
+
['borderStartStartRadius', 'borderTopLeftRadius'],
|
|
453
|
+
['borderStartEndRadius', 'borderTopRightRadius'],
|
|
454
|
+
['borderEndStartRadius', 'borderBottomLeftRadius'],
|
|
455
|
+
['borderEndEndRadius', 'borderBottomRightRadius'],
|
|
456
|
+
...['margin', 'padding'].flatMap((box) => [
|
|
457
|
+
[`${box}Start`, `${box}Left`],
|
|
458
|
+
[`${box}End`, `${box}Right`],
|
|
459
|
+
]),
|
|
460
|
+
...['Width', 'Color'].flatMap((part) => [
|
|
461
|
+
[`borderStart${part}`, `borderLeft${part}`],
|
|
462
|
+
[`borderEnd${part}`, `borderRight${part}`],
|
|
463
|
+
]),
|
|
464
|
+
['start', 'left'],
|
|
465
|
+
['end', 'right'],
|
|
466
|
+
];
|
|
467
|
+
const mirrored = (physical) => physical.includes('Left') || physical === 'left'
|
|
468
|
+
? physical.replace('Left', 'Right').replace('left', 'right')
|
|
469
|
+
: physical.replace('Right', 'Left').replace('right', 'left');
|
|
470
|
+
/** Each property's other form, both ways, in a layout of each direction. */
|
|
471
|
+
const twins = (pairs) => Object.fromEntries(pairs.flatMap(([logical, physical]) => [
|
|
472
|
+
[logical, physical],
|
|
473
|
+
[physical, logical],
|
|
474
|
+
]));
|
|
475
|
+
const TWIN = {
|
|
476
|
+
ltr: twins(SIDES_LTR),
|
|
477
|
+
rtl: twins(SIDES_LTR.map(([logical, physical]) => [logical, mirrored(physical)])),
|
|
478
|
+
};
|
|
479
|
+
const sidedOf = new WeakMap();
|
|
480
|
+
function sided(rule) {
|
|
481
|
+
let found = sidedOf.get(rule);
|
|
482
|
+
if (found !== undefined)
|
|
483
|
+
return found;
|
|
484
|
+
const twin = TWIN.ltr;
|
|
485
|
+
const plain = Object.keys(rule.declarations).filter((key) => key in twin);
|
|
486
|
+
const important = rule.important ? Object.keys(rule.important).filter((key) => key in twin) : [];
|
|
487
|
+
for (const declaration of rule.deferred ?? []) {
|
|
488
|
+
for (const prop of declaration.props) {
|
|
489
|
+
if (prop in twin)
|
|
490
|
+
(declaration.important ? important : plain).push(prop);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
found = plain.length || important.length ? { plain, important } : null;
|
|
494
|
+
sidedOf.set(rule, found);
|
|
495
|
+
return found;
|
|
496
|
+
}
|
|
497
|
+
/** Of `keys`, in the order a rule wrote them, each whose other form comes after it. */
|
|
498
|
+
function writtenEarlier(keys, twin) {
|
|
499
|
+
return keys.filter((key, at) => keys.indexOf(twin[key]) > at);
|
|
500
|
+
}
|
|
501
|
+
/** The other form of each of `keys`, as the record `overriddenBy` reads. */
|
|
502
|
+
function twinsOf(keys, twin) {
|
|
503
|
+
const out = {};
|
|
504
|
+
for (const key of keys)
|
|
505
|
+
out[twin[key]] = true;
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* One cascade's settling of the corners and edges set in both forms: each rule's are taken out of
|
|
510
|
+
* what the rules before it left, in the form the rule does not use.
|
|
511
|
+
*/
|
|
512
|
+
class OtherForms {
|
|
513
|
+
/** Worked out for the first rule that sets a corner or an edge, which most elements have none of. */
|
|
514
|
+
twin = null;
|
|
515
|
+
overImportant = null;
|
|
516
|
+
direction;
|
|
517
|
+
constructor(direction) {
|
|
518
|
+
this.direction = direction;
|
|
519
|
+
}
|
|
520
|
+
/** Before `rule` is applied: the other form of what it sets goes, as the same property would. */
|
|
521
|
+
before(rule, normal, important, deferred) {
|
|
522
|
+
const sides = sided(rule);
|
|
523
|
+
if (!sides)
|
|
524
|
+
return deferred;
|
|
525
|
+
const twin = (this.twin ??= TWIN[this.direction()]);
|
|
526
|
+
const plain = twinsOf(sides.plain, twin);
|
|
527
|
+
for (const key in plain)
|
|
528
|
+
delete normal[key];
|
|
529
|
+
let kept = deferred && overriddenBy(deferred, plain, false);
|
|
530
|
+
if (!sides.important.length)
|
|
531
|
+
return kept;
|
|
532
|
+
const gone = twinsOf(sides.important, twin);
|
|
533
|
+
for (const key in gone)
|
|
534
|
+
delete important[key];
|
|
535
|
+
kept &&= overriddenBy(kept, gone, true);
|
|
536
|
+
Object.assign((this.overImportant ??= {}), gone);
|
|
537
|
+
return kept;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Once `rule` is applied, its deferred values among `deferred`: where it wrote both forms, the
|
|
541
|
+
* one it wrote later stands, among its plain declarations and among its important ones. The
|
|
542
|
+
* order is the rule's own, sent with it, since a value settled on device is in another list.
|
|
543
|
+
*/
|
|
544
|
+
within(rule, normal, important, deferred) {
|
|
545
|
+
if (!rule.sides && !rule.importantSides)
|
|
546
|
+
return deferred;
|
|
547
|
+
const twin = (this.twin ??= TWIN[this.direction()]);
|
|
548
|
+
let kept = deferred;
|
|
549
|
+
const settle = (order, into, tier) => {
|
|
550
|
+
const lost = {};
|
|
551
|
+
for (const key of writtenEarlier(order, twin)) {
|
|
552
|
+
delete into[key];
|
|
553
|
+
lost[key] = true;
|
|
554
|
+
}
|
|
555
|
+
kept &&= overriddenBy(kept, lost, tier);
|
|
556
|
+
};
|
|
557
|
+
if (rule.sides)
|
|
558
|
+
settle(rule.sides, normal, false);
|
|
559
|
+
if (rule.importantSides)
|
|
560
|
+
settle(rule.importantSides, important, true);
|
|
561
|
+
return kept;
|
|
562
|
+
}
|
|
563
|
+
/** An important one stands over the other form wherever that was set, as over its own. */
|
|
564
|
+
after(normal, deferred) {
|
|
565
|
+
if (!this.overImportant)
|
|
566
|
+
return deferred;
|
|
567
|
+
for (const key in this.overImportant)
|
|
568
|
+
delete normal[key];
|
|
569
|
+
return deferred && overriddenBy(deferred, this.overImportant, false);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
335
572
|
/**
|
|
336
573
|
* The deferred values still standing once `rule` has been applied, plus its own.
|
|
337
574
|
*
|
|
@@ -353,6 +590,44 @@ function carryDeferred(deferred, rule) {
|
|
|
353
590
|
(kept ??= []).push(...rule.deferred);
|
|
354
591
|
return kept;
|
|
355
592
|
}
|
|
593
|
+
const NO_DECLARATIONS = Object.freeze({});
|
|
594
|
+
const pick = (from, names) => Object.fromEntries(names.map((name) => [name, from[name]]));
|
|
595
|
+
const isImportant = (rule) => rule.important !== undefined ||
|
|
596
|
+
rule.importantTokens !== undefined ||
|
|
597
|
+
rule.deferred?.some((one) => one.important) === true;
|
|
598
|
+
/**
|
|
599
|
+
* The matched rules in the order their declarations apply, weakest first.
|
|
600
|
+
*
|
|
601
|
+
* That is the order they matched in, unless a layered rule is important. Layers turn round for
|
|
602
|
+
* `!important`: an important declaration in a layer beats one in a later layer, and both beat one
|
|
603
|
+
* in no layer. So each such rule is split, its plain declarations where it matched, and its
|
|
604
|
+
* important ones after every plain one, in the layers' order reversed.
|
|
605
|
+
*/
|
|
606
|
+
function byImportance(matched, places) {
|
|
607
|
+
if (!matched.some((rule) => rule.layer !== undefined && isImportant(rule)))
|
|
608
|
+
return matched;
|
|
609
|
+
const plain = matched.map((rule) => isImportant(rule)
|
|
610
|
+
? {
|
|
611
|
+
...rule,
|
|
612
|
+
important: undefined,
|
|
613
|
+
importantTokens: undefined,
|
|
614
|
+
deferred: rule.deferred?.filter((one) => !one.important),
|
|
615
|
+
}
|
|
616
|
+
: rule);
|
|
617
|
+
const important = matched
|
|
618
|
+
.filter(isImportant)
|
|
619
|
+
// Stable, so rules of one layer keep the order their specificity gave them. Sorted before
|
|
620
|
+
// they are copied, since a rule's place is kept by the rule itself.
|
|
621
|
+
.sort((a, b) => layerOrder(places.get(b), places.get(a)))
|
|
622
|
+
.map((rule) => ({
|
|
623
|
+
...rule,
|
|
624
|
+
declarations: NO_DECLARATIONS,
|
|
625
|
+
// Its important custom properties alone: the plain ones were applied where it matched.
|
|
626
|
+
tokens: rule.importantTokens && pick(rule.tokens, rule.importantTokens),
|
|
627
|
+
deferred: rule.deferred?.filter((one) => one.important),
|
|
628
|
+
}));
|
|
629
|
+
return [...plain, ...important];
|
|
630
|
+
}
|
|
356
631
|
function countPosition(node, siblings, fromEnd) {
|
|
357
632
|
let position = 0;
|
|
358
633
|
let seen = 0;
|
|
@@ -388,6 +663,35 @@ function lastElement(siblings) {
|
|
|
388
663
|
* precedence people already have in their fingers rather than inventing a rule for native.
|
|
389
664
|
*/
|
|
390
665
|
const COMPONENT_SPECIFICITY_BUMP = 1_000;
|
|
666
|
+
/**
|
|
667
|
+
* Two rules' layers, weakest first: a layer before each one named after it, a layer's nested
|
|
668
|
+
* layers before its own rules, and every layered rule before a rule in no layer.
|
|
669
|
+
*/
|
|
670
|
+
function layerOrder(a, b) {
|
|
671
|
+
if (a === b)
|
|
672
|
+
return 0;
|
|
673
|
+
if (a === undefined)
|
|
674
|
+
return 1;
|
|
675
|
+
if (b === undefined)
|
|
676
|
+
return -1;
|
|
677
|
+
const shared = Math.min(a.length, b.length);
|
|
678
|
+
for (let depth = 0; depth < shared; depth++) {
|
|
679
|
+
if (a[depth] !== b[depth])
|
|
680
|
+
return a[depth] - b[depth];
|
|
681
|
+
}
|
|
682
|
+
// One is nested in the other, whose own rules are the stronger.
|
|
683
|
+
return b.length - a.length;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* The engine's own styles for HTML's text elements, as rules to match before any a stylesheet
|
|
687
|
+
* holds, so every rule an author writes beats them whatever its specificity, as a browser's own
|
|
688
|
+
* sheet is beaten. Offered only to a node marked `styled`, which few are.
|
|
689
|
+
*/
|
|
690
|
+
const ELEMENT_ENTRIES = ELEMENT_STYLES.rules.map((rule) => ({
|
|
691
|
+
rule,
|
|
692
|
+
sheet: ELEMENT_STYLES,
|
|
693
|
+
weight: rule.specificity,
|
|
694
|
+
}));
|
|
391
695
|
/**
|
|
392
696
|
* The bucket a rule is filed under: the most selective simple selector in its rightmost compound,
|
|
393
697
|
* which is what a browser calls the key selector.
|
|
@@ -499,12 +803,82 @@ function inheritFrom(parentInherited, own) {
|
|
|
499
803
|
for (const key of Object.keys(own)) {
|
|
500
804
|
if (!INHERITED.has(key))
|
|
501
805
|
continue;
|
|
806
|
+
// `pointer-events: inherit`, which only an inline style on an element no rule matches still
|
|
807
|
+
// holds here: what the parent hands down stands.
|
|
808
|
+
if (key === 'pointerEvents' && own[key] === 'inherit')
|
|
809
|
+
continue;
|
|
502
810
|
if (inherited === parentInherited)
|
|
503
811
|
inherited = { ...parentInherited };
|
|
504
|
-
|
|
812
|
+
if (key === 'pointerEvents' && !INHERITED_POINTER_EVENTS.has(own[key])) {
|
|
813
|
+
delete inherited[key];
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
inherited[key] = own[key];
|
|
817
|
+
}
|
|
505
818
|
}
|
|
506
819
|
return inherited;
|
|
507
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* A text decoration's colour, which travels with its line rather than inheriting on its own. A
|
|
823
|
+
* decoration is drawn under the text inside the element that declares it, in that element's
|
|
824
|
+
* decoration colour or its text colour, and a text's own decoration colour only colours a line it
|
|
825
|
+
* declares itself. `style` is the node's, and gets the colour its line is drawn in; what its
|
|
826
|
+
* children inherit is returned.
|
|
827
|
+
*/
|
|
828
|
+
function decorate(style, inherited, own) {
|
|
829
|
+
const declared = own['textDecorationLine'] !== undefined;
|
|
830
|
+
// With no line declared here or above, nothing is drawn and the colour is left as computed.
|
|
831
|
+
if (!declared && !drawsLine(inherited['textDecorationLine']))
|
|
832
|
+
return inherited;
|
|
833
|
+
const colour = declared
|
|
834
|
+
? (own['textDecorationColor'] ?? style['color'])
|
|
835
|
+
: inherited['textDecorationColor'];
|
|
836
|
+
if (colour === undefined)
|
|
837
|
+
delete style['textDecorationColor'];
|
|
838
|
+
else
|
|
839
|
+
style['textDecorationColor'] = colour;
|
|
840
|
+
if (!declared || inherited['textDecorationColor'] === colour)
|
|
841
|
+
return inherited;
|
|
842
|
+
const { textDecorationColor: _parent, ...rest } = inherited;
|
|
843
|
+
return colour === undefined ? rest : { ...rest, textDecorationColor: colour };
|
|
844
|
+
}
|
|
845
|
+
function drawsLine(line) {
|
|
846
|
+
return line !== undefined && line !== 'none';
|
|
847
|
+
}
|
|
848
|
+
/** Whether an inline style sets a property children inherit. Allocates nothing: it runs per set. */
|
|
849
|
+
export function setsInherited(style) {
|
|
850
|
+
if (!style || typeof style !== 'object')
|
|
851
|
+
return false;
|
|
852
|
+
if (Array.isArray(style))
|
|
853
|
+
return style.some(setsInherited);
|
|
854
|
+
for (const key in style) {
|
|
855
|
+
if (INHERITED.has(key) && style[key] != null)
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* The inherited properties a node's inline style sets, or null for none. Inline style is the
|
|
862
|
+
* strongest normal declaration on its element, so what it sets is inherited, and read by
|
|
863
|
+
* `color: inherit` and `currentColor`, as a rule's would be.
|
|
864
|
+
*/
|
|
865
|
+
export function inlineInherited(style) {
|
|
866
|
+
let out = null;
|
|
867
|
+
// Flattened first, as native applies it, so a later entry's null clears an earlier value.
|
|
868
|
+
for (const [key, value] of Object.entries(flattenInline(style, {}))) {
|
|
869
|
+
if (INHERITED.has(key) && value !== undefined && value !== null)
|
|
870
|
+
(out ??= {})[key] = value;
|
|
871
|
+
}
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
874
|
+
function flattenInline(style, into) {
|
|
875
|
+
if (Array.isArray(style))
|
|
876
|
+
for (const entry of style)
|
|
877
|
+
flattenInline(entry, into);
|
|
878
|
+
else if (style && typeof style === 'object')
|
|
879
|
+
Object.assign(into, style);
|
|
880
|
+
return into;
|
|
881
|
+
}
|
|
508
882
|
/**
|
|
509
883
|
* The runtime half of CSS for one engine: the global sheet, the conditions media queries are
|
|
510
884
|
* evaluated against, and the memo that makes resolution cost nodes x rules rather than
|
|
@@ -542,6 +916,21 @@ export class StyleResolver {
|
|
|
542
916
|
* single index.
|
|
543
917
|
*/
|
|
544
918
|
merged = new WeakMap();
|
|
919
|
+
/**
|
|
920
|
+
* Whether any sheet merged so far has a rule in a cascade layer. Until one does, rules are
|
|
921
|
+
* ordered by weight alone and the cascade does not look for layers, which is nearly every app.
|
|
922
|
+
* It stays set once a layered sheet has gone, which costs a look at each cascade and no more.
|
|
923
|
+
*/
|
|
924
|
+
layered = false;
|
|
925
|
+
/**
|
|
926
|
+
* Every layer the sheets seen so far have named, in the order each name was first seen, which
|
|
927
|
+
* is the one order a document has for them. A name keeps its place when its sheet is replaced.
|
|
928
|
+
*/
|
|
929
|
+
layerNames = { place: 0, nested: new Map() };
|
|
930
|
+
/** Each layered rule's place among them, set when its sheet is first merged. */
|
|
931
|
+
layerPlaces = new WeakMap();
|
|
932
|
+
layeredSheets = new WeakSet();
|
|
933
|
+
unnamedLayers = 0;
|
|
545
934
|
/**
|
|
546
935
|
* The application-level sheet, if any: the one set of rules allowed to match a node whatever
|
|
547
936
|
* component created it, which is what resets, utility classes and `:root` tokens all need.
|
|
@@ -640,12 +1029,17 @@ export class StyleResolver {
|
|
|
640
1029
|
cached.epoch = epoch;
|
|
641
1030
|
return cached;
|
|
642
1031
|
}
|
|
1032
|
+
const kept = this.kept(node, parentContext, epoch);
|
|
1033
|
+
if (kept)
|
|
1034
|
+
return kept;
|
|
643
1035
|
const parentInherited = parent ? parent.inherited : EMPTY;
|
|
644
|
-
|
|
1036
|
+
// A box the engine made around loose text is in no template, so no rule matches it.
|
|
1037
|
+
if (node.anonymous || this.hasNoRules(node)) {
|
|
645
1038
|
return this.unstyled(node, epoch, parent, parentContext, parentInherited);
|
|
646
1039
|
}
|
|
647
1040
|
styleStats.nodesResolved++;
|
|
648
|
-
const
|
|
1041
|
+
const candidates = this.rulesFor(node);
|
|
1042
|
+
const matched = this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
|
|
649
1043
|
const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
|
|
650
1044
|
const styled = this.styled(node, matched, parentTokens, parentInherited);
|
|
651
1045
|
const cache = {
|
|
@@ -654,11 +1048,70 @@ export class StyleResolver {
|
|
|
654
1048
|
context: {},
|
|
655
1049
|
parentContext,
|
|
656
1050
|
...styled,
|
|
1051
|
+
...this.record(matched),
|
|
657
1052
|
};
|
|
658
1053
|
node.styleCache = cache;
|
|
659
1054
|
node.styleDirty = false;
|
|
1055
|
+
node.hasDirty = false;
|
|
1056
|
+
node.stateDirty = false;
|
|
660
1057
|
return cache;
|
|
661
1058
|
}
|
|
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
|
+
/**
|
|
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.
|
|
1066
|
+
*/
|
|
1067
|
+
tracksHas = false;
|
|
1068
|
+
/**
|
|
1069
|
+
* The cache of a node whose subtree changed, and nothing else, when it matches the rules it
|
|
1070
|
+
* did: keeping the object is what leaves everything under it alone. Nothing when it has to be
|
|
1071
|
+
* resolved, as it does with no record of what it matched, made before any sheet asked.
|
|
1072
|
+
*/
|
|
1073
|
+
keptBeneath(node, parentContext, epoch) {
|
|
1074
|
+
const cached = node.styleCache;
|
|
1075
|
+
const stands = (node.hasDirty === true || node.stateDirty === true) &&
|
|
1076
|
+
cached?.matched !== undefined &&
|
|
1077
|
+
!node.styleDirty &&
|
|
1078
|
+
cached.parentContext === parentContext &&
|
|
1079
|
+
cached.generation === this.generation;
|
|
1080
|
+
if (!stands)
|
|
1081
|
+
return null;
|
|
1082
|
+
const candidates = this.rulesFor(node);
|
|
1083
|
+
const now = this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
|
|
1084
|
+
const before = cached.matched;
|
|
1085
|
+
if (now.length !== before.length || now.some((rule, i) => rule !== before[i]))
|
|
1086
|
+
return null;
|
|
1087
|
+
node.hasDirty = false;
|
|
1088
|
+
node.stateDirty = false;
|
|
1089
|
+
cached.epoch = epoch;
|
|
1090
|
+
return cached;
|
|
1091
|
+
}
|
|
1092
|
+
/** A cache that still stands for a node marked for its subtree or its state alone. */
|
|
1093
|
+
kept(node, parentContext, epoch) {
|
|
1094
|
+
return (this.keptBeneath(node, parentContext, epoch) ?? this.keptUnmatched(node, parentContext, epoch));
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* The cache of a node whose pressed state changed and nothing else, where no rule can match
|
|
1098
|
+
* it at all: it has no style of its own to change, so it and all under it stay as they are.
|
|
1099
|
+
*/
|
|
1100
|
+
keptUnmatched(node, parentContext, epoch) {
|
|
1101
|
+
const cached = node.styleCache;
|
|
1102
|
+
const stands = node.stateDirty === true &&
|
|
1103
|
+
cached !== null &&
|
|
1104
|
+
!node.styleDirty &&
|
|
1105
|
+
!node.hasDirty &&
|
|
1106
|
+
cached.parentContext === parentContext &&
|
|
1107
|
+
cached.generation === this.generation &&
|
|
1108
|
+
(node.anonymous === true || this.hasNoRules(node));
|
|
1109
|
+
if (!stands)
|
|
1110
|
+
return null;
|
|
1111
|
+
node.stateDirty = false;
|
|
1112
|
+
cached.epoch = epoch;
|
|
1113
|
+
return cached;
|
|
1114
|
+
}
|
|
662
1115
|
/**
|
|
663
1116
|
* Whether a node's last answer still stands under a parent that resolved to `parentContext`,
|
|
664
1117
|
* without resolving it. What lets a commit step over a clean child in one comparison.
|
|
@@ -673,6 +1126,8 @@ export class StyleResolver {
|
|
|
673
1126
|
reusable(cached, node, parentContext) {
|
|
674
1127
|
return (cached !== null &&
|
|
675
1128
|
!node.styleDirty &&
|
|
1129
|
+
!node.hasDirty &&
|
|
1130
|
+
!node.stateDirty &&
|
|
676
1131
|
cached.parentContext === parentContext &&
|
|
677
1132
|
cached.generation === this.generation);
|
|
678
1133
|
}
|
|
@@ -685,7 +1140,9 @@ export class StyleResolver {
|
|
|
685
1140
|
node.hostSheet === null &&
|
|
686
1141
|
this.globalSheet === null &&
|
|
687
1142
|
this.addedSheets.length === 0 &&
|
|
688
|
-
!node.customProperties
|
|
1143
|
+
!node.customProperties &&
|
|
1144
|
+
!node.boundStyle &&
|
|
1145
|
+
!node.styled);
|
|
689
1146
|
}
|
|
690
1147
|
/**
|
|
691
1148
|
* A node with no rules anywhere: it renders with what it inherits and passes the same map on.
|
|
@@ -698,7 +1155,10 @@ export class StyleResolver {
|
|
|
698
1155
|
*/
|
|
699
1156
|
unstyled(node, epoch, parent, parentContext, parentInherited) {
|
|
700
1157
|
const tokens = parent ? parent.tokens : this.tokensOnRoot;
|
|
701
|
-
|
|
1158
|
+
// What inline style sets is inherited too, with no rule to read it against.
|
|
1159
|
+
const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
|
|
1160
|
+
if (!inline &&
|
|
1161
|
+
parentInherited === EMPTY &&
|
|
702
1162
|
parentContext === ROOT_CONTEXT &&
|
|
703
1163
|
tokens === NO_TOKENS &&
|
|
704
1164
|
!node.styleDirty) {
|
|
@@ -714,7 +1174,7 @@ export class StyleResolver {
|
|
|
714
1174
|
context: {},
|
|
715
1175
|
parentContext,
|
|
716
1176
|
style: parentInherited,
|
|
717
|
-
inherited: parentInherited,
|
|
1177
|
+
inherited: inline ? inheritFrom(parentInherited, inline) : parentInherited,
|
|
718
1178
|
tokens,
|
|
719
1179
|
};
|
|
720
1180
|
node.styleCache = passthrough;
|
|
@@ -756,6 +1216,7 @@ export class StyleResolver {
|
|
|
756
1216
|
const add = (from, bump) => {
|
|
757
1217
|
if (!from)
|
|
758
1218
|
return;
|
|
1219
|
+
this.placeLayers(from);
|
|
759
1220
|
for (const rule of from.rules) {
|
|
760
1221
|
entries.push({ rule, sheet: from, weight: rule.specificity + bump });
|
|
761
1222
|
}
|
|
@@ -765,7 +1226,41 @@ export class StyleResolver {
|
|
|
765
1226
|
add(sheet, COMPONENT_SPECIFICITY_BUMP);
|
|
766
1227
|
for (const added of this.addedSheets)
|
|
767
1228
|
add(added, 0);
|
|
768
|
-
|
|
1229
|
+
if (!entries.some((entry) => entry.rule.layer !== undefined)) {
|
|
1230
|
+
return entries.sort((a, b) => a.weight - b.weight);
|
|
1231
|
+
}
|
|
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);
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Gives each of a sheet's layers its place among every sheet's, the first time the sheet is
|
|
1238
|
+
* merged, and each of its layered rules that place. Sheets are merged in the order a document
|
|
1239
|
+
* holds them: the app's global sheet, then each component's as it first renders.
|
|
1240
|
+
*/
|
|
1241
|
+
placeLayers(sheet) {
|
|
1242
|
+
if (!sheet.layers || this.layeredSheets.has(sheet))
|
|
1243
|
+
return;
|
|
1244
|
+
this.layeredSheets.add(sheet);
|
|
1245
|
+
// A block with no name is a layer of this sheet's alone.
|
|
1246
|
+
const unnamed = `\0${this.unnamedLayers++}`;
|
|
1247
|
+
const places = sheet.layers.map((path) => {
|
|
1248
|
+
const place = [];
|
|
1249
|
+
let names = this.layerNames;
|
|
1250
|
+
for (const part of path) {
|
|
1251
|
+
const name = part.startsWith('\0') ? unnamed + part : part;
|
|
1252
|
+
let nested = names.nested.get(name);
|
|
1253
|
+
if (!nested)
|
|
1254
|
+
names.nested.set(name, (nested = { place: names.nested.size, nested: new Map() }));
|
|
1255
|
+
place.push(nested.place);
|
|
1256
|
+
names = nested;
|
|
1257
|
+
}
|
|
1258
|
+
return place;
|
|
1259
|
+
});
|
|
1260
|
+
for (const rule of sheet.rules) {
|
|
1261
|
+
if (rule.layer !== undefined)
|
|
1262
|
+
this.layerPlaces.set(rule, places[rule.layer]);
|
|
1263
|
+
}
|
|
769
1264
|
}
|
|
770
1265
|
/**
|
|
771
1266
|
* A node's style from the rules it matched, shared with every other node that matched the same
|
|
@@ -777,8 +1272,11 @@ export class StyleResolver {
|
|
|
777
1272
|
* written to after, so sharing it is safe; each node still has a cache and a context of its own.
|
|
778
1273
|
*/
|
|
779
1274
|
styled(node, matched, parentTokens, parentInherited) {
|
|
780
|
-
|
|
781
|
-
|
|
1275
|
+
// Shared between nodes that match alike, unless the node sets something of its own.
|
|
1276
|
+
const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
|
|
1277
|
+
if (node.customProperties || inline || node.boundStyle) {
|
|
1278
|
+
return this.style(node, matched, parentTokens, parentInherited, inline);
|
|
1279
|
+
}
|
|
782
1280
|
if (this.sharedGeneration !== this.generation) {
|
|
783
1281
|
this.shared.clear();
|
|
784
1282
|
this.sharedGeneration = this.generation;
|
|
@@ -808,8 +1306,15 @@ export class StyleResolver {
|
|
|
808
1306
|
return id;
|
|
809
1307
|
}
|
|
810
1308
|
/** A node's style from the rules it matched, worked out. */
|
|
811
|
-
style(node, matched, parentTokens, parentInherited) {
|
|
812
|
-
|
|
1309
|
+
style(node, matched, parentTokens, parentInherited, inline = null) {
|
|
1310
|
+
// Inline style is the last normal declaration, under every important one.
|
|
1311
|
+
const bound = node.boundStyle;
|
|
1312
|
+
const result = this.cascade(inline || bound
|
|
1313
|
+
? [
|
|
1314
|
+
...matched,
|
|
1315
|
+
{ declarations: inline ?? EMPTY, ...(bound && { deferred: bound }) },
|
|
1316
|
+
]
|
|
1317
|
+
: matched, parentInherited);
|
|
813
1318
|
// Tokens are in scope for this node's own declarations as well as its descendants', so they
|
|
814
1319
|
// are merged before any `var()` here is resolved.
|
|
815
1320
|
const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
|
|
@@ -819,11 +1324,11 @@ export class StyleResolver {
|
|
|
819
1324
|
}
|
|
820
1325
|
if (own['borderStyle'] === 'none')
|
|
821
1326
|
drawNoBorder(own);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
};
|
|
1327
|
+
// `pointer-events: inherit` won the cascade: what the parent hands down stands.
|
|
1328
|
+
if (own['pointerEvents'] === 'inherit')
|
|
1329
|
+
delete own['pointerEvents'];
|
|
1330
|
+
const style = { ...parentInherited, ...own };
|
|
1331
|
+
return { style, inherited: decorate(style, inheritFrom(parentInherited, own), own), tokens };
|
|
827
1332
|
}
|
|
828
1333
|
/** The rules that apply to a node, weakest first. */
|
|
829
1334
|
matched(node, entries) {
|
|
@@ -835,13 +1340,16 @@ export class StyleResolver {
|
|
|
835
1340
|
return out;
|
|
836
1341
|
}
|
|
837
1342
|
/** The declarations of the rules a node matched. The list is sorted, so later simply wins. */
|
|
838
|
-
cascade(matched) {
|
|
1343
|
+
cascade(matched, parentInherited) {
|
|
839
1344
|
const normal = {};
|
|
840
1345
|
const important = {};
|
|
841
1346
|
let hasImportant = false;
|
|
842
1347
|
let tokens = null;
|
|
1348
|
+
let importantTokens = null;
|
|
843
1349
|
let deferred = null;
|
|
844
|
-
|
|
1350
|
+
const forms = new OtherForms(() => this.directionOf(matched, parentInherited));
|
|
1351
|
+
for (const rule of this.layered ? byImportance(matched, this.layerPlaces) : matched) {
|
|
1352
|
+
deferred = forms.before(rule, normal, important, deferred);
|
|
845
1353
|
Object.assign(normal, rule.declarations);
|
|
846
1354
|
if (rule.important) {
|
|
847
1355
|
Object.assign(important, rule.important);
|
|
@@ -849,8 +1357,15 @@ export class StyleResolver {
|
|
|
849
1357
|
}
|
|
850
1358
|
if (rule.tokens)
|
|
851
1359
|
Object.assign((tokens ??= {}), rule.tokens);
|
|
852
|
-
|
|
1360
|
+
// An important custom property beats every plain one, whatever the rules' order.
|
|
1361
|
+
if (rule.importantTokens) {
|
|
1362
|
+
Object.assign((importantTokens ??= {}), pick(rule.tokens, rule.importantTokens));
|
|
1363
|
+
}
|
|
1364
|
+
deferred = forms.within(rule, normal, important, carryDeferred(deferred, rule));
|
|
853
1365
|
}
|
|
1366
|
+
if (importantTokens)
|
|
1367
|
+
Object.assign(tokens, importantTokens);
|
|
1368
|
+
deferred = forms.after(normal, deferred);
|
|
854
1369
|
return {
|
|
855
1370
|
declarations: hasImportant ? { ...normal, ...important } : normal,
|
|
856
1371
|
important: hasImportant ? important : null,
|
|
@@ -858,6 +1373,37 @@ export class StyleResolver {
|
|
|
858
1373
|
deferred,
|
|
859
1374
|
};
|
|
860
1375
|
}
|
|
1376
|
+
/**
|
|
1377
|
+
* An inline style over the style the cascade settled, both already in `style`: a corner or an
|
|
1378
|
+
* edge the inline style sets takes the other form of it out, as a later rule would.
|
|
1379
|
+
*/
|
|
1380
|
+
overOtherForms(inline, style) {
|
|
1381
|
+
let twin = null;
|
|
1382
|
+
const written = Object.keys(inline);
|
|
1383
|
+
for (const [at, key] of written.entries()) {
|
|
1384
|
+
if (!(key in TWIN.ltr))
|
|
1385
|
+
continue;
|
|
1386
|
+
twin ??= TWIN[(style['direction'] ?? this.conditions.direction) === 'rtl' ? 'rtl' : 'ltr'];
|
|
1387
|
+
const other = twin[key];
|
|
1388
|
+
// Where the inline style sets both, the one it set later stands.
|
|
1389
|
+
if (written.indexOf(other) < at)
|
|
1390
|
+
delete style[other];
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* The direction an element is laid out in: the last `direction` its own rules give it, the one
|
|
1395
|
+
* it inherits, or the app's. A logical corner is a different physical one in each.
|
|
1396
|
+
*/
|
|
1397
|
+
directionOf(matched, parentInherited) {
|
|
1398
|
+
let plain;
|
|
1399
|
+
let important;
|
|
1400
|
+
for (const rule of matched) {
|
|
1401
|
+
plain = rule.declarations['direction'] ?? plain;
|
|
1402
|
+
important = rule.important?.['direction'] ?? important;
|
|
1403
|
+
}
|
|
1404
|
+
const direction = important ?? plain ?? parentInherited['direction'] ?? this.conditions.direction;
|
|
1405
|
+
return direction === 'rtl' ? 'rtl' : 'ltr';
|
|
1406
|
+
}
|
|
861
1407
|
/**
|
|
862
1408
|
* Whether a rule's media query holds, memoised per rule.
|
|
863
1409
|
*
|
|
@@ -895,7 +1441,9 @@ export class StyleResolver {
|
|
|
895
1441
|
}
|
|
896
1442
|
/** What the device answers for a feature that is a keyword rather than a measurement. */
|
|
897
1443
|
preference(feature) {
|
|
898
|
-
const { width, height, colorScheme, reducedMotion } = this.conditions;
|
|
1444
|
+
const { width, height, colorScheme, reducedMotion, direction } = this.conditions;
|
|
1445
|
+
if (feature === 'direction')
|
|
1446
|
+
return direction ?? 'ltr';
|
|
899
1447
|
if (feature === 'orientation')
|
|
900
1448
|
return width > height ? 'landscape' : 'portrait';
|
|
901
1449
|
if (feature === 'prefers-color-scheme')
|
|
@@ -969,6 +1517,14 @@ export class StyleResolver {
|
|
|
969
1517
|
const values = lineValues(line, tokens);
|
|
970
1518
|
if (!values)
|
|
971
1519
|
return undefined;
|
|
1520
|
+
if (line.colour !== undefined) {
|
|
1521
|
+
// A colour that cannot be worked out makes the whole line invalid, as an unset token does.
|
|
1522
|
+
const colour = this.settledWithin(line.colour, declaration, own, parentInherited, tokens);
|
|
1523
|
+
if (colour === undefined)
|
|
1524
|
+
return undefined;
|
|
1525
|
+
for (const prop of line.colors)
|
|
1526
|
+
values[prop] = colour;
|
|
1527
|
+
}
|
|
972
1528
|
// `currentcolor` is the colour in scope, which only the node knows.
|
|
973
1529
|
if (values[line.colors[0]] === 'currentcolor') {
|
|
974
1530
|
const colour = own['color'] ?? parentInherited['color'] ?? 'black';
|
|
@@ -993,7 +1549,7 @@ export class StyleResolver {
|
|
|
993
1549
|
return;
|
|
994
1550
|
if (declaration.alternatives?.some((alternative) => alternative in tokens))
|
|
995
1551
|
return;
|
|
996
|
-
this.onUndefinedToken(name, declaration.props);
|
|
1552
|
+
this.onUndefinedToken(name, declaration.props, declaration.on);
|
|
997
1553
|
}
|
|
998
1554
|
/** A display token set to a value native has no display for. */
|
|
999
1555
|
reportDisplay(declaration, tokens) {
|
|
@@ -1022,13 +1578,15 @@ export class StyleResolver {
|
|
|
1022
1578
|
const fill = (part) => this.filledIn(part, declaration, own, parentInherited, tokens);
|
|
1023
1579
|
if (Array.isArray(value))
|
|
1024
1580
|
return this.filledList(value, fill, tokens);
|
|
1025
|
-
const marked = settledMarker(value, tokens);
|
|
1026
1581
|
// `currentcolor` is the colour in scope, which only the node knows: Tailwind's ring default.
|
|
1027
1582
|
// On `color` itself that is the inherited colour, as CSS reads it.
|
|
1028
|
-
|
|
1583
|
+
const current = () => {
|
|
1029
1584
|
const ownColour = declaration.props.includes('color') ? undefined : own['color'];
|
|
1030
|
-
return ownColour ?? parentInherited['color'] ?? 'black';
|
|
1031
|
-
}
|
|
1585
|
+
return (ownColour ?? parentInherited['color'] ?? 'black');
|
|
1586
|
+
};
|
|
1587
|
+
const marked = settledMarker(value, tokens, current);
|
|
1588
|
+
if (marked === 'currentcolor')
|
|
1589
|
+
return current();
|
|
1032
1590
|
if (marked !== NOT_A_MARKER)
|
|
1033
1591
|
return marked ?? UNSETTLED;
|
|
1034
1592
|
const pending = value.__defer;
|
|
@@ -1531,6 +2089,75 @@ function tokenForm(token, kind) {
|
|
|
1531
2089
|
const value = formOf(token, kind);
|
|
1532
2090
|
return value === undefined && kind === 'color' && isCurrentColour(token) ? CURRENT_COLOUR : value;
|
|
1533
2091
|
}
|
|
2092
|
+
const BOUND_NUMBERS = new Set([
|
|
2093
|
+
'opacity',
|
|
2094
|
+
'flex',
|
|
2095
|
+
'flexGrow',
|
|
2096
|
+
'flexShrink',
|
|
2097
|
+
'zIndex',
|
|
2098
|
+
'aspectRatio',
|
|
2099
|
+
'elevation',
|
|
2100
|
+
]);
|
|
2101
|
+
const BOUND_LENGTH = /^(?:(?:min|max)?(?:width|height)|top|right|bottom|left|start|end|gap|fontSize|letterSpacing)$|(?:margin|padding|inset|Width|Radius|Gap|Basis)/i;
|
|
2102
|
+
/** The properties whose token is read in a form of its own. */
|
|
2103
|
+
const BOUND_KINDS = {
|
|
2104
|
+
display: 'display',
|
|
2105
|
+
transform: 'transform',
|
|
2106
|
+
boxShadow: 'shadow',
|
|
2107
|
+
filter: 'filter',
|
|
2108
|
+
};
|
|
2109
|
+
/** What a token is read as for a bound declaration of `prop`, as the compiler says for a rule. */
|
|
2110
|
+
function boundKind(prop) {
|
|
2111
|
+
// `fill` and `stroke` are a shape's paint, which is a colour.
|
|
2112
|
+
if (/color$/i.test(prop) || prop === 'fill' || prop === 'stroke')
|
|
2113
|
+
return 'color';
|
|
2114
|
+
if (prop === 'fontWeight')
|
|
2115
|
+
return 'weight';
|
|
2116
|
+
if (prop === 'fontFamily')
|
|
2117
|
+
return 'family';
|
|
2118
|
+
if (prop === 'lineHeight')
|
|
2119
|
+
return 'lineHeight';
|
|
2120
|
+
if (prop in BOUND_KINDS)
|
|
2121
|
+
return BOUND_KINDS[prop];
|
|
2122
|
+
if (BOUND_NUMBERS.has(prop))
|
|
2123
|
+
return 'number';
|
|
2124
|
+
return BOUND_LENGTH.test(prop) ? 'length' : 'keyword';
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* A bound declaration whose value is one `var()`, `var(--brand, red)`, as the deferred declaration
|
|
2128
|
+
* the compiler writes for the same value in a rule. `token` is the value as a token, which is an
|
|
2129
|
+
* alias when it is a `var()`; null for any other.
|
|
2130
|
+
*
|
|
2131
|
+
* ponytail: the whole value has to be the `var()`. One inside a longer value, `calc()` included,
|
|
2132
|
+
* is what the compiler parses at build time and nothing here does; write the sum in a custom
|
|
2133
|
+
* property, `[style.--gap]`, and read that in the stylesheet.
|
|
2134
|
+
*/
|
|
2135
|
+
export function boundDeclaration(prop, token, on) {
|
|
2136
|
+
if (!token?.alias)
|
|
2137
|
+
return null;
|
|
2138
|
+
const kind = boundKind(prop);
|
|
2139
|
+
const [reference, ...alternatives] = aliasesOf(token);
|
|
2140
|
+
for (; token?.alias; token = token.fallback)
|
|
2141
|
+
;
|
|
2142
|
+
// `tokenForm`, so a fallback of `currentColor` is the colour of the element, as in a rule.
|
|
2143
|
+
const fallback = token && !isDerived(token) ? tokenForm(token, kind) : undefined;
|
|
2144
|
+
return {
|
|
2145
|
+
props: [prop],
|
|
2146
|
+
...(on === undefined ? {} : { on }),
|
|
2147
|
+
kind,
|
|
2148
|
+
reference: reference,
|
|
2149
|
+
...(alternatives.length ? { alternatives } : {}),
|
|
2150
|
+
...(fallback !== undefined ? { fallback } : {}),
|
|
2151
|
+
...(token && isDerived(token) ? { fallbackToken: token } : {}),
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
/** The names a `var()` tries in turn: its own, then each `var()` nested as its fallback. */
|
|
2155
|
+
function aliasesOf(token) {
|
|
2156
|
+
const names = [];
|
|
2157
|
+
for (; token?.alias; token = token.fallback)
|
|
2158
|
+
names.push(token.alias);
|
|
2159
|
+
return names;
|
|
2160
|
+
}
|
|
1534
2161
|
/** The fallback written, or one made of other tokens, worked out from the tokens where it is used. */
|
|
1535
2162
|
function fallbackOf(declaration, tokens) {
|
|
1536
2163
|
const token = declaration.fallbackToken;
|
|
@@ -1744,8 +2371,11 @@ function placed(position, tokens) {
|
|
|
1744
2371
|
/**
|
|
1745
2372
|
* A colour expression with its tokens looked up and any mix in it worked out. Undefined when a
|
|
1746
2373
|
* token it needs is not defined anywhere and has nothing to fall back to.
|
|
2374
|
+
*
|
|
2375
|
+
* `current` is the node's colour, for a `currentColor` inside a mix or a relative colour. One that
|
|
2376
|
+
* is the whole expression is answered as `currentcolor`, for the caller to fill in.
|
|
1747
2377
|
*/
|
|
1748
|
-
function resolveColour(expression, tokens) {
|
|
2378
|
+
function resolveColour(expression, tokens, current) {
|
|
1749
2379
|
if ('color' in expression)
|
|
1750
2380
|
return expression.color;
|
|
1751
2381
|
if ('channels' in expression)
|
|
@@ -1756,15 +2386,27 @@ function resolveColour(expression, tokens) {
|
|
|
1756
2386
|
return tokenColour(expression, tokens);
|
|
1757
2387
|
if ('relative' in expression) {
|
|
1758
2388
|
const { space, from, channels, alpha } = expression.relative;
|
|
1759
|
-
const origin =
|
|
2389
|
+
const origin = innerColour(from, tokens, current);
|
|
1760
2390
|
return origin === undefined ? undefined : relativeColour(space, origin, channels, alpha);
|
|
1761
2391
|
}
|
|
1762
|
-
const sides = mixSides(expression.mix, tokens);
|
|
2392
|
+
const sides = mixSides(expression.mix, tokens, current);
|
|
1763
2393
|
return sides && mixColours(expression.mix.space, ...sides, expression.mix.hue);
|
|
1764
2394
|
}
|
|
2395
|
+
/** A colour inside another: `currentColor` there, written or in a token, is the node's colour. */
|
|
2396
|
+
function innerColour(expression, tokens, current) {
|
|
2397
|
+
if (!current)
|
|
2398
|
+
return resolveColour(expression, tokens);
|
|
2399
|
+
if ('reference' in expression) {
|
|
2400
|
+
const names = [expression.reference, ...(expression.alternatives ?? [])];
|
|
2401
|
+
if (isCurrentColour(firstSet(names, tokens)))
|
|
2402
|
+
return current();
|
|
2403
|
+
}
|
|
2404
|
+
const colour = resolveColour(expression, tokens, current);
|
|
2405
|
+
return colour?.toLowerCase() === 'currentcolor' ? current() : colour;
|
|
2406
|
+
}
|
|
1765
2407
|
/** A mix's two sides, a mix inside it unrounded, as CSS rounds only the colour at the end. */
|
|
1766
|
-
function mixSides(mix, tokens) {
|
|
1767
|
-
const colour = (side) => 'mix' in side ? unroundedMix(side.mix, tokens) :
|
|
2408
|
+
function mixSides(mix, tokens, current) {
|
|
2409
|
+
const colour = (side) => 'mix' in side ? unroundedMix(side.mix, tokens, current) : innerColour(side, tokens, current);
|
|
1768
2410
|
const first = colour(mix.a);
|
|
1769
2411
|
const second = colour(mix.b);
|
|
1770
2412
|
if (first === undefined || second === undefined)
|
|
@@ -1774,8 +2416,8 @@ function mixSides(mix, tokens) {
|
|
|
1774
2416
|
{ colour: second, percentage: mix.bPercentage },
|
|
1775
2417
|
];
|
|
1776
2418
|
}
|
|
1777
|
-
function unroundedMix(mix, tokens) {
|
|
1778
|
-
const sides = mixSides(mix, tokens);
|
|
2419
|
+
function unroundedMix(mix, tokens, current) {
|
|
2420
|
+
const sides = mixSides(mix, tokens, current);
|
|
1779
2421
|
return sides && mixChannels(mix.space, ...sides, mix.hue);
|
|
1780
2422
|
}
|
|
1781
2423
|
/** A calc marker worked out, as its slot takes it; undefined when a token in it is not set. */
|
|
@@ -1951,10 +2593,10 @@ const NEEDS_FILLING = new WeakMap();
|
|
|
1951
2593
|
/** What `settledMarker` says of a part that is not a token's marker at all. */
|
|
1952
2594
|
const NOT_A_MARKER = Symbol('not a marker');
|
|
1953
2595
|
/** A colour, length or calc marker settled from the tokens, or undefined when one cannot be. */
|
|
1954
|
-
function settledMarker(value, tokens) {
|
|
2596
|
+
function settledMarker(value, tokens, current) {
|
|
1955
2597
|
const colour = colourMarker(value);
|
|
1956
2598
|
if (colour)
|
|
1957
|
-
return resolveColour(colour, tokens);
|
|
2599
|
+
return resolveColour(colour, tokens, current);
|
|
1958
2600
|
// Whether a shadow is inset, from a token that is the word or nothing: `ring-inset`.
|
|
1959
2601
|
const inset = value.__inset;
|
|
1960
2602
|
if (inset)
|