@ng-native/fabric 0.1.3 → 0.3.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/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 +93 -5
- package/dist/css.js +597 -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/engine.d.ts +142 -14
- package/dist/engine.js +451 -60
- package/dist/engine.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 +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inline-derived-token.d.ts +6 -0
- package/dist/inline-derived-token.js +487 -0
- package/dist/inline-derived-token.js.map +1 -0
- package/dist/inline-token.d.ts +6 -4
- package/dist/inline-token.js +47 -6
- package/dist/inline-token.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,26 @@
|
|
|
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
9
|
import { relativeColour } from "./relative-colour.js";
|
|
10
|
+
/** A run of CSS whitespace: spaces, tabs, newlines, carriage returns and form feeds, and no other. */
|
|
11
|
+
export const CSS_SPACE = /[ \t\n\r\f]+/;
|
|
10
12
|
/**
|
|
11
13
|
* Where a form a token does not have can be read from instead. A length is already a
|
|
12
14
|
* line-height and a single word is already a family, so neither is stored twice in every token
|
|
13
15
|
* that happens to be one. The compiler reads fallbacks with the same table.
|
|
14
16
|
*/
|
|
15
|
-
const STAND_IN = {
|
|
17
|
+
const STAND_IN = {
|
|
18
|
+
lineHeight: 'length',
|
|
19
|
+
family: 'keyword',
|
|
20
|
+
};
|
|
16
21
|
function formOf(token, kind) {
|
|
17
22
|
if (!token)
|
|
18
23
|
return undefined;
|
|
24
|
+
// A display is a word, read as one native has where it is used: see `displayOf`. Two words,
|
|
25
|
+
// `inline flex`, are a family when the stylesheet wrote them, as a font stack's first name is.
|
|
26
|
+
if (kind === 'display')
|
|
27
|
+
return token.keyword ?? token.family;
|
|
19
28
|
const stand = STAND_IN[kind];
|
|
20
29
|
return token[kind] ?? (stand ? token[stand] : undefined);
|
|
21
30
|
}
|
|
@@ -538,6 +547,12 @@ export class StyleResolver {
|
|
|
538
547
|
* component created it, which is what resets, utility classes and `:root` tokens all need.
|
|
539
548
|
*/
|
|
540
549
|
globalSheet;
|
|
550
|
+
/**
|
|
551
|
+
* Global sheets added once the engine is running, in the order they came: a
|
|
552
|
+
* `ViewEncapsulation.None` component's, which a browser adds to the document unscoped when the
|
|
553
|
+
* component first renders, after the application's own. See `addGlobalSheet`.
|
|
554
|
+
*/
|
|
555
|
+
addedSheets = [];
|
|
541
556
|
/**
|
|
542
557
|
* Custom properties the host supplies rather than the stylesheet: the safe-area insets, and
|
|
543
558
|
* whatever else is a property of the device instead of the design.
|
|
@@ -556,10 +571,46 @@ export class StyleResolver {
|
|
|
556
571
|
* which is dropped. Set only in development, where it is how the engine says so.
|
|
557
572
|
*/
|
|
558
573
|
onUndefinedToken = null;
|
|
574
|
+
/**
|
|
575
|
+
* Told of a `display: var()` whose token holds no display native has, `grid` or `table`, which
|
|
576
|
+
* leaves display unset. Set only in development, where it is how the engine says so: the
|
|
577
|
+
* compiler refuses the same value written out, but a token is only known here.
|
|
578
|
+
*/
|
|
579
|
+
onUnreadDisplay = null;
|
|
559
580
|
constructor(globalSheet, conditions) {
|
|
560
581
|
this.globalSheet = globalSheet;
|
|
561
582
|
this.conditions = conditions;
|
|
562
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* Match `sheet` against every node from now on, as a global sheet, after the application's;
|
|
586
|
+
* false when it already is. Every cached result goes, as on a theme switch: rules that were not
|
|
587
|
+
* there can now match anything. `:host` in it matches nothing, since no node hosts it.
|
|
588
|
+
*
|
|
589
|
+
* With `replacing`, a sheet added before, it takes that one's place, and so its order: a hot
|
|
590
|
+
* swap of the component's CSS edits the sheet where it is.
|
|
591
|
+
*/
|
|
592
|
+
addGlobalSheet(sheet, replacing) {
|
|
593
|
+
if (sheet === this.globalSheet || this.addedSheets.includes(sheet))
|
|
594
|
+
return false;
|
|
595
|
+
const at = replacing ? this.addedSheets.indexOf(replacing) : -1;
|
|
596
|
+
if (at === -1)
|
|
597
|
+
this.addedSheets.push(sheet);
|
|
598
|
+
else
|
|
599
|
+
this.addedSheets[at] = sheet;
|
|
600
|
+
this.merged = new WeakMap();
|
|
601
|
+
this.generation = ++generations;
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
/** Stop matching a sheet `addGlobalSheet` added; false when it is not one. */
|
|
605
|
+
removeGlobalSheet(sheet) {
|
|
606
|
+
const at = this.addedSheets.indexOf(sheet);
|
|
607
|
+
if (at === -1)
|
|
608
|
+
return false;
|
|
609
|
+
this.addedSheets.splice(at, 1);
|
|
610
|
+
this.merged = new WeakMap();
|
|
611
|
+
this.generation = ++generations;
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
563
614
|
setConditions(next) {
|
|
564
615
|
this.conditions = next;
|
|
565
616
|
this.generation = ++generations;
|
|
@@ -633,6 +684,7 @@ export class StyleResolver {
|
|
|
633
684
|
return (node.sheet === null &&
|
|
634
685
|
node.hostSheet === null &&
|
|
635
686
|
this.globalSheet === null &&
|
|
687
|
+
this.addedSheets.length === 0 &&
|
|
636
688
|
!node.customProperties);
|
|
637
689
|
}
|
|
638
690
|
/**
|
|
@@ -640,10 +692,16 @@ export class StyleResolver {
|
|
|
640
692
|
*
|
|
641
693
|
* It still mints a context, because a rule further down may match on one of *its* classes.
|
|
642
694
|
* 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.
|
|
695
|
+
* an application that writes no CSS allocates nothing per node. Not when there are tokens to
|
|
696
|
+
* carry: the shared object holds none, and the surface root answering with it dropped every
|
|
697
|
+
* token the device had seeded.
|
|
644
698
|
*/
|
|
645
699
|
unstyled(node, epoch, parent, parentContext, parentInherited) {
|
|
646
|
-
|
|
700
|
+
const tokens = parent ? parent.tokens : this.tokensOnRoot;
|
|
701
|
+
if (parentInherited === EMPTY &&
|
|
702
|
+
parentContext === ROOT_CONTEXT &&
|
|
703
|
+
tokens === NO_TOKENS &&
|
|
704
|
+
!node.styleDirty) {
|
|
647
705
|
if (this.emptyCache.epoch !== epoch || this.emptyCache.generation !== this.generation) {
|
|
648
706
|
this.emptyCache = emptyCacheFor(epoch, this.generation);
|
|
649
707
|
}
|
|
@@ -657,7 +715,7 @@ export class StyleResolver {
|
|
|
657
715
|
parentContext,
|
|
658
716
|
style: parentInherited,
|
|
659
717
|
inherited: parentInherited,
|
|
660
|
-
tokens
|
|
718
|
+
tokens,
|
|
661
719
|
};
|
|
662
720
|
node.styleCache = passthrough;
|
|
663
721
|
node.styleDirty = false;
|
|
@@ -688,6 +746,10 @@ export class StyleResolver {
|
|
|
688
746
|
* created it. Both carry the bump; the creator comes later and so wins a tie, as the outer
|
|
689
747
|
* document does over `:host` on the web. The sort is stable, so within one sheet source order
|
|
690
748
|
* survives and a later sheet wins equal weight.
|
|
749
|
+
*
|
|
750
|
+
* A sheet added with `addGlobalSheet` comes last, with no bump: a browser adds a None
|
|
751
|
+
* component's CSS when the component first renders, after the styles of the components around
|
|
752
|
+
* it, so it loses to their extra attribute and wins a tie with them.
|
|
691
753
|
*/
|
|
692
754
|
merge(sheet, hostSheet) {
|
|
693
755
|
const entries = [];
|
|
@@ -701,6 +763,8 @@ export class StyleResolver {
|
|
|
701
763
|
add(this.globalSheet, 0);
|
|
702
764
|
add(hostSheet, COMPONENT_SPECIFICITY_BUMP);
|
|
703
765
|
add(sheet, COMPONENT_SPECIFICITY_BUMP);
|
|
766
|
+
for (const added of this.addedSheets)
|
|
767
|
+
add(added, 0);
|
|
704
768
|
return entries.sort((a, b) => a.weight - b.weight);
|
|
705
769
|
}
|
|
706
770
|
/**
|
|
@@ -862,13 +926,17 @@ export class StyleResolver {
|
|
|
862
926
|
if (settled === undefined && this.onUndefinedToken) {
|
|
863
927
|
this.reportUndefined(declaration, tokens);
|
|
864
928
|
}
|
|
929
|
+
const outranked = (prop) => !declaration.important && important !== null && prop in important;
|
|
930
|
+
// Said only of a display that is written: an important one wins over the token's.
|
|
931
|
+
const unread = settled === undefined && declaration.kind === 'display';
|
|
932
|
+
if (unread && this.onUnreadDisplay && !outranked('display')) {
|
|
933
|
+
this.reportDisplay(declaration, tokens);
|
|
934
|
+
}
|
|
865
935
|
const value = settled ?? declaration.unset;
|
|
866
|
-
if (value === undefined)
|
|
867
|
-
continue;
|
|
868
936
|
for (const prop of declaration.props) {
|
|
869
|
-
if (
|
|
937
|
+
if (outranked(prop))
|
|
870
938
|
continue;
|
|
871
|
-
own
|
|
939
|
+
write(own, prop, value, declaration.line !== undefined);
|
|
872
940
|
}
|
|
873
941
|
}
|
|
874
942
|
}
|
|
@@ -879,6 +947,8 @@ export class StyleResolver {
|
|
|
879
947
|
}
|
|
880
948
|
if (declaration.compute)
|
|
881
949
|
return this.computed(declaration, own, parentInherited);
|
|
950
|
+
if (declaration.line)
|
|
951
|
+
return this.settledLine(declaration, own, parentInherited, tokens);
|
|
882
952
|
const value = referenced(declaration, tokens);
|
|
883
953
|
if (declaration.whenSet !== undefined)
|
|
884
954
|
return value === undefined ? undefined : declaration.whenSet;
|
|
@@ -893,15 +963,45 @@ export class StyleResolver {
|
|
|
893
963
|
? this.settledWithin(value, declaration, own, parentInherited, tokens)
|
|
894
964
|
: value;
|
|
895
965
|
}
|
|
966
|
+
/** A line's longhands from its tokens, with a width in `em` worked out against the font size. */
|
|
967
|
+
settledLine(declaration, own, parentInherited, tokens) {
|
|
968
|
+
const line = declaration.line;
|
|
969
|
+
const values = lineValues(line, tokens);
|
|
970
|
+
if (!values)
|
|
971
|
+
return undefined;
|
|
972
|
+
// `currentcolor` is the colour in scope, which only the node knows.
|
|
973
|
+
if (values[line.colors[0]] === 'currentcolor') {
|
|
974
|
+
const colour = own['color'] ?? parentInherited['color'] ?? 'black';
|
|
975
|
+
for (const prop of line.colors)
|
|
976
|
+
values[prop] = colour;
|
|
977
|
+
}
|
|
978
|
+
const width = values[line.widths[0]];
|
|
979
|
+
if (!width?.__defer)
|
|
980
|
+
return values;
|
|
981
|
+
// No less than zero, as CSS clamps a line's width: `calc(var(--w) - 2px)` with an em token.
|
|
982
|
+
const points = this.computed({ ...declaration, compute: width.__defer }, own, parentInherited);
|
|
983
|
+
for (const prop of line.widths)
|
|
984
|
+
values[prop] = Math.max(points, 0);
|
|
985
|
+
return values;
|
|
986
|
+
}
|
|
896
987
|
/** A reference with nothing defined for it and nothing to fall back to. */
|
|
897
988
|
reportUndefined(declaration, tokens) {
|
|
898
989
|
const name = declaration.reference;
|
|
899
|
-
if (name === undefined ||
|
|
990
|
+
if (name === undefined || name in tokens)
|
|
991
|
+
return;
|
|
992
|
+
if (declaration.fallback !== undefined || declaration.fallbackToken)
|
|
900
993
|
return;
|
|
901
994
|
if (declaration.alternatives?.some((alternative) => alternative in tokens))
|
|
902
995
|
return;
|
|
903
996
|
this.onUndefinedToken(name, declaration.props);
|
|
904
997
|
}
|
|
998
|
+
/** A display token set to a value native has no display for. */
|
|
999
|
+
reportDisplay(declaration, tokens) {
|
|
1000
|
+
const names = [declaration.reference, ...(declaration.alternatives ?? [])];
|
|
1001
|
+
const name = names.find((candidate) => tokens[candidate] !== undefined);
|
|
1002
|
+
if (name)
|
|
1003
|
+
this.onUnreadDisplay(name, formOf(tokens[name], 'display'));
|
|
1004
|
+
}
|
|
905
1005
|
/**
|
|
906
1006
|
* A structured value with every deferred length and colour inside it settled, or undefined when
|
|
907
1007
|
* a colour in it cannot be: a shadow with no colour is not a shadow, as CSS reads it.
|
|
@@ -924,8 +1024,11 @@ export class StyleResolver {
|
|
|
924
1024
|
return this.filledList(value, fill, tokens);
|
|
925
1025
|
const marked = settledMarker(value, tokens);
|
|
926
1026
|
// `currentcolor` is the colour in scope, which only the node knows: Tailwind's ring default.
|
|
927
|
-
|
|
928
|
-
|
|
1027
|
+
// On `color` itself that is the inherited colour, as CSS reads it.
|
|
1028
|
+
if (marked === 'currentcolor') {
|
|
1029
|
+
const ownColour = declaration.props.includes('color') ? undefined : own['color'];
|
|
1030
|
+
return ownColour ?? parentInherited['color'] ?? 'black';
|
|
1031
|
+
}
|
|
929
1032
|
if (marked !== NOT_A_MARKER)
|
|
930
1033
|
return marked ?? UNSETTLED;
|
|
931
1034
|
const pending = value.__defer;
|
|
@@ -955,7 +1058,7 @@ export class StyleResolver {
|
|
|
955
1058
|
parts.push(filled);
|
|
956
1059
|
continue;
|
|
957
1060
|
}
|
|
958
|
-
const filled = fill(
|
|
1061
|
+
const filled = fill(substitute(slot.marker, tokens, (token) => slotEntries(token, slot.kind)));
|
|
959
1062
|
if (filled === UNSETTLED || !Array.isArray(filled))
|
|
960
1063
|
return UNSETTLED;
|
|
961
1064
|
parts.push(...filled);
|
|
@@ -1011,38 +1114,189 @@ function settlingOrder(deferred) {
|
|
|
1011
1114
|
*/
|
|
1012
1115
|
function tokensInScope(parentTokens, ruleTokens, custom) {
|
|
1013
1116
|
const own = custom ? { ...ruleTokens, ...custom } : ruleTokens;
|
|
1014
|
-
return own ? resolveAliases(own, { ...parentTokens, ...own }) : parentTokens;
|
|
1117
|
+
return own ? resolveAliases(own, { ...parentTokens, ...own }, parentTokens) : parentTokens;
|
|
1015
1118
|
}
|
|
1016
|
-
function resolveAliases(own, merged) {
|
|
1119
|
+
function resolveAliases(own, merged, parentTokens) {
|
|
1017
1120
|
const names = Object.keys(own);
|
|
1121
|
+
for (const name of names) {
|
|
1122
|
+
const word = own[name].keyword;
|
|
1123
|
+
if (word !== undefined && CSS_WIDE.has(word))
|
|
1124
|
+
cssWide(merged, name, word, parentTokens);
|
|
1125
|
+
}
|
|
1018
1126
|
const aliases = names.filter((name) => own[name].alias);
|
|
1019
1127
|
followAliases(aliases, merged);
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1128
|
+
// Left to work out: each token made of others and each alias that came to one, by a link or a
|
|
1129
|
+
// fallback, `var(--missing, calc(var(--gap) * 2))`, which is worked out here as well.
|
|
1130
|
+
const pending = new Set(names.filter((name) => isDerived(merged[name])));
|
|
1131
|
+
if (pending.size)
|
|
1132
|
+
settleDerived(own, merged, pending);
|
|
1133
|
+
return merged;
|
|
1134
|
+
}
|
|
1135
|
+
const CSS_WIDE = new Set([
|
|
1136
|
+
'initial',
|
|
1137
|
+
'inherit',
|
|
1138
|
+
'unset',
|
|
1139
|
+
'revert',
|
|
1140
|
+
'revert-layer',
|
|
1141
|
+
]);
|
|
1142
|
+
/**
|
|
1143
|
+
* A token set to a CSS-wide keyword, as CSS has one: its parent's value for `inherit` and `unset`,
|
|
1144
|
+
* since a custom property inherits, and otherwise its initial value, which is to be unset. Tailwind
|
|
1145
|
+
* writes `--tw-blur: initial` for each filter slot a node has not filled.
|
|
1146
|
+
*/
|
|
1147
|
+
function cssWide(merged, name, word, parentTokens) {
|
|
1148
|
+
const inherited = word === 'inherit' || word === 'unset' ? parentTokens[name] : undefined;
|
|
1149
|
+
if (inherited)
|
|
1150
|
+
merged[name] = inherited;
|
|
1151
|
+
else
|
|
1152
|
+
delete merged[name];
|
|
1153
|
+
}
|
|
1154
|
+
/** What a token not yet worked out reads as while another is: set, with no form of any kind. */
|
|
1155
|
+
const PENDING = Object.freeze({});
|
|
1156
|
+
/**
|
|
1157
|
+
* The tokens left to work out, each settled once none it reads is still to be, whatever order they
|
|
1158
|
+
* are defined in. One that reads another still to be waits, rather than taking a fallback: a
|
|
1159
|
+
* fallback is for a token that is unset or invalid. When every one left waits for another, those
|
|
1160
|
+
* in a cycle, counting only the references read, are invalid, and the rest go on without them,
|
|
1161
|
+
* taking their fallbacks, as CSS has it.
|
|
1162
|
+
*/
|
|
1163
|
+
function settleDerived(own, merged, pending) {
|
|
1164
|
+
const waitingOn = new Map();
|
|
1165
|
+
let read = new Set();
|
|
1166
|
+
const view = new Proxy(merged, {
|
|
1167
|
+
get: (tokens, name) => {
|
|
1168
|
+
if (!pending.has(name))
|
|
1169
|
+
return tokens[name];
|
|
1170
|
+
read.add(name);
|
|
1171
|
+
return PENDING;
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
while (pending.size) {
|
|
1175
|
+
let settled = false;
|
|
1176
|
+
for (const name of pending) {
|
|
1177
|
+
read = new Set();
|
|
1178
|
+
const token = own[name];
|
|
1179
|
+
const value = token.alias ? substitutedIn(token, view) : workedOut(token, view);
|
|
1180
|
+
if (read.size) {
|
|
1181
|
+
waitingOn.set(name, read);
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
pending.delete(name);
|
|
1185
|
+
settle(merged, name, value);
|
|
1032
1186
|
settled = true;
|
|
1033
|
-
|
|
1034
|
-
|
|
1187
|
+
}
|
|
1188
|
+
if (settled)
|
|
1189
|
+
continue;
|
|
1190
|
+
for (const name of inCycles(pending, waitingOn)) {
|
|
1191
|
+
pending.delete(name);
|
|
1192
|
+
delete merged[name];
|
|
1193
|
+
}
|
|
1035
1194
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1195
|
+
}
|
|
1196
|
+
/** An alias's target, or the first of its fallbacks that is set, with one made of others worked out. */
|
|
1197
|
+
function substitutedIn(token, tokens) {
|
|
1198
|
+
let link = token;
|
|
1199
|
+
while (link?.alias) {
|
|
1200
|
+
const target = tokens[link.alias];
|
|
1201
|
+
if (target !== undefined)
|
|
1202
|
+
return target;
|
|
1203
|
+
link = link.fallback;
|
|
1044
1204
|
}
|
|
1045
|
-
return
|
|
1205
|
+
return link && isDerived(link) ? workedOut(link, tokens) : link;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* A token that is set but makes no valid value. A use site finds it set, so takes no fallback, and
|
|
1209
|
+
* in no form, so its property is unset, as CSS substitutes such a token and then unsets it.
|
|
1210
|
+
*/
|
|
1211
|
+
const INVALID = Object.freeze({});
|
|
1212
|
+
/**
|
|
1213
|
+
* A token made of others, worked out: invalid when it makes nothing valid, as `hsl()` of a
|
|
1214
|
+
* percentage hue does, and unset only when a `var()` in it has nothing to be substituted with.
|
|
1215
|
+
*/
|
|
1216
|
+
function workedOut(token, tokens) {
|
|
1217
|
+
return derived(token, tokens) ?? (substitutable(token, tokens) ? INVALID : undefined);
|
|
1218
|
+
}
|
|
1219
|
+
/** Whether each `var()` in a token made of others names a token that is set, or has a fallback. */
|
|
1220
|
+
function substitutable(token, tokens) {
|
|
1221
|
+
if (token.hsl)
|
|
1222
|
+
return hslSubstitutable(token.hsl, tokens);
|
|
1223
|
+
if (token.deferredColour)
|
|
1224
|
+
return colourSubstitutable(token.deferredColour, tokens);
|
|
1225
|
+
// Each marker is one reading of the same text, and a fallback one cannot read is dropped from it.
|
|
1226
|
+
return token.deferredCalc.some((marker) => calcSubstitutable(marker.expression, tokens));
|
|
1227
|
+
}
|
|
1228
|
+
function hslSubstitutable(hsl, tokens) {
|
|
1229
|
+
return [hsl.h, hsl.s, hsl.l, hsl.alpha].every((channel) => typeof channel !== 'object' ||
|
|
1230
|
+
tokens[channel.reference] !== undefined ||
|
|
1231
|
+
channel.fallback !== undefined);
|
|
1232
|
+
}
|
|
1233
|
+
function calcSubstitutable(expression, tokens) {
|
|
1234
|
+
if (Array.isArray(expression)) {
|
|
1235
|
+
return expression.slice(1).every((side) => calcSubstitutable(side, tokens));
|
|
1236
|
+
}
|
|
1237
|
+
if (typeof expression !== 'object' || 'percentage' in expression)
|
|
1238
|
+
return true;
|
|
1239
|
+
const leaf = expression;
|
|
1240
|
+
if (tokens[leaf.reference] !== undefined)
|
|
1241
|
+
return true;
|
|
1242
|
+
return leaf.fallback !== undefined && calcSubstitutable(leaf.fallback, tokens);
|
|
1243
|
+
}
|
|
1244
|
+
function colourSubstitutable(expression, tokens) {
|
|
1245
|
+
if ('color' in expression)
|
|
1246
|
+
return true;
|
|
1247
|
+
if ('hsl' in expression)
|
|
1248
|
+
return hslSubstitutable(expression.hsl, tokens);
|
|
1249
|
+
if ('relative' in expression)
|
|
1250
|
+
return colourSubstitutable(expression.relative.from, tokens);
|
|
1251
|
+
if ('mix' in expression) {
|
|
1252
|
+
const { a, b } = expression.mix;
|
|
1253
|
+
return colourSubstitutable(a, tokens) && colourSubstitutable(b, tokens);
|
|
1254
|
+
}
|
|
1255
|
+
if ('channels' in expression) {
|
|
1256
|
+
const { channels, alpha } = expression;
|
|
1257
|
+
const set = (one) => tokens[one.reference] !== undefined || one.fallback !== undefined;
|
|
1258
|
+
return set(channels) && (typeof alpha !== 'object' || set(alpha));
|
|
1259
|
+
}
|
|
1260
|
+
return referenceSubstitutable(expression, tokens);
|
|
1261
|
+
}
|
|
1262
|
+
/** A colour's `var()`: a token or alternative that is set, or a fallback that can be worked out. */
|
|
1263
|
+
function referenceSubstitutable(expression, tokens) {
|
|
1264
|
+
const names = [expression.reference, ...(expression.alternatives ?? [])];
|
|
1265
|
+
if (names.some((name) => tokens[name] !== undefined))
|
|
1266
|
+
return true;
|
|
1267
|
+
if (expression.fallback !== undefined)
|
|
1268
|
+
return true;
|
|
1269
|
+
const token = expression.fallbackToken;
|
|
1270
|
+
return token !== undefined && (!isDerived(token) || substitutable(token, tokens));
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* A reference read from its token when that is set, in whatever form `read` finds, and its
|
|
1274
|
+
* fallback only when it is not.
|
|
1275
|
+
*/
|
|
1276
|
+
function substitute(marker, tokens, read) {
|
|
1277
|
+
const token = tokens[marker.reference];
|
|
1278
|
+
return token === undefined ? marker.fallback : read(token);
|
|
1279
|
+
}
|
|
1280
|
+
/** The first of the names that is set: the one a `var()` and its `var()` fallbacks substitute. */
|
|
1281
|
+
function firstSet(names, tokens) {
|
|
1282
|
+
for (const name of names)
|
|
1283
|
+
if (tokens[name] !== undefined)
|
|
1284
|
+
return tokens[name];
|
|
1285
|
+
return undefined;
|
|
1286
|
+
}
|
|
1287
|
+
/** The names among `names` that reach themselves by what each waits on. */
|
|
1288
|
+
function inCycles(names, waitingOn) {
|
|
1289
|
+
const reaches = (from, to, seen) => {
|
|
1290
|
+
for (const next of waitingOn.get(from) ?? []) {
|
|
1291
|
+
if (next === to)
|
|
1292
|
+
return true;
|
|
1293
|
+
if (names.has(next) && !seen.has(next) && seen.add(next) && reaches(next, to, seen)) {
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return false;
|
|
1298
|
+
};
|
|
1299
|
+
return [...names].filter((name) => reaches(name, name, new Set()));
|
|
1046
1300
|
}
|
|
1047
1301
|
/** Whether a token is made of others, and so worked out where it is defined. */
|
|
1048
1302
|
function isDerived(token) {
|
|
@@ -1085,24 +1339,78 @@ function calcToken(markers, tokens) {
|
|
|
1085
1339
|
token = { ...token, length: value };
|
|
1086
1340
|
else if (typeof value === 'number')
|
|
1087
1341
|
token = { ...token, number: value };
|
|
1342
|
+
// Arithmetic of a percentage, `calc(var(--half) * 2)`, is a percentage, as a token written
|
|
1343
|
+
// `100%` is, so an hsl() does not read it as a bare number of percent.
|
|
1344
|
+
const percentage = marker.kind === 'number' && calcType(marker.expression, tokens) === '%';
|
|
1345
|
+
if (percentage && typeof value === 'number' && token?.length === undefined) {
|
|
1346
|
+
token = { ...token, length: `${Math.round(value * 100000) / 1000}%` };
|
|
1347
|
+
}
|
|
1088
1348
|
}
|
|
1089
1349
|
return token;
|
|
1090
1350
|
}
|
|
1351
|
+
/**
|
|
1352
|
+
* What a calc() of numbers and percentage tokens makes, as CSS types it: a number, `''`, a
|
|
1353
|
+
* percentage, `'%'`, or neither, as a percentage added to a number is, or one percentage
|
|
1354
|
+
* multiplied by another.
|
|
1355
|
+
*/
|
|
1356
|
+
function calcType(expression, tokens) {
|
|
1357
|
+
if (typeof expression === 'number')
|
|
1358
|
+
return '';
|
|
1359
|
+
if (!Array.isArray(expression))
|
|
1360
|
+
return leafType(expression, tokens);
|
|
1361
|
+
const [op, a, b] = expression;
|
|
1362
|
+
const left = calcType(a, tokens);
|
|
1363
|
+
const right = calcType(b, tokens);
|
|
1364
|
+
if (left === undefined || right === undefined)
|
|
1365
|
+
return undefined;
|
|
1366
|
+
if (op === '*')
|
|
1367
|
+
return left && right ? undefined : left || right;
|
|
1368
|
+
if (op === '/')
|
|
1369
|
+
return right ? undefined : left;
|
|
1370
|
+
return left === right ? left : undefined;
|
|
1371
|
+
}
|
|
1372
|
+
/** What one token in a calc() is, as `calcType` reads it: its fallback's type when it is unset. */
|
|
1373
|
+
function leafType(leaf, tokens) {
|
|
1374
|
+
if ('percentage' in leaf)
|
|
1375
|
+
return '%';
|
|
1376
|
+
const token = tokens[leaf.reference];
|
|
1377
|
+
if (!token)
|
|
1378
|
+
return leaf.fallback === undefined ? undefined : calcType(leaf.fallback, tokens);
|
|
1379
|
+
return typeof token.length === 'string' && token.length.endsWith('%') ? '%' : '';
|
|
1380
|
+
}
|
|
1091
1381
|
/** An `hsl()` token's channels, read from the tokens in scope, as the colour they make. */
|
|
1092
1382
|
function resolveHsl(hsl, tokens) {
|
|
1093
|
-
const h = hslChannel(hsl.h, tokens);
|
|
1094
|
-
const s = hslChannel(hsl.s, tokens);
|
|
1095
|
-
const l = hslChannel(hsl.l, tokens);
|
|
1096
|
-
const alpha = hsl.alpha === undefined ? 1 : hslChannel(hsl.alpha, tokens);
|
|
1383
|
+
const h = hslChannel(hsl.h, tokens, 'hue');
|
|
1384
|
+
const s = hslChannel(hsl.s, tokens, hsl.legacy ? 'legacy' : 'percentage');
|
|
1385
|
+
const l = hslChannel(hsl.l, tokens, hsl.legacy ? 'legacy' : 'percentage');
|
|
1386
|
+
const alpha = hsl.alpha === undefined ? 1 : hslChannel(hsl.alpha, tokens, 'alpha');
|
|
1097
1387
|
if (h === undefined || s === undefined || l === undefined || alpha === undefined) {
|
|
1098
1388
|
return undefined;
|
|
1099
1389
|
}
|
|
1100
1390
|
return hslToRgb(h, s, l, alpha);
|
|
1101
1391
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1392
|
+
/**
|
|
1393
|
+
* A channel's number, or undefined when its token is the wrong kind for the slot, as the token's
|
|
1394
|
+
* text substituted into the hsl() would read: a hue is a number or an angle, read in degrees, as
|
|
1395
|
+
* `0.5turn` is 180 and not 0.5, and never a percentage; a saturation or a lightness is a
|
|
1396
|
+
* percentage, or a bare number of percent, `--s: 100`, though not in the legacy comma syntax; an
|
|
1397
|
+
* alpha is a number or a percentage. The fallback is only for a token that is not set, as a set
|
|
1398
|
+
* one is substituted, whatever it holds.
|
|
1399
|
+
*/
|
|
1400
|
+
function hslChannel(channel, tokens, slot) {
|
|
1401
|
+
if (typeof channel === 'number')
|
|
1402
|
+
return channel;
|
|
1403
|
+
const token = tokens[channel.reference];
|
|
1404
|
+
if (!token)
|
|
1405
|
+
return channel.fallback;
|
|
1406
|
+
const percentage = typeof token.length === 'string' && token.length.endsWith('%');
|
|
1407
|
+
if (slot === 'hue')
|
|
1408
|
+
return percentage ? undefined : (token.angle ?? token.number);
|
|
1409
|
+
if (token.angle !== undefined || token.number === undefined)
|
|
1410
|
+
return undefined;
|
|
1411
|
+
if (percentage || slot === 'alpha')
|
|
1412
|
+
return token.number;
|
|
1413
|
+
return slot === 'legacy' ? undefined : token.number / 100;
|
|
1106
1414
|
}
|
|
1107
1415
|
/**
|
|
1108
1416
|
* `hsl(h, s, l)` converted to sRGB, printed the same way `fromChannels` prints an rgba() built
|
|
@@ -1111,7 +1419,11 @@ function hslChannel(channel, tokens) {
|
|
|
1111
1419
|
* `h` is degrees; `s` and `l` are fractions, which is what a percentage token already reads as
|
|
1112
1420
|
* (see `formOf`), so nothing here rescales them.
|
|
1113
1421
|
*/
|
|
1114
|
-
function hslToRgb(h,
|
|
1422
|
+
function hslToRgb(h, saturation, l, opacity) {
|
|
1423
|
+
// Out of range as CSS Color 4 takes it: a saturation below 0 is 0, an alpha is clamped, and
|
|
1424
|
+
// anything else is converted as it is, each channel it makes then clamped into sRGB.
|
|
1425
|
+
const s = Math.max(0, saturation);
|
|
1426
|
+
const alpha = Math.min(1, Math.max(0, opacity));
|
|
1115
1427
|
const hue = ((h % 360) + 360) % 360;
|
|
1116
1428
|
const chroma = (1 - Math.abs(2 * l - 1)) * s;
|
|
1117
1429
|
const x = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
@@ -1127,9 +1439,7 @@ function hslToRgb(h, s, l, alpha) {
|
|
|
1127
1439
|
: hue < 300
|
|
1128
1440
|
? [x, 0, chroma]
|
|
1129
1441
|
: [chroma, 0, x];
|
|
1130
|
-
const r = Math.round((
|
|
1131
|
-
const g = Math.round((g1 + m) * 255);
|
|
1132
|
-
const b = Math.round((b1 + m) * 255);
|
|
1442
|
+
const [r, g, b] = [r1, g1, b1].map((c) => Math.round(Math.min(1, Math.max(0, c + m)) * 255));
|
|
1133
1443
|
return alpha >= 1
|
|
1134
1444
|
? `rgb(${r}, ${g}, ${b})`
|
|
1135
1445
|
: `rgba(${r}, ${g}, ${b}, ${Math.round(alpha * 1000) / 1000})`;
|
|
@@ -1169,23 +1479,167 @@ function substituted(value, tokens, path, depth) {
|
|
|
1169
1479
|
function referenced(declaration, tokens) {
|
|
1170
1480
|
if (declaration.gradient)
|
|
1171
1481
|
return paintLayers(declaration.gradient, tokens);
|
|
1172
|
-
|
|
1482
|
+
// The first token set is substituted, whatever it holds: one of the wrong kind leaves the
|
|
1483
|
+
// property unset rather than trying the next, or the fallback.
|
|
1484
|
+
const names = [declaration.reference, ...(declaration.alternatives ?? [])];
|
|
1485
|
+
const token = firstSet(names, tokens);
|
|
1486
|
+
let value = token ? tokenForm(token, declaration.kind) : fallbackOf(declaration, tokens);
|
|
1173
1487
|
// `calc(var(--n) * 1px)`: the arithmetic gives a unitless token its unit, which is the usual
|
|
1174
|
-
// way to turn a count into a length. So
|
|
1175
|
-
if (
|
|
1176
|
-
value =
|
|
1177
|
-
}
|
|
1178
|
-
for (const alternative of declaration.alternatives ?? []) {
|
|
1179
|
-
if (value !== undefined)
|
|
1180
|
-
break;
|
|
1181
|
-
value = formOf(tokens[alternative], declaration.kind);
|
|
1488
|
+
// way to turn a count into a length. So it reads the bare number, and a length is no such thing.
|
|
1489
|
+
if (token && declaration.adjust?.number && declaration.kind === 'length') {
|
|
1490
|
+
value = token.number;
|
|
1182
1491
|
}
|
|
1183
|
-
|
|
1492
|
+
if (declaration.kind === 'display')
|
|
1493
|
+
return displayOf(value);
|
|
1184
1494
|
const base = CHANNEL_KINDS.has(declaration.kind)
|
|
1185
1495
|
? fromChannels(value, declaration.alpha, tokens, declaration.space)
|
|
1186
1496
|
: value;
|
|
1187
1497
|
return declaration.adjust ? adjusted(base, declaration.adjust) : base;
|
|
1188
1498
|
}
|
|
1499
|
+
/**
|
|
1500
|
+
* What native displays for a keyword: flex, none or contents. A block, inline, inline-block,
|
|
1501
|
+
* flow-root or inline-flex box is flex, as the compiler reads one written out, and so is `flow`,
|
|
1502
|
+
* which is a block. Anything else, a grid or a table, is no display native has, so the property
|
|
1503
|
+
* is unset.
|
|
1504
|
+
*/
|
|
1505
|
+
const DISPLAYS = new Map([
|
|
1506
|
+
...['flex', 'block', 'inline', 'inline-block', 'flow-root', 'inline-flex', 'flow'].map((word) => [word, 'flex']),
|
|
1507
|
+
['none', 'none'],
|
|
1508
|
+
['contents', 'contents'],
|
|
1509
|
+
]);
|
|
1510
|
+
/**
|
|
1511
|
+
* The two-keyword form, an outer display and an inner one in either order: `inline flex` is
|
|
1512
|
+
* `inline-flex` and `block flow` is `block`, as Chrome computes them, so each is flex here.
|
|
1513
|
+
*/
|
|
1514
|
+
const OUTER = new Set(['block', 'inline']);
|
|
1515
|
+
const INNER = new Set(['flow', 'flow-root', 'flex']);
|
|
1516
|
+
function displayOf(value) {
|
|
1517
|
+
if (typeof value !== 'string')
|
|
1518
|
+
return undefined;
|
|
1519
|
+
const words = value.toLowerCase().split(CSS_SPACE);
|
|
1520
|
+
if (words.length === 1)
|
|
1521
|
+
return DISPLAYS.get(words[0]);
|
|
1522
|
+
const [first, second] = words;
|
|
1523
|
+
const pair = (OUTER.has(first) && INNER.has(second)) || (INNER.has(first) && OUTER.has(second));
|
|
1524
|
+
return words.length === 2 && pair ? 'flex' : undefined;
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* A set token's value in the form wanted. A token of `currentColor` is the colour in scope where
|
|
1528
|
+
* it is used, not where it is set, so it is the marker the node fills in.
|
|
1529
|
+
*/
|
|
1530
|
+
function tokenForm(token, kind) {
|
|
1531
|
+
const value = formOf(token, kind);
|
|
1532
|
+
return value === undefined && kind === 'color' && isCurrentColour(token) ? CURRENT_COLOUR : value;
|
|
1533
|
+
}
|
|
1534
|
+
/** The fallback written, or one made of other tokens, worked out from the tokens where it is used. */
|
|
1535
|
+
function fallbackOf(declaration, tokens) {
|
|
1536
|
+
const token = declaration.fallbackToken;
|
|
1537
|
+
return declaration.fallback ?? (token && formOf(derived(token, tokens), declaration.kind));
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* One longhand of a deferred declaration written to the node's own style. Unset when the value is
|
|
1541
|
+
* nothing, as CSS has a declaration invalid once its tokens are known: it still wins the cascade,
|
|
1542
|
+
* so what a weaker rule set goes, and the property inherits or starts over. A line's value is one
|
|
1543
|
+
* per longhand, and leaves the ones it was written with alone.
|
|
1544
|
+
*/
|
|
1545
|
+
function write(own, prop, value, perProp) {
|
|
1546
|
+
if (value === undefined)
|
|
1547
|
+
delete own[prop];
|
|
1548
|
+
else if (!perProp)
|
|
1549
|
+
own[prop] = value;
|
|
1550
|
+
else if (prop in value) {
|
|
1551
|
+
own[prop] = value[prop];
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
/** The styles a line's token can be, as the compiler reads a written one. */
|
|
1555
|
+
const LINE_STYLES = new Set([
|
|
1556
|
+
'solid',
|
|
1557
|
+
'dashed',
|
|
1558
|
+
'dotted',
|
|
1559
|
+
'double',
|
|
1560
|
+
'groove',
|
|
1561
|
+
'ridge',
|
|
1562
|
+
'inset',
|
|
1563
|
+
'none',
|
|
1564
|
+
'hidden',
|
|
1565
|
+
]);
|
|
1566
|
+
const LINE_WIDTHS = { thin: 1, medium: 3, thick: 5 };
|
|
1567
|
+
/** What a line's token substitutes to: a token, or nothing at all, `var(--x,)` or `--x: ;`. */
|
|
1568
|
+
const NOTHING = Symbol('nothing');
|
|
1569
|
+
/**
|
|
1570
|
+
* A line's longhands, each token given the one role its form says and the written parts did not
|
|
1571
|
+
* take. Undefined when a token is unset with nothing to fall back to, fits no role left, or would
|
|
1572
|
+
* take a role twice: CSS makes the whole shorthand invalid then.
|
|
1573
|
+
*/
|
|
1574
|
+
function lineValues(line, tokens) {
|
|
1575
|
+
const open = new Set(line.roles);
|
|
1576
|
+
const values = {};
|
|
1577
|
+
for (const reference of line.references) {
|
|
1578
|
+
const token = lineToken(reference, tokens);
|
|
1579
|
+
if (token === undefined)
|
|
1580
|
+
return undefined;
|
|
1581
|
+
if (token === NOTHING)
|
|
1582
|
+
continue;
|
|
1583
|
+
const found = reference.adjust
|
|
1584
|
+
? computedWidth(token, open, reference.adjust)
|
|
1585
|
+
: lineRole(token, open);
|
|
1586
|
+
if (!found)
|
|
1587
|
+
return undefined;
|
|
1588
|
+
open.delete(found.role);
|
|
1589
|
+
fillRole(line, values, found.role, found.value);
|
|
1590
|
+
}
|
|
1591
|
+
// A part no token filled takes its initial value, as one left out of a written line does.
|
|
1592
|
+
for (const role of open)
|
|
1593
|
+
fillRole(line, values, role, LINE_INITIAL[role]);
|
|
1594
|
+
return values;
|
|
1595
|
+
}
|
|
1596
|
+
/** A token of `currentColor`, in any case. */
|
|
1597
|
+
const isCurrentColour = (token) => token?.keyword?.toLowerCase() === 'currentcolor';
|
|
1598
|
+
/** The marker the compiler writes for `currentColor`, which the node fills in. */
|
|
1599
|
+
const CURRENT_COLOUR = { __colour: { color: 'currentcolor' } };
|
|
1600
|
+
/** What a line's width, style and colour are when it leaves them out. */
|
|
1601
|
+
const LINE_INITIAL = {
|
|
1602
|
+
width: LINE_WIDTHS['medium'],
|
|
1603
|
+
style: 'none',
|
|
1604
|
+
color: 'currentcolor',
|
|
1605
|
+
};
|
|
1606
|
+
function fillRole(line, values, role, value) {
|
|
1607
|
+
const props = { width: line.widths, color: line.colors, style: [line.style] }[role];
|
|
1608
|
+
for (const prop of props)
|
|
1609
|
+
values[prop] = value;
|
|
1610
|
+
}
|
|
1611
|
+
function lineToken(reference, tokens) {
|
|
1612
|
+
const set = firstSet([reference.reference, ...(reference.alternatives ?? [])], tokens);
|
|
1613
|
+
if (set)
|
|
1614
|
+
return set.keyword?.trim() === '' ? NOTHING : set;
|
|
1615
|
+
if (reference.empty)
|
|
1616
|
+
return NOTHING;
|
|
1617
|
+
const { fallback } = reference;
|
|
1618
|
+
return fallback && isDerived(fallback) ? derived(fallback, tokens) : fallback;
|
|
1619
|
+
}
|
|
1620
|
+
/** The role a token's form says it has, among those still open. */
|
|
1621
|
+
function lineRole(token, open) {
|
|
1622
|
+
const word = token.keyword;
|
|
1623
|
+
const width = word && word in LINE_WIDTHS ? LINE_WIDTHS[word] : token.length;
|
|
1624
|
+
if (open.has('width') && (typeof width === 'number' || isDeferredLength(width))) {
|
|
1625
|
+
return { role: 'width', value: width };
|
|
1626
|
+
}
|
|
1627
|
+
if (open.has('style') && word && LINE_STYLES.has(word))
|
|
1628
|
+
return { role: 'style', value: word };
|
|
1629
|
+
const color = isCurrentColour(token) ? 'currentcolor' : formOf(token, 'color');
|
|
1630
|
+
return open.has('color') && color !== undefined ? { role: 'color', value: color } : undefined;
|
|
1631
|
+
}
|
|
1632
|
+
/** A `calc()` of a token, which is a width, and no less than zero, as CSS clamps one. */
|
|
1633
|
+
function computedWidth(token, open, adjust) {
|
|
1634
|
+
// A bare number only when the calc() gives it its unit: `calc(var(--n) * 1px)`.
|
|
1635
|
+
const length = adjust.number ? token.number : token.length;
|
|
1636
|
+
if (!open.has('width') || (typeof length !== 'number' && !isDeferredLength(length))) {
|
|
1637
|
+
return undefined;
|
|
1638
|
+
}
|
|
1639
|
+
const value = adjusted(length, adjust);
|
|
1640
|
+
return { role: 'width', value: typeof value === 'number' ? Math.max(value, 0) : value };
|
|
1641
|
+
}
|
|
1642
|
+
const isDeferredLength = (value) => typeof value === 'object' && value !== null && '__defer' in value;
|
|
1189
1643
|
const CHANNEL_KINDS = new Set(['channels', 'hslChannels']);
|
|
1190
1644
|
/**
|
|
1191
1645
|
* `13, 110, 253` as the colour it is, at the alpha the token beside it says, if one does: the
|
|
@@ -1195,8 +1649,8 @@ function fromChannels(value, alpha, tokens, space) {
|
|
|
1195
1649
|
if (!Array.isArray(value) || value.length !== 3)
|
|
1196
1650
|
return undefined;
|
|
1197
1651
|
// An alpha naming a token nothing set, with no fallback, is no colour: CSS drops it.
|
|
1198
|
-
const opacity = alpha ? (
|
|
1199
|
-
if (opacity
|
|
1652
|
+
const opacity = alpha ? substitute(alpha, tokens, (token) => token.number) : 1;
|
|
1653
|
+
if (typeof opacity !== 'number')
|
|
1200
1654
|
return undefined;
|
|
1201
1655
|
if (space === 'hsl')
|
|
1202
1656
|
return hslToRgb(value[0], value[1], value[2], opacity);
|
|
@@ -1227,29 +1681,47 @@ function paintLayers(gradient, tokens) {
|
|
|
1227
1681
|
*/
|
|
1228
1682
|
function paintGradient(template, tokens) {
|
|
1229
1683
|
const colorStops = paintedStops(template, tokens);
|
|
1230
|
-
if (colorStops.length < 2)
|
|
1684
|
+
if (!colorStops || colorStops.length < 2)
|
|
1231
1685
|
return undefined;
|
|
1232
1686
|
const position = template.position && placed(template.position, tokens);
|
|
1233
1687
|
if (position === null)
|
|
1234
1688
|
return undefined;
|
|
1235
1689
|
return [{ ...template, colorStops, ...(position ? { position } : {}) }];
|
|
1236
1690
|
}
|
|
1237
|
-
/**
|
|
1691
|
+
/**
|
|
1692
|
+
* A template's stops with their tokens read, leaving out any whose colour is not set. Undefined
|
|
1693
|
+
* when a stop's colour or position token is set but no such thing, which makes the gradient
|
|
1694
|
+
* invalid, as CSS substitutes a set token rather than falling back.
|
|
1695
|
+
*/
|
|
1238
1696
|
function paintedStops(template, tokens) {
|
|
1239
1697
|
const colorStops = [];
|
|
1240
1698
|
for (const stop of template.colorStops) {
|
|
1241
|
-
const
|
|
1699
|
+
const token = stop.colour ? undefined : tokens[stop.reference];
|
|
1700
|
+
const color = stop.colour ? resolveColour(stop.colour, tokens) : token?.color;
|
|
1701
|
+
// `none` is how the Tailwind flattening writes `via-none`: the stop left out, not invalid.
|
|
1702
|
+
if (color === undefined && token && token.keyword !== 'none')
|
|
1703
|
+
return undefined;
|
|
1242
1704
|
if (color === undefined)
|
|
1243
1705
|
continue;
|
|
1244
|
-
const position = stop
|
|
1245
|
-
|
|
1246
|
-
|
|
1706
|
+
const position = stopPosition(stop, tokens);
|
|
1707
|
+
if (position === INVALID_POSITION)
|
|
1708
|
+
return undefined;
|
|
1247
1709
|
// Written even when there is none: Fabric looks the key up before it looks at the value, and
|
|
1248
1710
|
// silently skips a stop that has not got one.
|
|
1249
1711
|
colorStops.push({ color, position: position ?? null });
|
|
1250
1712
|
}
|
|
1251
1713
|
return colorStops;
|
|
1252
1714
|
}
|
|
1715
|
+
const INVALID_POSITION = Symbol('invalid position');
|
|
1716
|
+
/** A stop's position: its token's when that is set, the one written otherwise. */
|
|
1717
|
+
function stopPosition(stop, tokens) {
|
|
1718
|
+
if (!stop.positionReference)
|
|
1719
|
+
return stop.position;
|
|
1720
|
+
const token = tokens[stop.positionReference];
|
|
1721
|
+
if (!token)
|
|
1722
|
+
return stop.position;
|
|
1723
|
+
return token.length ?? INVALID_POSITION;
|
|
1724
|
+
}
|
|
1253
1725
|
/**
|
|
1254
1726
|
* A radial gradient's centre with any token in it read, or null when a token it needs is not set
|
|
1255
1727
|
* and has nothing written beside it: CSS would not paint the gradient then, and neither does this.
|
|
@@ -1261,8 +1733,8 @@ function placed(position, tokens) {
|
|
|
1261
1733
|
out[edge] = offset;
|
|
1262
1734
|
continue;
|
|
1263
1735
|
}
|
|
1264
|
-
const
|
|
1265
|
-
const value = tokens
|
|
1736
|
+
const marker = offset;
|
|
1737
|
+
const value = substitute(marker, tokens, (token) => token.length);
|
|
1266
1738
|
if (value === undefined || typeof value === 'object')
|
|
1267
1739
|
return null;
|
|
1268
1740
|
out[edge] = value;
|
|
@@ -1287,12 +1759,24 @@ function resolveColour(expression, tokens) {
|
|
|
1287
1759
|
const origin = resolveColour(from, tokens);
|
|
1288
1760
|
return origin === undefined ? undefined : relativeColour(space, origin, channels, alpha);
|
|
1289
1761
|
}
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1762
|
+
const sides = mixSides(expression.mix, tokens);
|
|
1763
|
+
return sides && mixColours(expression.mix.space, ...sides, expression.mix.hue);
|
|
1764
|
+
}
|
|
1765
|
+
/** 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) : resolveColour(side, tokens);
|
|
1768
|
+
const first = colour(mix.a);
|
|
1769
|
+
const second = colour(mix.b);
|
|
1293
1770
|
if (first === undefined || second === undefined)
|
|
1294
1771
|
return undefined;
|
|
1295
|
-
return
|
|
1772
|
+
return [
|
|
1773
|
+
{ colour: first, percentage: mix.aPercentage },
|
|
1774
|
+
{ colour: second, percentage: mix.bPercentage },
|
|
1775
|
+
];
|
|
1776
|
+
}
|
|
1777
|
+
function unroundedMix(mix, tokens) {
|
|
1778
|
+
const sides = mixSides(mix, tokens);
|
|
1779
|
+
return sides && mixChannels(mix.space, ...sides, mix.hue);
|
|
1296
1780
|
}
|
|
1297
1781
|
/** A calc marker worked out, as its slot takes it; undefined when a token in it is not set. */
|
|
1298
1782
|
function resolveCalc(marker, tokens) {
|
|
@@ -1301,6 +1785,9 @@ function resolveCalc(marker, tokens) {
|
|
|
1301
1785
|
return percentage;
|
|
1302
1786
|
if (bareNumberAsLength(marker, tokens) || notAnAngle(marker, tokens))
|
|
1303
1787
|
return undefined;
|
|
1788
|
+
// A percentage and a number added together, `calc(var(--n) + 10%)`, are neither, and invalid.
|
|
1789
|
+
if (marker.kind === 'number' && calcType(marker.expression, tokens) === undefined)
|
|
1790
|
+
return undefined;
|
|
1304
1791
|
const value = calculated(marker.expression, marker.kind, tokens);
|
|
1305
1792
|
if (value === undefined || !Number.isFinite(value))
|
|
1306
1793
|
return undefined;
|
|
@@ -1354,10 +1841,8 @@ function wholePercentage(marker, tokens) {
|
|
|
1354
1841
|
function calculated(expression, kind, tokens) {
|
|
1355
1842
|
if (typeof expression === 'number')
|
|
1356
1843
|
return expression;
|
|
1357
|
-
if (!Array.isArray(expression))
|
|
1358
|
-
|
|
1359
|
-
return tokenNumber(tokens[leaf.reference], kind) ?? leaf.fallback;
|
|
1360
|
-
}
|
|
1844
|
+
if (!Array.isArray(expression))
|
|
1845
|
+
return leafValue(expression, kind, tokens);
|
|
1361
1846
|
const [op, a, b] = expression;
|
|
1362
1847
|
const left = calculated(a, kind, tokens);
|
|
1363
1848
|
const right = calculated(b, kind, tokens);
|
|
@@ -1375,6 +1860,15 @@ function calculated(expression, kind, tokens) {
|
|
|
1375
1860
|
return Math.min(left, right);
|
|
1376
1861
|
return left / right;
|
|
1377
1862
|
}
|
|
1863
|
+
/** A written percentage, or a token, as `calculated` reads it: its fallback when it is unset. */
|
|
1864
|
+
function leafValue(leaf, kind, tokens) {
|
|
1865
|
+
if ('percentage' in leaf)
|
|
1866
|
+
return leaf.percentage;
|
|
1867
|
+
const token = tokens[leaf.reference];
|
|
1868
|
+
if (token)
|
|
1869
|
+
return tokenNumber(token, kind);
|
|
1870
|
+
return leaf.fallback === undefined ? undefined : calculated(leaf.fallback, kind, tokens);
|
|
1871
|
+
}
|
|
1378
1872
|
/** Where each kind of slot reads a token from, before its bare number. */
|
|
1379
1873
|
const FORM_OF_KIND = {
|
|
1380
1874
|
length: (token) => token.length,
|
|
@@ -1399,6 +1893,10 @@ function tokenNumber(token, kind) {
|
|
|
1399
1893
|
return undefined;
|
|
1400
1894
|
return token.number;
|
|
1401
1895
|
}
|
|
1896
|
+
/** What a token set in a slot stands for: its entries, or none for one set to nothing, `--tw-blur: ;`. */
|
|
1897
|
+
function slotEntries(token, kind) {
|
|
1898
|
+
return formOf(token, kind) ?? (token.keyword?.trim() === '' ? [] : undefined);
|
|
1899
|
+
}
|
|
1402
1900
|
/** The slot a list entry can be instead of an entry: one per list property that has them. */
|
|
1403
1901
|
const SLOT_KINDS = {
|
|
1404
1902
|
__shadows: 'shadow',
|
|
@@ -1419,7 +1917,9 @@ function slotOf(part) {
|
|
|
1419
1917
|
return undefined;
|
|
1420
1918
|
}
|
|
1421
1919
|
function resolveLength(marker, tokens) {
|
|
1422
|
-
|
|
1920
|
+
// A number when the arithmetic gives it its unit, as `referenced` reads one.
|
|
1921
|
+
const form = marker.adjust?.number ? 'number' : 'length';
|
|
1922
|
+
const value = substitute(marker, tokens, (token) => formOf(token, form));
|
|
1423
1923
|
if (typeof value !== 'number')
|
|
1424
1924
|
return undefined;
|
|
1425
1925
|
return marker.adjust ? adjusted(value, marker.adjust) : value;
|
|
@@ -1488,7 +1988,7 @@ const UNSETTLED = Symbol('unsettled');
|
|
|
1488
1988
|
function channelsColour(expression, tokens) {
|
|
1489
1989
|
const { space } = expression.channels;
|
|
1490
1990
|
const kind = space === 'hsl' ? 'hslChannels' : 'channels';
|
|
1491
|
-
const value =
|
|
1991
|
+
const value = substitute(expression.channels, tokens, (token) => formOf(token, kind));
|
|
1492
1992
|
const { alpha } = expression;
|
|
1493
1993
|
if (typeof alpha !== 'number')
|
|
1494
1994
|
return fromChannels(value, alpha, tokens, space);
|
|
@@ -1497,13 +1997,23 @@ function channelsColour(expression, tokens) {
|
|
|
1497
1997
|
}
|
|
1498
1998
|
/** A token's colour, or the first of its alternatives that is defined, or its fallback. */
|
|
1499
1999
|
function tokenColour(expression, tokens) {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
2000
|
+
// The first token set is substituted, whatever it holds, as `referenced` reads one.
|
|
2001
|
+
const set = firstSet([expression.reference, ...(expression.alternatives ?? [])], tokens);
|
|
2002
|
+
if (expression.orInset)
|
|
2003
|
+
return insetOrColour(set);
|
|
2004
|
+
if (set)
|
|
2005
|
+
return formOf(set, 'color');
|
|
2006
|
+
const token = expression.fallbackToken;
|
|
2007
|
+
return expression.fallback ?? (token && formOf(derived(token, tokens), 'color'));
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* A shadow's colour from a token that is the colour or the word `inset`: currentcolor, which the
|
|
2011
|
+
* node fills in, when it is `inset` or unset, and nothing, so no shadow, when it is anything else.
|
|
2012
|
+
*/
|
|
2013
|
+
function insetOrColour(token) {
|
|
2014
|
+
if (!token || token.keyword === 'inset')
|
|
2015
|
+
return 'currentcolor';
|
|
2016
|
+
return formOf(token, 'color');
|
|
1507
2017
|
}
|
|
1508
2018
|
/** A marker the compiler leaves in a structured value for a colour only the device can settle. */
|
|
1509
2019
|
function colourMarker(value) {
|