@ng-native/fabric 0.2.0 → 0.4.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.d.ts +12 -2
- package/dist/color-mix.js +16 -5
- package/dist/color-mix.js.map +1 -1
- package/dist/css.d.ts +160 -5
- package/dist/css.js +842 -87
- package/dist/css.js.map +1 -1
- package/dist/defer-triggers.js +3 -1
- package/dist/defer-triggers.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 +289 -20
- package/dist/engine.js +1045 -120
- 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/host.d.ts +7 -0
- package/dist/host.js +7 -0
- package/dist/host.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.d.ts +3 -12
- package/dist/inline-derived-token.js +249 -13
- package/dist/inline-derived-token.js.map +1 -1
- package/dist/inline-token.d.ts +12 -4
- package/dist/inline-token.js +76 -5
- 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/transition.d.ts +15 -4
- package/dist/transition.js +21 -3
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
package/dist/css.js
CHANGED
|
@@ -5,17 +5,27 @@
|
|
|
5
5
|
* specificity maths, sorting), so this only walks a pre-sorted rule list and merges. It is
|
|
6
6
|
* framework-agnostic and knows nothing about Angular.
|
|
7
7
|
*/
|
|
8
|
-
import { mixColours } from "./color-mix.js";
|
|
8
|
+
import { mixChannels, mixColours, } from "./color-mix.js";
|
|
9
|
+
import { ELEMENT_STYLES } from "./element-styles.js";
|
|
9
10
|
import { relativeColour } from "./relative-colour.js";
|
|
11
|
+
/** A run of CSS whitespace: spaces, tabs, newlines, carriage returns and form feeds, and no other. */
|
|
12
|
+
export const CSS_SPACE = /[ \t\n\r\f]+/;
|
|
10
13
|
/**
|
|
11
14
|
* Where a form a token does not have can be read from instead. A length is already a
|
|
12
15
|
* line-height and a single word is already a family, so neither is stored twice in every token
|
|
13
16
|
* that happens to be one. The compiler reads fallbacks with the same table.
|
|
14
17
|
*/
|
|
15
|
-
const STAND_IN = {
|
|
18
|
+
const STAND_IN = {
|
|
19
|
+
lineHeight: 'length',
|
|
20
|
+
family: 'keyword',
|
|
21
|
+
};
|
|
16
22
|
function formOf(token, kind) {
|
|
17
23
|
if (!token)
|
|
18
24
|
return undefined;
|
|
25
|
+
// A display is a word, read as one native has where it is used: see `displayOf`. Two words,
|
|
26
|
+
// `inline flex`, are a family when the stylesheet wrote them, as a font stack's first name is.
|
|
27
|
+
if (kind === 'display')
|
|
28
|
+
return token.keyword ?? token.family;
|
|
19
29
|
const stand = STAND_IN[kind];
|
|
20
30
|
return token[kind] ?? (stand ? token[stand] : undefined);
|
|
21
31
|
}
|
|
@@ -48,7 +58,15 @@ const INHERITED = new Set([
|
|
|
48
58
|
'textShadowRadius',
|
|
49
59
|
'textShadowColor',
|
|
50
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',
|
|
51
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']);
|
|
52
70
|
/**
|
|
53
71
|
* `border-style: none`, which the compiler writes as a style native does not have. On the web it
|
|
54
72
|
* makes every border width 0, whichever rule set the width, so `border-hidden border-x` draws no
|
|
@@ -344,6 +362,44 @@ function carryDeferred(deferred, rule) {
|
|
|
344
362
|
(kept ??= []).push(...rule.deferred);
|
|
345
363
|
return kept;
|
|
346
364
|
}
|
|
365
|
+
const NO_DECLARATIONS = Object.freeze({});
|
|
366
|
+
const pick = (from, names) => Object.fromEntries(names.map((name) => [name, from[name]]));
|
|
367
|
+
const isImportant = (rule) => rule.important !== undefined ||
|
|
368
|
+
rule.importantTokens !== undefined ||
|
|
369
|
+
rule.deferred?.some((one) => one.important) === true;
|
|
370
|
+
/**
|
|
371
|
+
* The matched rules in the order their declarations apply, weakest first.
|
|
372
|
+
*
|
|
373
|
+
* That is the order they matched in, unless a layered rule is important. Layers turn round for
|
|
374
|
+
* `!important`: an important declaration in a layer beats one in a later layer, and both beat one
|
|
375
|
+
* in no layer. So each such rule is split, its plain declarations where it matched, and its
|
|
376
|
+
* important ones after every plain one, in the layers' order reversed.
|
|
377
|
+
*/
|
|
378
|
+
function byImportance(matched, places) {
|
|
379
|
+
if (!matched.some((rule) => rule.layer !== undefined && isImportant(rule)))
|
|
380
|
+
return matched;
|
|
381
|
+
const plain = matched.map((rule) => isImportant(rule)
|
|
382
|
+
? {
|
|
383
|
+
...rule,
|
|
384
|
+
important: undefined,
|
|
385
|
+
importantTokens: undefined,
|
|
386
|
+
deferred: rule.deferred?.filter((one) => !one.important),
|
|
387
|
+
}
|
|
388
|
+
: rule);
|
|
389
|
+
const important = matched
|
|
390
|
+
.filter(isImportant)
|
|
391
|
+
// Stable, so rules of one layer keep the order their specificity gave them. Sorted before
|
|
392
|
+
// they are copied, since a rule's place is kept by the rule itself.
|
|
393
|
+
.sort((a, b) => layerOrder(places.get(b), places.get(a)))
|
|
394
|
+
.map((rule) => ({
|
|
395
|
+
...rule,
|
|
396
|
+
declarations: NO_DECLARATIONS,
|
|
397
|
+
// Its important custom properties alone: the plain ones were applied where it matched.
|
|
398
|
+
tokens: rule.importantTokens && pick(rule.tokens, rule.importantTokens),
|
|
399
|
+
deferred: rule.deferred?.filter((one) => one.important),
|
|
400
|
+
}));
|
|
401
|
+
return [...plain, ...important];
|
|
402
|
+
}
|
|
347
403
|
function countPosition(node, siblings, fromEnd) {
|
|
348
404
|
let position = 0;
|
|
349
405
|
let seen = 0;
|
|
@@ -379,6 +435,35 @@ function lastElement(siblings) {
|
|
|
379
435
|
* precedence people already have in their fingers rather than inventing a rule for native.
|
|
380
436
|
*/
|
|
381
437
|
const COMPONENT_SPECIFICITY_BUMP = 1_000;
|
|
438
|
+
/**
|
|
439
|
+
* Two rules' layers, weakest first: a layer before each one named after it, a layer's nested
|
|
440
|
+
* layers before its own rules, and every layered rule before a rule in no layer.
|
|
441
|
+
*/
|
|
442
|
+
function layerOrder(a, b) {
|
|
443
|
+
if (a === b)
|
|
444
|
+
return 0;
|
|
445
|
+
if (a === undefined)
|
|
446
|
+
return 1;
|
|
447
|
+
if (b === undefined)
|
|
448
|
+
return -1;
|
|
449
|
+
const shared = Math.min(a.length, b.length);
|
|
450
|
+
for (let depth = 0; depth < shared; depth++) {
|
|
451
|
+
if (a[depth] !== b[depth])
|
|
452
|
+
return a[depth] - b[depth];
|
|
453
|
+
}
|
|
454
|
+
// One is nested in the other, whose own rules are the stronger.
|
|
455
|
+
return b.length - a.length;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* The engine's own styles for HTML's text elements, as rules to match before any a stylesheet
|
|
459
|
+
* holds, so every rule an author writes beats them whatever its specificity, as a browser's own
|
|
460
|
+
* sheet is beaten. Offered only to a node marked `styled`, which few are.
|
|
461
|
+
*/
|
|
462
|
+
const ELEMENT_ENTRIES = ELEMENT_STYLES.rules.map((rule) => ({
|
|
463
|
+
rule,
|
|
464
|
+
sheet: ELEMENT_STYLES,
|
|
465
|
+
weight: rule.specificity,
|
|
466
|
+
}));
|
|
382
467
|
/**
|
|
383
468
|
* The bucket a rule is filed under: the most selective simple selector in its rightmost compound,
|
|
384
469
|
* which is what a browser calls the key selector.
|
|
@@ -490,12 +575,82 @@ function inheritFrom(parentInherited, own) {
|
|
|
490
575
|
for (const key of Object.keys(own)) {
|
|
491
576
|
if (!INHERITED.has(key))
|
|
492
577
|
continue;
|
|
578
|
+
// `pointer-events: inherit`, which only an inline style on an element no rule matches still
|
|
579
|
+
// holds here: what the parent hands down stands.
|
|
580
|
+
if (key === 'pointerEvents' && own[key] === 'inherit')
|
|
581
|
+
continue;
|
|
493
582
|
if (inherited === parentInherited)
|
|
494
583
|
inherited = { ...parentInherited };
|
|
495
|
-
|
|
584
|
+
if (key === 'pointerEvents' && !INHERITED_POINTER_EVENTS.has(own[key])) {
|
|
585
|
+
delete inherited[key];
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
inherited[key] = own[key];
|
|
589
|
+
}
|
|
496
590
|
}
|
|
497
591
|
return inherited;
|
|
498
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* A text decoration's colour, which travels with its line rather than inheriting on its own. A
|
|
595
|
+
* decoration is drawn under the text inside the element that declares it, in that element's
|
|
596
|
+
* decoration colour or its text colour, and a text's own decoration colour only colours a line it
|
|
597
|
+
* declares itself. `style` is the node's, and gets the colour its line is drawn in; what its
|
|
598
|
+
* children inherit is returned.
|
|
599
|
+
*/
|
|
600
|
+
function decorate(style, inherited, own) {
|
|
601
|
+
const declared = own['textDecorationLine'] !== undefined;
|
|
602
|
+
// With no line declared here or above, nothing is drawn and the colour is left as computed.
|
|
603
|
+
if (!declared && !drawsLine(inherited['textDecorationLine']))
|
|
604
|
+
return inherited;
|
|
605
|
+
const colour = declared
|
|
606
|
+
? (own['textDecorationColor'] ?? style['color'])
|
|
607
|
+
: inherited['textDecorationColor'];
|
|
608
|
+
if (colour === undefined)
|
|
609
|
+
delete style['textDecorationColor'];
|
|
610
|
+
else
|
|
611
|
+
style['textDecorationColor'] = colour;
|
|
612
|
+
if (!declared || inherited['textDecorationColor'] === colour)
|
|
613
|
+
return inherited;
|
|
614
|
+
const { textDecorationColor: _parent, ...rest } = inherited;
|
|
615
|
+
return colour === undefined ? rest : { ...rest, textDecorationColor: colour };
|
|
616
|
+
}
|
|
617
|
+
function drawsLine(line) {
|
|
618
|
+
return line !== undefined && line !== 'none';
|
|
619
|
+
}
|
|
620
|
+
/** Whether an inline style sets a property children inherit. Allocates nothing: it runs per set. */
|
|
621
|
+
export function setsInherited(style) {
|
|
622
|
+
if (!style || typeof style !== 'object')
|
|
623
|
+
return false;
|
|
624
|
+
if (Array.isArray(style))
|
|
625
|
+
return style.some(setsInherited);
|
|
626
|
+
for (const key in style) {
|
|
627
|
+
if (INHERITED.has(key) && style[key] != null)
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* The inherited properties a node's inline style sets, or null for none. Inline style is the
|
|
634
|
+
* strongest normal declaration on its element, so what it sets is inherited, and read by
|
|
635
|
+
* `color: inherit` and `currentColor`, as a rule's would be.
|
|
636
|
+
*/
|
|
637
|
+
export function inlineInherited(style) {
|
|
638
|
+
let out = null;
|
|
639
|
+
// Flattened first, as native applies it, so a later entry's null clears an earlier value.
|
|
640
|
+
for (const [key, value] of Object.entries(flattenInline(style, {}))) {
|
|
641
|
+
if (INHERITED.has(key) && value !== undefined && value !== null)
|
|
642
|
+
(out ??= {})[key] = value;
|
|
643
|
+
}
|
|
644
|
+
return out;
|
|
645
|
+
}
|
|
646
|
+
function flattenInline(style, into) {
|
|
647
|
+
if (Array.isArray(style))
|
|
648
|
+
for (const entry of style)
|
|
649
|
+
flattenInline(entry, into);
|
|
650
|
+
else if (style && typeof style === 'object')
|
|
651
|
+
Object.assign(into, style);
|
|
652
|
+
return into;
|
|
653
|
+
}
|
|
499
654
|
/**
|
|
500
655
|
* The runtime half of CSS for one engine: the global sheet, the conditions media queries are
|
|
501
656
|
* evaluated against, and the memo that makes resolution cost nodes x rules rather than
|
|
@@ -533,11 +688,32 @@ export class StyleResolver {
|
|
|
533
688
|
* single index.
|
|
534
689
|
*/
|
|
535
690
|
merged = new WeakMap();
|
|
691
|
+
/**
|
|
692
|
+
* Whether any sheet merged so far has a rule in a cascade layer. Until one does, rules are
|
|
693
|
+
* ordered by weight alone and the cascade does not look for layers, which is nearly every app.
|
|
694
|
+
* It stays set once a layered sheet has gone, which costs a look at each cascade and no more.
|
|
695
|
+
*/
|
|
696
|
+
layered = false;
|
|
697
|
+
/**
|
|
698
|
+
* Every layer the sheets seen so far have named, in the order each name was first seen, which
|
|
699
|
+
* is the one order a document has for them. A name keeps its place when its sheet is replaced.
|
|
700
|
+
*/
|
|
701
|
+
layerNames = { place: 0, nested: new Map() };
|
|
702
|
+
/** Each layered rule's place among them, set when its sheet is first merged. */
|
|
703
|
+
layerPlaces = new WeakMap();
|
|
704
|
+
layeredSheets = new WeakSet();
|
|
705
|
+
unnamedLayers = 0;
|
|
536
706
|
/**
|
|
537
707
|
* The application-level sheet, if any: the one set of rules allowed to match a node whatever
|
|
538
708
|
* component created it, which is what resets, utility classes and `:root` tokens all need.
|
|
539
709
|
*/
|
|
540
710
|
globalSheet;
|
|
711
|
+
/**
|
|
712
|
+
* Global sheets added once the engine is running, in the order they came: a
|
|
713
|
+
* `ViewEncapsulation.None` component's, which a browser adds to the document unscoped when the
|
|
714
|
+
* component first renders, after the application's own. See `addGlobalSheet`.
|
|
715
|
+
*/
|
|
716
|
+
addedSheets = [];
|
|
541
717
|
/**
|
|
542
718
|
* Custom properties the host supplies rather than the stylesheet: the safe-area insets, and
|
|
543
719
|
* whatever else is a property of the device instead of the design.
|
|
@@ -556,10 +732,46 @@ export class StyleResolver {
|
|
|
556
732
|
* which is dropped. Set only in development, where it is how the engine says so.
|
|
557
733
|
*/
|
|
558
734
|
onUndefinedToken = null;
|
|
735
|
+
/**
|
|
736
|
+
* Told of a `display: var()` whose token holds no display native has, `grid` or `table`, which
|
|
737
|
+
* leaves display unset. Set only in development, where it is how the engine says so: the
|
|
738
|
+
* compiler refuses the same value written out, but a token is only known here.
|
|
739
|
+
*/
|
|
740
|
+
onUnreadDisplay = null;
|
|
559
741
|
constructor(globalSheet, conditions) {
|
|
560
742
|
this.globalSheet = globalSheet;
|
|
561
743
|
this.conditions = conditions;
|
|
562
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* Match `sheet` against every node from now on, as a global sheet, after the application's;
|
|
747
|
+
* false when it already is. Every cached result goes, as on a theme switch: rules that were not
|
|
748
|
+
* there can now match anything. `:host` in it matches nothing, since no node hosts it.
|
|
749
|
+
*
|
|
750
|
+
* With `replacing`, a sheet added before, it takes that one's place, and so its order: a hot
|
|
751
|
+
* swap of the component's CSS edits the sheet where it is.
|
|
752
|
+
*/
|
|
753
|
+
addGlobalSheet(sheet, replacing) {
|
|
754
|
+
if (sheet === this.globalSheet || this.addedSheets.includes(sheet))
|
|
755
|
+
return false;
|
|
756
|
+
const at = replacing ? this.addedSheets.indexOf(replacing) : -1;
|
|
757
|
+
if (at === -1)
|
|
758
|
+
this.addedSheets.push(sheet);
|
|
759
|
+
else
|
|
760
|
+
this.addedSheets[at] = sheet;
|
|
761
|
+
this.merged = new WeakMap();
|
|
762
|
+
this.generation = ++generations;
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
/** Stop matching a sheet `addGlobalSheet` added; false when it is not one. */
|
|
766
|
+
removeGlobalSheet(sheet) {
|
|
767
|
+
const at = this.addedSheets.indexOf(sheet);
|
|
768
|
+
if (at === -1)
|
|
769
|
+
return false;
|
|
770
|
+
this.addedSheets.splice(at, 1);
|
|
771
|
+
this.merged = new WeakMap();
|
|
772
|
+
this.generation = ++generations;
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
563
775
|
setConditions(next) {
|
|
564
776
|
this.conditions = next;
|
|
565
777
|
this.generation = ++generations;
|
|
@@ -590,11 +802,13 @@ export class StyleResolver {
|
|
|
590
802
|
return cached;
|
|
591
803
|
}
|
|
592
804
|
const parentInherited = parent ? parent.inherited : EMPTY;
|
|
593
|
-
|
|
805
|
+
// A box the engine made around loose text is in no template, so no rule matches it.
|
|
806
|
+
if (node.anonymous || this.hasNoRules(node)) {
|
|
594
807
|
return this.unstyled(node, epoch, parent, parentContext, parentInherited);
|
|
595
808
|
}
|
|
596
809
|
styleStats.nodesResolved++;
|
|
597
|
-
const
|
|
810
|
+
const candidates = this.rulesFor(node);
|
|
811
|
+
const matched = this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
|
|
598
812
|
const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
|
|
599
813
|
const styled = this.styled(node, matched, parentTokens, parentInherited);
|
|
600
814
|
const cache = {
|
|
@@ -633,17 +847,29 @@ export class StyleResolver {
|
|
|
633
847
|
return (node.sheet === null &&
|
|
634
848
|
node.hostSheet === null &&
|
|
635
849
|
this.globalSheet === null &&
|
|
636
|
-
|
|
850
|
+
this.addedSheets.length === 0 &&
|
|
851
|
+
!node.customProperties &&
|
|
852
|
+
!node.boundStyle &&
|
|
853
|
+
!node.styled);
|
|
637
854
|
}
|
|
638
855
|
/**
|
|
639
856
|
* A node with no rules anywhere: it renders with what it inherits and passes the same map on.
|
|
640
857
|
*
|
|
641
858
|
* It still mints a context, because a rule further down may match on one of *its* classes.
|
|
642
859
|
* When there is nothing to inherit either, one shared cache object serves every such node, so
|
|
643
|
-
* an application that writes no CSS allocates nothing per node.
|
|
860
|
+
* an application that writes no CSS allocates nothing per node. Not when there are tokens to
|
|
861
|
+
* carry: the shared object holds none, and the surface root answering with it dropped every
|
|
862
|
+
* token the device had seeded.
|
|
644
863
|
*/
|
|
645
864
|
unstyled(node, epoch, parent, parentContext, parentInherited) {
|
|
646
|
-
|
|
865
|
+
const tokens = parent ? parent.tokens : this.tokensOnRoot;
|
|
866
|
+
// What inline style sets is inherited too, with no rule to read it against.
|
|
867
|
+
const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
|
|
868
|
+
if (!inline &&
|
|
869
|
+
parentInherited === EMPTY &&
|
|
870
|
+
parentContext === ROOT_CONTEXT &&
|
|
871
|
+
tokens === NO_TOKENS &&
|
|
872
|
+
!node.styleDirty) {
|
|
647
873
|
if (this.emptyCache.epoch !== epoch || this.emptyCache.generation !== this.generation) {
|
|
648
874
|
this.emptyCache = emptyCacheFor(epoch, this.generation);
|
|
649
875
|
}
|
|
@@ -656,8 +882,8 @@ export class StyleResolver {
|
|
|
656
882
|
context: {},
|
|
657
883
|
parentContext,
|
|
658
884
|
style: parentInherited,
|
|
659
|
-
inherited: parentInherited,
|
|
660
|
-
tokens
|
|
885
|
+
inherited: inline ? inheritFrom(parentInherited, inline) : parentInherited,
|
|
886
|
+
tokens,
|
|
661
887
|
};
|
|
662
888
|
node.styleCache = passthrough;
|
|
663
889
|
node.styleDirty = false;
|
|
@@ -688,12 +914,17 @@ export class StyleResolver {
|
|
|
688
914
|
* created it. Both carry the bump; the creator comes later and so wins a tie, as the outer
|
|
689
915
|
* document does over `:host` on the web. The sort is stable, so within one sheet source order
|
|
690
916
|
* survives and a later sheet wins equal weight.
|
|
917
|
+
*
|
|
918
|
+
* A sheet added with `addGlobalSheet` comes last, with no bump: a browser adds a None
|
|
919
|
+
* component's CSS when the component first renders, after the styles of the components around
|
|
920
|
+
* it, so it loses to their extra attribute and wins a tie with them.
|
|
691
921
|
*/
|
|
692
922
|
merge(sheet, hostSheet) {
|
|
693
923
|
const entries = [];
|
|
694
924
|
const add = (from, bump) => {
|
|
695
925
|
if (!from)
|
|
696
926
|
return;
|
|
927
|
+
this.placeLayers(from);
|
|
697
928
|
for (const rule of from.rules) {
|
|
698
929
|
entries.push({ rule, sheet: from, weight: rule.specificity + bump });
|
|
699
930
|
}
|
|
@@ -701,7 +932,43 @@ export class StyleResolver {
|
|
|
701
932
|
add(this.globalSheet, 0);
|
|
702
933
|
add(hostSheet, COMPONENT_SPECIFICITY_BUMP);
|
|
703
934
|
add(sheet, COMPONENT_SPECIFICITY_BUMP);
|
|
704
|
-
|
|
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);
|
|
939
|
+
}
|
|
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);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Gives each of a sheet's layers its place among every sheet's, the first time the sheet is
|
|
946
|
+
* merged, and each of its layered rules that place. Sheets are merged in the order a document
|
|
947
|
+
* holds them: the app's global sheet, then each component's as it first renders.
|
|
948
|
+
*/
|
|
949
|
+
placeLayers(sheet) {
|
|
950
|
+
if (!sheet.layers || this.layeredSheets.has(sheet))
|
|
951
|
+
return;
|
|
952
|
+
this.layeredSheets.add(sheet);
|
|
953
|
+
// A block with no name is a layer of this sheet's alone.
|
|
954
|
+
const unnamed = `\0${this.unnamedLayers++}`;
|
|
955
|
+
const places = sheet.layers.map((path) => {
|
|
956
|
+
const place = [];
|
|
957
|
+
let names = this.layerNames;
|
|
958
|
+
for (const part of path) {
|
|
959
|
+
const name = part.startsWith('\0') ? unnamed + part : part;
|
|
960
|
+
let nested = names.nested.get(name);
|
|
961
|
+
if (!nested)
|
|
962
|
+
names.nested.set(name, (nested = { place: names.nested.size, nested: new Map() }));
|
|
963
|
+
place.push(nested.place);
|
|
964
|
+
names = nested;
|
|
965
|
+
}
|
|
966
|
+
return place;
|
|
967
|
+
});
|
|
968
|
+
for (const rule of sheet.rules) {
|
|
969
|
+
if (rule.layer !== undefined)
|
|
970
|
+
this.layerPlaces.set(rule, places[rule.layer]);
|
|
971
|
+
}
|
|
705
972
|
}
|
|
706
973
|
/**
|
|
707
974
|
* A node's style from the rules it matched, shared with every other node that matched the same
|
|
@@ -713,8 +980,11 @@ export class StyleResolver {
|
|
|
713
980
|
* written to after, so sharing it is safe; each node still has a cache and a context of its own.
|
|
714
981
|
*/
|
|
715
982
|
styled(node, matched, parentTokens, parentInherited) {
|
|
716
|
-
|
|
717
|
-
|
|
983
|
+
// Shared between nodes that match alike, unless the node sets something of its own.
|
|
984
|
+
const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
|
|
985
|
+
if (node.customProperties || inline || node.boundStyle) {
|
|
986
|
+
return this.style(node, matched, parentTokens, parentInherited, inline);
|
|
987
|
+
}
|
|
718
988
|
if (this.sharedGeneration !== this.generation) {
|
|
719
989
|
this.shared.clear();
|
|
720
990
|
this.sharedGeneration = this.generation;
|
|
@@ -744,8 +1014,15 @@ export class StyleResolver {
|
|
|
744
1014
|
return id;
|
|
745
1015
|
}
|
|
746
1016
|
/** A node's style from the rules it matched, worked out. */
|
|
747
|
-
style(node, matched, parentTokens, parentInherited) {
|
|
748
|
-
|
|
1017
|
+
style(node, matched, parentTokens, parentInherited, inline = null) {
|
|
1018
|
+
// Inline style is the last normal declaration, under every important one.
|
|
1019
|
+
const bound = node.boundStyle;
|
|
1020
|
+
const result = this.cascade(inline || bound
|
|
1021
|
+
? [
|
|
1022
|
+
...matched,
|
|
1023
|
+
{ declarations: inline ?? EMPTY, ...(bound && { deferred: bound }) },
|
|
1024
|
+
]
|
|
1025
|
+
: matched);
|
|
749
1026
|
// Tokens are in scope for this node's own declarations as well as its descendants', so they
|
|
750
1027
|
// are merged before any `var()` here is resolved.
|
|
751
1028
|
const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
|
|
@@ -755,11 +1032,11 @@ export class StyleResolver {
|
|
|
755
1032
|
}
|
|
756
1033
|
if (own['borderStyle'] === 'none')
|
|
757
1034
|
drawNoBorder(own);
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
};
|
|
1035
|
+
// `pointer-events: inherit` won the cascade: what the parent hands down stands.
|
|
1036
|
+
if (own['pointerEvents'] === 'inherit')
|
|
1037
|
+
delete own['pointerEvents'];
|
|
1038
|
+
const style = { ...parentInherited, ...own };
|
|
1039
|
+
return { style, inherited: decorate(style, inheritFrom(parentInherited, own), own), tokens };
|
|
763
1040
|
}
|
|
764
1041
|
/** The rules that apply to a node, weakest first. */
|
|
765
1042
|
matched(node, entries) {
|
|
@@ -776,8 +1053,9 @@ export class StyleResolver {
|
|
|
776
1053
|
const important = {};
|
|
777
1054
|
let hasImportant = false;
|
|
778
1055
|
let tokens = null;
|
|
1056
|
+
let importantTokens = null;
|
|
779
1057
|
let deferred = null;
|
|
780
|
-
for (const rule of matched) {
|
|
1058
|
+
for (const rule of this.layered ? byImportance(matched, this.layerPlaces) : matched) {
|
|
781
1059
|
Object.assign(normal, rule.declarations);
|
|
782
1060
|
if (rule.important) {
|
|
783
1061
|
Object.assign(important, rule.important);
|
|
@@ -785,8 +1063,14 @@ export class StyleResolver {
|
|
|
785
1063
|
}
|
|
786
1064
|
if (rule.tokens)
|
|
787
1065
|
Object.assign((tokens ??= {}), rule.tokens);
|
|
1066
|
+
// An important custom property beats every plain one, whatever the rules' order.
|
|
1067
|
+
if (rule.importantTokens) {
|
|
1068
|
+
Object.assign((importantTokens ??= {}), pick(rule.tokens, rule.importantTokens));
|
|
1069
|
+
}
|
|
788
1070
|
deferred = carryDeferred(deferred, rule);
|
|
789
1071
|
}
|
|
1072
|
+
if (importantTokens)
|
|
1073
|
+
Object.assign(tokens, importantTokens);
|
|
790
1074
|
return {
|
|
791
1075
|
declarations: hasImportant ? { ...normal, ...important } : normal,
|
|
792
1076
|
important: hasImportant ? important : null,
|
|
@@ -862,13 +1146,17 @@ export class StyleResolver {
|
|
|
862
1146
|
if (settled === undefined && this.onUndefinedToken) {
|
|
863
1147
|
this.reportUndefined(declaration, tokens);
|
|
864
1148
|
}
|
|
1149
|
+
const outranked = (prop) => !declaration.important && important !== null && prop in important;
|
|
1150
|
+
// Said only of a display that is written: an important one wins over the token's.
|
|
1151
|
+
const unread = settled === undefined && declaration.kind === 'display';
|
|
1152
|
+
if (unread && this.onUnreadDisplay && !outranked('display')) {
|
|
1153
|
+
this.reportDisplay(declaration, tokens);
|
|
1154
|
+
}
|
|
865
1155
|
const value = settled ?? declaration.unset;
|
|
866
|
-
if (value === undefined)
|
|
867
|
-
continue;
|
|
868
1156
|
for (const prop of declaration.props) {
|
|
869
|
-
if (
|
|
1157
|
+
if (outranked(prop))
|
|
870
1158
|
continue;
|
|
871
|
-
own
|
|
1159
|
+
write(own, prop, value, declaration.line !== undefined);
|
|
872
1160
|
}
|
|
873
1161
|
}
|
|
874
1162
|
}
|
|
@@ -879,6 +1167,8 @@ export class StyleResolver {
|
|
|
879
1167
|
}
|
|
880
1168
|
if (declaration.compute)
|
|
881
1169
|
return this.computed(declaration, own, parentInherited);
|
|
1170
|
+
if (declaration.line)
|
|
1171
|
+
return this.settledLine(declaration, own, parentInherited, tokens);
|
|
882
1172
|
const value = referenced(declaration, tokens);
|
|
883
1173
|
if (declaration.whenSet !== undefined)
|
|
884
1174
|
return value === undefined ? undefined : declaration.whenSet;
|
|
@@ -893,6 +1183,35 @@ export class StyleResolver {
|
|
|
893
1183
|
? this.settledWithin(value, declaration, own, parentInherited, tokens)
|
|
894
1184
|
: value;
|
|
895
1185
|
}
|
|
1186
|
+
/** A line's longhands from its tokens, with a width in `em` worked out against the font size. */
|
|
1187
|
+
settledLine(declaration, own, parentInherited, tokens) {
|
|
1188
|
+
const line = declaration.line;
|
|
1189
|
+
const values = lineValues(line, tokens);
|
|
1190
|
+
if (!values)
|
|
1191
|
+
return undefined;
|
|
1192
|
+
if (line.colour !== undefined) {
|
|
1193
|
+
// A colour that cannot be worked out makes the whole line invalid, as an unset token does.
|
|
1194
|
+
const colour = this.settledWithin(line.colour, declaration, own, parentInherited, tokens);
|
|
1195
|
+
if (colour === undefined)
|
|
1196
|
+
return undefined;
|
|
1197
|
+
for (const prop of line.colors)
|
|
1198
|
+
values[prop] = colour;
|
|
1199
|
+
}
|
|
1200
|
+
// `currentcolor` is the colour in scope, which only the node knows.
|
|
1201
|
+
if (values[line.colors[0]] === 'currentcolor') {
|
|
1202
|
+
const colour = own['color'] ?? parentInherited['color'] ?? 'black';
|
|
1203
|
+
for (const prop of line.colors)
|
|
1204
|
+
values[prop] = colour;
|
|
1205
|
+
}
|
|
1206
|
+
const width = values[line.widths[0]];
|
|
1207
|
+
if (!width?.__defer)
|
|
1208
|
+
return values;
|
|
1209
|
+
// No less than zero, as CSS clamps a line's width: `calc(var(--w) - 2px)` with an em token.
|
|
1210
|
+
const points = this.computed({ ...declaration, compute: width.__defer }, own, parentInherited);
|
|
1211
|
+
for (const prop of line.widths)
|
|
1212
|
+
values[prop] = Math.max(points, 0);
|
|
1213
|
+
return values;
|
|
1214
|
+
}
|
|
896
1215
|
/** A reference with nothing defined for it and nothing to fall back to. */
|
|
897
1216
|
reportUndefined(declaration, tokens) {
|
|
898
1217
|
const name = declaration.reference;
|
|
@@ -902,7 +1221,14 @@ export class StyleResolver {
|
|
|
902
1221
|
return;
|
|
903
1222
|
if (declaration.alternatives?.some((alternative) => alternative in tokens))
|
|
904
1223
|
return;
|
|
905
|
-
this.onUndefinedToken(name, declaration.props);
|
|
1224
|
+
this.onUndefinedToken(name, declaration.props, declaration.on);
|
|
1225
|
+
}
|
|
1226
|
+
/** A display token set to a value native has no display for. */
|
|
1227
|
+
reportDisplay(declaration, tokens) {
|
|
1228
|
+
const names = [declaration.reference, ...(declaration.alternatives ?? [])];
|
|
1229
|
+
const name = names.find((candidate) => tokens[candidate] !== undefined);
|
|
1230
|
+
if (name)
|
|
1231
|
+
this.onUnreadDisplay(name, formOf(tokens[name], 'display'));
|
|
906
1232
|
}
|
|
907
1233
|
/**
|
|
908
1234
|
* A structured value with every deferred length and colour inside it settled, or undefined when
|
|
@@ -924,10 +1250,15 @@ export class StyleResolver {
|
|
|
924
1250
|
const fill = (part) => this.filledIn(part, declaration, own, parentInherited, tokens);
|
|
925
1251
|
if (Array.isArray(value))
|
|
926
1252
|
return this.filledList(value, fill, tokens);
|
|
927
|
-
const marked = settledMarker(value, tokens);
|
|
928
1253
|
// `currentcolor` is the colour in scope, which only the node knows: Tailwind's ring default.
|
|
1254
|
+
// On `color` itself that is the inherited colour, as CSS reads it.
|
|
1255
|
+
const current = () => {
|
|
1256
|
+
const ownColour = declaration.props.includes('color') ? undefined : own['color'];
|
|
1257
|
+
return (ownColour ?? parentInherited['color'] ?? 'black');
|
|
1258
|
+
};
|
|
1259
|
+
const marked = settledMarker(value, tokens, current);
|
|
929
1260
|
if (marked === 'currentcolor')
|
|
930
|
-
return
|
|
1261
|
+
return current();
|
|
931
1262
|
if (marked !== NOT_A_MARKER)
|
|
932
1263
|
return marked ?? UNSETTLED;
|
|
933
1264
|
const pending = value.__defer;
|
|
@@ -957,7 +1288,7 @@ export class StyleResolver {
|
|
|
957
1288
|
parts.push(filled);
|
|
958
1289
|
continue;
|
|
959
1290
|
}
|
|
960
|
-
const filled = fill(
|
|
1291
|
+
const filled = fill(substitute(slot.marker, tokens, (token) => slotEntries(token, slot.kind)));
|
|
961
1292
|
if (filled === UNSETTLED || !Array.isArray(filled))
|
|
962
1293
|
return UNSETTLED;
|
|
963
1294
|
parts.push(...filled);
|
|
@@ -1013,10 +1344,15 @@ function settlingOrder(deferred) {
|
|
|
1013
1344
|
*/
|
|
1014
1345
|
function tokensInScope(parentTokens, ruleTokens, custom) {
|
|
1015
1346
|
const own = custom ? { ...ruleTokens, ...custom } : ruleTokens;
|
|
1016
|
-
return own ? resolveAliases(own, { ...parentTokens, ...own }) : parentTokens;
|
|
1347
|
+
return own ? resolveAliases(own, { ...parentTokens, ...own }, parentTokens) : parentTokens;
|
|
1017
1348
|
}
|
|
1018
|
-
function resolveAliases(own, merged) {
|
|
1349
|
+
function resolveAliases(own, merged, parentTokens) {
|
|
1019
1350
|
const names = Object.keys(own);
|
|
1351
|
+
for (const name of names) {
|
|
1352
|
+
const word = own[name].keyword;
|
|
1353
|
+
if (word !== undefined && CSS_WIDE.has(word))
|
|
1354
|
+
cssWide(merged, name, word, parentTokens);
|
|
1355
|
+
}
|
|
1020
1356
|
const aliases = names.filter((name) => own[name].alias);
|
|
1021
1357
|
followAliases(aliases, merged);
|
|
1022
1358
|
// Left to work out: each token made of others and each alias that came to one, by a link or a
|
|
@@ -1026,6 +1362,25 @@ function resolveAliases(own, merged) {
|
|
|
1026
1362
|
settleDerived(own, merged, pending);
|
|
1027
1363
|
return merged;
|
|
1028
1364
|
}
|
|
1365
|
+
const CSS_WIDE = new Set([
|
|
1366
|
+
'initial',
|
|
1367
|
+
'inherit',
|
|
1368
|
+
'unset',
|
|
1369
|
+
'revert',
|
|
1370
|
+
'revert-layer',
|
|
1371
|
+
]);
|
|
1372
|
+
/**
|
|
1373
|
+
* A token set to a CSS-wide keyword, as CSS has one: its parent's value for `inherit` and `unset`,
|
|
1374
|
+
* since a custom property inherits, and otherwise its initial value, which is to be unset. Tailwind
|
|
1375
|
+
* writes `--tw-blur: initial` for each filter slot a node has not filled.
|
|
1376
|
+
*/
|
|
1377
|
+
function cssWide(merged, name, word, parentTokens) {
|
|
1378
|
+
const inherited = word === 'inherit' || word === 'unset' ? parentTokens[name] : undefined;
|
|
1379
|
+
if (inherited)
|
|
1380
|
+
merged[name] = inherited;
|
|
1381
|
+
else
|
|
1382
|
+
delete merged[name];
|
|
1383
|
+
}
|
|
1029
1384
|
/** What a token not yet worked out reads as while another is: set, with no form of any kind. */
|
|
1030
1385
|
const PENDING = Object.freeze({});
|
|
1031
1386
|
/**
|
|
@@ -1051,7 +1406,7 @@ function settleDerived(own, merged, pending) {
|
|
|
1051
1406
|
for (const name of pending) {
|
|
1052
1407
|
read = new Set();
|
|
1053
1408
|
const token = own[name];
|
|
1054
|
-
const value = token.alias ? substitutedIn(token, view) :
|
|
1409
|
+
const value = token.alias ? substitutedIn(token, view) : workedOut(token, view);
|
|
1055
1410
|
if (read.size) {
|
|
1056
1411
|
waitingOn.set(name, read);
|
|
1057
1412
|
continue;
|
|
@@ -1077,7 +1432,87 @@ function substitutedIn(token, tokens) {
|
|
|
1077
1432
|
return target;
|
|
1078
1433
|
link = link.fallback;
|
|
1079
1434
|
}
|
|
1080
|
-
return link && isDerived(link) ?
|
|
1435
|
+
return link && isDerived(link) ? workedOut(link, tokens) : link;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* A token that is set but makes no valid value. A use site finds it set, so takes no fallback, and
|
|
1439
|
+
* in no form, so its property is unset, as CSS substitutes such a token and then unsets it.
|
|
1440
|
+
*/
|
|
1441
|
+
const INVALID = Object.freeze({});
|
|
1442
|
+
/**
|
|
1443
|
+
* A token made of others, worked out: invalid when it makes nothing valid, as `hsl()` of a
|
|
1444
|
+
* percentage hue does, and unset only when a `var()` in it has nothing to be substituted with.
|
|
1445
|
+
*/
|
|
1446
|
+
function workedOut(token, tokens) {
|
|
1447
|
+
return derived(token, tokens) ?? (substitutable(token, tokens) ? INVALID : undefined);
|
|
1448
|
+
}
|
|
1449
|
+
/** Whether each `var()` in a token made of others names a token that is set, or has a fallback. */
|
|
1450
|
+
function substitutable(token, tokens) {
|
|
1451
|
+
if (token.hsl)
|
|
1452
|
+
return hslSubstitutable(token.hsl, tokens);
|
|
1453
|
+
if (token.deferredColour)
|
|
1454
|
+
return colourSubstitutable(token.deferredColour, tokens);
|
|
1455
|
+
// Each marker is one reading of the same text, and a fallback one cannot read is dropped from it.
|
|
1456
|
+
return token.deferredCalc.some((marker) => calcSubstitutable(marker.expression, tokens));
|
|
1457
|
+
}
|
|
1458
|
+
function hslSubstitutable(hsl, tokens) {
|
|
1459
|
+
return [hsl.h, hsl.s, hsl.l, hsl.alpha].every((channel) => typeof channel !== 'object' ||
|
|
1460
|
+
tokens[channel.reference] !== undefined ||
|
|
1461
|
+
channel.fallback !== undefined);
|
|
1462
|
+
}
|
|
1463
|
+
function calcSubstitutable(expression, tokens) {
|
|
1464
|
+
if (Array.isArray(expression)) {
|
|
1465
|
+
return expression.slice(1).every((side) => calcSubstitutable(side, tokens));
|
|
1466
|
+
}
|
|
1467
|
+
if (typeof expression !== 'object' || 'percentage' in expression)
|
|
1468
|
+
return true;
|
|
1469
|
+
const leaf = expression;
|
|
1470
|
+
if (tokens[leaf.reference] !== undefined)
|
|
1471
|
+
return true;
|
|
1472
|
+
return leaf.fallback !== undefined && calcSubstitutable(leaf.fallback, tokens);
|
|
1473
|
+
}
|
|
1474
|
+
function colourSubstitutable(expression, tokens) {
|
|
1475
|
+
if ('color' in expression)
|
|
1476
|
+
return true;
|
|
1477
|
+
if ('hsl' in expression)
|
|
1478
|
+
return hslSubstitutable(expression.hsl, tokens);
|
|
1479
|
+
if ('relative' in expression)
|
|
1480
|
+
return colourSubstitutable(expression.relative.from, tokens);
|
|
1481
|
+
if ('mix' in expression) {
|
|
1482
|
+
const { a, b } = expression.mix;
|
|
1483
|
+
return colourSubstitutable(a, tokens) && colourSubstitutable(b, tokens);
|
|
1484
|
+
}
|
|
1485
|
+
if ('channels' in expression) {
|
|
1486
|
+
const { channels, alpha } = expression;
|
|
1487
|
+
const set = (one) => tokens[one.reference] !== undefined || one.fallback !== undefined;
|
|
1488
|
+
return set(channels) && (typeof alpha !== 'object' || set(alpha));
|
|
1489
|
+
}
|
|
1490
|
+
return referenceSubstitutable(expression, tokens);
|
|
1491
|
+
}
|
|
1492
|
+
/** A colour's `var()`: a token or alternative that is set, or a fallback that can be worked out. */
|
|
1493
|
+
function referenceSubstitutable(expression, tokens) {
|
|
1494
|
+
const names = [expression.reference, ...(expression.alternatives ?? [])];
|
|
1495
|
+
if (names.some((name) => tokens[name] !== undefined))
|
|
1496
|
+
return true;
|
|
1497
|
+
if (expression.fallback !== undefined)
|
|
1498
|
+
return true;
|
|
1499
|
+
const token = expression.fallbackToken;
|
|
1500
|
+
return token !== undefined && (!isDerived(token) || substitutable(token, tokens));
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* A reference read from its token when that is set, in whatever form `read` finds, and its
|
|
1504
|
+
* fallback only when it is not.
|
|
1505
|
+
*/
|
|
1506
|
+
function substitute(marker, tokens, read) {
|
|
1507
|
+
const token = tokens[marker.reference];
|
|
1508
|
+
return token === undefined ? marker.fallback : read(token);
|
|
1509
|
+
}
|
|
1510
|
+
/** The first of the names that is set: the one a `var()` and its `var()` fallbacks substitute. */
|
|
1511
|
+
function firstSet(names, tokens) {
|
|
1512
|
+
for (const name of names)
|
|
1513
|
+
if (tokens[name] !== undefined)
|
|
1514
|
+
return tokens[name];
|
|
1515
|
+
return undefined;
|
|
1081
1516
|
}
|
|
1082
1517
|
/** The names among `names` that reach themselves by what each waits on. */
|
|
1083
1518
|
function inCycles(names, waitingOn) {
|
|
@@ -1134,36 +1569,78 @@ function calcToken(markers, tokens) {
|
|
|
1134
1569
|
token = { ...token, length: value };
|
|
1135
1570
|
else if (typeof value === 'number')
|
|
1136
1571
|
token = { ...token, number: value };
|
|
1572
|
+
// Arithmetic of a percentage, `calc(var(--half) * 2)`, is a percentage, as a token written
|
|
1573
|
+
// `100%` is, so an hsl() does not read it as a bare number of percent.
|
|
1574
|
+
const percentage = marker.kind === 'number' && calcType(marker.expression, tokens) === '%';
|
|
1575
|
+
if (percentage && typeof value === 'number' && token?.length === undefined) {
|
|
1576
|
+
token = { ...token, length: `${Math.round(value * 100000) / 1000}%` };
|
|
1577
|
+
}
|
|
1137
1578
|
}
|
|
1138
1579
|
return token;
|
|
1139
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* What a calc() of numbers and percentage tokens makes, as CSS types it: a number, `''`, a
|
|
1583
|
+
* percentage, `'%'`, or neither, as a percentage added to a number is, or one percentage
|
|
1584
|
+
* multiplied by another.
|
|
1585
|
+
*/
|
|
1586
|
+
function calcType(expression, tokens) {
|
|
1587
|
+
if (typeof expression === 'number')
|
|
1588
|
+
return '';
|
|
1589
|
+
if (!Array.isArray(expression))
|
|
1590
|
+
return leafType(expression, tokens);
|
|
1591
|
+
const [op, a, b] = expression;
|
|
1592
|
+
const left = calcType(a, tokens);
|
|
1593
|
+
const right = calcType(b, tokens);
|
|
1594
|
+
if (left === undefined || right === undefined)
|
|
1595
|
+
return undefined;
|
|
1596
|
+
if (op === '*')
|
|
1597
|
+
return left && right ? undefined : left || right;
|
|
1598
|
+
if (op === '/')
|
|
1599
|
+
return right ? undefined : left;
|
|
1600
|
+
return left === right ? left : undefined;
|
|
1601
|
+
}
|
|
1602
|
+
/** What one token in a calc() is, as `calcType` reads it: its fallback's type when it is unset. */
|
|
1603
|
+
function leafType(leaf, tokens) {
|
|
1604
|
+
if ('percentage' in leaf)
|
|
1605
|
+
return '%';
|
|
1606
|
+
const token = tokens[leaf.reference];
|
|
1607
|
+
if (!token)
|
|
1608
|
+
return leaf.fallback === undefined ? undefined : calcType(leaf.fallback, tokens);
|
|
1609
|
+
return typeof token.length === 'string' && token.length.endsWith('%') ? '%' : '';
|
|
1610
|
+
}
|
|
1140
1611
|
/** An `hsl()` token's channels, read from the tokens in scope, as the colour they make. */
|
|
1141
1612
|
function resolveHsl(hsl, tokens) {
|
|
1142
|
-
const h = hslChannel(hsl.h, tokens,
|
|
1143
|
-
const s = hslChannel(hsl.s, tokens);
|
|
1144
|
-
const l = hslChannel(hsl.l, tokens);
|
|
1145
|
-
const alpha = hsl.alpha === undefined ? 1 : hslChannel(hsl.alpha, tokens);
|
|
1613
|
+
const h = hslChannel(hsl.h, tokens, 'hue');
|
|
1614
|
+
const s = hslChannel(hsl.s, tokens, hsl.legacy ? 'legacy' : 'percentage');
|
|
1615
|
+
const l = hslChannel(hsl.l, tokens, hsl.legacy ? 'legacy' : 'percentage');
|
|
1616
|
+
const alpha = hsl.alpha === undefined ? 1 : hslChannel(hsl.alpha, tokens, 'alpha');
|
|
1146
1617
|
if (h === undefined || s === undefined || l === undefined || alpha === undefined) {
|
|
1147
1618
|
return undefined;
|
|
1148
1619
|
}
|
|
1149
1620
|
return hslToRgb(h, s, l, alpha);
|
|
1150
1621
|
}
|
|
1151
1622
|
/**
|
|
1152
|
-
* A channel's number, or undefined when its token is the wrong kind for the slot
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1623
|
+
* A channel's number, or undefined when its token is the wrong kind for the slot, as the token's
|
|
1624
|
+
* text substituted into the hsl() would read: a hue is a number or an angle, read in degrees, as
|
|
1625
|
+
* `0.5turn` is 180 and not 0.5, and never a percentage; a saturation or a lightness is a
|
|
1626
|
+
* percentage, or a bare number of percent, `--s: 100`, though not in the legacy comma syntax; an
|
|
1627
|
+
* alpha is a number or a percentage. The fallback is only for a token that is not set, as a set
|
|
1628
|
+
* one is substituted, whatever it holds.
|
|
1156
1629
|
*/
|
|
1157
|
-
function hslChannel(channel, tokens,
|
|
1630
|
+
function hslChannel(channel, tokens, slot) {
|
|
1158
1631
|
if (typeof channel === 'number')
|
|
1159
1632
|
return channel;
|
|
1160
1633
|
const token = tokens[channel.reference];
|
|
1161
1634
|
if (!token)
|
|
1162
1635
|
return channel.fallback;
|
|
1163
|
-
if (!hue)
|
|
1164
|
-
return token.angle === undefined ? token.number : undefined;
|
|
1165
1636
|
const percentage = typeof token.length === 'string' && token.length.endsWith('%');
|
|
1166
|
-
|
|
1637
|
+
if (slot === 'hue')
|
|
1638
|
+
return percentage ? undefined : (token.angle ?? token.number);
|
|
1639
|
+
if (token.angle !== undefined || token.number === undefined)
|
|
1640
|
+
return undefined;
|
|
1641
|
+
if (percentage || slot === 'alpha')
|
|
1642
|
+
return token.number;
|
|
1643
|
+
return slot === 'legacy' ? undefined : token.number / 100;
|
|
1167
1644
|
}
|
|
1168
1645
|
/**
|
|
1169
1646
|
* `hsl(h, s, l)` converted to sRGB, printed the same way `fromChannels` prints an rgba() built
|
|
@@ -1232,28 +1709,236 @@ function substituted(value, tokens, path, depth) {
|
|
|
1232
1709
|
function referenced(declaration, tokens) {
|
|
1233
1710
|
if (declaration.gradient)
|
|
1234
1711
|
return paintLayers(declaration.gradient, tokens);
|
|
1235
|
-
|
|
1712
|
+
// The first token set is substituted, whatever it holds: one of the wrong kind leaves the
|
|
1713
|
+
// property unset rather than trying the next, or the fallback.
|
|
1714
|
+
const names = [declaration.reference, ...(declaration.alternatives ?? [])];
|
|
1715
|
+
const token = firstSet(names, tokens);
|
|
1716
|
+
let value = token ? tokenForm(token, declaration.kind) : fallbackOf(declaration, tokens);
|
|
1236
1717
|
// `calc(var(--n) * 1px)`: the arithmetic gives a unitless token its unit, which is the usual
|
|
1237
|
-
// way to turn a count into a length. So
|
|
1238
|
-
if (
|
|
1239
|
-
value =
|
|
1240
|
-
}
|
|
1241
|
-
for (const alternative of declaration.alternatives ?? []) {
|
|
1242
|
-
if (value !== undefined)
|
|
1243
|
-
break;
|
|
1244
|
-
value = formOf(tokens[alternative], declaration.kind);
|
|
1718
|
+
// way to turn a count into a length. So it reads the bare number, and a length is no such thing.
|
|
1719
|
+
if (token && declaration.adjust?.number && declaration.kind === 'length') {
|
|
1720
|
+
value = token.number;
|
|
1245
1721
|
}
|
|
1246
|
-
|
|
1722
|
+
if (declaration.kind === 'display')
|
|
1723
|
+
return displayOf(value);
|
|
1247
1724
|
const base = CHANNEL_KINDS.has(declaration.kind)
|
|
1248
1725
|
? fromChannels(value, declaration.alpha, tokens, declaration.space)
|
|
1249
1726
|
: value;
|
|
1250
1727
|
return declaration.adjust ? adjusted(base, declaration.adjust) : base;
|
|
1251
1728
|
}
|
|
1729
|
+
/**
|
|
1730
|
+
* What native displays for a keyword: flex, none or contents. A block, inline, inline-block,
|
|
1731
|
+
* flow-root or inline-flex box is flex, as the compiler reads one written out, and so is `flow`,
|
|
1732
|
+
* which is a block. Anything else, a grid or a table, is no display native has, so the property
|
|
1733
|
+
* is unset.
|
|
1734
|
+
*/
|
|
1735
|
+
const DISPLAYS = new Map([
|
|
1736
|
+
...['flex', 'block', 'inline', 'inline-block', 'flow-root', 'inline-flex', 'flow'].map((word) => [word, 'flex']),
|
|
1737
|
+
['none', 'none'],
|
|
1738
|
+
['contents', 'contents'],
|
|
1739
|
+
]);
|
|
1740
|
+
/**
|
|
1741
|
+
* The two-keyword form, an outer display and an inner one in either order: `inline flex` is
|
|
1742
|
+
* `inline-flex` and `block flow` is `block`, as Chrome computes them, so each is flex here.
|
|
1743
|
+
*/
|
|
1744
|
+
const OUTER = new Set(['block', 'inline']);
|
|
1745
|
+
const INNER = new Set(['flow', 'flow-root', 'flex']);
|
|
1746
|
+
function displayOf(value) {
|
|
1747
|
+
if (typeof value !== 'string')
|
|
1748
|
+
return undefined;
|
|
1749
|
+
const words = value.toLowerCase().split(CSS_SPACE);
|
|
1750
|
+
if (words.length === 1)
|
|
1751
|
+
return DISPLAYS.get(words[0]);
|
|
1752
|
+
const [first, second] = words;
|
|
1753
|
+
const pair = (OUTER.has(first) && INNER.has(second)) || (INNER.has(first) && OUTER.has(second));
|
|
1754
|
+
return words.length === 2 && pair ? 'flex' : undefined;
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* A set token's value in the form wanted. A token of `currentColor` is the colour in scope where
|
|
1758
|
+
* it is used, not where it is set, so it is the marker the node fills in.
|
|
1759
|
+
*/
|
|
1760
|
+
function tokenForm(token, kind) {
|
|
1761
|
+
const value = formOf(token, kind);
|
|
1762
|
+
return value === undefined && kind === 'color' && isCurrentColour(token) ? CURRENT_COLOUR : value;
|
|
1763
|
+
}
|
|
1764
|
+
const BOUND_NUMBERS = new Set([
|
|
1765
|
+
'opacity',
|
|
1766
|
+
'flex',
|
|
1767
|
+
'flexGrow',
|
|
1768
|
+
'flexShrink',
|
|
1769
|
+
'zIndex',
|
|
1770
|
+
'aspectRatio',
|
|
1771
|
+
'elevation',
|
|
1772
|
+
]);
|
|
1773
|
+
const BOUND_LENGTH = /^(?:(?:min|max)?(?:width|height)|top|right|bottom|left|start|end|gap|fontSize|letterSpacing)$|(?:margin|padding|inset|Width|Radius|Gap|Basis)/i;
|
|
1774
|
+
/** The properties whose token is read in a form of its own. */
|
|
1775
|
+
const BOUND_KINDS = {
|
|
1776
|
+
display: 'display',
|
|
1777
|
+
transform: 'transform',
|
|
1778
|
+
boxShadow: 'shadow',
|
|
1779
|
+
filter: 'filter',
|
|
1780
|
+
};
|
|
1781
|
+
/** What a token is read as for a bound declaration of `prop`, as the compiler says for a rule. */
|
|
1782
|
+
function boundKind(prop) {
|
|
1783
|
+
// `fill` and `stroke` are a shape's paint, which is a colour.
|
|
1784
|
+
if (/color$/i.test(prop) || prop === 'fill' || prop === 'stroke')
|
|
1785
|
+
return 'color';
|
|
1786
|
+
if (prop === 'fontWeight')
|
|
1787
|
+
return 'weight';
|
|
1788
|
+
if (prop === 'fontFamily')
|
|
1789
|
+
return 'family';
|
|
1790
|
+
if (prop === 'lineHeight')
|
|
1791
|
+
return 'lineHeight';
|
|
1792
|
+
if (prop in BOUND_KINDS)
|
|
1793
|
+
return BOUND_KINDS[prop];
|
|
1794
|
+
if (BOUND_NUMBERS.has(prop))
|
|
1795
|
+
return 'number';
|
|
1796
|
+
return BOUND_LENGTH.test(prop) ? 'length' : 'keyword';
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* A bound declaration whose value is one `var()`, `var(--brand, red)`, as the deferred declaration
|
|
1800
|
+
* the compiler writes for the same value in a rule. `token` is the value as a token, which is an
|
|
1801
|
+
* alias when it is a `var()`; null for any other.
|
|
1802
|
+
*
|
|
1803
|
+
* ponytail: the whole value has to be the `var()`. One inside a longer value, `calc()` included,
|
|
1804
|
+
* is what the compiler parses at build time and nothing here does; write the sum in a custom
|
|
1805
|
+
* property, `[style.--gap]`, and read that in the stylesheet.
|
|
1806
|
+
*/
|
|
1807
|
+
export function boundDeclaration(prop, token, on) {
|
|
1808
|
+
if (!token?.alias)
|
|
1809
|
+
return null;
|
|
1810
|
+
const kind = boundKind(prop);
|
|
1811
|
+
const [reference, ...alternatives] = aliasesOf(token);
|
|
1812
|
+
for (; token?.alias; token = token.fallback)
|
|
1813
|
+
;
|
|
1814
|
+
// `tokenForm`, so a fallback of `currentColor` is the colour of the element, as in a rule.
|
|
1815
|
+
const fallback = token && !isDerived(token) ? tokenForm(token, kind) : undefined;
|
|
1816
|
+
return {
|
|
1817
|
+
props: [prop],
|
|
1818
|
+
...(on === undefined ? {} : { on }),
|
|
1819
|
+
kind,
|
|
1820
|
+
reference: reference,
|
|
1821
|
+
...(alternatives.length ? { alternatives } : {}),
|
|
1822
|
+
...(fallback !== undefined ? { fallback } : {}),
|
|
1823
|
+
...(token && isDerived(token) ? { fallbackToken: token } : {}),
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
/** The names a `var()` tries in turn: its own, then each `var()` nested as its fallback. */
|
|
1827
|
+
function aliasesOf(token) {
|
|
1828
|
+
const names = [];
|
|
1829
|
+
for (; token?.alias; token = token.fallback)
|
|
1830
|
+
names.push(token.alias);
|
|
1831
|
+
return names;
|
|
1832
|
+
}
|
|
1252
1833
|
/** The fallback written, or one made of other tokens, worked out from the tokens where it is used. */
|
|
1253
1834
|
function fallbackOf(declaration, tokens) {
|
|
1254
1835
|
const token = declaration.fallbackToken;
|
|
1255
1836
|
return declaration.fallback ?? (token && formOf(derived(token, tokens), declaration.kind));
|
|
1256
1837
|
}
|
|
1838
|
+
/**
|
|
1839
|
+
* One longhand of a deferred declaration written to the node's own style. Unset when the value is
|
|
1840
|
+
* nothing, as CSS has a declaration invalid once its tokens are known: it still wins the cascade,
|
|
1841
|
+
* so what a weaker rule set goes, and the property inherits or starts over. A line's value is one
|
|
1842
|
+
* per longhand, and leaves the ones it was written with alone.
|
|
1843
|
+
*/
|
|
1844
|
+
function write(own, prop, value, perProp) {
|
|
1845
|
+
if (value === undefined)
|
|
1846
|
+
delete own[prop];
|
|
1847
|
+
else if (!perProp)
|
|
1848
|
+
own[prop] = value;
|
|
1849
|
+
else if (prop in value) {
|
|
1850
|
+
own[prop] = value[prop];
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
/** The styles a line's token can be, as the compiler reads a written one. */
|
|
1854
|
+
const LINE_STYLES = new Set([
|
|
1855
|
+
'solid',
|
|
1856
|
+
'dashed',
|
|
1857
|
+
'dotted',
|
|
1858
|
+
'double',
|
|
1859
|
+
'groove',
|
|
1860
|
+
'ridge',
|
|
1861
|
+
'inset',
|
|
1862
|
+
'none',
|
|
1863
|
+
'hidden',
|
|
1864
|
+
]);
|
|
1865
|
+
const LINE_WIDTHS = { thin: 1, medium: 3, thick: 5 };
|
|
1866
|
+
/** What a line's token substitutes to: a token, or nothing at all, `var(--x,)` or `--x: ;`. */
|
|
1867
|
+
const NOTHING = Symbol('nothing');
|
|
1868
|
+
/**
|
|
1869
|
+
* A line's longhands, each token given the one role its form says and the written parts did not
|
|
1870
|
+
* take. Undefined when a token is unset with nothing to fall back to, fits no role left, or would
|
|
1871
|
+
* take a role twice: CSS makes the whole shorthand invalid then.
|
|
1872
|
+
*/
|
|
1873
|
+
function lineValues(line, tokens) {
|
|
1874
|
+
const open = new Set(line.roles);
|
|
1875
|
+
const values = {};
|
|
1876
|
+
for (const reference of line.references) {
|
|
1877
|
+
const token = lineToken(reference, tokens);
|
|
1878
|
+
if (token === undefined)
|
|
1879
|
+
return undefined;
|
|
1880
|
+
if (token === NOTHING)
|
|
1881
|
+
continue;
|
|
1882
|
+
const found = reference.adjust
|
|
1883
|
+
? computedWidth(token, open, reference.adjust)
|
|
1884
|
+
: lineRole(token, open);
|
|
1885
|
+
if (!found)
|
|
1886
|
+
return undefined;
|
|
1887
|
+
open.delete(found.role);
|
|
1888
|
+
fillRole(line, values, found.role, found.value);
|
|
1889
|
+
}
|
|
1890
|
+
// A part no token filled takes its initial value, as one left out of a written line does.
|
|
1891
|
+
for (const role of open)
|
|
1892
|
+
fillRole(line, values, role, LINE_INITIAL[role]);
|
|
1893
|
+
return values;
|
|
1894
|
+
}
|
|
1895
|
+
/** A token of `currentColor`, in any case. */
|
|
1896
|
+
const isCurrentColour = (token) => token?.keyword?.toLowerCase() === 'currentcolor';
|
|
1897
|
+
/** The marker the compiler writes for `currentColor`, which the node fills in. */
|
|
1898
|
+
const CURRENT_COLOUR = { __colour: { color: 'currentcolor' } };
|
|
1899
|
+
/** What a line's width, style and colour are when it leaves them out. */
|
|
1900
|
+
const LINE_INITIAL = {
|
|
1901
|
+
width: LINE_WIDTHS['medium'],
|
|
1902
|
+
style: 'none',
|
|
1903
|
+
color: 'currentcolor',
|
|
1904
|
+
};
|
|
1905
|
+
function fillRole(line, values, role, value) {
|
|
1906
|
+
const props = { width: line.widths, color: line.colors, style: [line.style] }[role];
|
|
1907
|
+
for (const prop of props)
|
|
1908
|
+
values[prop] = value;
|
|
1909
|
+
}
|
|
1910
|
+
function lineToken(reference, tokens) {
|
|
1911
|
+
const set = firstSet([reference.reference, ...(reference.alternatives ?? [])], tokens);
|
|
1912
|
+
if (set)
|
|
1913
|
+
return set.keyword?.trim() === '' ? NOTHING : set;
|
|
1914
|
+
if (reference.empty)
|
|
1915
|
+
return NOTHING;
|
|
1916
|
+
const { fallback } = reference;
|
|
1917
|
+
return fallback && isDerived(fallback) ? derived(fallback, tokens) : fallback;
|
|
1918
|
+
}
|
|
1919
|
+
/** The role a token's form says it has, among those still open. */
|
|
1920
|
+
function lineRole(token, open) {
|
|
1921
|
+
const word = token.keyword;
|
|
1922
|
+
const width = word && word in LINE_WIDTHS ? LINE_WIDTHS[word] : token.length;
|
|
1923
|
+
if (open.has('width') && (typeof width === 'number' || isDeferredLength(width))) {
|
|
1924
|
+
return { role: 'width', value: width };
|
|
1925
|
+
}
|
|
1926
|
+
if (open.has('style') && word && LINE_STYLES.has(word))
|
|
1927
|
+
return { role: 'style', value: word };
|
|
1928
|
+
const color = isCurrentColour(token) ? 'currentcolor' : formOf(token, 'color');
|
|
1929
|
+
return open.has('color') && color !== undefined ? { role: 'color', value: color } : undefined;
|
|
1930
|
+
}
|
|
1931
|
+
/** A `calc()` of a token, which is a width, and no less than zero, as CSS clamps one. */
|
|
1932
|
+
function computedWidth(token, open, adjust) {
|
|
1933
|
+
// A bare number only when the calc() gives it its unit: `calc(var(--n) * 1px)`.
|
|
1934
|
+
const length = adjust.number ? token.number : token.length;
|
|
1935
|
+
if (!open.has('width') || (typeof length !== 'number' && !isDeferredLength(length))) {
|
|
1936
|
+
return undefined;
|
|
1937
|
+
}
|
|
1938
|
+
const value = adjusted(length, adjust);
|
|
1939
|
+
return { role: 'width', value: typeof value === 'number' ? Math.max(value, 0) : value };
|
|
1940
|
+
}
|
|
1941
|
+
const isDeferredLength = (value) => typeof value === 'object' && value !== null && '__defer' in value;
|
|
1257
1942
|
const CHANNEL_KINDS = new Set(['channels', 'hslChannels']);
|
|
1258
1943
|
/**
|
|
1259
1944
|
* `13, 110, 253` as the colour it is, at the alpha the token beside it says, if one does: the
|
|
@@ -1263,8 +1948,8 @@ function fromChannels(value, alpha, tokens, space) {
|
|
|
1263
1948
|
if (!Array.isArray(value) || value.length !== 3)
|
|
1264
1949
|
return undefined;
|
|
1265
1950
|
// An alpha naming a token nothing set, with no fallback, is no colour: CSS drops it.
|
|
1266
|
-
const opacity = alpha ? (
|
|
1267
|
-
if (opacity
|
|
1951
|
+
const opacity = alpha ? substitute(alpha, tokens, (token) => token.number) : 1;
|
|
1952
|
+
if (typeof opacity !== 'number')
|
|
1268
1953
|
return undefined;
|
|
1269
1954
|
if (space === 'hsl')
|
|
1270
1955
|
return hslToRgb(value[0], value[1], value[2], opacity);
|
|
@@ -1295,29 +1980,47 @@ function paintLayers(gradient, tokens) {
|
|
|
1295
1980
|
*/
|
|
1296
1981
|
function paintGradient(template, tokens) {
|
|
1297
1982
|
const colorStops = paintedStops(template, tokens);
|
|
1298
|
-
if (colorStops.length < 2)
|
|
1983
|
+
if (!colorStops || colorStops.length < 2)
|
|
1299
1984
|
return undefined;
|
|
1300
1985
|
const position = template.position && placed(template.position, tokens);
|
|
1301
1986
|
if (position === null)
|
|
1302
1987
|
return undefined;
|
|
1303
1988
|
return [{ ...template, colorStops, ...(position ? { position } : {}) }];
|
|
1304
1989
|
}
|
|
1305
|
-
/**
|
|
1990
|
+
/**
|
|
1991
|
+
* A template's stops with their tokens read, leaving out any whose colour is not set. Undefined
|
|
1992
|
+
* when a stop's colour or position token is set but no such thing, which makes the gradient
|
|
1993
|
+
* invalid, as CSS substitutes a set token rather than falling back.
|
|
1994
|
+
*/
|
|
1306
1995
|
function paintedStops(template, tokens) {
|
|
1307
1996
|
const colorStops = [];
|
|
1308
1997
|
for (const stop of template.colorStops) {
|
|
1309
|
-
const
|
|
1998
|
+
const token = stop.colour ? undefined : tokens[stop.reference];
|
|
1999
|
+
const color = stop.colour ? resolveColour(stop.colour, tokens) : token?.color;
|
|
2000
|
+
// `none` is how the Tailwind flattening writes `via-none`: the stop left out, not invalid.
|
|
2001
|
+
if (color === undefined && token && token.keyword !== 'none')
|
|
2002
|
+
return undefined;
|
|
1310
2003
|
if (color === undefined)
|
|
1311
2004
|
continue;
|
|
1312
|
-
const position = stop
|
|
1313
|
-
|
|
1314
|
-
|
|
2005
|
+
const position = stopPosition(stop, tokens);
|
|
2006
|
+
if (position === INVALID_POSITION)
|
|
2007
|
+
return undefined;
|
|
1315
2008
|
// Written even when there is none: Fabric looks the key up before it looks at the value, and
|
|
1316
2009
|
// silently skips a stop that has not got one.
|
|
1317
2010
|
colorStops.push({ color, position: position ?? null });
|
|
1318
2011
|
}
|
|
1319
2012
|
return colorStops;
|
|
1320
2013
|
}
|
|
2014
|
+
const INVALID_POSITION = Symbol('invalid position');
|
|
2015
|
+
/** A stop's position: its token's when that is set, the one written otherwise. */
|
|
2016
|
+
function stopPosition(stop, tokens) {
|
|
2017
|
+
if (!stop.positionReference)
|
|
2018
|
+
return stop.position;
|
|
2019
|
+
const token = tokens[stop.positionReference];
|
|
2020
|
+
if (!token)
|
|
2021
|
+
return stop.position;
|
|
2022
|
+
return token.length ?? INVALID_POSITION;
|
|
2023
|
+
}
|
|
1321
2024
|
/**
|
|
1322
2025
|
* A radial gradient's centre with any token in it read, or null when a token it needs is not set
|
|
1323
2026
|
* and has nothing written beside it: CSS would not paint the gradient then, and neither does this.
|
|
@@ -1329,8 +2032,8 @@ function placed(position, tokens) {
|
|
|
1329
2032
|
out[edge] = offset;
|
|
1330
2033
|
continue;
|
|
1331
2034
|
}
|
|
1332
|
-
const
|
|
1333
|
-
const value = tokens
|
|
2035
|
+
const marker = offset;
|
|
2036
|
+
const value = substitute(marker, tokens, (token) => token.length);
|
|
1334
2037
|
if (value === undefined || typeof value === 'object')
|
|
1335
2038
|
return null;
|
|
1336
2039
|
out[edge] = value;
|
|
@@ -1340,8 +2043,11 @@ function placed(position, tokens) {
|
|
|
1340
2043
|
/**
|
|
1341
2044
|
* A colour expression with its tokens looked up and any mix in it worked out. Undefined when a
|
|
1342
2045
|
* token it needs is not defined anywhere and has nothing to fall back to.
|
|
2046
|
+
*
|
|
2047
|
+
* `current` is the node's colour, for a `currentColor` inside a mix or a relative colour. One that
|
|
2048
|
+
* is the whole expression is answered as `currentcolor`, for the caller to fill in.
|
|
1343
2049
|
*/
|
|
1344
|
-
function resolveColour(expression, tokens) {
|
|
2050
|
+
function resolveColour(expression, tokens, current) {
|
|
1345
2051
|
if ('color' in expression)
|
|
1346
2052
|
return expression.color;
|
|
1347
2053
|
if ('channels' in expression)
|
|
@@ -1352,15 +2058,39 @@ function resolveColour(expression, tokens) {
|
|
|
1352
2058
|
return tokenColour(expression, tokens);
|
|
1353
2059
|
if ('relative' in expression) {
|
|
1354
2060
|
const { space, from, channels, alpha } = expression.relative;
|
|
1355
|
-
const origin =
|
|
2061
|
+
const origin = innerColour(from, tokens, current);
|
|
1356
2062
|
return origin === undefined ? undefined : relativeColour(space, origin, channels, alpha);
|
|
1357
2063
|
}
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
2064
|
+
const sides = mixSides(expression.mix, tokens, current);
|
|
2065
|
+
return sides && mixColours(expression.mix.space, ...sides, expression.mix.hue);
|
|
2066
|
+
}
|
|
2067
|
+
/** A colour inside another: `currentColor` there, written or in a token, is the node's colour. */
|
|
2068
|
+
function innerColour(expression, tokens, current) {
|
|
2069
|
+
if (!current)
|
|
2070
|
+
return resolveColour(expression, tokens);
|
|
2071
|
+
if ('reference' in expression) {
|
|
2072
|
+
const names = [expression.reference, ...(expression.alternatives ?? [])];
|
|
2073
|
+
if (isCurrentColour(firstSet(names, tokens)))
|
|
2074
|
+
return current();
|
|
2075
|
+
}
|
|
2076
|
+
const colour = resolveColour(expression, tokens, current);
|
|
2077
|
+
return colour?.toLowerCase() === 'currentcolor' ? current() : colour;
|
|
2078
|
+
}
|
|
2079
|
+
/** A mix's two sides, a mix inside it unrounded, as CSS rounds only the colour at the end. */
|
|
2080
|
+
function mixSides(mix, tokens, current) {
|
|
2081
|
+
const colour = (side) => 'mix' in side ? unroundedMix(side.mix, tokens, current) : innerColour(side, tokens, current);
|
|
2082
|
+
const first = colour(mix.a);
|
|
2083
|
+
const second = colour(mix.b);
|
|
1361
2084
|
if (first === undefined || second === undefined)
|
|
1362
2085
|
return undefined;
|
|
1363
|
-
return
|
|
2086
|
+
return [
|
|
2087
|
+
{ colour: first, percentage: mix.aPercentage },
|
|
2088
|
+
{ colour: second, percentage: mix.bPercentage },
|
|
2089
|
+
];
|
|
2090
|
+
}
|
|
2091
|
+
function unroundedMix(mix, tokens, current) {
|
|
2092
|
+
const sides = mixSides(mix, tokens, current);
|
|
2093
|
+
return sides && mixChannels(mix.space, ...sides, mix.hue);
|
|
1364
2094
|
}
|
|
1365
2095
|
/** A calc marker worked out, as its slot takes it; undefined when a token in it is not set. */
|
|
1366
2096
|
function resolveCalc(marker, tokens) {
|
|
@@ -1369,6 +2099,9 @@ function resolveCalc(marker, tokens) {
|
|
|
1369
2099
|
return percentage;
|
|
1370
2100
|
if (bareNumberAsLength(marker, tokens) || notAnAngle(marker, tokens))
|
|
1371
2101
|
return undefined;
|
|
2102
|
+
// A percentage and a number added together, `calc(var(--n) + 10%)`, are neither, and invalid.
|
|
2103
|
+
if (marker.kind === 'number' && calcType(marker.expression, tokens) === undefined)
|
|
2104
|
+
return undefined;
|
|
1372
2105
|
const value = calculated(marker.expression, marker.kind, tokens);
|
|
1373
2106
|
if (value === undefined || !Number.isFinite(value))
|
|
1374
2107
|
return undefined;
|
|
@@ -1422,11 +2155,8 @@ function wholePercentage(marker, tokens) {
|
|
|
1422
2155
|
function calculated(expression, kind, tokens) {
|
|
1423
2156
|
if (typeof expression === 'number')
|
|
1424
2157
|
return expression;
|
|
1425
|
-
if (!Array.isArray(expression))
|
|
1426
|
-
|
|
1427
|
-
const own = tokenNumber(tokens[leaf.reference], kind);
|
|
1428
|
-
return (own ?? (leaf.fallback === undefined ? undefined : calculated(leaf.fallback, kind, tokens)));
|
|
1429
|
-
}
|
|
2158
|
+
if (!Array.isArray(expression))
|
|
2159
|
+
return leafValue(expression, kind, tokens);
|
|
1430
2160
|
const [op, a, b] = expression;
|
|
1431
2161
|
const left = calculated(a, kind, tokens);
|
|
1432
2162
|
const right = calculated(b, kind, tokens);
|
|
@@ -1444,6 +2174,15 @@ function calculated(expression, kind, tokens) {
|
|
|
1444
2174
|
return Math.min(left, right);
|
|
1445
2175
|
return left / right;
|
|
1446
2176
|
}
|
|
2177
|
+
/** A written percentage, or a token, as `calculated` reads it: its fallback when it is unset. */
|
|
2178
|
+
function leafValue(leaf, kind, tokens) {
|
|
2179
|
+
if ('percentage' in leaf)
|
|
2180
|
+
return leaf.percentage;
|
|
2181
|
+
const token = tokens[leaf.reference];
|
|
2182
|
+
if (token)
|
|
2183
|
+
return tokenNumber(token, kind);
|
|
2184
|
+
return leaf.fallback === undefined ? undefined : calculated(leaf.fallback, kind, tokens);
|
|
2185
|
+
}
|
|
1447
2186
|
/** Where each kind of slot reads a token from, before its bare number. */
|
|
1448
2187
|
const FORM_OF_KIND = {
|
|
1449
2188
|
length: (token) => token.length,
|
|
@@ -1468,6 +2207,10 @@ function tokenNumber(token, kind) {
|
|
|
1468
2207
|
return undefined;
|
|
1469
2208
|
return token.number;
|
|
1470
2209
|
}
|
|
2210
|
+
/** What a token set in a slot stands for: its entries, or none for one set to nothing, `--tw-blur: ;`. */
|
|
2211
|
+
function slotEntries(token, kind) {
|
|
2212
|
+
return formOf(token, kind) ?? (token.keyword?.trim() === '' ? [] : undefined);
|
|
2213
|
+
}
|
|
1471
2214
|
/** The slot a list entry can be instead of an entry: one per list property that has them. */
|
|
1472
2215
|
const SLOT_KINDS = {
|
|
1473
2216
|
__shadows: 'shadow',
|
|
@@ -1488,7 +2231,9 @@ function slotOf(part) {
|
|
|
1488
2231
|
return undefined;
|
|
1489
2232
|
}
|
|
1490
2233
|
function resolveLength(marker, tokens) {
|
|
1491
|
-
|
|
2234
|
+
// A number when the arithmetic gives it its unit, as `referenced` reads one.
|
|
2235
|
+
const form = marker.adjust?.number ? 'number' : 'length';
|
|
2236
|
+
const value = substitute(marker, tokens, (token) => formOf(token, form));
|
|
1492
2237
|
if (typeof value !== 'number')
|
|
1493
2238
|
return undefined;
|
|
1494
2239
|
return marker.adjust ? adjusted(value, marker.adjust) : value;
|
|
@@ -1520,10 +2265,10 @@ const NEEDS_FILLING = new WeakMap();
|
|
|
1520
2265
|
/** What `settledMarker` says of a part that is not a token's marker at all. */
|
|
1521
2266
|
const NOT_A_MARKER = Symbol('not a marker');
|
|
1522
2267
|
/** A colour, length or calc marker settled from the tokens, or undefined when one cannot be. */
|
|
1523
|
-
function settledMarker(value, tokens) {
|
|
2268
|
+
function settledMarker(value, tokens, current) {
|
|
1524
2269
|
const colour = colourMarker(value);
|
|
1525
2270
|
if (colour)
|
|
1526
|
-
return resolveColour(colour, tokens);
|
|
2271
|
+
return resolveColour(colour, tokens, current);
|
|
1527
2272
|
// Whether a shadow is inset, from a token that is the word or nothing: `ring-inset`.
|
|
1528
2273
|
const inset = value.__inset;
|
|
1529
2274
|
if (inset)
|
|
@@ -1557,7 +2302,7 @@ const UNSETTLED = Symbol('unsettled');
|
|
|
1557
2302
|
function channelsColour(expression, tokens) {
|
|
1558
2303
|
const { space } = expression.channels;
|
|
1559
2304
|
const kind = space === 'hsl' ? 'hslChannels' : 'channels';
|
|
1560
|
-
const value =
|
|
2305
|
+
const value = substitute(expression.channels, tokens, (token) => formOf(token, kind));
|
|
1561
2306
|
const { alpha } = expression;
|
|
1562
2307
|
if (typeof alpha !== 'number')
|
|
1563
2308
|
return fromChannels(value, alpha, tokens, space);
|
|
@@ -1566,13 +2311,23 @@ function channelsColour(expression, tokens) {
|
|
|
1566
2311
|
}
|
|
1567
2312
|
/** A token's colour, or the first of its alternatives that is defined, or its fallback. */
|
|
1568
2313
|
function tokenColour(expression, tokens) {
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2314
|
+
// The first token set is substituted, whatever it holds, as `referenced` reads one.
|
|
2315
|
+
const set = firstSet([expression.reference, ...(expression.alternatives ?? [])], tokens);
|
|
2316
|
+
if (expression.orInset)
|
|
2317
|
+
return insetOrColour(set);
|
|
2318
|
+
if (set)
|
|
2319
|
+
return formOf(set, 'color');
|
|
2320
|
+
const token = expression.fallbackToken;
|
|
2321
|
+
return expression.fallback ?? (token && formOf(derived(token, tokens), 'color'));
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* A shadow's colour from a token that is the colour or the word `inset`: currentcolor, which the
|
|
2325
|
+
* node fills in, when it is `inset` or unset, and nothing, so no shadow, when it is anything else.
|
|
2326
|
+
*/
|
|
2327
|
+
function insetOrColour(token) {
|
|
2328
|
+
if (!token || token.keyword === 'inset')
|
|
2329
|
+
return 'currentcolor';
|
|
2330
|
+
return formOf(token, 'color');
|
|
1576
2331
|
}
|
|
1577
2332
|
/** A marker the compiler leaves in a structured value for a colour only the device can settle. */
|
|
1578
2333
|
function colourMarker(value) {
|