@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/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 = { lineHeight: 'length', family: 'keyword' };
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
- if (parentInherited === EMPTY && parentContext === ROOT_CONTEXT && !node.styleDirty) {
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: parent ? parent.tokens : this.tokensOnRoot,
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 (!declaration.important && important && prop in important)
937
+ if (outranked(prop))
870
938
  continue;
871
- own[prop] = value;
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 || declaration.fallback !== undefined || name in tokens)
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
- if (marked === 'currentcolor')
928
- return own['color'] ?? parentInherited['color'] ?? 'black';
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(formOf(tokens[slot.marker.reference], slot.kind) ?? slot.marker.fallback);
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
- // Until a pass settles nothing more: one made of another defined after it, or of one not yet
1021
- // settled, cannot be worked out until that one is. What is left then is a cycle, or reads a
1022
- // token that is not there, and is unset.
1023
- const derivedNames = names.filter((name) => isDerived(own[name]));
1024
- let pending = derivedNames;
1025
- for (let settled = true; settled && pending.length;) {
1026
- settled = false;
1027
- pending = pending.filter((name) => {
1028
- const value = derived(own[name], merged);
1029
- if (!value)
1030
- return true;
1031
- merged[name] = value;
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
- return false;
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
- for (const name of pending)
1037
- delete merged[name];
1038
- // A chain that ends at one of those, by a link or a fallback, copied it unsettled above into
1039
- // every alias on the way: follow them all again, from what they were defined as, now it is.
1040
- if (derivedNames.length && aliases.length) {
1041
- for (const name of aliases)
1042
- merged[name] = own[name];
1043
- followAliases(aliases, merged);
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 merged;
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
- function hslChannel(channel, tokens) {
1103
- return typeof channel === 'number'
1104
- ? channel
1105
- : (tokens[channel.reference]?.number ?? channel.fallback);
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, s, l, alpha) {
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((r1 + m) * 255);
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
- let value = formOf(tokens[declaration.reference], declaration.kind);
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 a length with arithmetic reads the bare number too.
1175
- if (value === undefined && declaration.adjust && declaration.kind === 'length') {
1176
- value = tokens[declaration.reference]?.number;
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
- value ??= declaration.fallback;
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 ? (tokens[alpha.reference]?.number ?? alpha.fallback) : 1;
1199
- if (opacity === undefined)
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
- /** A template's stops with their tokens read, leaving out any whose colour is not set. */
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 color = stop.colour ? resolveColour(stop.colour, tokens) : tokens[stop.reference]?.color;
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.positionReference
1245
- ? (tokens[stop.positionReference]?.length ?? stop.position)
1246
- : stop.position;
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 { reference, fallback } = offset;
1265
- const value = tokens[reference]?.length ?? fallback;
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 { space, hue, a, aPercentage, b, bPercentage } = expression.mix;
1291
- const first = resolveColour(a, tokens);
1292
- const second = resolveColour(b, tokens);
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 mixColours(space, { colour: first, percentage: aPercentage }, { colour: second, percentage: bPercentage }, hue);
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
- const leaf = expression;
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
- const value = formOf(tokens[marker.reference], 'length') ?? marker.fallback;
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 = formOf(tokens[expression.channels.reference], kind) ?? expression.channels.fallback;
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
- let value = formOf(tokens[expression.reference], 'color');
1501
- for (const alternative of expression.alternatives ?? []) {
1502
- if (value !== undefined)
1503
- break;
1504
- value = formOf(tokens[alternative], 'color');
1505
- }
1506
- return value ?? expression.fallback;
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) {