@ng-native/fabric 0.3.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
@@ -6,6 +6,7 @@
6
6
  * framework-agnostic and knows nothing about Angular.
7
7
  */
8
8
  import { mixChannels, mixColours, } from "./color-mix.js";
9
+ import { ELEMENT_STYLES } from "./element-styles.js";
9
10
  import { relativeColour } from "./relative-colour.js";
10
11
  /** A run of CSS whitespace: spaces, tabs, newlines, carriage returns and form feeds, and no other. */
11
12
  export const CSS_SPACE = /[ \t\n\r\f]+/;
@@ -57,7 +58,15 @@ const INHERITED = new Set([
57
58
  'textShadowRadius',
58
59
  'textShadowColor',
59
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',
60
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']);
61
70
  /**
62
71
  * `border-style: none`, which the compiler writes as a style native does not have. On the web it
63
72
  * makes every border width 0, whichever rule set the width, so `border-hidden border-x` draws no
@@ -353,6 +362,44 @@ function carryDeferred(deferred, rule) {
353
362
  (kept ??= []).push(...rule.deferred);
354
363
  return kept;
355
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
+ }
356
403
  function countPosition(node, siblings, fromEnd) {
357
404
  let position = 0;
358
405
  let seen = 0;
@@ -388,6 +435,35 @@ function lastElement(siblings) {
388
435
  * precedence people already have in their fingers rather than inventing a rule for native.
389
436
  */
390
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
+ }));
391
467
  /**
392
468
  * The bucket a rule is filed under: the most selective simple selector in its rightmost compound,
393
469
  * which is what a browser calls the key selector.
@@ -499,12 +575,82 @@ function inheritFrom(parentInherited, own) {
499
575
  for (const key of Object.keys(own)) {
500
576
  if (!INHERITED.has(key))
501
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;
502
582
  if (inherited === parentInherited)
503
583
  inherited = { ...parentInherited };
504
- 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
+ }
505
590
  }
506
591
  return inherited;
507
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
+ }
508
654
  /**
509
655
  * The runtime half of CSS for one engine: the global sheet, the conditions media queries are
510
656
  * evaluated against, and the memo that makes resolution cost nodes x rules rather than
@@ -542,6 +688,21 @@ export class StyleResolver {
542
688
  * single index.
543
689
  */
544
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;
545
706
  /**
546
707
  * The application-level sheet, if any: the one set of rules allowed to match a node whatever
547
708
  * component created it, which is what resets, utility classes and `:root` tokens all need.
@@ -641,11 +802,13 @@ export class StyleResolver {
641
802
  return cached;
642
803
  }
643
804
  const parentInherited = parent ? parent.inherited : EMPTY;
644
- 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)) {
645
807
  return this.unstyled(node, epoch, parent, parentContext, parentInherited);
646
808
  }
647
809
  styleStats.nodesResolved++;
648
- 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);
649
812
  const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
650
813
  const styled = this.styled(node, matched, parentTokens, parentInherited);
651
814
  const cache = {
@@ -685,7 +848,9 @@ export class StyleResolver {
685
848
  node.hostSheet === null &&
686
849
  this.globalSheet === null &&
687
850
  this.addedSheets.length === 0 &&
688
- !node.customProperties);
851
+ !node.customProperties &&
852
+ !node.boundStyle &&
853
+ !node.styled);
689
854
  }
690
855
  /**
691
856
  * A node with no rules anywhere: it renders with what it inherits and passes the same map on.
@@ -698,7 +863,10 @@ export class StyleResolver {
698
863
  */
