@ng-native/fabric 0.2.0 → 0.4.0

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