@ng-native/fabric 0.0.1 → 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -1
  3. package/dist/color-mix.d.ts +20 -0
  4. package/dist/color-mix.js +306 -0
  5. package/dist/color-mix.js.map +1 -0
  6. package/dist/css.d.ts +615 -0
  7. package/dist/css.js +1405 -0
  8. package/dist/css.js.map +1 -0
  9. package/dist/defer-triggers.d.ts +53 -0
  10. package/dist/defer-triggers.js +184 -0
  11. package/dist/defer-triggers.js.map +1 -0
  12. package/dist/engine.d.ts +1066 -0
  13. package/dist/engine.js +2764 -0
  14. package/dist/engine.js.map +1 -0
  15. package/dist/fabric.d.ts +20 -0
  16. package/dist/fabric.js +44 -0
  17. package/dist/fabric.js.map +1 -0
  18. package/dist/forms-inputs.d.ts +15 -0
  19. package/dist/forms-inputs.js +54 -0
  20. package/dist/forms-inputs.js.map +1 -0
  21. package/dist/host.d.ts +133 -0
  22. package/dist/host.js +55 -0
  23. package/dist/host.js.map +1 -0
  24. package/dist/index.d.ts +20 -0
  25. package/dist/index.js +21 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/inline-token.d.ts +12 -0
  28. package/dist/inline-token.js +52 -0
  29. package/dist/inline-token.js.map +1 -0
  30. package/dist/inline-transform.d.ts +14 -0
  31. package/dist/inline-transform.js +41 -0
  32. package/dist/inline-transform.js.map +1 -0
  33. package/dist/native-drive.d.ts +100 -0
  34. package/dist/native-drive.js +318 -0
  35. package/dist/native-drive.js.map +1 -0
  36. package/dist/relative-colour.d.ts +17 -0
  37. package/dist/relative-colour.js +71 -0
  38. package/dist/relative-colour.js.map +1 -0
  39. package/dist/scroll-animation.d.ts +41 -0
  40. package/dist/scroll-animation.js +169 -0
  41. package/dist/scroll-animation.js.map +1 -0
  42. package/dist/transition.d.ts +139 -0
  43. package/dist/transition.js +569 -0
  44. package/dist/transition.js.map +1 -0
  45. package/package.json +43 -3