699
864
  unstyled(node, epoch, parent, parentContext, parentInherited) {
700
865
  const tokens = parent ? parent.tokens : this.tokensOnRoot;
701
- if (parentInherited === EMPTY &&
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 &&
702
870
  parentContext === ROOT_CONTEXT &&
703
871
  tokens === NO_TOKENS &&
704
872
  !node.styleDirty) {
@@ -714,7 +882,7 @@ export class StyleResolver {
714
882
  context: {},
715
883
  parentContext,
716
884
  style: parentInherited,
717
- inherited: parentInherited,
885
+ inherited: inline ? inheritFrom(parentInherited, inline) : parentInherited,
718
886
  tokens,
719
887
  };
720
888
  node.styleCache = passthrough;
@@ -756,6 +924,7 @@ export class StyleResolver {
756
924
  const add = (from, bump) => {
757
925
  if (!from)
758
926
  return;
927
+ this.placeLayers(from);
759
928
  for (const rule of from.rules) {
760
929
  entries.push({ rule, sheet: from, weight: rule.specificity + bump });
761
930
  }
@@ -765,7 +934,41 @@ export class StyleResolver {
765
934
  add(sheet, COMPONENT_SPECIFICITY_BUMP);
766
935
  for (const added of this.addedSheets)
767
936
  add(added, 0);
768
- return entries.sort((a, b) => a.weight - b.weight);
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
+ }
769
972
  }
770
973
  /**
771
974
  * A node's style from the rules it matched, shared with every other node that matched the same
@@ -777,8 +980,11 @@ export class StyleResolver {
777
980
  * written to after, so sharing it is safe; each node still has a cache and a context of its own.
778
981
  */
779
982
  styled(node, matched, parentTokens, parentInherited) {
780
- if (node.customProperties)
781
- 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
+ }
782
988
  if (this.sharedGeneration !== this.generation) {
783
989
  this.shared.clear();
784
990
  this.sharedGeneration = this.generation;
@@ -808,8 +1014,15 @@ export class StyleResolver {
808
1014
  return id;
809
1015
  }
810
1016
  /** A node's style from the rules it matched, worked out. */
811
- style(node, matched, parentTokens, parentInherited) {
812
- 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);
813
1026
  // Tokens are in scope for this node's own declarations as well as its descendants', so they
814
1027
  // are merged before any `var()` here is resolved.
815
1028
  const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
@@ -819,11 +1032,11 @@ export class StyleResolver {
819
1032
  }
820
1033
  if (own['borderStyle'] === 'none')
821
1034
  drawNoBorder(own);
822
- return {
823
- style: { ...parentInherited, ...own },
824
- inherited: inheritFrom(parentInherited, own),
825
- tokens,
826
- };
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 };
827
1040
  }
828
1041
  /** The rules that apply to a node, weakest first. */
829
1042
  matched(node, entries) {
@@ -840,8 +1053,9 @@ export class StyleResolver {
840
1053
  const important = {};
841
1054
  let hasImportant = false;
842
1055
  let tokens = null;
1056
+ let importantTokens = null;
843
1057
  let deferred = null;
844
- for (const rule of matched) {
1058
+ for (const rule of this.layered ? byImportance(matched, this.layerPlaces) : matched) {
845
1059
  Object.assign(normal, rule.declarations);
846
1060
  if (rule.important) {
847
1061
  Object.assign(important, rule.important);
@@ -849,8 +1063,14 @@ export class StyleResolver {
849
1063
  }
850
1064
  if (rule.tokens)
851
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
+ }
852
1070
  deferred = carryDeferred(deferred, rule);
853
1071
  }
1072
+ if (importantTokens)
1073
+ Object.assign(tokens, importantTokens);
854
1074
  return {
855
1075
  declarations: hasImportant ? { ...normal, ...important } : normal,
856
1076
  important: hasImportant ? important : null,
@@ -969,6 +1189,14 @@ export class StyleResolver {
969
1189
  const values = lineValues(line, tokens);
970
1190
  if (!values)
971
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
+ }
972
1200
  // `currentcolor` is the colour in scope, which only the node knows.
973
1201
  if (values[line.colors[0]] === 'currentcolor') {
974
1202
  const colour = own['color'] ?? parentInherited['color'] ?? 'black';
@@ -993,7 +1221,7 @@ export class StyleResolver {
993
1221
  return;
994
1222
  if (declaration.alternatives?.some((alternative) => alternative in tokens))
995
1223
  return;
996
- this.onUndefinedToken(name, declaration.props);
1224
+ this.onUndefinedToken(name, declaration.props, declaration.on);
997
1225
  }
998
1226
  /** A display token set to a value native has no display for. */
999
1227
  reportDisplay(declaration, tokens) {
@@ -1022,13 +1250,15 @@ export class StyleResolver {
1022
1250
  const fill = (part) => this.filledIn(part, declaration, own, parentInherited, tokens);
1023
1251
  if (Array.isArray(value))
1024
1252
  return this.filledList(value, fill, tokens);
1025
- const marked = settledMarker(value, tokens);
1026
1253
  // `currentcolor` is the colour in scope, which only the node knows: Tailwind's ring default.
1027
1254
  // On `color` itself that is the inherited colour, as CSS reads it.
1028
- if (marked === 'currentcolor') {
1255
+ const current = () => {
1029
1256
  const ownColour = declaration.props.includes('color') ? undefined : own['color'];
1030
- return ownColour ?? parentInherited['color'] ?? 'black';
1031
- }
1257
+ return (ownColour ?? parentInherited['color'] ?? 'black');
1258
+ };
1259
+ const marked = settledMarker(value, tokens, current);
1260
+ if (marked === 'currentcolor')
1261
+ return current();
1032
1262
  if (marked !== NOT_A_MARKER)
1033
1263
  return marked ?? UNSETTLED;
1034
1264
  const pending = value.__defer;
@@ -1531,6 +1761,75 @@ function tokenForm(token, kind) {
1531
1761
  const value = formOf(token, kind);
1532
1762
  return value === undefined && kind === 'color' && isCurrentColour(token) ? CURRENT_COLOUR : value;
1533
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
+ }
1534
1833
  /** The fallback written, or one made of other tokens, worked out from the tokens where it is used. */
1535
1834
  function fallbackOf(declaration, tokens) {
1536
1835
  const token = declaration.fallbackToken;
@@ -1744,8 +2043,11 @@ function placed(position, tokens) {
1744
2043
  /**
1745
2044
  * A colour expression with its tokens looked up and any mix in it worked out. Undefined when a
1746
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.
1747
2049
  */
1748
- function resolveColour(expression, tokens) {
2050
+ function resolveColour(expression, tokens, current) {
1749
2051
  if ('color' in expression)
1750
2052
  return expression.color;
1751
2053
  if ('channels' in expression)
@@ -1756,15 +2058,27 @@ function resolveColour(expression, tokens) {
1756
2058
  return tokenColour(expression, tokens);
1757
2059
  if ('relative' in expression) {
1758
2060
  const { space, from, channels, alpha } = expression.relative;
1759
- const origin = resolveColour(from, tokens);
2061
+ const origin = innerColour(from, tokens, current);
1760
2062
  return origin === undefined ? undefined : relativeColour(space, origin, channels, alpha);
1761
2063
  }
1762
- const sides = mixSides(expression.mix, tokens);
2064
+ const sides = mixSides(expression.mix, tokens, current);
1763
2065
  return sides && mixColours(expression.mix.space, ...sides, expression.mix.hue);
1764
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
+ }
1765
2079
  /** 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);
2080
+ function mixSides(mix, tokens, current) {
2081
+ const colour = (side) => 'mix' in side ? unroundedMix(side.mix, tokens, current) : innerColour(side, tokens, current);
1768
2082
  const first = colour(mix.a);
1769
2083
  const second = colour(mix.b);
1770
2084
  if (first === undefined || second === undefined)
@@ -1774,8 +2088,8 @@ function mixSides(mix, tokens) {
1774
2088
  { colour: second, percentage: mix.bPercentage },
1775
2089
  ];
1776
2090
  }
1777
- function unroundedMix(mix, tokens) {
1778
- const sides = mixSides(mix, tokens);
2091
+ function unroundedMix(mix, tokens, current) {
2092
+ const sides = mixSides(mix, tokens, current);
1779
2093
  return sides && mixChannels(mix.space, ...sides, mix.hue);
1780
2094
  }
1781
2095
  /** A calc marker worked out, as its slot takes it; undefined when a token in it is not set. */
@@ -1951,10 +2265,10 @@ const NEEDS_FILLING = new WeakMap();
1951
2265
  /** What `settledMarker` says of a part that is not a token's marker at all. */
1952
2266
  const NOT_A_MARKER = Symbol('not a marker');
1953
2267
  /** A colour, length or calc marker settled from the tokens, or undefined when one cannot be. */
1954
- function settledMarker(value, tokens) {
2268
+ function settledMarker(value, tokens, current) {
1955
2269
  const colour = colourMarker(value);
1956
2270
  if (colour)
1957
- return resolveColour(colour, tokens);
2271
+ return resolveColour(colour, tokens, current);
1958
2272
  // Whether a shadow is inset, from a token that is the word or nothing: `ring-inset`.
1959
2273
  const inset = value.__inset;
1960
2274
  if (inset)