@ng-native/fabric 0.1.2 → 0.2.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/README.md +1 -1
- package/dist/css.d.ts +69 -13
- package/dist/css.js +291 -59
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +56 -3
- package/dist/engine.js +144 -9
- package/dist/engine.js.map +1 -1
- package/dist/font-faces.d.ts +42 -0
- package/dist/font-faces.js +112 -0
- package/dist/font-faces.js.map +1 -0
- package/dist/forms-inputs.js +1 -1
- package/dist/forms-inputs.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/inline-derived-token.d.ts +15 -0
- package/dist/inline-derived-token.js +252 -0
- package/dist/inline-derived-token.js.map +1 -0
- package/dist/inline-token.d.ts +4 -2
- package/dist/inline-token.js +90 -2
- package/dist/inline-token.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ const platform = nativePlatform(); // 'ios' | 'android'
|
|
|
49
49
|
[what CSS reaches a device](https://ng-native.com/packages/fabric/supported-css) and
|
|
50
50
|
[animation](https://ng-native.com/packages/fabric/animation)
|
|
51
51
|
- [Root README](https://github.com/ng-native/ng-native/blob/main/README.md) and
|
|
52
|
-
[ARCHITECTURE.md](https://github.com/ng-native/ng-native/blob/main/ARCHITECTURE.md)
|
|
52
|
+
[ARCHITECTURE.md](https://github.com/ng-native/ng-native/blob/main/docs/ARCHITECTURE.md)
|
|
53
53
|
|
|
54
54
|
## License
|
|
55
55
|
|
package/dist/css.d.ts
CHANGED
|
@@ -125,15 +125,24 @@ export interface TokenValue {
|
|
|
125
125
|
readonly transform?: readonly unknown[];
|
|
126
126
|
/** One font variant, as the one-entry list `fontVariant` takes: a numeric variant's slot. */
|
|
127
127
|
readonly fontVariant?: readonly string[];
|
|
128
|
-
/** Bare colour channels, `13, 110, 253
|
|
128
|
+
/** Bare colour channels as `rgb()` reads them, 0 to 255: `13, 110, 253` or `100% 0% 0%`. */
|
|
129
129
|
readonly channels?: readonly number[];
|
|
130
|
+
/** The same as `hsl()` reads them: degrees, then saturation and lightness as fractions. */
|
|
131
|
+
readonly hslChannels?: readonly number[];
|
|
132
|
+
/** A transition's curve, as the cubic-bezier control points a timing keyword stands for. */
|
|
133
|
+
readonly easing?: readonly number[];
|
|
134
|
+
/** A whole animation, as the spec the engine plays: `--animate-spin`. */
|
|
135
|
+
readonly animation?: Readonly<Record<string, unknown>>;
|
|
130
136
|
/**
|
|
131
|
-
* Defined as `rgba(var(--channels), <alpha>)
|
|
132
|
-
*
|
|
137
|
+
* Defined as a colour made of other tokens: `rgba(var(--channels), <alpha>)`, or a `color-mix()`
|
|
138
|
+
* of one. Settled on the node that defines it, as an `hsl` one is.
|
|
133
139
|
*/
|
|
134
|
-
readonly deferredColour?:
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
readonly deferredColour?: ColourExpression;
|
|
141
|
+
/**
|
|
142
|
+
* Defined as arithmetic of other tokens, `calc(var(--spacing) * 4)`, read as a length and as a
|
|
143
|
+
* number. Settled on the node that defines it, as an `hsl` one is.
|
|
144
|
+
*/
|
|
145
|
+
readonly deferredCalc?: readonly CalcMarker[];
|
|
137
146
|
/**
|
|
138
147
|
* Defined as another token, `var(--name)`. Resolved on the node that defines it, against the
|
|
139
148
|
* tokens in scope there, and passed down resolved - which is what a browser does.
|
|
@@ -159,7 +168,7 @@ export type HslChannel = number | {
|
|
|
159
168
|
readonly fallback?: number;
|
|
160
169
|
};
|
|
161
170
|
/** The forms a token can be read in: one per kind of value a use site can need. */
|
|
162
|
-
export type TokenKind = 'length' | 'color' | 'keyword' | 'number' | 'weight' | 'family' | 'lineHeight' | 'shadow' | 'filter' | 'transform' | 'fontVariant' | 'channels';
|
|
171
|
+
export type TokenKind = 'length' | 'color' | 'keyword' | 'number' | 'weight' | 'family' | 'lineHeight' | 'shadow' | 'filter' | 'transform' | 'fontVariant' | 'channels' | 'hslChannels' | 'easing' | 'time' | 'animation';
|
|
163
172
|
export interface DeferredDeclaration {
|
|
164
173
|
/**
|
|
165
174
|
* The React Native style properties this lands in. Usually one, but a four-sided shorthand is
|
|
@@ -175,6 +184,11 @@ export interface DeferredDeclaration {
|
|
|
175
184
|
*/
|
|
176
185
|
readonly alternatives?: readonly string[];
|
|
177
186
|
readonly fallback?: unknown;
|
|
187
|
+
/**
|
|
188
|
+
* A fallback made of other tokens, `var(--missing, calc(var(--gap) * 2))`, worked out from the
|
|
189
|
+
* tokens in scope where it is used, as a browser substitutes it there.
|
|
190
|
+
*/
|
|
191
|
+
readonly fallbackToken?: TokenValue;
|
|
178
192
|
/**
|
|
179
193
|
* What to write when the reference resolves, whatever it resolves to: `flex: var(--grow)` sets a
|
|
180
194
|
* shrink of 1 and a basis of 0%, but only when there is a grow for them to go with.
|
|
@@ -194,6 +208,8 @@ export interface DeferredDeclaration {
|
|
|
194
208
|
readonly reference: string;
|
|
195
209
|
readonly fallback?: number;
|
|
196
210
|
};
|
|
211
|
+
/** For a colour built from `channels`, read through `hsl()` rather than `rgb()`. */
|
|
212
|
+
readonly space?: ChannelSpace;
|
|
197
213
|
/**
|
|
198
214
|
* A gradient whose stops are custom properties, filled in once a node's tokens are known. What
|
|
199
215
|
* a utility framework's `from-*` and `to-*` classes compile to, since the class that paints the
|
|
@@ -247,6 +263,7 @@ export type ColourExpression = {
|
|
|
247
263
|
readonly channels: {
|
|
248
264
|
readonly reference: string;
|
|
249
265
|
readonly fallback?: readonly number[];
|
|
266
|
+
readonly space?: ChannelSpace;
|
|
250
267
|
};
|
|
251
268
|
readonly alpha?: number | {
|
|
252
269
|
readonly reference: string;
|
|
@@ -314,12 +331,18 @@ export interface StyleSheet {
|
|
|
314
331
|
* has to re-resolve a whole child list when it changes. It only does that where a sheet asks.
|
|
315
332
|
*/
|
|
316
333
|
readonly structural?: true;
|
|
334
|
+
/**
|
|
335
|
+
* The `@font-face` rules the sheet declares, each `source` the bundler's `require` of the file.
|
|
336
|
+
* The engine does not read them: `loadFonts()` in `@ng-native/expo/fonts` registers them with
|
|
337
|
+
* the platform before mount.
|
|
338
|
+
*/
|
|
339
|
+
readonly fonts?: readonly {
|
|
340
|
+
readonly family: string;
|
|
341
|
+
readonly source: unknown;
|
|
342
|
+
readonly weight?: number;
|
|
343
|
+
readonly style?: string;
|
|
344
|
+
}[];
|
|
317
345
|
}
|
|
318
|
-
/**
|
|
319
|
-
* What one node resolved to. `style` is what it renders with; `inherited` is what its children
|
|
320
|
-
* start from, which is the parent's own `inherited` plus whatever inheritable values this node
|
|
321
|
-
* set. Keeping the two apart is what lets resolution go downwards.
|
|
322
|
-
*/
|
|
323
346
|
export interface StyleCache {
|
|
324
347
|
epoch: number;
|
|
325
348
|
/**
|
|
@@ -560,7 +583,26 @@ export declare class StyleResolver {
|
|
|
560
583
|
* survives and a later sheet wins equal weight.
|
|
561
584
|
*/
|
|
562
585
|
private merge;
|
|
563
|
-
/**
|
|
586
|
+
/**
|
|
587
|
+
* A node's style from the rules it matched, shared with every other node that matched the same
|
|
588
|
+
* rules under the same inherited values and tokens: a list's rows, and what is inside each.
|
|
589
|
+
*
|
|
590
|
+
* Everything past matching depends on those alone, and it is most of the work where a sheet
|
|
591
|
+
* leaves values to the device: settling each `var()`, mixing colours, filling a shadow's slots.
|
|
592
|
+
* A node that sets custom properties of its own is worked out alone. What is shared is never
|
|
593
|
+
* written to after, so sharing it is safe; each node still has a cache and a context of its own.
|
|
594
|
+
*/
|
|
595
|
+
private styled;
|
|
596
|
+
private readonly shared;
|
|
597
|
+
private sharedGeneration;
|
|
598
|
+
private readonly ruleIds;
|
|
599
|
+
private nextRuleId;
|
|
600
|
+
private ruleId;
|
|
601
|
+
/** A node's style from the rules it matched, worked out. */
|
|
602
|
+
private style;
|
|
603
|
+
/** The rules that apply to a node, weakest first. */
|
|
604
|
+
private matched;
|
|
605
|
+
/** The declarations of the rules a node matched. The list is sorted, so later simply wins. */
|
|
564
606
|
private cascade;
|
|
565
607
|
/**
|
|
566
608
|
* Whether a rule's media query holds, memoised per rule.
|
|
@@ -601,6 +643,20 @@ export declare class StyleResolver {
|
|
|
601
643
|
/** A length relative to the viewport or to the font size in scope. */
|
|
602
644
|
private computed;
|
|
603
645
|
}
|
|
646
|
+
/** `hsl()` reads a channels token as hue, saturation and lightness; `rgb()` as sRGB. */
|
|
647
|
+
type ChannelSpace = 'rgb' | 'hsl';
|
|
648
|
+
/**
|
|
649
|
+
* Arithmetic with tokens in it, as the compiler leaves it: numbers, token references and
|
|
650
|
+
* `[op, a, b]`, in points, degrees or plain numbers as the slot it fills counts.
|
|
651
|
+
*/
|
|
652
|
+
type CalcExpression = number | {
|
|
653
|
+
readonly reference: string;
|
|
654
|
+
readonly fallback?: CalcExpression;
|
|
655
|
+
} | readonly ['+' | '-' | '*' | '/' | 'max' | 'min', CalcExpression, CalcExpression];
|
|
656
|
+
interface CalcMarker {
|
|
657
|
+
readonly expression: CalcExpression;
|
|
658
|
+
readonly kind: 'length' | 'angle' | 'time' | 'number';
|
|
659
|
+
}
|
|
604
660
|
/**
|
|
605
661
|
* Where a component's compiled sheet lives: a static on the component class, written by the
|
|
606
662
|
* Metro transformer at build time.
|
package/dist/css.js
CHANGED
|
@@ -594,25 +594,15 @@ export class StyleResolver {
|
|
|
594
594
|
return this.unstyled(node, epoch, parent, parentContext, parentInherited);
|
|
595
595
|
}
|
|
596
596
|
styleStats.nodesResolved++;
|
|
597
|
-
const
|
|
598
|
-
// Tokens are in scope for this node's own declarations as well as its descendants', so they
|
|
599
|
-
// are merged before any `var()` here is resolved.
|
|
597
|
+
const matched = this.matched(node, this.rulesFor(node));
|
|
600
598
|
const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
|
|
601
|
-
const
|
|
602
|
-
const own = result.declarations;
|
|
603
|
-
if (result.deferred) {
|
|
604
|
-
this.applyDeferred(result.deferred, own, tokens, parentInherited, result.important);
|
|
605
|
-
}
|
|
606
|
-
if (own['borderStyle'] === 'none')
|
|
607
|
-
drawNoBorder(own);
|
|
599
|
+
const styled = this.styled(node, matched, parentTokens, parentInherited);
|
|
608
600
|
const cache = {
|
|
609
601
|
epoch,
|
|
610
602
|
generation: this.generation,
|
|
611
603
|
context: {},
|
|
612
604
|
parentContext,
|
|
613
|
-
|
|
614
|
-
inherited: inheritFrom(parentInherited, own),
|
|
615
|
-
tokens,
|
|
605
|
+
...styled,
|
|
616
606
|
};
|
|
617
607
|
node.styleCache = cache;
|
|
618
608
|
node.styleDirty = false;
|
|
@@ -713,16 +703,81 @@ export class StyleResolver {
|
|
|
713
703
|
add(sheet, COMPONENT_SPECIFICITY_BUMP);
|
|
714
704
|
return entries.sort((a, b) => a.weight - b.weight);
|
|
715
705
|
}
|
|
716
|
-
/**
|
|
717
|
-
|
|
706
|
+
/**
|
|
707
|
+
* A node's style from the rules it matched, shared with every other node that matched the same
|
|
708
|
+
* rules under the same inherited values and tokens: a list's rows, and what is inside each.
|
|
709
|
+
*
|
|
710
|
+
* Everything past matching depends on those alone, and it is most of the work where a sheet
|
|
711
|
+
* leaves values to the device: settling each `var()`, mixing colours, filling a shadow's slots.
|
|
712
|
+
* A node that sets custom properties of its own is worked out alone. What is shared is never
|
|
713
|
+
* written to after, so sharing it is safe; each node still has a cache and a context of its own.
|
|
714
|
+
*/
|
|
715
|
+
styled(node, matched, parentTokens, parentInherited) {
|
|
716
|
+
if (node.customProperties)
|
|
717
|
+
return this.style(node, matched, parentTokens, parentInherited);
|
|
718
|
+
if (this.sharedGeneration !== this.generation) {
|
|
719
|
+
this.shared.clear();
|
|
720
|
+
this.sharedGeneration = this.generation;
|
|
721
|
+
}
|
|
722
|
+
const key = matched.map((rule) => this.ruleId(rule)).join(',');
|
|
723
|
+
let byTokens = this.shared.get(key);
|
|
724
|
+
if (!byTokens)
|
|
725
|
+
this.shared.set(key, (byTokens = new WeakMap()));
|
|
726
|
+
let byInherited = byTokens.get(parentTokens);
|
|
727
|
+
if (!byInherited)
|
|
728
|
+
byTokens.set(parentTokens, (byInherited = new WeakMap()));
|
|
729
|
+
let styled = byInherited.get(parentInherited);
|
|
730
|
+
if (!styled) {
|
|
731
|
+
styled = this.style(node, matched, parentTokens, parentInherited);
|
|
732
|
+
byInherited.set(parentInherited, styled);
|
|
733
|
+
}
|
|
734
|
+
return styled;
|
|
735
|
+
}
|
|
736
|
+
shared = new Map();
|
|
737
|
+
sharedGeneration = 0;
|
|
738
|
+
ruleIds = new WeakMap();
|
|
739
|
+
nextRuleId = 0;
|
|
740
|
+
ruleId(rule) {
|
|
741
|
+
let id = this.ruleIds.get(rule);
|
|
742
|
+
if (id === undefined)
|
|
743
|
+
this.ruleIds.set(rule, (id = this.nextRuleId++));
|
|
744
|
+
return id;
|
|
745
|
+
}
|
|
746
|
+
/** A node's style from the rules it matched, worked out. */
|
|
747
|
+
style(node, matched, parentTokens, parentInherited) {
|
|
748
|
+
const result = this.cascade(matched);
|
|
749
|
+
// Tokens are in scope for this node's own declarations as well as its descendants', so they
|
|
750
|
+
// are merged before any `var()` here is resolved.
|
|
751
|
+
const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
|
|
752
|
+
const own = result.declarations;
|
|
753
|
+
if (result.deferred) {
|
|
754
|
+
this.applyDeferred(result.deferred, own, tokens, parentInherited, result.important);
|
|
755
|
+
}
|
|
756
|
+
if (own['borderStyle'] === 'none')
|
|
757
|
+
drawNoBorder(own);
|
|
758
|
+
return {
|
|
759
|
+
style: { ...parentInherited, ...own },
|
|
760
|
+
inherited: inheritFrom(parentInherited, own),
|
|
761
|
+
tokens,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
/** The rules that apply to a node, weakest first. */
|
|
765
|
+
matched(node, entries) {
|
|
766
|
+
const out = [];
|
|
767
|
+
for (const { rule, sheet } of entries) {
|
|
768
|
+
if (this.conditionHolds(rule) && matches(node, rule, sheet))
|
|
769
|
+
out.push(rule);
|
|
770
|
+
}
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
773
|
+
/** The declarations of the rules a node matched. The list is sorted, so later simply wins. */
|
|
774
|
+
cascade(matched) {
|
|
718
775
|
const normal = {};
|
|
719
776
|
const important = {};
|
|
720
777
|
let hasImportant = false;
|
|
721
778
|
let tokens = null;
|
|
722
779
|
let deferred = null;
|
|
723
|
-
for (const
|
|
724
|
-
if (!this.conditionHolds(rule) || !matches(node, rule, sheet))
|
|
725
|
-
continue;
|
|
780
|
+
for (const rule of matched) {
|
|
726
781
|
Object.assign(normal, rule.declarations);
|
|
727
782
|
if (rule.important) {
|
|
728
783
|
Object.assign(important, rule.important);
|
|
@@ -841,7 +896,9 @@ export class StyleResolver {
|
|
|
841
896
|
/** A reference with nothing defined for it and nothing to fall back to. */
|
|
842
897
|
reportUndefined(declaration, tokens) {
|
|
843
898
|
const name = declaration.reference;
|
|
844
|
-
if (name === undefined ||
|
|
899
|
+
if (name === undefined || name in tokens)
|
|
900
|
+
return;
|
|
901
|
+
if (declaration.fallback !== undefined || declaration.fallbackToken)
|
|
845
902
|
return;
|
|
846
903
|
if (declaration.alternatives?.some((alternative) => alternative in tokens))
|
|
847
904
|
return;
|
|
@@ -917,7 +974,10 @@ export class StyleResolver {
|
|
|
917
974
|
const base = declaration.props.includes('fontSize')
|
|
918
975
|
? parentInherited['fontSize']
|
|
919
976
|
: (own['fontSize'] ?? parentInherited['fontSize']);
|
|
920
|
-
|
|
977
|
+
// To a thousandth of a point: a factor rounded at build time, times a font size, is otherwise
|
|
978
|
+
// 19.99998 where the web draws 20.
|
|
979
|
+
const points = factor * (typeof base === 'number' ? base : DEFAULT_FONT_SIZE) + offset;
|
|
980
|
+
return Math.round(points * 1000) / 1000;
|
|
921
981
|
}
|
|
922
982
|
}
|
|
923
983
|
/**
|
|
@@ -957,25 +1017,103 @@ function tokensInScope(parentTokens, ruleTokens, custom) {
|
|
|
957
1017
|
}
|
|
958
1018
|
function resolveAliases(own, merged) {
|
|
959
1019
|
const names = Object.keys(own);
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1020
|
+
const aliases = names.filter((name) => own[name].alias);
|
|
1021
|
+
followAliases(aliases, merged);
|
|
1022
|
+
// Left to work out: each token made of others and each alias that came to one, by a link or a
|
|
1023
|
+
// fallback, `var(--missing, calc(var(--gap) * 2))`, which is worked out here as well.
|
|
1024
|
+
const pending = new Set(names.filter((name) => isDerived(merged[name])));
|
|
1025
|
+
if (pending.size)
|
|
1026
|
+
settleDerived(own, merged, pending);
|
|
1027
|
+
return merged;
|
|
1028
|
+
}
|
|
1029
|
+
/** What a token not yet worked out reads as while another is: set, with no form of any kind. */
|
|
1030
|
+
const PENDING = Object.freeze({});
|
|
1031
|
+
/**
|
|
1032
|
+
* The tokens left to work out, each settled once none it reads is still to be, whatever order they
|
|
1033
|
+
* are defined in. One that reads another still to be waits, rather than taking a fallback: a
|
|
1034
|
+
* fallback is for a token that is unset or invalid. When every one left waits for another, those
|
|
1035
|
+
* in a cycle, counting only the references read, are invalid, and the rest go on without them,
|
|
1036
|
+
* taking their fallbacks, as CSS has it.
|
|
1037
|
+
*/
|
|
1038
|
+
function settleDerived(own, merged, pending) {
|
|
1039
|
+
const waitingOn = new Map();
|
|
1040
|
+
let read = new Set();
|
|
1041
|
+
const view = new Proxy(merged, {
|
|
1042
|
+
get: (tokens, name) => {
|
|
1043
|
+
if (!pending.has(name))
|
|
1044
|
+
return tokens[name];
|
|
1045
|
+
read.add(name);
|
|
1046
|
+
return PENDING;
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
while (pending.size) {
|
|
1050
|
+
let settled = false;
|
|
1051
|
+
for (const name of pending) {
|
|
1052
|
+
read = new Set();
|
|
1053
|
+
const token = own[name];
|
|
1054
|
+
const value = token.alias ? substitutedIn(token, view) : derived(token, view);
|
|
1055
|
+
if (read.size) {
|
|
1056
|
+
waitingOn.set(name, read);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
pending.delete(name);
|
|
1060
|
+
settle(merged, name, value);
|
|
1061
|
+
settled = true;
|
|
1062
|
+
}
|
|
1063
|
+
if (settled)
|
|
975
1064
|
continue;
|
|
976
|
-
|
|
1065
|
+
for (const name of inCycles(pending, waitingOn)) {
|
|
1066
|
+
pending.delete(name);
|
|
1067
|
+
delete merged[name];
|
|
1068
|
+
}
|
|
977
1069
|
}
|
|
978
|
-
|
|
1070
|
+
}
|
|
1071
|
+
/** An alias's target, or the first of its fallbacks that is set, with one made of others worked out. */
|
|
1072
|
+
function substitutedIn(token, tokens) {
|
|
1073
|
+
let link = token;
|
|
1074
|
+
while (link?.alias) {
|
|
1075
|
+
const target = tokens[link.alias];
|
|
1076
|
+
if (target !== undefined)
|
|
1077
|
+
return target;
|
|
1078
|
+
link = link.fallback;
|
|
1079
|
+
}
|
|
1080
|
+
return link && isDerived(link) ? derived(link, tokens) : link;
|
|
1081
|
+
}
|
|
1082
|
+
/** The names among `names` that reach themselves by what each waits on. */
|
|
1083
|
+
function inCycles(names, waitingOn) {
|
|
1084
|
+
const reaches = (from, to, seen) => {
|
|
1085
|
+
for (const next of waitingOn.get(from) ?? []) {
|
|
1086
|
+
if (next === to)
|
|
1087
|
+
return true;
|
|
1088
|
+
if (names.has(next) && !seen.has(next) && seen.add(next) && reaches(next, to, seen)) {
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return false;
|
|
1093
|
+
};
|
|
1094
|
+
return [...names].filter((name) => reaches(name, name, new Set()));
|
|
1095
|
+
}
|
|
1096
|
+
/** Whether a token is made of others, and so worked out where it is defined. */
|
|
1097
|
+
function isDerived(token) {
|
|
1098
|
+
return Boolean(token?.hsl || token?.deferredColour || token?.deferredCalc);
|
|
1099
|
+
}
|
|
1100
|
+
/** A token made of others, worked out from the tokens in scope. */
|
|
1101
|
+
function derived(token, tokens) {
|
|
1102
|
+
if (token.hsl)
|
|
1103
|
+
return colourToken(resolveHsl(token.hsl, tokens));
|
|
1104
|
+
if (token.deferredColour)
|
|
1105
|
+
return colourToken(resolveColour(token.deferredColour, tokens));
|
|
1106
|
+
return calcToken(token.deferredCalc, tokens);
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Each alias settled to what it names. `follow` writes back every one that resolves; one in a
|
|
1110
|
+
* cycle is removed only once all are followed, as removed any sooner, the next one round the
|
|
1111
|
+
* cycle would find it unset and take its own fallback.
|
|
1112
|
+
*/
|
|
1113
|
+
function followAliases(aliases, merged) {
|
|
1114
|
+
const cyclic = aliases.filter((name) => typeof follow(name, merged, []) === 'number');
|
|
1115
|
+
for (const name of cyclic)
|
|
1116
|
+
delete merged[name];
|
|
979
1117
|
}
|
|
980
1118
|
/** A token set to what it resolved to, or removed when it resolved to nothing, as CSS unsets it. */
|
|
981
1119
|
function settle(merged, name, value) {
|
|
@@ -985,9 +1123,23 @@ function settle(merged, name, value) {
|
|
|
985
1123
|
delete merged[name];
|
|
986
1124
|
}
|
|
987
1125
|
const colourToken = (color) => color === undefined ? undefined : { color };
|
|
1126
|
+
/** A `calc()` token's forms, from the tokens in scope: undefined when it makes neither. */
|
|
1127
|
+
function calcToken(markers, tokens) {
|
|
1128
|
+
let token;
|
|
1129
|
+
for (const marker of markers) {
|
|
1130
|
+
const value = resolveCalc(marker, tokens);
|
|
1131
|
+
if (value === undefined)
|
|
1132
|
+
continue;
|
|
1133
|
+
if (marker.kind === 'length')
|
|
1134
|
+
token = { ...token, length: value };
|
|
1135
|
+
else if (typeof value === 'number')
|
|
1136
|
+
token = { ...token, number: value };
|
|
1137
|
+
}
|
|
1138
|
+
return token;
|
|
1139
|
+
}
|
|
988
1140
|
/** An `hsl()` token's channels, read from the tokens in scope, as the colour they make. */
|
|
989
1141
|
function resolveHsl(hsl, tokens) {
|
|
990
|
-
const h = hslChannel(hsl.h, tokens);
|
|
1142
|
+
const h = hslChannel(hsl.h, tokens, true);
|
|
991
1143
|
const s = hslChannel(hsl.s, tokens);
|
|
992
1144
|
const l = hslChannel(hsl.l, tokens);
|
|
993
1145
|
const alpha = hsl.alpha === undefined ? 1 : hslChannel(hsl.alpha, tokens);
|
|
@@ -996,10 +1148,22 @@ function resolveHsl(hsl, tokens) {
|
|
|
996
1148
|
}
|
|
997
1149
|
return hslToRgb(h, s, l, alpha);
|
|
998
1150
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1151
|
+
/**
|
|
1152
|
+
* A channel's number, or undefined when its token is the wrong kind for the slot: a hue is a
|
|
1153
|
+
* number or an angle, read in degrees, as `0.5turn` is 180 and not 0.5, and never a percentage; a
|
|
1154
|
+
* saturation, a lightness or an alpha is never an angle. The fallback is only for a token that is
|
|
1155
|
+
* not set, as a set one is substituted, whatever it holds.
|
|
1156
|
+
*/
|
|
1157
|
+
function hslChannel(channel, tokens, hue = false) {
|
|
1158
|
+
if (typeof channel === 'number')
|
|
1159
|
+
return channel;
|
|
1160
|
+
const token = tokens[channel.reference];
|
|
1161
|
+
if (!token)
|
|
1162
|
+
return channel.fallback;
|
|
1163
|
+
if (!hue)
|
|
1164
|
+
return token.angle === undefined ? token.number : undefined;
|
|
1165
|
+
const percentage = typeof token.length === 'string' && token.length.endsWith('%');
|
|
1166
|
+
return percentage ? undefined : (token.angle ?? token.number);
|
|
1003
1167
|
}
|
|
1004
1168
|
/**
|
|
1005
1169
|
* `hsl(h, s, l)` converted to sRGB, printed the same way `fromChannels` prints an rgba() built
|
|
@@ -1008,7 +1172,11 @@ function hslChannel(channel, tokens) {
|
|
|
1008
1172
|
* `h` is degrees; `s` and `l` are fractions, which is what a percentage token already reads as
|
|
1009
1173
|
* (see `formOf`), so nothing here rescales them.
|
|
1010
1174
|
*/
|
|
1011
|
-
function hslToRgb(h,
|
|
1175
|
+
function hslToRgb(h, saturation, l, opacity) {
|
|
1176
|
+
// Out of range as CSS Color 4 takes it: a saturation below 0 is 0, an alpha is clamped, and
|
|
1177
|
+
// anything else is converted as it is, each channel it makes then clamped into sRGB.
|
|
1178
|
+
const s = Math.max(0, saturation);
|
|
1179
|
+
const alpha = Math.min(1, Math.max(0, opacity));
|
|
1012
1180
|
const hue = ((h % 360) + 360) % 360;
|
|
1013
1181
|
const chroma = (1 - Math.abs(2 * l - 1)) * s;
|
|
1014
1182
|
const x = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
@@ -1024,21 +1192,41 @@ function hslToRgb(h, s, l, alpha) {
|
|
|
1024
1192
|
: hue < 300
|
|
1025
1193
|
? [x, 0, chroma]
|
|
1026
1194
|
: [chroma, 0, x];
|
|
1027
|
-
const r = Math.round((
|
|
1028
|
-
const g = Math.round((g1 + m) * 255);
|
|
1029
|
-
const b = Math.round((b1 + m) * 255);
|
|
1195
|
+
const [r, g, b] = [r1, g1, b1].map((c) => Math.round(Math.min(1, Math.max(0, c + m)) * 255));
|
|
1030
1196
|
return alpha >= 1
|
|
1031
1197
|
? `rgb(${r}, ${g}, ${b})`
|
|
1032
1198
|
: `rgba(${r}, ${g}, ${b}, ${Math.round(alpha * 1000) / 1000})`;
|
|
1033
1199
|
}
|
|
1034
|
-
|
|
1200
|
+
/**
|
|
1201
|
+
* A token with its `var()` substituted, through as many aliases and fallbacks as it takes.
|
|
1202
|
+
*
|
|
1203
|
+
* `path` is the aliases being followed on the way here. A number is a cycle, and the index in
|
|
1204
|
+
* `path` where it starts: every property from there on is in it and invalid at computed-value
|
|
1205
|
+
* time, so none of them takes its fallback, while one before it names an invalid property and
|
|
1206
|
+
* does. Anything else is final wherever the chain started, so it is written back to `tokens`, and
|
|
1207
|
+
* no alias is followed twice.
|
|
1208
|
+
*/
|
|
1209
|
+
function follow(name, tokens, path) {
|
|
1035
1210
|
const value = tokens[name];
|
|
1036
1211
|
if (!value?.alias)
|
|
1037
1212
|
return value;
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1213
|
+
const start = path.indexOf(name);
|
|
1214
|
+
if (start >= 0)
|
|
1215
|
+
return start;
|
|
1216
|
+
const depth = path.push(name) - 1;
|
|
1217
|
+
const result = substituted(value, tokens, path, depth);
|
|
1218
|
+
path.pop();
|
|
1219
|
+
if (typeof result !== 'number')
|
|
1220
|
+
settle(tokens, name, result);
|
|
1221
|
+
return result;
|
|
1222
|
+
}
|
|
1223
|
+
/** An alias's target, or its fallback, which may be another `var()`, when the target is unset. */
|
|
1224
|
+
function substituted(value, tokens, path, depth) {
|
|
1225
|
+
const target = follow(value.alias, tokens, path);
|
|
1226
|
+
if (typeof target === 'object' || (typeof target === 'number' && target <= depth))
|
|
1227
|
+
return target;
|
|
1228
|
+
const { fallback } = value;
|
|
1229
|
+
return fallback?.alias ? substituted(fallback, tokens, path, depth) : fallback;
|
|
1042
1230
|
}
|
|
1043
1231
|
/** What a `var()` resolves to: a gradient's stops, or one value with its arithmetic applied. */
|
|
1044
1232
|
function referenced(declaration, tokens) {
|
|
@@ -1055,18 +1243,31 @@ function referenced(declaration, tokens) {
|
|
|
1055
1243
|
break;
|
|
1056
1244
|
value = formOf(tokens[alternative], declaration.kind);
|
|
1057
1245
|
}
|
|
1058
|
-
value ??= declaration
|
|
1059
|
-
const base =
|
|
1246
|
+
value ??= fallbackOf(declaration, tokens);
|
|
1247
|
+
const base = CHANNEL_KINDS.has(declaration.kind)
|
|
1248
|
+
? fromChannels(value, declaration.alpha, tokens, declaration.space)
|
|
1249
|
+
: value;
|
|
1060
1250
|
return declaration.adjust ? adjusted(base, declaration.adjust) : base;
|
|
1061
1251
|
}
|
|
1062
|
-
/**
|
|
1063
|
-
function
|
|
1252
|
+
/** The fallback written, or one made of other tokens, worked out from the tokens where it is used. */
|
|
1253
|
+
function fallbackOf(declaration, tokens) {
|
|
1254
|
+
const token = declaration.fallbackToken;
|
|
1255
|
+
return declaration.fallback ?? (token && formOf(derived(token, tokens), declaration.kind));
|
|
1256
|
+
}
|
|
1257
|
+
const CHANNEL_KINDS = new Set(['channels', 'hslChannels']);
|
|
1258
|
+
/**
|
|
1259
|
+
* `13, 110, 253` as the colour it is, at the alpha the token beside it says, if one does: the
|
|
1260
|
+
* channels form for `rgb()`, the hslChannels form for `hsl()`.
|
|
1261
|
+
*/
|
|
1262
|
+
function fromChannels(value, alpha, tokens, space) {
|
|
1064
1263
|
if (!Array.isArray(value) || value.length !== 3)
|
|
1065
1264
|
return undefined;
|
|
1066
1265
|
// An alpha naming a token nothing set, with no fallback, is no colour: CSS drops it.
|
|
1067
1266
|
const opacity = alpha ? (tokens[alpha.reference]?.number ?? alpha.fallback) : 1;
|
|
1068
1267
|
if (opacity === undefined)
|
|
1069
1268
|
return undefined;
|
|
1269
|
+
if (space === 'hsl')
|
|
1270
|
+
return hslToRgb(value[0], value[1], value[2], opacity);
|
|
1070
1271
|
const [r, g, b] = value;
|
|
1071
1272
|
return opacity >= 1
|
|
1072
1273
|
? `rgb(${r}, ${g}, ${b})`
|
|
@@ -1223,7 +1424,8 @@ function calculated(expression, kind, tokens) {
|
|
|
1223
1424
|
return expression;
|
|
1224
1425
|
if (!Array.isArray(expression)) {
|
|
1225
1426
|
const leaf = expression;
|
|
1226
|
-
|
|
1427
|
+
const own = tokenNumber(tokens[leaf.reference], kind);
|
|
1428
|
+
return (own ?? (leaf.fallback === undefined ? undefined : calculated(leaf.fallback, kind, tokens)));
|
|
1227
1429
|
}
|
|
1228
1430
|
const [op, a, b] = expression;
|
|
1229
1431
|
const left = calculated(a, kind, tokens);
|
|
@@ -1236,6 +1438,10 @@ function calculated(expression, kind, tokens) {
|
|
|
1236
1438
|
return left - right;
|
|
1237
1439
|
if (op === '*')
|
|
1238
1440
|
return left * right;
|
|
1441
|
+
if (op === 'max')
|
|
1442
|
+
return Math.max(left, right);
|
|
1443
|
+
if (op === 'min')
|
|
1444
|
+
return Math.min(left, right);
|
|
1239
1445
|
return left / right;
|
|
1240
1446
|
}
|
|
1241
1447
|
/** Where each kind of slot reads a token from, before its bare number. */
|
|
@@ -1328,17 +1534,34 @@ function settledMarker(value, tokens) {
|
|
|
1328
1534
|
const sum = value.__calc;
|
|
1329
1535
|
if (sum)
|
|
1330
1536
|
return resolveCalc(sum, tokens);
|
|
1537
|
+
const shadow = value.__dropShadow;
|
|
1538
|
+
if (shadow)
|
|
1539
|
+
return dropShadowOf(tokens[shadow.reference]?.shadow);
|
|
1331
1540
|
return NOT_A_MARKER;
|
|
1332
1541
|
}
|
|
1542
|
+
/**
|
|
1543
|
+
* A shadow token as the drop shadow a filter draws: `drop-shadow(var(--drop-shadow-xs))`. One
|
|
1544
|
+
* shadow, never inset and with no spread, which is all a drop shadow can be; nothing otherwise.
|
|
1545
|
+
*/
|
|
1546
|
+
function dropShadowOf(shadow) {
|
|
1547
|
+
if (!Array.isArray(shadow) || shadow.length !== 1)
|
|
1548
|
+
return undefined;
|
|
1549
|
+
const [{ offsetX, offsetY, blurRadius, spreadDistance, color, inset }] = shadow;
|
|
1550
|
+
if (spreadDistance !== 0 || inset || typeof color !== 'string')
|
|
1551
|
+
return undefined;
|
|
1552
|
+
return { offsetX, offsetY, standardDeviation: blurRadius, color };
|
|
1553
|
+
}
|
|
1333
1554
|
/** What a colour that cannot be settled makes of the whole value it is in. */
|
|
1334
1555
|
const UNSETTLED = Symbol('unsettled');
|
|
1335
1556
|
/** `rgba(var(--channels), <alpha>)`: the token's channels, at the alpha written or tokened. */
|
|
1336
1557
|
function channelsColour(expression, tokens) {
|
|
1337
|
-
const
|
|
1558
|
+
const { space } = expression.channels;
|
|
1559
|
+
const kind = space === 'hsl' ? 'hslChannels' : 'channels';
|
|
1560
|
+
const value = formOf(tokens[expression.channels.reference], kind) ?? expression.channels.fallback;
|
|
1338
1561
|
const { alpha } = expression;
|
|
1339
1562
|
if (typeof alpha !== 'number')
|
|
1340
|
-
return fromChannels(value, alpha, tokens);
|
|
1341
|
-
const opaque = fromChannels(value, undefined, tokens);
|
|
1563
|
+
return fromChannels(value, alpha, tokens, space);
|
|
1564
|
+
const opaque = fromChannels(value, undefined, tokens, space);
|
|
1342
1565
|
return opaque === undefined ? undefined : faded(opaque, alpha);
|
|
1343
1566
|
}
|
|
1344
1567
|
/** A token's colour, or the first of its alternatives that is defined, or its fallback. */
|
|
@@ -1364,11 +1587,20 @@ function colourMarker(value) {
|
|
|
1364
1587
|
function adjusted(value, adjust) {
|
|
1365
1588
|
if (adjust.alpha !== undefined)
|
|
1366
1589
|
return faded(value, adjust.alpha);
|
|
1590
|
+
const em = value?.__defer;
|
|
1591
|
+
if (em && adjust.floor === undefined)
|
|
1592
|
+
return adjustedEm(em, adjust);
|
|
1367
1593
|
if (typeof value !== 'number')
|
|
1368
1594
|
return value;
|
|
1369
1595
|
const scaled = value * (adjust.scale ?? 1) + (adjust.offset ?? 0);
|
|
1370
1596
|
return adjust.floor === undefined ? scaled : Math.max(scaled, adjust.floor);
|
|
1371
1597
|
}
|
|
1598
|
+
/** A length in em, worked out against the font size later, with the arithmetic moved inside it. */
|
|
1599
|
+
function adjustedEm(em, adjust) {
|
|
1600
|
+
const scale = adjust.scale ?? 1;
|
|
1601
|
+
const offset = (em.offset ?? 0) * scale + (adjust.offset ?? 0);
|
|
1602
|
+
return { __defer: { ...em, factor: em.factor * scale, ...(offset ? { offset } : {}) } };
|
|
1603
|
+
}
|
|
1372
1604
|
/** `rgb(r, g, b)` and `rgba(r, g, b, a)`, which is every colour the compiler emits. */
|
|
1373
1605
|
const RGB = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/;
|
|
1374
1606
|
/**
|