package/dist/css.js ADDED
@@ -0,0 +1,1405 @@
1
+ /**
2
+ * Runtime half of the CSS system: selector matching and the cascade.
3
+ *
4
+ * All the expensive work happened at build time (parsing, longhand expansion, value conversion,
5
+ * specificity maths, sorting), so this only walks a pre-sorted rule list and merges. It is
6
+ * framework-agnostic and knows nothing about Angular.
7
+ */
8
+ import { mixColours } from "./color-mix.js";
9
+ import { relativeColour } from "./relative-colour.js";
10
+ /**
11
+ * Where a form a token does not have can be read from instead. A length is already a
12
+ * line-height and a single word is already a family, so neither is stored twice in every token
13
+ * that happens to be one. The compiler reads fallbacks with the same table.
14
+ */
15
+ const STAND_IN = { lineHeight: 'length', family: 'keyword' };
16
+ function formOf(token, kind) {
17
+ if (!token)
18
+ return undefined;
19
+ const stand = STAND_IN[kind];
20
+ return token[kind] ?? (stand ? token[stand] : undefined);
21
+ }
22
+ /**
23
+ * Properties CSS inherits that mean something on native.
24
+ *
25
+ * RN does not inherit these: a `color` on a view has no effect on a text inside it, only a text
26
+ * inside a text inherits. CSS authors expect otherwise, and a stylesheet written against that
27
+ * expectation would silently lose every colour, so the cascade emulates inheritance for exactly
28
+ * this set. NativeScript does the same.
29
+ */
30
+ const INHERITED = new Set([
31
+ 'color',
32
+ // Yoga inherits it for layout already; carried here as well so a paragraph can align by it.
33
+ 'direction',
34
+ 'fontFamily',
35
+ 'fontSize',
36
+ 'fontStyle',
37
+ 'fontWeight',
38
+ 'fontVariant',
39
+ 'letterSpacing',
40
+ 'lineHeight',
41
+ 'textAlign',
42
+ 'textTransform',
43
+ 'textDecorationLine',
44
+ 'writingDirection',
45
+ // CSS inherits a text shadow, and `user-select: auto` takes the parent's, so a card's
46
+ // `text-shadow-md` or `select-none` reaches the text inside it.
47
+ 'textShadowOffset',
48
+ 'textShadowRadius',
49
+ 'textShadowColor',
50
+ 'selectable',
51
+ ]);
52
+ /**
53
+ * `border-style: none`, which the compiler writes as a style native does not have. On the web it
54
+ * makes every border width 0, whichever rule set the width, so `border-hidden border-x` draws no
55
+ * side. Settled here, once the cascade has picked a style, and not sent on.
56
+ */
57
+ function drawNoBorder(own) {
58
+ delete own['borderStyle'];
59
+ for (const key of Object.keys(own))
60
+ if (/^border\w*Width$/.test(key))
61
+ own[key] = 0;
62
+ own['borderWidth'] = 0;
63
+ }
64
+ /**
65
+ * Deterministic work counters for the matcher.
66
+ *
67
+ * Wall-clock is noise; a compound comparison is not. Asserting on these catches an algorithmic
68
+ * regression (a walk that became quadratic, a cache that stopped hitting) in a way that a timing
69
+ * assertion never could without being flaky. `test/css-cost.test.ts` holds the baselines.
70
+ *
71
+ * The cost is one integer increment per comparison, against a Set lookup and several property
72
+ * reads, so it does not distort what it measures.
73
+ */
74
+ export const styleStats = {
75
+ /** Calls to `matchesCompound`, the innermost unit of matching work. */
76
+ compoundTests: 0,
77
+ /** Rules considered, i.e. calls to `matches`. */
78
+ ruleTests: 0,
79
+ /** Nodes cascaded, as opposed to handed back from the memo. */
80
+ nodesResolved: 0,
81
+ };
82
+ export function resetStyleStats() {
83
+ styleStats.compoundTests = 0;
84
+ styleStats.ruleTests = 0;
85
+ styleStats.nodesResolved = 0;
86
+ }
87
+ /** A prop's value as an attribute test compares it: in lower case, under the i flag. */
88
+ const attributeText = (actual, test) => test.insensitive ? String(actual).toLowerCase() : String(actual);
89
+ /**
90
+ * `[disabled]`, and the value operators.
91
+ *
92
+ * A prop set to `false` or `null` counts as absent, matching the web, where an attribute is either
93
+ * present or not and a `disabled` binding that evaluates false removes it. Everything else is
94
+ * compared as a string, so `[tabIndex="0"]` works against a numeric prop.
95
+ */
96
+ function matchesAttribute(node, test) {
97
+ const actual = node.props[test.name];
98
+ if (actual === undefined || actual === null || actual === false)
99
+ return false;
100
+ if (test.operator === undefined)
101
+ return true;
102
+ const value = attributeText(actual, test);
103
+ switch (test.operator) {
104
+ case 'equal':
105
+ return value === test.value;
106
+ case 'prefix':
107
+ return value.startsWith(test.value);
108
+ case 'suffix':
109
+ return value.endsWith(test.value);
110
+ case 'substring':
111
+ return value.includes(test.value);
112
+ case 'includes':
113
+ // `~=` matches one whole word of a space-separated list.
114
+ return value.split(/\s+/).includes(test.value);
115
+ case 'dash-match':
116
+ // `|=` matches the value itself or the value followed by a hyphen, as `lang` uses.
117
+ return value === test.value || value.startsWith(`${test.value}-`);
118
+ }
119
+ }
120
+ /*
121
+ * Exempt from the complexity limit deliberately. This is a flat conjunction of independent
122
+ * guards, not nested logic: cyclomatic complexity scores the two the same, and they are not the
123
+ * same thing. It is also the hottest function in the matcher, called tens of thousands of times
124
+ * per commit, so splitting each guard into its own call would cost more than it reads better.
125
+ */
126
+ // eslint-disable-next-line complexity
127
+ function matchesCompound(node, compound, sheet) {
128
+ styleStats.compoundTests++;
129
+ if (compound.host !== undefined && node.hostSheet !== sheet)
130
+ return false;
131
+ if (compound.root !== undefined && node.parent !== null)
132
+ return false;
133
+ if (compound.pseudo !== undefined) {
134
+ for (const state of compound.pseudo) {
135
+ if (state === 'focus' ? !node.focused : !node.active)
136
+ return false;
137
+ }
138
+ }
139
+ if (compound.type !== undefined && compound.type !== node.name)
140
+ return false;
141
+ if (compound.id !== undefined && node.props['nativeID'] !== compound.id)
142
+ return false;
143
+ for (const className of compound.classes) {
144
+ if (!node.classes?.has(className))
145
+ return false;
146
+ }
147
+ if (compound.attributes) {
148
+ for (const test of compound.attributes) {
149
+ if (!matchesAttribute(node, test))
150
+ return false;
151
+ }
152
+ }
153
+ // An anchor is a comment on the web, and :empty does not see comments.
154
+ if (compound.empty !== undefined && node.children?.some((child) => child.kind !== 'anchor')) {
155
+ return false;
156
+ }
157
+ if (compound.nth !== undefined) {
158
+ for (const test of compound.nth) {
159
+ if (!matchesNth(node, test))
160
+ return false;
161
+ }
162
+ }
163
+ if (compound.not) {
164
+ for (const excluded of compound.not) {
165
+ if (matchesCompound(node, excluded, sheet))
166
+ return false;
167
+ }
168
+ }
169
+ if (compound.is) {
170
+ for (const alternatives of compound.is) {
171
+ if (!alternatives.some((option) => matchesCompound(node, option, sheet)))
172
+ return false;
173
+ }
174
+ }
175
+ if (compound.ancestors) {
176
+ for (const ancestor of compound.ancestors) {
177
+ let found = false;
178
+ // From the parent: a descendant combinator never matches the node against itself.
179
+ for (let n = node.parent; n && !found; n = n.parent) {
180
+ found = matchesCompound(n, ancestor, sheet);
181
+ }
182
+ if (!found)
183
+ return false;
184
+ }
185
+ }
186
+ if (compound.hostContext) {
187
+ // The host itself counts, as it does on the web.
188
+ let found = false;
189
+ for (let n = node; n && !found; n = n.parent) {
190
+ found = compound.hostContext.some((option) => matchesCompound(n, option, sheet));
191
+ }
192
+ if (!found)
193
+ return false;
194
+ }
195
+ return true;
196
+ }
197
+ /**
198
+ * Right-to-left matching, as every CSS engine does it: the rightmost compound is the cheapest
199
+ * filter and rejects almost everything immediately.
200
+ */
201
+ export function matches(node, rule, sheet = null) {
202
+ styleStats.ruleTests++;
203
+ const last = rule.compounds.length - 1;
204
+ const subject = rule.compounds[last];
205
+ // From the sheet of the component this node hosts, only a :host rule reaches it. Its other
206
+ // rules are for the elements inside that component, which is how emulated encapsulation scopes
207
+ // them on the web; without this, a class the parent puts on the host picks up the child's rule
208
+ // of the same name.
209
+ if (sheet !== null &&
210
+ sheet === node.hostSheet &&
211
+ sheet !== node.sheet &&
212
+ subject.host === undefined) {
213
+ return false;
214
+ }
215
+ if (!matchesCompound(node, subject, sheet))
216
+ return false;
217
+ return matchPrefix(node, rule, last - 1, sheet);
218
+ }
219
+ /**
220
+ * Match the part of a selector to the left of an already-matched compound.
221
+ *
222
+ * `index` is the compound still to match, counting leftwards, and `from` is where to start
223
+ * looking. A descendant combinator tries every ancestor and, crucially, keeps trying when the
224
+ * rest of the selector does not work out from there. Taking the first ancestor that fits and
225
+ * committing to it misses `.a > .b .c` whenever the nearest `.b` fails the child combinator while
226
+ * a further one satisfies it, which is what the browser differential suite caught.
227
+ */
228
+ function matchPrefix(matched, rule, index, sheet) {
229
+ if (index < 0)
230
+ return true;
231
+ const compound = rule.compounds[index];
232
+ // `combinators[index]` joins this compound to the one on its right.
233
+ switch (rule.combinators[index]) {
234
+ case 'child':
235
+ return matchStep(matched.parent, rule, index, compound, sheet);
236
+ case 'next-sibling':
237
+ return matchStep(previousElement(matched), rule, index, compound, sheet);
238
+ case 'later-sibling':
239
+ return matchAny(previousElement(matched), previousElement, rule, index, compound, sheet);
240
+ default:
241
+ return matchAny(matched.parent, (node) => node.parent, rule, index, compound, sheet);
242
+ }
243
+ }
244
+ /** A combinator with exactly one candidate: the parent, or the sibling immediately before. */
245
+ function matchStep(candidate, rule, index, compound, sheet) {
246
+ if (!candidate || !matchesCompound(candidate, compound, sheet))
247
+ return false;
248
+ return matchPrefix(candidate, rule, index - 1, sheet);
249
+ }
250
+ /**
251
+ * A combinator with many candidates: every ancestor, or every earlier sibling.
252
+ *
253
+ * Crucially it keeps trying when the rest of the selector does not work out from a candidate that
254
+ * fits. Taking the first and committing to it misses `.a > .b .c` whenever the nearest `.b` fails
255
+ * the child combinator while a further one satisfies it, which is what the browser differential
256
+ * suite caught.
257
+ */
258
+ function matchAny(first, next, rule, index, compound, sheet) {
259
+ for (let candidate = first; candidate; candidate = next(candidate)) {
260
+ if (matchesCompound(candidate, compound, sheet) &&
261
+ matchPrefix(candidate, rule, index - 1, sheet)) {
262
+ return true;
263
+ }
264
+ }
265
+ return false;
266
+ }
267
+ /** Whether a node is one of the children CSS counts. Text and anchors are children, not elements. */
268
+ const isElement = (node) => node.kind === undefined || node.kind === 'element';
269
+ /** The element before this one, skipping everything that is not one. */
270
+ function previousElement(node) {
271
+ const siblings = node.parent?.children;
272
+ if (!siblings)
273
+ return null;
274
+ for (let i = siblings.indexOf(node) - 1; i >= 0; i--) {
275
+ if (isElement(siblings[i]))
276
+ return siblings[i];
277
+ }
278
+ return null;
279
+ }
280
+ /**
281
+ * Whether a node's position satisfies `an + b`.
282
+ *
283
+ * ponytail: the general case counts the node's siblings, so a rule like `:nth-child(2n)` over a
284
+ * long list is quadratic in it. The two that matter - first and last - are answered without
285
+ * counting, and the rest wait for a list that is actually slow.
286
+ */
287
+ function matchesNth(node, test) {
288
+ const siblings = node.parent?.children;
289
+ if (!siblings)
290
+ return false;
291
+ if (test.a === 0 && test.b === 1) {
292
+ return (test.fromEnd ? lastElement(siblings) : firstElement(siblings)) === node;
293
+ }
294
+ const from = countPosition(node, siblings, test.fromEnd === true);
295
+ if (from === 0)
296
+ return false;
297
+ // `an + b` for some whole number of steps, which is what CSS counts from.
298
+ if (test.a === 0)
299
+ return from === test.b;
300
+ const steps = (from - test.b) / test.a;
301
+ return Number.isInteger(steps) && steps >= 0;
302
+ }
303
+ /** Which element this is among its siblings, counting from one end or the other. Zero if absent. */
304
+ /**
305
+ * Drop from the pending deferred set every property a later rule has now set outright.
306
+ *
307
+ * Per property rather than per declaration: one deferred declaration can cover four sides, and a
308
+ * later `padding-left` should take only that side away from it.
309
+ */
310
+ function overriddenBy(deferred, declarations, important) {
311
+ const kept = [];
312
+ for (const declaration of deferred) {
313
+ // A plain declaration cannot take anything from an important one.
314
+ if (declaration.important && !important) {
315
+ kept.push(declaration);
316
+ continue;
317
+ }
318
+ const props = declaration.props.filter((prop) => !(prop in declarations));
319
+ if (props.length === declaration.props.length)
320
+ kept.push(declaration);
321
+ else if (props.length)
322
+ kept.push({ ...declaration, props });
323
+ }
324
+ return kept.length ? kept : null;
325
+ }
326
+ /**
327
+ * The deferred values still standing once `rule` has been applied, plus its own.
328
+ *
329
+ * A plain declaration from this rule beats a `var()` from an earlier one. Deferred values are
330
+ * resolved after the cascade has run, so without this a token would win whatever the cascade
331
+ * said: `.row { background: var(--surface) }` would repaint over
332
+ * `.row:nth-child(odd) { background: ... }`, which is a rule with a pseudo-class on top of the
333
+ * same class and should win. An important one survives a plain declaration, and only an important
334
+ * declaration takes it away.
335
+ */
336
+ function carryDeferred(deferred, rule) {
337
+ let kept = deferred;
338
+ if (kept && Object.keys(rule.declarations).length) {
339
+ kept = overriddenBy(kept, rule.declarations, false);
340
+ }
341
+ if (kept && rule.important)
342
+ kept = overriddenBy(kept, rule.important, true);
343
+ if (rule.deferred)
344
+ (kept ??= []).push(...rule.deferred);
345
+ return kept;
346
+ }
347
+ function countPosition(node, siblings, fromEnd) {
348
+ let position = 0;
349
+ let seen = 0;
350
+ for (const sibling of siblings) {
351
+ if (!isElement(sibling))
352
+ continue;
353
+ seen++;
354
+ if (sibling === node)
355
+ position = seen;
356
+ }
357
+ if (position === 0)
358
+ return 0;
359
+ return fromEnd ? seen - position + 1 : position;
360
+ }
361
+ function firstElement(siblings) {
362
+ for (const sibling of siblings)
363
+ if (isElement(sibling))
364
+ return sibling;
365
+ return null;
366
+ }
367
+ function lastElement(siblings) {
368
+ for (let i = siblings.length - 1; i >= 0; i--) {
369
+ if (isElement(siblings[i]))
370
+ return siblings[i];
371
+ }
372
+ return null;
373
+ }
374
+ /**
375
+ * How much specificity a component's own rules gain over the application's global sheet.
376
+ *
377
+ * One class, which is precisely what Angular's emulated encapsulation already grants them by
378
+ * rewriting a component rule to carry an `[_ngcontent-x]` attribute. Matching that keeps the
379
+ * precedence people already have in their fingers rather than inventing a rule for native.
380
+ */
381
+ const COMPONENT_SPECIFICITY_BUMP = 1_000;
382
+ /**
383
+ * The bucket a rule is filed under: the most selective simple selector in its rightmost compound,
384
+ * which is what a browser calls the key selector.
385
+ *
386
+ * Matching counts leftwards from that compound, so if it cannot match a node then nothing to its
387
+ * left needs looking at. Anything that could match a node whose name, id and classes say nothing -
388
+ * an attribute test, `:host`, `:is()`, a bare pseudo-class, `*` - goes in the universal bucket and
389
+ * is offered to everything, because the alternative is a rule that silently stops applying.
390
+ */
391
+ export function ruleKey(rule) {
392
+ const key = rule.compounds[rule.compounds.length - 1];
393
+ if (!key)
394
+ return UNIVERSAL;
395
+ if (key.classes.length)
396
+ return `class:${key.classes[0]}`;
397
+ if (key.id !== undefined)
398
+ return `id:${key.id}`;
399
+ if (key.type !== undefined)
400
+ return `type:${key.type}`;
401
+ return UNIVERSAL;
402
+ }
403
+ const UNIVERSAL = '*';
404
+ /** File a merged, sorted rule list by key selector. Done once per sheet pair, not per node. */
405
+ export function indexRules(entries) {
406
+ const buckets = new Map();
407
+ const universal = [];
408
+ entries.forEach((entry, at) => {
409
+ const key = ruleKey(entry.rule);
410
+ if (key === UNIVERSAL) {
411
+ universal.push({ entry, at });
412
+ return;
413
+ }
414
+ let bucket = buckets.get(key);
415
+ if (!bucket)
416
+ buckets.set(key, (bucket = []));
417
+ bucket.push({ entry, at });
418
+ });
419
+ return { buckets, universal };
420
+ }
421
+ /**
422
+ * The rules worth trying against a node, in the order they were merged in.
423
+ *
424
+ * Each bucket is already in order, so the collected list only needs sorting when more than one
425
+ * contributed - which for a node with one class and a name is the common case, and cheap: the
426
+ * lists are short by construction, which is the entire point of the index.
427
+ */
428
+ export function candidateRules(node, index) {
429
+ const found = [];
430
+ if (index.universal.length)
431
+ found.push(index.universal);
432
+ const byName = index.buckets.get(`type:${node.name}`);
433
+ if (byName)
434
+ found.push(byName);
435
+ const id = node.props['nativeID'];
436
+ if (typeof id === 'string') {
437
+ const byId = index.buckets.get(`id:${id}`);
438
+ if (byId)
439
+ found.push(byId);
440
+ }
441
+ if (node.classes) {
442
+ for (const name of node.classes) {
443
+ const byClass = index.buckets.get(`class:${name}`);
444
+ if (byClass)
445
+ found.push(byClass);
446
+ }
447
+ }
448
+ if (!found.length)
449
+ return [];
450
+ if (found.length === 1)
451
+ return found[0].map((indexed) => indexed.entry);
452
+ const merged = found.flat();
453
+ merged.sort((a, b) => a.at - b.at);
454
+ return merged.map((indexed) => indexed.entry);
455
+ }
456
+ /** What `em` falls back to when nothing in scope has set a font size. The web's default. */
457
+ const DEFAULT_FONT_SIZE = 16;
458
+ /** Shared empty result. Never handed out anywhere that mutates it. */
459
+ const EMPTY = {};
460
+ /** Stands in for the parent of a root node, so the comparison needs no null case. */
461
+ const ROOT_CONTEXT = {};
462
+ /** Shared empty token map, for the overwhelmingly common case of a subtree defining none. */
463
+ const NO_TOKENS = {};
464
+ /** Stands in for "created by a component with no sheet" as a cache key. */
465
+ const NO_SHEET = { rules: [] };
466
+ /**
467
+ * Conditions generations are unique across every resolver in the process, not merely
468
+ * increasing within one, because the memo they stamp lives on the compiled rule and a sheet is
469
+ * shared by every engine that mounts the component.
470
+ */
471
+ let generations = 0;
472
+ function emptyCacheFor(epoch, generation) {
473
+ return {
474
+ epoch,
475
+ generation,
476
+ context: ROOT_CONTEXT,
477
+ parentContext: ROOT_CONTEXT,
478
+ style: EMPTY,
479
+ inherited: EMPTY,
480
+ tokens: NO_TOKENS,
481
+ };
482
+ }
483
+ /**
484
+ * What this node's children inherit: the parent's map, overlaid with any inheritable value the
485
+ * node set itself. Copied on first write only, so a node that sets none hands the same object on
486
+ * and a whole subtree can share it.
487
+ */
488
+ function inheritFrom(parentInherited, own) {
489
+ let inherited = parentInherited;
490
+ for (const key of Object.keys(own)) {
491
+ if (!INHERITED.has(key))
492
+ continue;
493
+ if (inherited === parentInherited)
494
+ inherited = { ...parentInherited };
495
+ inherited[key] = own[key];
496
+ }
497
+ return inherited;
498
+ }
499
+ /**
500
+ * The runtime half of CSS for one engine: the global sheet, the conditions media queries are
501
+ * evaluated against, and the memo that makes resolution cost nodes x rules rather than
502
+ * nodes x rules x depth.
503
+ *
504
+ * One per engine rather than module state, so two surfaces in one process (or two engines in
505
+ * one test) cannot overwrite each other's sheet or viewport.
506
+ *
507
+ * Inherited state is pushed **down** rather than walked up: a node takes what its parent already
508
+ * resolved and overlays its own declarations. That is how real CSS engines do it, and it matters
509
+ * for two separate reasons.
510
+ *
511
+ * Correctness first. Walking up meant matching every ancestor against the *starting node's*
512
+ * sheet, but an ancestor almost always belongs to a different component, so a component's rules
513
+ * reached nodes it did not own while a parent's real values never arrived. Reading `sheet` off
514
+ * each node fixes both directions at once.
515
+ *
516
+ * Cost second. Walking up re-derived, for every node, everything its parent had already worked
517
+ * out. Memoising by epoch makes it nodes x rules, and a node that sets no inheritable value hands
518
+ * its parent's map straight on rather than copying it, so the common case allocates nothing.
519
+ */
520
+ export class StyleResolver {
521
+ conditions;
522
+ /**
523
+ * Bumped whenever the conditions move. Every cached result and every per-rule condition memo
524
+ * is stamped with it, so a rotation or a theme switch invalidates the lot in one assignment
525
+ * rather than walking the tree.
526
+ */
527
+ generation = ++generations;
528
+ emptyCache = emptyCacheFor(-1, -1);
529
+ /**
530
+ * The rules a node is matched against, merged across the global sheet, the sheet of the
531
+ * component it hosts and the sheet that created it, sorted once by weight and then filed by key
532
+ * selector. Keyed by the creating sheet and then the hosted one, so a component's nodes share a
533
+ * single index.
534
+ */
535
+ merged = new WeakMap();
536
+ /**
537
+ * The application-level sheet, if any: the one set of rules allowed to match a node whatever
538
+ * component created it, which is what resets, utility classes and `:root` tokens all need.
539
+ */
540
+ globalSheet;
541
+ /**
542
+ * Custom properties the host supplies rather than the stylesheet: the safe-area insets, and
543
+ * whatever else is a property of the device instead of the design.
544
+ *
545
+ * Seeded rather than forced. They stand in for the root's inherited tokens, so a `:root` rule
546
+ * in the app's own sheet overrides them the way it would override anything it inherited, and an
547
+ * app that wants to pin an inset for a tablet layout or a screenshot still can.
548
+ */
549
+ tokensOnRoot = NO_TOKENS;
550
+ /** What the host has supplied so far, so a second source can add to it rather than replace it. */
551
+ get rootTokens() {
552
+ return this.tokensOnRoot;
553
+ }
554
+ /**
555
+ * Told of a `var()` that names a custom property nothing in scope defines and has no fallback,
556
+ * which is dropped. Set only in development, where it is how the engine says so.
557
+ */
558
+ onUndefinedToken = null;
559
+ constructor(globalSheet, conditions) {
560
+ this.globalSheet = globalSheet;
561
+ this.conditions = conditions;
562
+ }
563
+ setConditions(next) {
564
+ this.conditions = next;
565
+ this.generation = ++generations;
566
+ }
567
+ setRootTokens(next) {
568
+ this.tokensOnRoot = next;
569
+ this.generation = ++generations;
570
+ }
571
+ /**
572
+ * Resolve a node, memoised for the life of one epoch.
573
+ *
574
+ * `epoch` scopes the memo. The engine bumps it once per commit, so within a commit each node
575
+ * resolves at most once however many descendants ask for its inherited values.
576
+ */
577
+ resolve(node, epoch) {
578
+ const cached = node.styleCache;
579
+ // Within one commit a node is asked for its inherited map once per descendant, so this is the
580
+ // hot path and it must not walk anywhere.
581
+ if (cached !== null && cached.epoch === epoch)
582
+ return cached;
583
+ const parent = node.parent ? this.resolve(node.parent, epoch) : null;
584
+ const parentContext = parent ? parent.context : ROOT_CONTEXT;
585
+ // Across commits: nothing about this node changed and nothing above it moved, so the last
586
+ // answer still holds. Reusing the object, rather than recomputing an equal one, is what lets
587
+ // a descendant tell "unchanged" from "changed" by reference alone.
588
+ if (this.reusable(cached, node, parentContext)) {
589
+ cached.epoch = epoch;
590
+ return cached;
591
+ }
592
+ const parentInherited = parent ? parent.inherited : EMPTY;
593
+ if (this.hasNoRules(node)) {
594
+ return this.unstyled(node, epoch, parent, parentContext, parentInherited);
595
+ }
596
+ styleStats.nodesResolved++;
597
+ const result = this.cascade(node, this.rulesFor(node));
598
+ // Tokens are in scope for this node's own declarations as well as its descendants', so they
599
+ // are merged before any `var()` here is resolved.
600
+ const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
601
+ const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
602
+ const own = result.declarations;
603
+ if (result.deferred) {
604
+ this.applyDeferred(result.deferred, own, tokens, parentInherited, result.important);
605
+ }
606
+ if (own['borderStyle'] === 'none')
607
+ drawNoBorder(own);
608
+ const cache = {
609
+ epoch,
610
+ generation: this.generation,
611
+ context: {},
612
+ parentContext,
613
+ style: { ...parentInherited, ...own },
614
+ inherited: inheritFrom(parentInherited, own),
615
+ tokens,
616
+ };
617
+ node.styleCache = cache;
618
+ node.styleDirty = false;
619
+ return cache;
620
+ }
621
+ /**
622
+ * Whether a node's last answer still stands under a parent that resolved to `parentContext`,
623
+ * without resolving it. What lets a commit step over a clean child in one comparison.
624
+ */
625
+ holds(node, parentContext) {
626
+ return this.reusable(node.styleCache, node, parentContext);
627
+ }
628
+ /**
629
+ * Whether a previous result still stands: nothing about the node changed, nothing above it
630
+ * moved, and the conditions have not.
631
+ */
632
+ reusable(cached, node, parentContext) {
633
+ return (cached !== null &&
634
+ !node.styleDirty &&
635
+ cached.parentContext === parentContext &&
636
+ cached.generation === this.generation);
637
+ }
638
+ /**
639
+ * Nothing anywhere can match this node, and it sets no custom property of its own, so its style
640
+ * is whatever it inherits.
641
+ */
642
+ hasNoRules(node) {
643
+ return (node.sheet === null &&
644
+ node.hostSheet === null &&
645
+ this.globalSheet === null &&
646
+ !node.customProperties);
647
+ }
648
+ /**
649
+ * A node with no rules anywhere: it renders with what it inherits and passes the same map on.
650
+ *
651
+ * It still mints a context, because a rule further down may match on one of *its* classes.
652
+ * When there is nothing to inherit either, one shared cache object serves every such node, so
653
+ * an application that writes no CSS allocates nothing per node.
654
+ */
655
+ unstyled(node, epoch, parent, parentContext, parentInherited) {
656
+ if (parentInherited === EMPTY && parentContext === ROOT_CONTEXT && !node.styleDirty) {
657
+ if (this.emptyCache.epoch !== epoch || this.emptyCache.generation !== this.generation) {
658
+ this.emptyCache = emptyCacheFor(epoch, this.generation);
659
+ }
660
+ node.styleCache = this.emptyCache;
661
+ return this.emptyCache;
662
+ }
663
+ const passthrough = {
664
+ epoch,
665
+ generation: this.generation,
666
+ context: {},
667
+ parentContext,
668
+ style: parentInherited,
669
+ inherited: parentInherited,
670
+ tokens: parent ? parent.tokens : this.tokensOnRoot,
671
+ };
672
+ node.styleCache = passthrough;
673
+ node.styleDirty = false;
674
+ return passthrough;
675
+ }
676
+ /**
677
+ * The rules worth trying against a node.
678
+ *
679
+ * The merged list is built once per (creating sheet, hosted sheet) pair and indexed by key
680
+ * selector; what comes back here is only the buckets this node can reach. For a component's own
681
+ * sheet that changes little, but a global utility sheet is hundreds of rules and a screen is a
682
+ * thousand nodes, and the difference is between a million match attempts and a few thousand.
683
+ */
684
+ rulesFor(node) {
685
+ const creator = node.sheet ?? NO_SHEET;
686
+ let byHost = this.merged.get(creator);
687
+ if (!byHost)
688
+ this.merged.set(creator, (byHost = new Map()));
689
+ let index = byHost.get(node.hostSheet);
690
+ if (!index) {
691
+ byHost.set(node.hostSheet, (index = indexRules(this.merge(node.sheet, node.hostSheet))));
692
+ }
693
+ return candidateRules(node, index);
694
+ }
695
+ /**
696
+ * Weakest first. A host node is matched against two component sheets: the one belonging to the
697
+ * component it hosts, whose `:host` rules exist to reach exactly this node, and the one that
698
+ * created it. Both carry the bump; the creator comes later and so wins a tie, as the outer
699
+ * document does over `:host` on the web. The sort is stable, so within one sheet source order
700
+ * survives and a later sheet wins equal weight.
701
+ */
702
+ merge(sheet, hostSheet) {
703
+ const entries = [];
704
+ const add = (from, bump) => {
705
+ if (!from)
706
+ return;
707
+ for (const rule of from.rules) {
708
+ entries.push({ rule, sheet: from, weight: rule.specificity + bump });
709
+ }
710
+ };
711
+ add(this.globalSheet, 0);
712
+ add(hostSheet, COMPONENT_SPECIFICITY_BUMP);
713
+ add(sheet, COMPONENT_SPECIFICITY_BUMP);
714
+ return entries.sort((a, b) => a.weight - b.weight);
715
+ }
716
+ /** The declarations that apply to a node. The list is sorted, so later simply wins. */
717
+ cascade(node, entries) {
718
+ const normal = {};
719
+ const important = {};
720
+ let hasImportant = false;
721
+ let tokens = null;
722
+ let deferred = null;
723
+ for (const { rule, sheet } of entries) {
724
+ if (!this.conditionHolds(rule) || !matches(node, rule, sheet))
725
+ continue;
726
+ Object.assign(normal, rule.declarations);
727
+ if (rule.important) {
728
+ Object.assign(important, rule.important);
729
+ hasImportant = true;
730
+ }
731
+ if (rule.tokens)
732
+ Object.assign((tokens ??= {}), rule.tokens);
733
+ deferred = carryDeferred(deferred, rule);
734
+ }
735
+ return {
736
+ declarations: hasImportant ? { ...normal, ...important } : normal,
737
+ important: hasImportant ? important : null,
738
+ tokens,
739
+ deferred,
740
+ };
741
+ }
742
+ /**
743
+ * Whether a rule's media query holds, memoised per rule.
744
+ *
745
+ * Conditions are the same for every node, so this is answered once per rule per change rather
746
+ * than once per rule per node.
747
+ */
748
+ conditionHolds(rule) {
749
+ if (rule.condition === undefined)
750
+ return true;
751
+ if (rule.activeAt !== this.generation) {
752
+ rule.activeAt = this.generation;
753
+ rule.active = this.evaluate(rule.condition);
754
+ }
755
+ return rule.active;
756
+ }
757
+ evaluate(condition) {
758
+ if ('all' in condition)
759
+ return condition.all.every((part) => this.evaluate(part));
760
+ if ('any' in condition)
761
+ return condition.any.some((part) => this.evaluate(part));
762
+ if (!('op' in condition))
763
+ return this.preference(condition.feature) === condition.value;
764
+ const { width, height } = this.conditions;
765
+ const actual = condition.feature === 'width' ? width : height;
766
+ const { op, value } = condition;
767
+ if (op === 'gt')
768
+ return actual > value;
769
+ if (op === 'gte')
770
+ return actual >= value;
771
+ if (op === 'lt')
772
+ return actual < value;
773
+ if (op === 'lte')
774
+ return actual <= value;
775
+ return actual === value;
776
+ }
777
+ /** What the device answers for a feature that is a keyword rather than a measurement. */
778
+ preference(feature) {
779
+ const { width, height, colorScheme, reducedMotion } = this.conditions;
780
+ if (feature === 'orientation')
781
+ return width > height ? 'landscape' : 'portrait';
782
+ if (feature === 'prefers-color-scheme')
783
+ return colorScheme;
784
+ return reducedMotion ? 'reduce' : 'no-preference';
785
+ }
786
+ /** A length relative to the viewport, in points. */
787
+ viewportLength(unit, factor) {
788
+ const { width, height } = this.conditions;
789
+ const basis = {
790
+ vw: width,
791
+ vh: height,
792
+ vmin: Math.min(width, height),
793
+ vmax: Math.max(width, height),
794
+ };
795
+ return (factor * basis[unit]) / 100;
796
+ }
797
+ /**
798
+ * Write the declarations that could not be settled at build time: a `var()` reference, or a
799
+ * length relative to the viewport or to the font size in scope.
800
+ *
801
+ * A declaration with no token and no usable fallback is dropped rather than written as
802
+ * undefined, which is what a browser does with a value it cannot resolve.
803
+ */
804
+ applyDeferred(deferred, own, tokens, parentInherited, important) {
805
+ for (const declaration of settlingOrder(deferred)) {
806
+ const settled = this.settled(declaration, own, parentInherited, tokens);
807
+ if (settled === undefined && this.onUndefinedToken) {
808
+ this.reportUndefined(declaration, tokens);
809
+ }
810
+ const value = settled ?? declaration.unset;
811
+ if (value === undefined)
812
+ continue;
813
+ for (const prop of declaration.props) {
814
+ if (!declaration.important && important && prop in important)
815
+ continue;
816
+ own[prop] = value;
817
+ }
818
+ }
819
+ }
820
+ /** One deferred declaration's value, from the tokens in scope and the node's own style. */
821
+ settled(declaration, own, parentInherited, tokens) {
822
+ if (declaration.within !== undefined) {
823
+ return this.settledWithin(declaration.within, declaration, own, parentInherited, tokens);
824
+ }
825
+ if (declaration.compute)
826
+ return this.computed(declaration, own, parentInherited);
827
+ const value = referenced(declaration, tokens);
828
+ if (declaration.whenSet !== undefined)
829
+ return value === undefined ? undefined : declaration.whenSet;
830
+ // A token defined in `em` or a viewport unit cannot be settled where it is defined: a
831
+ // browser substitutes it and works it out where it is used, so the same here.
832
+ const pending = value?.__defer;
833
+ if (pending)
834
+ return this.computed({ ...declaration, compute: pending }, own, parentInherited);
835
+ // A token can hold a structured value with markers of its own: a shadow token whose colour
836
+ // is another token, as Tailwind's rings are.
837
+ return typeof value === 'object' && value !== null
838
+ ? this.settledWithin(value, declaration, own, parentInherited, tokens)
839
+ : value;
840
+ }
841
+ /** A reference with nothing defined for it and nothing to fall back to. */
842
+ reportUndefined(declaration, tokens) {
843
+ const name = declaration.reference;
844
+ if (name === undefined || declaration.fallback !== undefined || name in tokens)
845
+ return;
846
+ if (declaration.alternatives?.some((alternative) => alternative in tokens))
847
+ return;
848
+ this.onUndefinedToken(name, declaration.props);
849
+ }
850
+ /**
851
+ * A structured value with every deferred length and colour inside it settled, or undefined when
852
+ * a colour in it cannot be: a shadow with no colour is not a shadow, as CSS reads it.
853
+ */
854
+ settledWithin(value, declaration, own, parentInherited, tokens) {
855
+ const filled = this.filledIn(value, declaration, own, parentInherited, tokens);
856
+ if (filled === UNSETTLED)
857
+ return undefined;
858
+ // As the compiler drops a shadow nobody can see: Tailwind composes five, four of them nothing.
859
+ return declaration.props.includes('boxShadow') && Array.isArray(filled)
860
+ ? filled.filter((shadow) => !invisible(shadow.color))
861
+ : filled;
862
+ }
863
+ /** A structured value with every deferred length and colour inside it settled. */
864
+ filledIn(value, declaration, own, parentInherited, tokens) {
865
+ if (!needsFilling(value))
866
+ return value;
867
+ const fill = (part) => this.filledIn(part, declaration, own, parentInherited, tokens);
868
+ if (Array.isArray(value))
869
+ return this.filledList(value, fill, tokens);
870
+ const marked = settledMarker(value, tokens);
871
+ // `currentcolor` is the colour in scope, which only the node knows: Tailwind's ring default.
872
+ if (marked === 'currentcolor')
873
+ return own['color'] ?? parentInherited['color'] ?? 'black';
874
+ if (marked !== NOT_A_MARKER)
875
+ return marked ?? UNSETTLED;
876
+ const pending = value.__defer;
877
+ if (pending)
878
+ return this.computed({ ...declaration, compute: pending }, own, parentInherited);
879
+ const out = {};
880
+ for (const [key, part] of Object.entries(value)) {
881
+ const filled = fill(part);
882
+ if (filled === UNSETTLED)
883
+ return UNSETTLED;
884
+ out[key] = filled;
885
+ }
886
+ return out;
887
+ }
888
+ /**
889
+ * A list's parts filled in, a whole shadow or filter token standing for however many entries it
890
+ * holds: none, for an empty filter slot.
891
+ */
892
+ filledList(value, fill, tokens) {
893
+ const parts = [];
894
+ for (const part of value) {
895
+ const slot = slotOf(part);
896
+ if (!slot) {
897
+ const filled = fill(part);
898
+ if (filled === UNSETTLED)
899
+ return UNSETTLED;
900
+ parts.push(filled);
901
+ continue;
902
+ }
903
+ const filled = fill(formOf(tokens[slot.marker.reference], slot.kind) ?? slot.marker.fallback);
904
+ if (filled === UNSETTLED || !Array.isArray(filled))
905
+ return UNSETTLED;
906
+ parts.push(...filled);
907
+ }
908
+ return parts;
909
+ }
910
+ /** A length relative to the viewport or to the font size in scope. */
911
+ computed(declaration, own, parentInherited) {
912
+ const { unit, factor, offset = 0 } = declaration.compute;
913
+ if (unit !== 'em')
914
+ return this.viewportLength(unit, factor) + offset;
915
+ // The font size in scope. On `font-size` itself that is the inherited size rather than the
916
+ // one being computed, which is what makes a nested `1.5em` compound as on the web.
917
+ const base = declaration.props.includes('fontSize')
918
+ ? parentInherited['fontSize']
919
+ : (own['fontSize'] ?? parentInherited['fontSize']);
920
+ return factor * (typeof base === 'number' ? base : DEFAULT_FONT_SIZE) + offset;
921
+ }
922
+ }
923
+ /**
924
+ * The order a node's deferred declarations are settled in.
925
+ *
926
+ * Plain ones first and important ones after, so an important one wins whatever order they matched
927
+ * in; and a plain one never writes over an important declaration. The font size before everything
928
+ * else, because an em anywhere on the node is measured against it, whichever rule it came from:
929
+ * '.x { padding: 1em } .y { font-size: 2em }'. Then the colour, because a `currentcolor` anywhere
930
+ * on the node is the node's own colour: a ring's default, beside a `color: var(--c)`.
931
+ */
932
+ function settlingOrder(deferred) {
933
+ const byImportance = deferred.some((one) => one.important)
934
+ ? [...deferred.filter((one) => !one.important), ...deferred.filter((one) => one.important)]
935
+ : deferred;
936
+ const rank = (one) => one.props.includes('fontSize') ? 0 : one.props.includes('color') ? 1 : 2;
937
+ return byImportance.some((one) => rank(one) < 2)
938
+ ? [0, 1, 2].flatMap((wanted) => byImportance.filter((one) => rank(one) === wanted))
939
+ : byImportance;
940
+ }
941
+ /**
942
+ * The aliases, hsl() colours and channel colours among a node's own definitions, resolved against everything in
943
+ * scope on it. Done once, where they are defined, so a descendant only ever sees values. A name
944
+ * that resolves to nothing - undefined, part of a cycle, or an hsl() missing a channel - is
945
+ * dropped, as a browser treats it as unset.
946
+ *
947
+ * Aliases first: an hsl() channel that names a plain token reads it whichever order these run in,
948
+ * but an alias that names an hsl() token needs the hsl() already turned into a colour.
949
+ */
950
+ /**
951
+ * The tokens in scope at a node: its parent's, overlaid with what its rules define, overlaid with
952
+ * what it sets itself, which wins as inline style does.
953
+ */
954
+ function tokensInScope(parentTokens, ruleTokens, custom) {
955
+ const own = custom ? { ...ruleTokens, ...custom } : ruleTokens;
956
+ return own ? resolveAliases(own, { ...parentTokens, ...own }) : parentTokens;
957
+ }
958
+ function resolveAliases(own, merged) {
959
+ const names = Object.keys(own);
960
+ for (const name of names) {
961
+ if (own[name].alias)
962
+ settle(merged, name, followAlias(name, merged, new Set()));
963
+ }
964
+ for (const name of names) {
965
+ const { hsl, deferredColour } = own[name];
966
+ if (hsl)
967
+ settle(merged, name, colourToken(resolveHsl(hsl, merged)));
968
+ if (deferredColour)
969
+ settle(merged, name, colourToken(channelsColour(deferredColour, merged)));
970
+ }
971
+ // An alias to one of those copied it unsettled above: follow it again, now it is a colour.
972
+ for (const name of names) {
973
+ const target = own[name].alias;
974
+ if (!target || !(own[target]?.hsl || own[target]?.deferredColour))
975
+ continue;
976
+ settle(merged, name, followAlias(name, { ...merged, [name]: own[name] }, new Set()));
977
+ }
978
+ return merged;
979
+ }
980
+ /** A token set to what it resolved to, or removed when it resolved to nothing, as CSS unsets it. */
981
+ function settle(merged, name, value) {
982
+ if (value)
983
+ merged[name] = value;
984
+ else
985
+ delete merged[name];
986
+ }
987
+ const colourToken = (color) => color === undefined ? undefined : { color };
988
+ /** An `hsl()` token's channels, read from the tokens in scope, as the colour they make. */
989
+ function resolveHsl(hsl, tokens) {
990
+ const h = hslChannel(hsl.h, tokens);
991
+ const s = hslChannel(hsl.s, tokens);
992
+ const l = hslChannel(hsl.l, tokens);
993
+ const alpha = hsl.alpha === undefined ? 1 : hslChannel(hsl.alpha, tokens);
994
+ if (h === undefined || s === undefined || l === undefined || alpha === undefined) {
995
+ return undefined;
996
+ }
997
+ return hslToRgb(h, s, l, alpha);
998
+ }
999
+ function hslChannel(channel, tokens) {
1000
+ return typeof channel === 'number'
1001
+ ? channel
1002
+ : (tokens[channel.reference]?.number ?? channel.fallback);
1003
+ }
1004
+ /**
1005
+ * `hsl(h, s, l)` converted to sRGB, printed the same way `fromChannels` prints an rgba() built
1006
+ * from tokens: a use site never has to know which of the two ever built a colour.
1007
+ *
1008
+ * `h` is degrees; `s` and `l` are fractions, which is what a percentage token already reads as
1009
+ * (see `formOf`), so nothing here rescales them.
1010
+ */
1011
+ function hslToRgb(h, s, l, alpha) {
1012
+ const hue = ((h % 360) + 360) % 360;
1013
+ const chroma = (1 - Math.abs(2 * l - 1)) * s;
1014
+ const x = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
1015
+ const m = l - chroma / 2;
1016
+ const [r1, g1, b1] = hue < 60
1017
+ ? [chroma, x, 0]
1018
+ : hue < 120
1019
+ ? [x, chroma, 0]
1020
+ : hue < 180
1021
+ ? [0, chroma, x]
1022
+ : hue < 240
1023
+ ? [0, x, chroma]
1024
+ : hue < 300
1025
+ ? [x, 0, chroma]
1026
+ : [chroma, 0, x];
1027
+ const r = Math.round((r1 + m) * 255);
1028
+ const g = Math.round((g1 + m) * 255);
1029
+ const b = Math.round((b1 + m) * 255);
1030
+ return alpha >= 1
1031
+ ? `rgb(${r}, ${g}, ${b})`
1032
+ : `rgba(${r}, ${g}, ${b}, ${Math.round(alpha * 1000) / 1000})`;
1033
+ }
1034
+ function followAlias(name, tokens, seen) {
1035
+ const value = tokens[name];
1036
+ if (!value?.alias)
1037
+ return value;
1038
+ if (seen.has(name))
1039
+ return undefined;
1040
+ seen.add(name);
1041
+ return followAlias(value.alias, tokens, seen) ?? value.fallback;
1042
+ }
1043
+ /** What a `var()` resolves to: a gradient's stops, or one value with its arithmetic applied. */
1044
+ function referenced(declaration, tokens) {
1045
+ if (declaration.gradient)
1046
+ return paintLayers(declaration.gradient, tokens);
1047
+ let value = formOf(tokens[declaration.reference], declaration.kind);
1048
+ // `calc(var(--n) * 1px)`: the arithmetic gives a unitless token its unit, which is the usual
1049
+ // way to turn a count into a length. So a length with arithmetic reads the bare number too.
1050
+ if (value === undefined && declaration.adjust && declaration.kind === 'length') {
1051
+ value = tokens[declaration.reference]?.number;
1052
+ }
1053
+ for (const alternative of declaration.alternatives ?? []) {
1054
+ if (value !== undefined)
1055
+ break;
1056
+ value = formOf(tokens[alternative], declaration.kind);
1057
+ }
1058
+ value ??= declaration.fallback;
1059
+ const base = declaration.kind === 'channels' ? fromChannels(value, declaration.alpha, tokens) : value;
1060
+ return declaration.adjust ? adjusted(base, declaration.adjust) : base;
1061
+ }
1062
+ /** `13, 110, 253` as the colour it is, at the alpha the token beside it says, if one does. */
1063
+ function fromChannels(value, alpha, tokens) {
1064
+ if (!Array.isArray(value) || value.length !== 3)
1065
+ return undefined;
1066
+ // An alpha naming a token nothing set, with no fallback, is no colour: CSS drops it.
1067
+ const opacity = alpha ? (tokens[alpha.reference]?.number ?? alpha.fallback) : 1;
1068
+ if (opacity === undefined)
1069
+ return undefined;
1070
+ const [r, g, b] = value;
1071
+ return opacity >= 1
1072
+ ? `rgb(${r}, ${g}, ${b})`
1073
+ : `rgba(${r}, ${g}, ${b}, ${Math.round(opacity * 1000) / 1000})`;
1074
+ }
1075
+ /** One template, or a layered background of several: every layer painted, or, as CSS would, none. */
1076
+ function paintLayers(gradient, tokens) {
1077
+ if (!Array.isArray(gradient))
1078
+ return paintGradient(gradient, tokens);
1079
+ const painted = [];
1080
+ for (const layer of gradient) {
1081
+ const one = paintGradient(layer, tokens);
1082
+ if (!one)
1083
+ return undefined;
1084
+ painted.push(...one);
1085
+ }
1086
+ return painted;
1087
+ }
1088
+ /**
1089
+ * A gradient template with its stops filled in from the cascade.
1090
+ *
1091
+ * A stop whose colour is not defined anywhere is dropped rather than painted transparent: that is
1092
+ * what makes an optional middle colour optional, and it is what a browser does with the same
1093
+ * stylesheet. Fewer than two colours is not a gradient, and paints nothing at all.
1094
+ */
1095
+ function paintGradient(template, tokens) {
1096
+ const colorStops = paintedStops(template, tokens);
1097
+ if (colorStops.length < 2)
1098
+ return undefined;
1099
+ const position = template.position && placed(template.position, tokens);
1100
+ if (position === null)
1101
+ return undefined;
1102
+ return [{ ...template, colorStops, ...(position ? { position } : {}) }];
1103
+ }
1104
+ /** A template's stops with their tokens read, leaving out any whose colour is not set. */
1105
+ function paintedStops(template, tokens) {
1106
+ const colorStops = [];
1107
+ for (const stop of template.colorStops) {
1108
+ const color = stop.colour ? resolveColour(stop.colour, tokens) : tokens[stop.reference]?.color;
1109
+ if (color === undefined)
1110
+ continue;
1111
+ const position = stop.positionReference
1112
+ ? (tokens[stop.positionReference]?.length ?? stop.position)
1113
+ : stop.position;
1114
+ // Written even when there is none: Fabric looks the key up before it looks at the value, and
1115
+ // silently skips a stop that has not got one.
1116
+ colorStops.push({ color, position: position ?? null });
1117
+ }
1118
+ return colorStops;
1119
+ }
1120
+ /**
1121
+ * A radial gradient's centre with any token in it read, or null when a token it needs is not set
1122
+ * and has nothing written beside it: CSS would not paint the gradient then, and neither does this.
1123
+ */
1124
+ function placed(position, tokens) {
1125
+ const out = {};
1126
+ for (const [edge, offset] of Object.entries(position)) {
1127
+ if (offset === null || typeof offset !== 'object') {
1128
+ out[edge] = offset;
1129
+ continue;
1130
+ }
1131
+ const { reference, fallback } = offset;
1132
+ const value = tokens[reference]?.length ?? fallback;
1133
+ if (value === undefined || typeof value === 'object')
1134
+ return null;
1135
+ out[edge] = value;
1136
+ }
1137
+ return out;
1138
+ }
1139
+ /**
1140
+ * A colour expression with its tokens looked up and any mix in it worked out. Undefined when a
1141
+ * token it needs is not defined anywhere and has nothing to fall back to.
1142
+ */
1143
+ function resolveColour(expression, tokens) {
1144
+ if ('color' in expression)
1145
+ return expression.color;
1146
+ if ('channels' in expression)
1147
+ return channelsColour(expression, tokens);
1148
+ if ('hsl' in expression)
1149
+ return resolveHsl(expression.hsl, tokens);
1150
+ if ('reference' in expression)
1151
+ return tokenColour(expression, tokens);
1152
+ if ('relative' in expression) {
1153
+ const { space, from, channels, alpha } = expression.relative;
1154
+ const origin = resolveColour(from, tokens);
1155
+ return origin === undefined ? undefined : relativeColour(space, origin, channels, alpha);
1156
+ }
1157
+ const { space, hue, a, aPercentage, b, bPercentage } = expression.mix;
1158
+ const first = resolveColour(a, tokens);
1159
+ const second = resolveColour(b, tokens);
1160
+ if (first === undefined || second === undefined)
1161
+ return undefined;
1162
+ return mixColours(space, { colour: first, percentage: aPercentage }, { colour: second, percentage: bPercentage }, hue);
1163
+ }
1164
+ /** A calc marker worked out, as its slot takes it; undefined when a token in it is not set. */
1165
+ function resolveCalc(marker, tokens) {
1166
+ const percentage = wholePercentage(marker, tokens);
1167
+ if (percentage !== undefined)
1168
+ return percentage;
1169
+ if (bareNumberAsLength(marker, tokens) || notAnAngle(marker, tokens))
1170
+ return undefined;
1171
+ const value = calculated(marker.expression, marker.kind, tokens);
1172
+ if (value === undefined || !Number.isFinite(value))
1173
+ return undefined;
1174
+ const rounded = Math.round(value * 1000) / 1000;
1175
+ return marker.kind === 'angle' ? `${rounded}deg` : rounded;
1176
+ }
1177
+ /**
1178
+ * Whether a length is one token holding a bare number other than 0: `--tw-translate-x: 3`, from
1179
+ * `translate-x-[3]`, which a browser drops. Inside arithmetic a bare number is a factor, and fine.
1180
+ */
1181
+ function bareNumberAsLength(marker, tokens) {
1182
+ if (marker.kind !== 'length' || typeof marker.expression !== 'object')
1183
+ return false;
1184
+ if (Array.isArray(marker.expression))
1185
+ return false;
1186
+ const token = tokens[marker.expression.reference];
1187
+ return (token !== undefined &&
1188
+ token.length === undefined &&
1189
+ token.number !== undefined &&
1190
+ token.number !== 0);
1191
+ }
1192
+ /**
1193
+ * Whether an angle is one token that holds no angle: `--tw-rotate: 3` or `37%`, from
1194
+ * `rotate-[3]`, which a browser drops, taking the whole transform with it. A bare 0 is an angle;
1195
+ * `0%` is not.
1196
+ */
1197
+ function notAnAngle(marker, tokens) {
1198
+ if (marker.kind !== 'angle' || typeof marker.expression !== 'object')
1199
+ return false;
1200
+ if (Array.isArray(marker.expression))
1201
+ return false;
1202
+ const token = tokens[marker.expression.reference];
1203
+ if (token === undefined || token.angle !== undefined)
1204
+ return false;
1205
+ // `0%` has a number 0 as well, and is still a percentage, which no angle is.
1206
+ return token.number !== 0 || typeof token.length === 'string';
1207
+ }
1208
+ /**
1209
+ * A length that is one token holding a percentage, `translate-x-1/2`'s 50%, passed on as it is:
1210
+ * a share of a box nobody has laid out yet can be used, but not added to or scaled here.
1211
+ */
1212
+ function wholePercentage(marker, tokens) {
1213
+ if (marker.kind !== 'length' || typeof marker.expression !== 'object')
1214
+ return undefined;
1215
+ if (Array.isArray(marker.expression))
1216
+ return undefined;
1217
+ const leaf = marker.expression;
1218
+ const value = formOf(tokens[leaf.reference], 'length');
1219
+ return typeof value === 'string' && value.endsWith('%') ? value : undefined;
1220
+ }
1221
+ function calculated(expression, kind, tokens) {
1222
+ if (typeof expression === 'number')
1223
+ return expression;
1224
+ if (!Array.isArray(expression)) {
1225
+ const leaf = expression;
1226
+ return tokenNumber(tokens[leaf.reference], kind) ?? leaf.fallback;
1227
+ }
1228
+ const [op, a, b] = expression;
1229
+ const left = calculated(a, kind, tokens);
1230
+ const right = calculated(b, kind, tokens);
1231
+ if (left === undefined || right === undefined)
1232
+ return undefined;
1233
+ if (op === '+')
1234
+ return left + right;
1235
+ if (op === '-')
1236
+ return left - right;
1237
+ if (op === '*')
1238
+ return left * right;
1239
+ return left / right;
1240
+ }
1241
+ /** Where each kind of slot reads a token from, before its bare number. */
1242
+ const FORM_OF_KIND = {
1243
+ length: (token) => token.length,
1244
+ angle: (token) => token.angle,
1245
+ time: (token) => token.time,
1246
+ };
1247
+ /**
1248
+ * A token as a number in a slot of this kind: its length in points or its angle in degrees, or
1249
+ * a bare number, which multiplies (`calc(var(--n) * 1px)`). A length that is not points, a
1250
+ * percentage, is not one, and is not read as the bare number it also is.
1251
+ */
1252
+ function tokenNumber(token, kind) {
1253
+ if (!token)
1254
+ return undefined;
1255
+ const own = kind === 'number' ? undefined : FORM_OF_KIND[kind](token);
1256
+ if (own !== undefined)
1257
+ return typeof own === 'number' ? own : undefined;
1258
+ // An angle is a number of degrees to a hue, and neither a length nor a factor: `30deg` as a
1259
+ // translate moved a box thirty points, and as a scale made it thirty times the size, where a
1260
+ // browser drops the declaration.
1261
+ if (token.angle !== undefined)
1262
+ return undefined;
1263
+ return token.number;
1264
+ }
1265
+ /** The slot a list entry can be instead of an entry: one per list property that has them. */
1266
+ const SLOT_KINDS = {
1267
+ __shadows: 'shadow',
1268
+ __filters: 'filter',
1269
+ __transforms: 'transform',
1270
+ __variants: 'fontVariant',
1271
+ };
1272
+ const SLOT_ENTRIES = Object.entries(SLOT_KINDS);
1273
+ /** The slot a list entry is, and the form of token that fills it; undefined for an entry. */
1274
+ function slotOf(part) {
1275
+ if (part === null || typeof part !== 'object')
1276
+ return undefined;
1277
+ for (const [key, kind] of SLOT_ENTRIES) {
1278
+ const marker = part[key];
1279
+ if (marker)
1280
+ return { marker, kind };
1281
+ }
1282
+ return undefined;
1283
+ }
1284
+ function resolveLength(marker, tokens) {
1285
+ const value = formOf(tokens[marker.reference], 'length') ?? marker.fallback;
1286
+ if (typeof value !== 'number')
1287
+ return undefined;
1288
+ return marker.adjust ? adjusted(value, marker.adjust) : value;
1289
+ }
1290
+ /** A colour at no opacity at all. */
1291
+ function invisible(colour) {
1292
+ if (colour === 'transparent')
1293
+ return true;
1294
+ const parts = typeof colour === 'string' ? RGB.exec(colour) : null;
1295
+ return parts?.[4] !== undefined && Number(parts[4]) === 0;
1296
+ }
1297
+ /**
1298
+ * Whether a compiled value has something in it to settle: a marker, slot or deferred length at
1299
+ * any depth. One with none is used as it is. Asked of values from the sheet, which never change,
1300
+ * so the answer is kept: Tailwind's reset fills three of a shadow's five slots with one on every
1301
+ * node.
1302
+ */
1303
+ function needsFilling(value) {
1304
+ if (value === null || typeof value !== 'object')
1305
+ return false;
1306
+ let answer = NEEDS_FILLING.get(value);
1307
+ if (answer === undefined) {
1308
+ answer = Object.entries(value).some(([key, part]) => key.startsWith('__') || needsFilling(part));
1309
+ NEEDS_FILLING.set(value, answer);
1310
+ }
1311
+ return answer;
1312
+ }
1313
+ const NEEDS_FILLING = new WeakMap();
1314
+ /** What `settledMarker` says of a part that is not a token's marker at all. */
1315
+ const NOT_A_MARKER = Symbol('not a marker');
1316
+ /** A colour, length or calc marker settled from the tokens, or undefined when one cannot be. */
1317
+ function settledMarker(value, tokens) {
1318
+ const colour = colourMarker(value);
1319
+ if (colour)
1320
+ return resolveColour(colour, tokens);
1321
+ // Whether a shadow is inset, from a token that is the word or nothing: `ring-inset`.
1322
+ const inset = value.__inset;
1323
+ if (inset)
1324
+ return formOf(tokens[inset.reference], 'keyword') === 'inset';
1325
+ const token = value.__length;
1326
+ if (token)
1327
+ return resolveLength(token, tokens);
1328
+ const sum = value.__calc;
1329
+ if (sum)
1330
+ return resolveCalc(sum, tokens);
1331
+ return NOT_A_MARKER;
1332
+ }
1333
+ /** What a colour that cannot be settled makes of the whole value it is in. */
1334
+ const UNSETTLED = Symbol('unsettled');
1335
+ /** `rgba(var(--channels), <alpha>)`: the token's channels, at the alpha written or tokened. */
1336
+ function channelsColour(expression, tokens) {
1337
+ const value = formOf(tokens[expression.channels.reference], 'channels') ?? expression.channels.fallback;
1338
+ const { alpha } = expression;
1339
+ if (typeof alpha !== 'number')
1340
+ return fromChannels(value, alpha, tokens);
1341
+ const opaque = fromChannels(value, undefined, tokens);
1342
+ return opaque === undefined ? undefined : faded(opaque, alpha);
1343
+ }
1344
+ /** A token's colour, or the first of its alternatives that is defined, or its fallback. */
1345
+ function tokenColour(expression, tokens) {
1346
+ let value = formOf(tokens[expression.reference], 'color');
1347
+ for (const alternative of expression.alternatives ?? []) {
1348
+ if (value !== undefined)
1349
+ break;
1350
+ value = formOf(tokens[alternative], 'color');
1351
+ }
1352
+ return value ?? expression.fallback;
1353
+ }
1354
+ /** A marker the compiler leaves in a structured value for a colour only the device can settle. */
1355
+ function colourMarker(value) {
1356
+ return value?.__colour;
1357
+ }
1358
+ /**
1359
+ * A resolved length, moved by the arithmetic that surrounded its `var()`.
1360
+ *
1361
+ * A non-numeric value is left alone rather than coerced: the token was a keyword or a percentage
1362
+ * the app meant, and turning it into NaN would be a silent layout rather than a visible one.
1363
+ */
1364
+ function adjusted(value, adjust) {
1365
+ if (adjust.alpha !== undefined)
1366
+ return faded(value, adjust.alpha);
1367
+ if (typeof value !== 'number')
1368
+ return value;
1369
+ const scaled = value * (adjust.scale ?? 1) + (adjust.offset ?? 0);
1370
+ return adjust.floor === undefined ? scaled : Math.max(scaled, adjust.floor);
1371
+ }
1372
+ /** `rgb(r, g, b)` and `rgba(r, g, b, a)`, which is every colour the compiler emits. */
1373
+ const RGB = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/;
1374
+ /**
1375
+ * A colour at a fraction of its opacity.
1376
+ *
1377
+ * Multiplied rather than replaced, so a token that was already translucent stays relatively so -
1378
+ * `rgba(0, 0, 0, 0.5)` faded to 90% is 45% opaque, which is what a browser computes for the same
1379
+ * mix. A value that is not one of ours is returned untouched: it may be a platform colour, and a
1380
+ * colour nobody can parse is better painted as it stands than dropped.
1381
+ */
1382
+ function faded(value, fraction) {
1383
+ if (typeof value !== 'string')
1384
+ return value;
1385
+ const parts = RGB.exec(value);
1386
+ if (!parts)
1387
+ return value;
1388
+ const alpha = (parts[4] === undefined ? 1 : Number(parts[4])) * fraction;
1389
+ return `rgba(${parts[1]}, ${parts[2]}, ${parts[3]}, ${Math.round(alpha * 1000) / 1000})`;
1390
+ }
1391
+ /**
1392
+ * Where a component's compiled sheet lives: a static on the component class, written by the
1393
+ * Metro transformer at build time.
1394
+ *
1395
+ * `RendererFactory2.createRenderer(host, type)` receives the component definition, and a
1396
+ * definition references its class, so a renderer can find its own sheet with no registry and no
1397
+ * generated id. That is also how encapsulation is scoped for free: a component's renderer only
1398
+ * ever matches that component's rules, so its elements cannot be styled by anyone else's CSS.
1399
+ */
1400
+ export const NATIVE_STYLES = 'ɵnativeStyles';
1401
+ export function styleSheetOf(type) {
1402
+ const sheet = type?.[NATIVE_STYLES];
1403
+ return sheet ?? null;
1404
+ }
1405
+ //# sourceMappingURL=css.js.map