@tenphi/tasty 0.0.0-snapshot.4d272f6 → 0.0.0-snapshot.4e9dfa4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/async-storage-B7_o6FKt.js.map +1 -1
- package/dist/{collector-DM5o7SIi.js → collector-Crs9kGJW.js} +24 -11
- package/dist/collector-Crs9kGJW.js.map +1 -0
- package/dist/{collector-DMoQRxPK.d.ts → collector-osfWTeRd.d.ts} +12 -2
- package/dist/{config-CtJeETqW.js → config-BaxtQMS3.js} +987 -651
- package/dist/config-BaxtQMS3.js.map +1 -0
- package/dist/{config-DdnOynVC.d.ts → config-BoZDUHW5.d.ts} +65 -4
- package/dist/context-CkSg-kDT.js.map +1 -1
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +6 -6
- package/dist/{core-CD2f33ch.js → core-CmxaoZ-N.js} +18 -12
- package/dist/core-CmxaoZ-N.js.map +1 -0
- package/dist/{css-writer-CUbBb8gL.js → css-writer-CPy_cbFJ.js} +28 -6
- package/dist/css-writer-CPy_cbFJ.js.map +1 -0
- package/dist/format-global-rules-Dbc_1tc3.js.map +1 -1
- package/dist/{format-rules-BFgS3646.js → format-rules-CPirO_Mj.js} +2 -2
- package/dist/{format-rules-BFgS3646.js.map → format-rules-CPirO_Mj.js.map} +1 -1
- package/dist/{hydrate-BZ1SUmKW.js → hydrate-CVn-A_3y.js} +2 -2
- package/dist/{hydrate-BZ1SUmKW.js.map → hydrate-CVn-A_3y.js.map} +1 -1
- package/dist/{index-C-4JKlJ1.d.ts → index-B_k47mc_.d.ts} +74 -21
- package/dist/{index-DTXQTYHU.d.ts → index-tcHuMPFt.d.ts} +24 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +10 -10
- package/dist/index.js.map +1 -1
- package/dist/{keyframes-DNpu5N3R.js → keyframes-vpzoVdUR.js} +3 -2
- package/dist/{keyframes-DNpu5N3R.js.map → keyframes-vpzoVdUR.js.map} +1 -1
- package/dist/{merge-styles-Dwdz1IsM.d.ts → merge-styles-BMWcH6MF.d.ts} +2 -2
- package/dist/{merge-styles-QCiE0R2O.js → merge-styles-BzQutdAK.js} +2 -2
- package/dist/{merge-styles-QCiE0R2O.js.map → merge-styles-BzQutdAK.js.map} +1 -1
- package/dist/{resolve-recipes-CHSdSpso.js → resolve-recipes-DgH8A3Nn.js} +3 -3
- package/dist/{resolve-recipes-CHSdSpso.js.map → resolve-recipes-DgH8A3Nn.js.map} +1 -1
- package/dist/ssr/astro-client.js +1 -1
- package/dist/ssr/astro-client.js.map +1 -1
- package/dist/ssr/astro-middleware.js.map +1 -1
- package/dist/ssr/astro.d.ts +1 -12
- package/dist/ssr/astro.js +3 -3
- package/dist/ssr/astro.js.map +1 -1
- package/dist/ssr/index.d.ts +1 -1
- package/dist/ssr/index.js +3 -3
- package/dist/ssr/next.d.ts +1 -1
- package/dist/ssr/next.js +4 -4
- package/dist/ssr/next.js.map +1 -1
- package/dist/static/index.d.ts +2 -2
- package/dist/static/index.js +1 -1
- package/dist/static/index.js.map +1 -1
- package/dist/static/inject.js.map +1 -1
- package/dist/zero/babel.d.ts +1 -1
- package/dist/zero/babel.js +16 -8
- package/dist/zero/babel.js.map +1 -1
- package/dist/zero/index.d.ts +1 -1
- package/dist/zero/index.js +1 -1
- package/dist/zero/next.js.map +1 -1
- package/docs/configuration.md +44 -0
- package/docs/dsl.md +14 -12
- package/docs/injector.md +2 -2
- package/docs/methodology.md +26 -0
- package/docs/pipeline.md +40 -14
- package/docs/react-api.md +24 -0
- package/docs/ssr.md +5 -3
- package/docs/styles.md +9 -7
- package/docs/tasty-static.md +15 -0
- package/package.json +10 -9
- package/tasty.config.ts +1 -0
- package/dist/collector-DM5o7SIi.js.map +0 -1
- package/dist/config-CtJeETqW.js.map +0 -1
- package/dist/core-CD2f33ch.js.map +0 -1
- package/dist/css-writer-CUbBb8gL.js.map +0 -1
|
@@ -57,6 +57,7 @@ function canonicalFuncName(lowered) {
|
|
|
57
57
|
//#endregion
|
|
58
58
|
//#region src/parser/lru.ts
|
|
59
59
|
var Lru = class {
|
|
60
|
+
limit;
|
|
60
61
|
map = /* @__PURE__ */ new Map();
|
|
61
62
|
head = null;
|
|
62
63
|
tail = null;
|
|
@@ -1374,6 +1375,93 @@ function isDevEnv() {
|
|
|
1374
1375
|
return nodeEnv !== "test" && nodeEnv !== "production";
|
|
1375
1376
|
}
|
|
1376
1377
|
//#endregion
|
|
1378
|
+
//#region src/utils/name-prefix.ts
|
|
1379
|
+
/**
|
|
1380
|
+
* Name prefix utilities for generated identifiers.
|
|
1381
|
+
*
|
|
1382
|
+
* Tasty generates three kinds of identifiers from content hashes:
|
|
1383
|
+
* - class names (used in DOM `class` attribute)
|
|
1384
|
+
* - keyframe names (used in CSS `animation`)
|
|
1385
|
+
* - counter-style names (used in CSS `list-style-type`)
|
|
1386
|
+
*
|
|
1387
|
+
* All three derive from a single configurable prefix so that an app
|
|
1388
|
+
* can namespace every identifier under one string. Discriminator letters
|
|
1389
|
+
* (`k`, `c`) keep the three kinds visually distinct in devtools — they
|
|
1390
|
+
* are not required for correctness (CSS keeps these in separate
|
|
1391
|
+
* namespaces), only for readability.
|
|
1392
|
+
*
|
|
1393
|
+
* The runtime / SSR / RSC paths must agree on the prefix; otherwise the
|
|
1394
|
+
* client-side hash for a given style will not match the server-rendered
|
|
1395
|
+
* class and hydration breaks. The zero-runtime build path uses a
|
|
1396
|
+
* different default (`'ts'`) so its classes can't collide with runtime
|
|
1397
|
+
* (`'t'`) classes when both are loaded on the same page.
|
|
1398
|
+
*/
|
|
1399
|
+
/** Default prefix used by the runtime / SSR / RSC paths. */
|
|
1400
|
+
const DEFAULT_NAME_PREFIX = "t";
|
|
1401
|
+
/** Default prefix used by the zero-runtime (`tastyStatic`) build path. */
|
|
1402
|
+
const DEFAULT_ZERO_NAME_PREFIX = "ts";
|
|
1403
|
+
/**
|
|
1404
|
+
* Allowed shape: starts with a letter or underscore, then letters/
|
|
1405
|
+
* digits/underscore/hyphen. Length capped at 32 to keep generated
|
|
1406
|
+
* names sane. Matches the CSS identifier rules for the common case
|
|
1407
|
+
* while keeping the surface conservative.
|
|
1408
|
+
*/
|
|
1409
|
+
const NAME_PREFIX_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$/;
|
|
1410
|
+
/**
|
|
1411
|
+
* Validate a `namePrefix` value.
|
|
1412
|
+
* Throws a TypeError with a descriptive message on invalid input so
|
|
1413
|
+
* misconfiguration fails loudly at `configure()` time rather than
|
|
1414
|
+
* surfacing later as broken hydration.
|
|
1415
|
+
*/
|
|
1416
|
+
function validateNamePrefix(prefix) {
|
|
1417
|
+
if (typeof prefix !== "string") throw new TypeError(`[Tasty] namePrefix must be a string, got ${typeof prefix}.`);
|
|
1418
|
+
if (!NAME_PREFIX_PATTERN.test(prefix)) throw new TypeError(`[Tasty] namePrefix "${prefix}" is invalid. It must start with a letter (a-z, A-Z) or "_", contain only letters, digits, "_" or "-", and be 1-32 characters long. Examples: "t", "ts", "myapp-", "_foo".`);
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Build a class name: `${prefix}${hash}`.
|
|
1422
|
+
* The hash is appended verbatim — supply a separator inside the prefix
|
|
1423
|
+
* itself if you want one (e.g. `'myapp-'`).
|
|
1424
|
+
*/
|
|
1425
|
+
function makeClassName(prefix, hash) {
|
|
1426
|
+
return `${prefix}${hash}`;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Build a keyframe name: `${prefix}k${suffix}`.
|
|
1430
|
+
* The `k` discriminator keeps keyframe names visually distinct from
|
|
1431
|
+
* class names sharing the same prefix. `suffix` is typically a content
|
|
1432
|
+
* hash but may be a counter for ad-hoc allocation.
|
|
1433
|
+
*/
|
|
1434
|
+
function makeKeyframeName(prefix, suffix) {
|
|
1435
|
+
return `${prefix}k${suffix}`;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Build a counter-style name: `${prefix}c${suffix}`.
|
|
1439
|
+
* The `c` discriminator keeps counter-style names visually distinct
|
|
1440
|
+
* from class names sharing the same prefix.
|
|
1441
|
+
*/
|
|
1442
|
+
function makeCounterStyleName(prefix, suffix) {
|
|
1443
|
+
return `${prefix}c${suffix}`;
|
|
1444
|
+
}
|
|
1445
|
+
/** Escape a string for safe inclusion in a regex literal. */
|
|
1446
|
+
function escapeRegex(str) {
|
|
1447
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Regex matching any tasty class for the given prefix.
|
|
1451
|
+
* Used by the runtime GC's DOM scan and class-allocation bookkeeping.
|
|
1452
|
+
*/
|
|
1453
|
+
function tastyClassRegex(prefix) {
|
|
1454
|
+
return new RegExp(`^${escapeRegex(prefix)}[a-z0-9]+$`);
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Global regex extracting tasty class names from RSC-inlined CSS.
|
|
1458
|
+
* Looks for the doubled-specificity pattern `.cls.cls` that
|
|
1459
|
+
* `formatRules()` always emits, which makes extraction reliable.
|
|
1460
|
+
*/
|
|
1461
|
+
function rscClassRegexGlobal(prefix) {
|
|
1462
|
+
return new RegExp(`\\.(${escapeRegex(prefix)}[a-z0-9]+)\\.\\1`, "g");
|
|
1463
|
+
}
|
|
1464
|
+
//#endregion
|
|
1377
1465
|
//#region src/parser/types.ts
|
|
1378
1466
|
let Bucket = /* @__PURE__ */ function(Bucket) {
|
|
1379
1467
|
Bucket[Bucket["Color"] = 0] = "Color";
|
|
@@ -1439,7 +1527,7 @@ function resolveUntilStable(value, opts, recurse, maxIterations = 10) {
|
|
|
1439
1527
|
function classify(raw, opts, recurse) {
|
|
1440
1528
|
const token = raw.trim();
|
|
1441
1529
|
if (!token) return {
|
|
1442
|
-
bucket:
|
|
1530
|
+
bucket: 2,
|
|
1443
1531
|
processed: ""
|
|
1444
1532
|
};
|
|
1445
1533
|
{
|
|
@@ -1461,31 +1549,31 @@ function classify(raw, opts, recurse) {
|
|
|
1461
1549
|
if (depth !== 0) {
|
|
1462
1550
|
console.warn("tasty: skipped invalid function token with unmatched parentheses:", token);
|
|
1463
1551
|
return {
|
|
1464
|
-
bucket:
|
|
1552
|
+
bucket: 2,
|
|
1465
1553
|
processed: ""
|
|
1466
1554
|
};
|
|
1467
1555
|
}
|
|
1468
1556
|
}
|
|
1469
1557
|
if (token.startsWith("\"") && token.endsWith("\"") || token.startsWith("'") && token.endsWith("'")) return {
|
|
1470
|
-
bucket:
|
|
1558
|
+
bucket: 1,
|
|
1471
1559
|
processed: token
|
|
1472
1560
|
};
|
|
1473
1561
|
if (token.startsWith("$$")) {
|
|
1474
1562
|
const name = token.slice(2);
|
|
1475
1563
|
if (/^[a-z_][a-z0-9-_]*$/i.test(name)) return {
|
|
1476
|
-
bucket:
|
|
1564
|
+
bucket: 1,
|
|
1477
1565
|
processed: `--${name}`
|
|
1478
1566
|
};
|
|
1479
1567
|
}
|
|
1480
1568
|
if (token.startsWith("##")) {
|
|
1481
1569
|
const name = token.slice(2);
|
|
1482
1570
|
if (/^[a-z_][a-z0-9-_]*$/i.test(name)) return {
|
|
1483
|
-
bucket:
|
|
1571
|
+
bucket: 1,
|
|
1484
1572
|
processed: `--${name}-color`
|
|
1485
1573
|
};
|
|
1486
1574
|
}
|
|
1487
1575
|
if (token === "#current") return {
|
|
1488
|
-
bucket:
|
|
1576
|
+
bucket: 0,
|
|
1489
1577
|
processed: "currentcolor"
|
|
1490
1578
|
};
|
|
1491
1579
|
const currentAlphaMatch = token.match(/^#current\.(\$[a-z_][a-z0-9-_]*|[0-9]+)$/i);
|
|
@@ -1496,7 +1584,7 @@ function classify(raw, opts, recurse) {
|
|
|
1496
1584
|
else if (rawAlpha === "0") percentage = "0%";
|
|
1497
1585
|
else percentage = `${parseFloat("." + rawAlpha) * 100}%`;
|
|
1498
1586
|
return {
|
|
1499
|
-
bucket:
|
|
1587
|
+
bucket: 0,
|
|
1500
1588
|
processed: `color-mix(in oklab, currentcolor ${percentage}, transparent)`
|
|
1501
1589
|
};
|
|
1502
1590
|
}
|
|
@@ -1569,7 +1657,7 @@ function classify(raw, opts, recurse) {
|
|
|
1569
1657
|
const constructed = `${normalizedFunc}(${hasModernAlpha || hasLegacyAlpha ? normalizeArgs(hasModernAlpha ? args.slice(0, slashIdx).trim() : args.slice(0, lastTopLevelComma).trim()) : normalizeArgs(args)} / ${alpha})`;
|
|
1570
1658
|
if (!COLOR_FUNCS.has(normalizedFunc) && opts.funcs && normalizedFunc in opts.funcs) return classify(constructed, opts, recurse);
|
|
1571
1659
|
return {
|
|
1572
|
-
bucket:
|
|
1660
|
+
bucket: 0,
|
|
1573
1661
|
processed: constructed
|
|
1574
1662
|
};
|
|
1575
1663
|
}
|
|
@@ -1580,11 +1668,11 @@ function classify(raw, opts, recurse) {
|
|
|
1580
1668
|
}
|
|
1581
1669
|
}
|
|
1582
1670
|
if (token.match(/^var\(--([a-z0-9-]+)-color\)$/)) return {
|
|
1583
|
-
bucket:
|
|
1671
|
+
bucket: 0,
|
|
1584
1672
|
processed: token
|
|
1585
1673
|
};
|
|
1586
1674
|
if (token.startsWith("url(")) return {
|
|
1587
|
-
bucket:
|
|
1675
|
+
bucket: 1,
|
|
1588
1676
|
processed: token
|
|
1589
1677
|
};
|
|
1590
1678
|
if (token[0] === "$") {
|
|
@@ -1593,7 +1681,7 @@ function classify(raw, opts, recurse) {
|
|
|
1593
1681
|
const name = identMatch[1];
|
|
1594
1682
|
const processed = `var(--${name})`;
|
|
1595
1683
|
return {
|
|
1596
|
-
bucket: name.endsWith("-color") ?
|
|
1684
|
+
bucket: name.endsWith("-color") ? 0 : 1,
|
|
1597
1685
|
processed
|
|
1598
1686
|
};
|
|
1599
1687
|
}
|
|
@@ -1607,17 +1695,17 @@ function classify(raw, opts, recurse) {
|
|
|
1607
1695
|
else if (rawAlpha === "0") alpha = "0";
|
|
1608
1696
|
else alpha = `.${rawAlpha}`;
|
|
1609
1697
|
return {
|
|
1610
|
-
bucket:
|
|
1698
|
+
bucket: 0,
|
|
1611
1699
|
processed: `${getColorSpaceFunc()}(var(--${base}-color-${getColorSpaceSuffix()}) / ${alpha})`
|
|
1612
1700
|
};
|
|
1613
1701
|
}
|
|
1614
1702
|
const name = token.slice(1);
|
|
1615
1703
|
if (RE_HEX.test(name)) return {
|
|
1616
|
-
bucket:
|
|
1704
|
+
bucket: 0,
|
|
1617
1705
|
processed: `var(--${name}-color, #${name})`
|
|
1618
1706
|
};
|
|
1619
1707
|
return {
|
|
1620
|
-
bucket:
|
|
1708
|
+
bucket: 0,
|
|
1621
1709
|
processed: `var(--${name}-color)`
|
|
1622
1710
|
};
|
|
1623
1711
|
}
|
|
@@ -1628,7 +1716,7 @@ function classify(raw, opts, recurse) {
|
|
|
1628
1716
|
if (COLOR_FUNCS.has(fname)) {
|
|
1629
1717
|
const argProcessed = recurse(inner).output.replace(/,\s+/g, ",");
|
|
1630
1718
|
return {
|
|
1631
|
-
bucket:
|
|
1719
|
+
bucket: 0,
|
|
1632
1720
|
processed: `${canonicalFuncName(fname)}(${argProcessed})`
|
|
1633
1721
|
};
|
|
1634
1722
|
}
|
|
@@ -1641,7 +1729,7 @@ function classify(raw, opts, recurse) {
|
|
|
1641
1729
|
}
|
|
1642
1730
|
const argProcessed = recurse(inner).output;
|
|
1643
1731
|
return {
|
|
1644
|
-
bucket:
|
|
1732
|
+
bucket: 1,
|
|
1645
1733
|
processed: `${canonicalFuncName(fname)}(${argProcessed})`
|
|
1646
1734
|
};
|
|
1647
1735
|
}
|
|
@@ -1649,10 +1737,9 @@ function classify(raw, opts, recurse) {
|
|
|
1649
1737
|
const colorMatch = token.slice(1, -1).match(/^#([a-z0-9-]+)\s*,\s*(.*)$/i);
|
|
1650
1738
|
if (colorMatch) {
|
|
1651
1739
|
const [, name, fallback] = colorMatch;
|
|
1652
|
-
const processedFallback = recurse(fallback).output;
|
|
1653
1740
|
return {
|
|
1654
|
-
bucket:
|
|
1655
|
-
processed: `var(--${name}-color, ${
|
|
1741
|
+
bucket: 0,
|
|
1742
|
+
processed: `var(--${name}-color, ${recurse(fallback).output})`
|
|
1656
1743
|
};
|
|
1657
1744
|
}
|
|
1658
1745
|
}
|
|
@@ -1662,18 +1749,15 @@ function classify(raw, opts, recurse) {
|
|
|
1662
1749
|
const [, name, fallback] = match;
|
|
1663
1750
|
const processedFallback = recurse(fallback).output;
|
|
1664
1751
|
return {
|
|
1665
|
-
bucket: name.endsWith("-color") ?
|
|
1752
|
+
bucket: name.endsWith("-color") ? 0 : 1,
|
|
1666
1753
|
processed: `var(--${name}, ${processedFallback})`
|
|
1667
1754
|
};
|
|
1668
1755
|
}
|
|
1669
1756
|
}
|
|
1670
|
-
if (token[0] === "(" && token[token.length - 1] === ")") {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
processed: `calc(${innerProcessed})`
|
|
1675
|
-
};
|
|
1676
|
-
}
|
|
1757
|
+
if (token[0] === "(" && token[token.length - 1] === ")") return {
|
|
1758
|
+
bucket: 1,
|
|
1759
|
+
processed: `calc(${recurse(token.slice(1, -1)).output})`
|
|
1760
|
+
};
|
|
1677
1761
|
const um = token.match(RE_UNIT_NUM);
|
|
1678
1762
|
if (um) {
|
|
1679
1763
|
const unit = um[1];
|
|
@@ -1683,47 +1767,43 @@ function classify(raw, opts, recurse) {
|
|
|
1683
1767
|
const rawMatch = handler.match(RE_RAW_UNIT);
|
|
1684
1768
|
if (rawMatch) {
|
|
1685
1769
|
const [, baseNum, cssUnit] = rawMatch;
|
|
1686
|
-
const resolved = resolveUntilStable(`${numericPart * parseFloat(baseNum)}${cssUnit}`, opts, recurse);
|
|
1687
1770
|
return {
|
|
1688
|
-
bucket:
|
|
1689
|
-
processed:
|
|
1771
|
+
bucket: 1,
|
|
1772
|
+
processed: resolveUntilStable(`${numericPart * parseFloat(baseNum)}${cssUnit}`, opts, recurse)
|
|
1690
1773
|
};
|
|
1691
1774
|
}
|
|
1692
1775
|
const base = handler;
|
|
1693
1776
|
if (numericPart === 1) return {
|
|
1694
|
-
bucket:
|
|
1777
|
+
bucket: 1,
|
|
1695
1778
|
processed: base
|
|
1696
1779
|
};
|
|
1697
1780
|
return {
|
|
1698
|
-
bucket:
|
|
1781
|
+
bucket: 1,
|
|
1699
1782
|
processed: `calc(${numericPart} * ${base})`
|
|
1700
1783
|
};
|
|
1701
|
-
} else {
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
processed: inner
|
|
1706
|
-
};
|
|
1707
|
-
}
|
|
1784
|
+
} else return {
|
|
1785
|
+
bucket: 1,
|
|
1786
|
+
processed: handler(numericPart)
|
|
1787
|
+
};
|
|
1708
1788
|
}
|
|
1709
1789
|
if (/^[+-]?(?:\d*\.\d+|\d+)[a-z%]+$/.test(token)) return {
|
|
1710
|
-
bucket:
|
|
1790
|
+
bucket: 1,
|
|
1711
1791
|
processed: token
|
|
1712
1792
|
};
|
|
1713
1793
|
if (RE_NUMBER.test(token)) return {
|
|
1714
|
-
bucket:
|
|
1794
|
+
bucket: 1,
|
|
1715
1795
|
processed: token
|
|
1716
1796
|
};
|
|
1717
1797
|
if (VALUE_KEYWORDS.has(token)) return {
|
|
1718
|
-
bucket:
|
|
1798
|
+
bucket: 1,
|
|
1719
1799
|
processed: token
|
|
1720
1800
|
};
|
|
1721
1801
|
if (token === "transparent" || token === "currentcolor") return {
|
|
1722
|
-
bucket:
|
|
1802
|
+
bucket: 0,
|
|
1723
1803
|
processed: token
|
|
1724
1804
|
};
|
|
1725
1805
|
return {
|
|
1726
|
-
bucket:
|
|
1806
|
+
bucket: 2,
|
|
1727
1807
|
processed: token
|
|
1728
1808
|
};
|
|
1729
1809
|
}
|
|
@@ -1796,6 +1876,7 @@ function scan(src, cb) {
|
|
|
1796
1876
|
//#endregion
|
|
1797
1877
|
//#region src/parser/parser.ts
|
|
1798
1878
|
var StyleParser = class {
|
|
1879
|
+
opts;
|
|
1799
1880
|
cache;
|
|
1800
1881
|
constructor(opts = {}) {
|
|
1801
1882
|
this.opts = opts;
|
|
@@ -1823,13 +1904,13 @@ var StyleParser = class {
|
|
|
1823
1904
|
return;
|
|
1824
1905
|
}
|
|
1825
1906
|
switch (bucket) {
|
|
1826
|
-
case
|
|
1907
|
+
case 0:
|
|
1827
1908
|
currentPart.colors.push(processed);
|
|
1828
1909
|
break;
|
|
1829
|
-
case
|
|
1910
|
+
case 1:
|
|
1830
1911
|
currentPart.values.push(processed);
|
|
1831
1912
|
break;
|
|
1832
|
-
case
|
|
1913
|
+
case 2:
|
|
1833
1914
|
currentPart.mods.push(processed);
|
|
1834
1915
|
break;
|
|
1835
1916
|
}
|
|
@@ -3242,10 +3323,13 @@ function resolveFontFamily(font, fontFamily) {
|
|
|
3242
3323
|
/**
|
|
3243
3324
|
* Handles typography preset and individual font properties.
|
|
3244
3325
|
*
|
|
3245
|
-
* Preset syntax uses `/` to separate name from
|
|
3326
|
+
* Preset syntax uses `/` to separate the name from one or more
|
|
3327
|
+
* space-separated modifiers:
|
|
3246
3328
|
* - `preset="h1"` — name only
|
|
3247
3329
|
* - `preset="h2 / strong"` — name + modifier
|
|
3330
|
+
* - `preset="h2 / strong italic"` — name + multiple modifiers
|
|
3248
3331
|
* - `preset="bold"` — modifier-only shorthand (name defaults to `inherit`)
|
|
3332
|
+
* - `preset="bold italic"` — modifier-only shorthand with multiple modifiers
|
|
3249
3333
|
*
|
|
3250
3334
|
* When `preset` is defined, it sets up CSS custom properties for typography.
|
|
3251
3335
|
* Individual font props can be used with or without `preset`:
|
|
@@ -3265,14 +3349,17 @@ function presetStyle({ preset, fontSize, lineHeight, textTransform, letterSpacin
|
|
|
3265
3349
|
const { parts } = parseStyle(preset === true ? "" : String(preset)).groups[0] ?? { parts: [] };
|
|
3266
3350
|
const namePart = parts[0];
|
|
3267
3351
|
const modPart = parts[1];
|
|
3352
|
+
const nameTokens = namePart?.all ?? [];
|
|
3353
|
+
const isModOnly = nameTokens.length > 0 && nameTokens.every((t) => PRESET_MODIFIERS.has(t));
|
|
3268
3354
|
const nameToken = namePart?.mods[0] ?? namePart?.values[0] ?? "";
|
|
3269
|
-
const isModOnly = PRESET_MODIFIERS.has(nameToken);
|
|
3270
3355
|
const name = isModOnly ? "inherit" : nameToken || "inherit";
|
|
3271
|
-
const
|
|
3272
|
-
const
|
|
3273
|
-
const
|
|
3274
|
-
const
|
|
3275
|
-
const
|
|
3356
|
+
const modTokens = isModOnly ? nameTokens : modPart?.all ?? [];
|
|
3357
|
+
const activeMods = /* @__PURE__ */ new Set();
|
|
3358
|
+
for (const tok of modTokens) if (PRESET_MODIFIERS.has(tok)) activeMods.add(tok);
|
|
3359
|
+
const isStrong = activeMods.has("strong") || activeMods.has("bold");
|
|
3360
|
+
const isItalic = activeMods.has("italic");
|
|
3361
|
+
const isIcon = activeMods.has("icon");
|
|
3362
|
+
const isTight = activeMods.has("tight");
|
|
3276
3363
|
if (fontSize == null) setCSSValue(styles, "font-size", name, { cssOnly: true });
|
|
3277
3364
|
if (lineHeight == null) setCSSValue(styles, "line-height", name, { cssOnly: true });
|
|
3278
3365
|
if (letterSpacing == null) setCSSValue(styles, "letter-spacing", name, { cssOnly: true });
|
|
@@ -4137,7 +4224,7 @@ var SheetManager = class {
|
|
|
4137
4224
|
}
|
|
4138
4225
|
}
|
|
4139
4226
|
if (!anyInserted) {}
|
|
4140
|
-
} else console.warn("[tasty] Browser rejected CSS rule:", fullRule, e);
|
|
4227
|
+
} else if (!(fullRule.startsWith("@property ") && !this.engineSupportsAtProperty(registry, styleSheet))) console.warn("[tasty] Browser rejected CSS rule:", fullRule, e);
|
|
4141
4228
|
}
|
|
4142
4229
|
} else if (styleElement) {
|
|
4143
4230
|
const atomicRuleIndex = this.findAvailableRuleIndex(targetSheet);
|
|
@@ -4289,6 +4376,37 @@ var SheetManager = class {
|
|
|
4289
4376
|
return sheet.ruleCount;
|
|
4290
4377
|
}
|
|
4291
4378
|
/**
|
|
4379
|
+
* Probe whether the underlying CSS engine supports `@property` at-rules.
|
|
4380
|
+
* Result is cached per registry on `registry.atPropertySupported`.
|
|
4381
|
+
*
|
|
4382
|
+
* The probe inserts and immediately deletes a minimal known-valid rule
|
|
4383
|
+
* (`@property --__tasty_probe__ { syntax: "*"; inherits: true; }`).
|
|
4384
|
+
* Engines that lack `@property` support (jsdom, happy-dom) reject any
|
|
4385
|
+
* `@property` rule including this one, so a probe failure is a reliable
|
|
4386
|
+
* signal that further `@property` rejections are environmental noise and
|
|
4387
|
+
* not user-authored bugs.
|
|
4388
|
+
*
|
|
4389
|
+
* The probe is intentionally a separate operation from the user's failing
|
|
4390
|
+
* insertion: we don't want to leak `--__tasty_probe__` into the sheet, so
|
|
4391
|
+
* on success we delete the probe rule immediately, leaving `ruleCount`
|
|
4392
|
+
* and `cssRules.length` unchanged.
|
|
4393
|
+
*/
|
|
4394
|
+
engineSupportsAtProperty(registry, styleSheet) {
|
|
4395
|
+
if (registry.atPropertySupported !== void 0) return registry.atPropertySupported;
|
|
4396
|
+
const probeRule = "@property --__tasty_probe__ { syntax: \"*\"; inherits: true; }";
|
|
4397
|
+
try {
|
|
4398
|
+
const probeIdx = styleSheet.cssRules.length;
|
|
4399
|
+
styleSheet.insertRule(probeRule, probeIdx);
|
|
4400
|
+
try {
|
|
4401
|
+
styleSheet.deleteRule(probeIdx);
|
|
4402
|
+
} catch {}
|
|
4403
|
+
registry.atPropertySupported = true;
|
|
4404
|
+
} catch {
|
|
4405
|
+
registry.atPropertySupported = false;
|
|
4406
|
+
}
|
|
4407
|
+
return registry.atPropertySupported;
|
|
4408
|
+
}
|
|
4409
|
+
/**
|
|
4292
4410
|
* Force cleanup of unused styles
|
|
4293
4411
|
*/
|
|
4294
4412
|
forceCleanup(registry) {
|
|
@@ -4809,18 +4927,10 @@ function formatCounterStyleRule(name, descriptors) {
|
|
|
4809
4927
|
//#endregion
|
|
4810
4928
|
//#region src/injector/injector.ts
|
|
4811
4929
|
/**
|
|
4812
|
-
* Generate a deterministic class name from a cache key using content hash.
|
|
4813
|
-
* The same cache key always produces the same class name across environments.
|
|
4814
|
-
*/
|
|
4815
|
-
function generateClassName(cacheKey) {
|
|
4816
|
-
return `t${hashString(cacheKey)}`;
|
|
4817
|
-
}
|
|
4818
|
-
const RSC_CLASS_RE = /\.(t[a-z0-9]+)\.\1/g;
|
|
4819
|
-
/**
|
|
4820
4930
|
* Extract class names from `<style data-tasty-rsc>` tags.
|
|
4821
4931
|
* The doubled-specificity pattern `.tXXX.tXXX` makes extraction reliable.
|
|
4822
4932
|
*/
|
|
4823
|
-
function extractRSCClassNames() {
|
|
4933
|
+
function extractRSCClassNames(rscClassRegex) {
|
|
4824
4934
|
if (typeof document === "undefined") return [];
|
|
4825
4935
|
const styles = document.querySelectorAll("style[data-tasty-rsc]");
|
|
4826
4936
|
if (styles.length === 0) return [];
|
|
@@ -4829,8 +4939,8 @@ function extractRSCClassNames() {
|
|
|
4829
4939
|
const text = style.textContent;
|
|
4830
4940
|
if (!text) continue;
|
|
4831
4941
|
let match;
|
|
4832
|
-
|
|
4833
|
-
while ((match =
|
|
4942
|
+
rscClassRegex.lastIndex = 0;
|
|
4943
|
+
while ((match = rscClassRegex.exec(text)) !== null) classSet.add(match[1]);
|
|
4834
4944
|
}
|
|
4835
4945
|
return Array.from(classSet);
|
|
4836
4946
|
}
|
|
@@ -4844,7 +4954,7 @@ function extractRSCClassNames() {
|
|
|
4844
4954
|
* Called inside `inject()` / `allocateClassName()` to pick up
|
|
4845
4955
|
* class names rendered on the server (including during SPA navigation).
|
|
4846
4956
|
*/
|
|
4847
|
-
function syncServerClasses(registry) {
|
|
4957
|
+
function syncServerClasses(registry, rscClassRegex) {
|
|
4848
4958
|
if (typeof window === "undefined") return;
|
|
4849
4959
|
const classes = window.__TASTY__;
|
|
4850
4960
|
if (classes && classes.length > registry.serverClassSyncIndex) {
|
|
@@ -4853,7 +4963,7 @@ function syncServerClasses(registry) {
|
|
|
4853
4963
|
}
|
|
4854
4964
|
if (!registry.rscStylesScanned) {
|
|
4855
4965
|
registry.rscStylesScanned = true;
|
|
4856
|
-
for (const cls of extractRSCClassNames()) registerHydratedClass(registry, cls);
|
|
4966
|
+
for (const cls of extractRSCClassNames(rscClassRegex)) registerHydratedClass(registry, cls);
|
|
4857
4967
|
}
|
|
4858
4968
|
}
|
|
4859
4969
|
function registerHydratedClass(registry, className) {
|
|
@@ -4865,25 +4975,40 @@ function registerHydratedClass(registry, className) {
|
|
|
4865
4975
|
});
|
|
4866
4976
|
registry.refCounts.set(className, 0);
|
|
4867
4977
|
}
|
|
4868
|
-
var StyleInjector = class
|
|
4978
|
+
var StyleInjector = class {
|
|
4869
4979
|
sheetManager;
|
|
4870
4980
|
config;
|
|
4871
4981
|
globalRuleCounter = 0;
|
|
4872
4982
|
pendingGCHandle = null;
|
|
4983
|
+
namePrefix;
|
|
4984
|
+
classRegex;
|
|
4985
|
+
rscClassRegex;
|
|
4873
4986
|
/** @internal — exposed for debug utilities only */
|
|
4874
4987
|
get _sheetManager() {
|
|
4875
4988
|
return this.sheetManager;
|
|
4876
4989
|
}
|
|
4877
4990
|
constructor(config = {}) {
|
|
4991
|
+
if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
|
|
4878
4992
|
this.config = config;
|
|
4879
4993
|
this.sheetManager = new SheetManager(config);
|
|
4994
|
+
this.namePrefix = config.namePrefix ?? "t";
|
|
4995
|
+
this.classRegex = tastyClassRegex(this.namePrefix);
|
|
4996
|
+
this.rscClassRegex = rscClassRegexGlobal(this.namePrefix);
|
|
4997
|
+
}
|
|
4998
|
+
/**
|
|
4999
|
+
* Generate a deterministic class name from a cache key using content hash.
|
|
5000
|
+
* The same cache key always produces the same class name across environments
|
|
5001
|
+
* with the same `namePrefix`.
|
|
5002
|
+
*/
|
|
5003
|
+
generateClassName(cacheKey) {
|
|
5004
|
+
return makeClassName(this.namePrefix, hashString(cacheKey));
|
|
4880
5005
|
}
|
|
4881
5006
|
/**
|
|
4882
5007
|
* Check if `className` was hydrated from server-rendered styles and,
|
|
4883
5008
|
* if so, wire the cacheKey mapping. Returns true on hit.
|
|
4884
5009
|
*/
|
|
4885
5010
|
tryHydratedHit(registry, cacheKey, className) {
|
|
4886
|
-
syncServerClasses(registry);
|
|
5011
|
+
syncServerClasses(registry, this.rscClassRegex);
|
|
4887
5012
|
const rule = registry.rules.get(className);
|
|
4888
5013
|
if (rule && rule.ruleIndex === -2 && rule.sheetIndex === -2) {
|
|
4889
5014
|
registry.cacheKeyToClassName.set(cacheKey, className);
|
|
@@ -4902,7 +5027,7 @@ var StyleInjector = class StyleInjector {
|
|
|
4902
5027
|
className: registry.cacheKeyToClassName.get(cacheKey),
|
|
4903
5028
|
isNewAllocation: false
|
|
4904
5029
|
};
|
|
4905
|
-
const className = generateClassName(cacheKey);
|
|
5030
|
+
const className = this.generateClassName(cacheKey);
|
|
4906
5031
|
if (this.tryHydratedHit(registry, cacheKey, className)) return {
|
|
4907
5032
|
className,
|
|
4908
5033
|
isNewAllocation: false
|
|
@@ -4954,7 +5079,7 @@ var StyleInjector = class StyleInjector {
|
|
|
4954
5079
|
};
|
|
4955
5080
|
}
|
|
4956
5081
|
} else if (cacheKey) {
|
|
4957
|
-
className = generateClassName(cacheKey);
|
|
5082
|
+
className = this.generateClassName(cacheKey);
|
|
4958
5083
|
if (this.tryHydratedHit(registry, cacheKey, className)) {
|
|
4959
5084
|
registry.refCounts.set(className, (registry.refCounts.get(className) || 0) + 1);
|
|
4960
5085
|
if (registry.metrics) registry.metrics.hits++;
|
|
@@ -4963,7 +5088,10 @@ var StyleInjector = class StyleInjector {
|
|
|
4963
5088
|
dispose: () => this.dispose(className, registry)
|
|
4964
5089
|
};
|
|
4965
5090
|
}
|
|
4966
|
-
} else
|
|
5091
|
+
} else {
|
|
5092
|
+
const parts = rules.map((r) => `${r.selector}\0${r.declarations}`);
|
|
5093
|
+
className = makeClassName(this.namePrefix, hashString(parts.join("\n")));
|
|
5094
|
+
}
|
|
4967
5095
|
const rulesToInsert = rules.map((rule) => {
|
|
4968
5096
|
let newSelector = rule.selector;
|
|
4969
5097
|
if (rule.needsClassName) {
|
|
@@ -5181,8 +5309,23 @@ var StyleInjector = class StyleInjector {
|
|
|
5181
5309
|
}
|
|
5182
5310
|
const cssName = effectiveResult.cssName;
|
|
5183
5311
|
const definition = effectiveResult.definition;
|
|
5184
|
-
|
|
5185
|
-
if (
|
|
5312
|
+
this.insertPropertyRule(registry, root, cssName, definition, name);
|
|
5313
|
+
if (effectiveResult.isColor) {
|
|
5314
|
+
const companionCssName = `${cssName}-${getColorSpaceSuffix()}`;
|
|
5315
|
+
const companionDefinition = {
|
|
5316
|
+
syntax: getComponentPropertySyntax(),
|
|
5317
|
+
inherits: definition.inherits,
|
|
5318
|
+
initialValue: colorInitialValueToComponents(definition.initialValue)
|
|
5319
|
+
};
|
|
5320
|
+
this.insertPropertyRule(registry, root, companionCssName, companionDefinition, `${name}:components`);
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
/**
|
|
5324
|
+
* Build and insert a single `@property` rule into the given registry.
|
|
5325
|
+
* No-op if the property was already injected.
|
|
5326
|
+
*/
|
|
5327
|
+
insertPropertyRule(registry, root, cssName, definition, cacheKey) {
|
|
5328
|
+
if (registry.injectedProperties.has(cssName)) return;
|
|
5186
5329
|
const parts = [];
|
|
5187
5330
|
if (definition.syntax != null) {
|
|
5188
5331
|
let syntax = String(definition.syntax).trim();
|
|
@@ -5202,8 +5345,8 @@ var StyleInjector = class StyleInjector {
|
|
|
5202
5345
|
selector: `@property ${cssName}`,
|
|
5203
5346
|
declarations
|
|
5204
5347
|
};
|
|
5205
|
-
|
|
5206
|
-
|
|
5348
|
+
registry.injectedProperties.set(cssName, normalizePropertyDefinition(definition));
|
|
5349
|
+
this.sheetManager.insertGlobalRule(registry, [rule], `property:${cacheKey}`, root);
|
|
5207
5350
|
}
|
|
5208
5351
|
/**
|
|
5209
5352
|
* Check whether a given @property name was already injected by this injector.
|
|
@@ -5283,12 +5426,12 @@ var StyleInjector = class StyleInjector {
|
|
|
5283
5426
|
let actualName;
|
|
5284
5427
|
if (providedName) {
|
|
5285
5428
|
const existingContentForName = registry.keyframesNameToContent.get(providedName);
|
|
5286
|
-
if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}
|
|
5429
|
+
if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}-${makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++))}`;
|
|
5287
5430
|
else {
|
|
5288
5431
|
actualName = providedName;
|
|
5289
5432
|
registry.keyframesNameToContent.set(providedName, contentHash);
|
|
5290
5433
|
}
|
|
5291
|
-
} else actualName =
|
|
5434
|
+
} else actualName = makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++));
|
|
5292
5435
|
const result = this.sheetManager.insertKeyframes(registry, steps, actualName, root);
|
|
5293
5436
|
if (!result) return {
|
|
5294
5437
|
toString: () => "",
|
|
@@ -5337,7 +5480,6 @@ var StyleInjector = class StyleInjector {
|
|
|
5337
5480
|
}
|
|
5338
5481
|
}
|
|
5339
5482
|
}
|
|
5340
|
-
static TASTY_CLASS_RE = /^t[a-z0-9]+$/;
|
|
5341
5483
|
/**
|
|
5342
5484
|
* Record a render-time usage hit for one or more classNames.
|
|
5343
5485
|
* Handles space-separated multi-chunk classNames.
|
|
@@ -5353,7 +5495,7 @@ var StyleInjector = class StyleInjector {
|
|
|
5353
5495
|
const now = Date.now();
|
|
5354
5496
|
const parts = className.indexOf(" ") === -1 ? [className] : className.split(" ");
|
|
5355
5497
|
for (const cls of parts) {
|
|
5356
|
-
if (!
|
|
5498
|
+
if (!this.classRegex.test(cls)) continue;
|
|
5357
5499
|
if (!registry.rules.has(cls)) continue;
|
|
5358
5500
|
const entry = registry.usageMap.get(cls);
|
|
5359
5501
|
if (entry) entry.lastTouchedAt = now;
|
|
@@ -5406,7 +5548,7 @@ var StyleInjector = class StyleInjector {
|
|
|
5406
5548
|
if (registry.usageMap.size - activeCount <= capacity) return 0;
|
|
5407
5549
|
}
|
|
5408
5550
|
const liveClasses = /* @__PURE__ */ new Set();
|
|
5409
|
-
for (const el of root.querySelectorAll("[class]")) for (const token of el.classList) if (
|
|
5551
|
+
for (const el of root.querySelectorAll("[class]")) for (const token of el.classList) if (this.classRegex.test(token)) liveClasses.add(token);
|
|
5410
5552
|
let swept = 0;
|
|
5411
5553
|
if (force) for (const [className] of registry.usageMap) {
|
|
5412
5554
|
if (liveClasses.has(className)) continue;
|
|
@@ -6089,6 +6231,7 @@ function simplifyAnd(children) {
|
|
|
6089
6231
|
terms = sortTerms(terms);
|
|
6090
6232
|
terms = applyAbsorptionAnd(terms);
|
|
6091
6233
|
terms = applyConsensusAnd(terms);
|
|
6234
|
+
terms = pruneContradictedOrBranches(terms);
|
|
6092
6235
|
if (terms.length === 0) return trueCondition();
|
|
6093
6236
|
if (terms.length === 1) return terms[0];
|
|
6094
6237
|
return {
|
|
@@ -6375,11 +6518,14 @@ function hasContainerStyleConflict(terms) {
|
|
|
6375
6518
|
*/
|
|
6376
6519
|
function buildImpliedNegationCheck(terms) {
|
|
6377
6520
|
const positiveValues = /* @__PURE__ */ new Map();
|
|
6521
|
+
const negatedBooleanAttrs = /* @__PURE__ */ new Set();
|
|
6378
6522
|
for (const term of terms) {
|
|
6379
|
-
if (term.kind !== "state"
|
|
6380
|
-
if (term.
|
|
6381
|
-
if (term.
|
|
6382
|
-
|
|
6523
|
+
if (term.kind !== "state") continue;
|
|
6524
|
+
if (!term.negated) {
|
|
6525
|
+
if (term.type === "container" && term.subtype === "style") {
|
|
6526
|
+
if (term.propertyValue !== void 0) positiveValues.set(`c:${term.containerName || "_"}:${term.property}`, term.propertyValue);
|
|
6527
|
+
} else if (term.type === "modifier" && term.value !== void 0) positiveValues.set(`m:${term.attribute}`, term.value);
|
|
6528
|
+
} else if (term.type === "modifier" && term.value === void 0) negatedBooleanAttrs.add(term.attribute);
|
|
6383
6529
|
}
|
|
6384
6530
|
return (term) => {
|
|
6385
6531
|
if (term.kind !== "state" || !term.negated) return false;
|
|
@@ -6389,6 +6535,7 @@ function buildImpliedNegationCheck(terms) {
|
|
|
6389
6535
|
return pos !== void 0 && term.propertyValue !== pos;
|
|
6390
6536
|
}
|
|
6391
6537
|
if (term.type === "modifier" && term.value !== void 0) {
|
|
6538
|
+
if (negatedBooleanAttrs.has(term.attribute)) return true;
|
|
6392
6539
|
const pos = positiveValues.get(`m:${term.attribute}`);
|
|
6393
6540
|
return pos !== void 0 && term.value !== pos;
|
|
6394
6541
|
}
|
|
@@ -6736,437 +6883,97 @@ function tryResolvePair(aChildren, bChildren) {
|
|
|
6736
6883
|
if (common.length === 1) return common[0];
|
|
6737
6884
|
return or(...common);
|
|
6738
6885
|
}
|
|
6739
|
-
//#endregion
|
|
6740
|
-
//#region src/pipeline/exclusive.ts
|
|
6741
6886
|
/**
|
|
6742
|
-
*
|
|
6887
|
+
* For each OR child of an AND, prune branches that are impossible
|
|
6888
|
+
* given the other AND siblings.
|
|
6743
6889
|
*
|
|
6744
|
-
*
|
|
6745
|
-
*
|
|
6746
|
-
* For each entry, we compute:
|
|
6747
|
-
* exclusiveCondition = condition & !prior[0] & !prior[1] & ...
|
|
6748
|
-
*
|
|
6749
|
-
* This ensures exactly one condition matches at any time.
|
|
6750
|
-
*
|
|
6751
|
-
* Example:
|
|
6752
|
-
* Input (ordered highest to lowest priority):
|
|
6753
|
-
* A: value1 (priority 2)
|
|
6754
|
-
* B: value2 (priority 1)
|
|
6755
|
-
* C: value3 (priority 0)
|
|
6756
|
-
*
|
|
6757
|
-
* Output:
|
|
6758
|
-
* A: A
|
|
6759
|
-
* B: B & !A
|
|
6760
|
-
* C: C & !A & !B
|
|
6890
|
+
* Example: A & OR(!A & X, Y) → A & Y
|
|
6761
6891
|
*
|
|
6762
|
-
*
|
|
6763
|
-
*
|
|
6892
|
+
* This catches dead OR branches left by exclusive negation (Stage 2b)
|
|
6893
|
+
* where `NOT(combined)` introduces branches contradicting flat siblings.
|
|
6764
6894
|
*/
|
|
6765
|
-
function
|
|
6895
|
+
function pruneContradictedOrBranches(terms) {
|
|
6896
|
+
if (!terms.some((t) => t.kind === "compound" && t.operator === "OR")) return terms;
|
|
6897
|
+
let changed = false;
|
|
6766
6898
|
const result = [];
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
const simplified = simplifyCondition(exclusive);
|
|
6772
|
-
if (simplified.kind === "false") continue;
|
|
6773
|
-
result.push({
|
|
6774
|
-
...entry,
|
|
6775
|
-
exclusiveCondition: simplified
|
|
6776
|
-
});
|
|
6777
|
-
if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
|
|
6778
|
-
}
|
|
6779
|
-
return result;
|
|
6780
|
-
}
|
|
6781
|
-
/**
|
|
6782
|
-
* Parse style entries from a value mapping object.
|
|
6783
|
-
*
|
|
6784
|
-
* @param styleKey The style key (e.g., 'padding')
|
|
6785
|
-
* @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
|
|
6786
|
-
* @param parseCondition Function to parse state keys into conditions
|
|
6787
|
-
* @returns Parsed entries ordered by priority (highest first)
|
|
6788
|
-
*/
|
|
6789
|
-
function parseStyleEntries(styleKey, valueMap, parseCondition) {
|
|
6790
|
-
const entries = [];
|
|
6791
|
-
Object.keys(valueMap).forEach((stateKey, index) => {
|
|
6792
|
-
const value = valueMap[stateKey];
|
|
6793
|
-
const condition = stateKey === "" ? trueCondition() : parseCondition(stateKey);
|
|
6794
|
-
entries.push({
|
|
6795
|
-
styleKey,
|
|
6796
|
-
stateKey,
|
|
6797
|
-
value,
|
|
6798
|
-
condition,
|
|
6799
|
-
priority: index
|
|
6800
|
-
});
|
|
6801
|
-
});
|
|
6802
|
-
entries.reverse();
|
|
6803
|
-
return entries;
|
|
6804
|
-
}
|
|
6805
|
-
/**
|
|
6806
|
-
* Merge parsed entries that share the same value.
|
|
6807
|
-
*
|
|
6808
|
-
* When multiple **non-default** state keys map to the same value, their
|
|
6809
|
-
* conditions can be combined with OR and treated as a single entry.
|
|
6810
|
-
* This must happen **before** exclusive expansion and OR branch splitting
|
|
6811
|
-
* to avoid combinatorial explosion and duplicate CSS output.
|
|
6812
|
-
*
|
|
6813
|
-
* Default (TRUE) entries are **never** merged with non-default entries.
|
|
6814
|
-
* Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
|
|
6815
|
-
* condition's participation in exclusive building. That causes
|
|
6816
|
-
* intermediate-priority states to lose their `:not(X)` negation,
|
|
6817
|
-
* breaking mutual exclusivity when X and an intermediate state are
|
|
6818
|
-
* both active. Stage 6 `mergeByValue` handles combining rules with
|
|
6819
|
-
* identical CSS output after exclusive conditions are correctly built.
|
|
6820
|
-
*
|
|
6821
|
-
* Example: `{ '@dark': 'red', '@dark & @hc': 'red' }` merges into a
|
|
6822
|
-
* single entry with condition `@dark | (@dark & @hc)` = `@dark`.
|
|
6823
|
-
*
|
|
6824
|
-
* Entries are ordered highest-priority-first. The merged entry keeps the
|
|
6825
|
-
* highest priority of the group.
|
|
6826
|
-
*/
|
|
6827
|
-
function mergeEntriesByValue(entries) {
|
|
6828
|
-
if (entries.length <= 1) return entries;
|
|
6829
|
-
const groups = /* @__PURE__ */ new Map();
|
|
6830
|
-
for (const entry of entries) {
|
|
6831
|
-
const valueKey = serializeValue(entry.value);
|
|
6832
|
-
const group = groups.get(valueKey);
|
|
6833
|
-
if (group) {
|
|
6834
|
-
group.entries.push(entry);
|
|
6835
|
-
group.maxPriority = Math.max(group.maxPriority, entry.priority);
|
|
6836
|
-
} else groups.set(valueKey, {
|
|
6837
|
-
entries: [entry],
|
|
6838
|
-
maxPriority: entry.priority
|
|
6839
|
-
});
|
|
6840
|
-
}
|
|
6841
|
-
if (groups.size === entries.length) return entries;
|
|
6842
|
-
const merged = [];
|
|
6843
|
-
for (const [, group] of groups) {
|
|
6844
|
-
if (group.entries.length === 1) {
|
|
6845
|
-
merged.push(group.entries[0]);
|
|
6899
|
+
for (let i = 0; i < terms.length; i++) {
|
|
6900
|
+
const term = terms[i];
|
|
6901
|
+
if (term.kind !== "compound" || term.operator !== "OR") {
|
|
6902
|
+
result.push(term);
|
|
6846
6903
|
continue;
|
|
6847
6904
|
}
|
|
6848
|
-
const
|
|
6849
|
-
const
|
|
6850
|
-
for (const
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6905
|
+
const siblings = [...terms.slice(0, i), ...terms.slice(i + 1)];
|
|
6906
|
+
const surviving = [];
|
|
6907
|
+
for (const branch of term.children) if (simplifyInner({
|
|
6908
|
+
kind: "compound",
|
|
6909
|
+
operator: "AND",
|
|
6910
|
+
children: [branch, ...siblings]
|
|
6911
|
+
}).kind !== "false") surviving.push(branch);
|
|
6912
|
+
if (surviving.length === term.children.length) result.push(term);
|
|
6913
|
+
else if (surviving.length === 0) return [falseCondition()];
|
|
6914
|
+
else {
|
|
6915
|
+
changed = true;
|
|
6916
|
+
if (surviving.length === 1) result.push(surviving[0]);
|
|
6917
|
+
else result.push({
|
|
6918
|
+
kind: "compound",
|
|
6919
|
+
operator: "OR",
|
|
6920
|
+
children: surviving
|
|
6861
6921
|
});
|
|
6862
6922
|
}
|
|
6863
6923
|
}
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
6870
|
-
return JSON.stringify(value);
|
|
6924
|
+
if (!changed) return terms;
|
|
6925
|
+
const flattened = [];
|
|
6926
|
+
for (const term of result) if (term.kind === "compound" && term.operator === "AND") flattened.push(...term.children);
|
|
6927
|
+
else flattened.push(term);
|
|
6928
|
+
return flattened;
|
|
6871
6929
|
}
|
|
6930
|
+
//#endregion
|
|
6931
|
+
//#region src/pipeline/materialize-contradictions.ts
|
|
6872
6932
|
/**
|
|
6873
|
-
*
|
|
6874
|
-
*
|
|
6875
|
-
* When a value map contains compound AND state keys (e.g. `@dark & @hc`),
|
|
6876
|
-
* checks whether any state atom is a "don't-care" variable — i.e. the
|
|
6877
|
-
* value is the same whether that atom is present or absent. Redundant
|
|
6878
|
-
* atoms are removed from all keys and duplicate entries are collapsed.
|
|
6879
|
-
*
|
|
6880
|
-
* This runs **before** condition parsing so that downstream stages
|
|
6881
|
-
* (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
|
|
6882
|
-
* never see the irrelevant dimension, producing simpler, smaller CSS.
|
|
6883
|
-
*
|
|
6884
|
-
* Only pure top-level AND combinations are eligible. Keys that contain
|
|
6885
|
-
* `|`, `^`, or `,` at the top level are treated as opaque single atoms.
|
|
6886
|
-
*
|
|
6887
|
-
* @example
|
|
6888
|
-
* { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
|
|
6889
|
-
* // @hc is redundant → { '': A, '@dark': B }
|
|
6933
|
+
* Generic deduplication by a key extraction function.
|
|
6934
|
+
* Preserves insertion order, keeping the first occurrence of each key.
|
|
6890
6935
|
*/
|
|
6891
|
-
function
|
|
6892
|
-
const
|
|
6893
|
-
|
|
6894
|
-
const
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
const allAtoms = /* @__PURE__ */ new Set();
|
|
6901
|
-
for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
|
|
6902
|
-
const redundant = /* @__PURE__ */ new Set();
|
|
6903
|
-
for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
|
|
6904
|
-
if (redundant.size === 0) return valueMap;
|
|
6905
|
-
const newMap = {};
|
|
6906
|
-
for (const e of entries) {
|
|
6907
|
-
const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
|
|
6908
|
-
if (!(newKey in newMap)) newMap[newKey] = e.value;
|
|
6936
|
+
function dedupeByKey(items, getKey) {
|
|
6937
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6938
|
+
const result = [];
|
|
6939
|
+
for (const item of items) {
|
|
6940
|
+
const key = getKey(item);
|
|
6941
|
+
if (!seen.has(key)) {
|
|
6942
|
+
seen.add(key);
|
|
6943
|
+
result.push(item);
|
|
6944
|
+
}
|
|
6909
6945
|
}
|
|
6910
|
-
return
|
|
6946
|
+
return result;
|
|
6947
|
+
}
|
|
6948
|
+
function dedupeMediaConditions(conditions) {
|
|
6949
|
+
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
6950
|
+
}
|
|
6951
|
+
function dedupeContainerConditions(conditions) {
|
|
6952
|
+
return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
|
|
6953
|
+
}
|
|
6954
|
+
function dedupeSupportsConditions(conditions) {
|
|
6955
|
+
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
6911
6956
|
}
|
|
6912
6957
|
/**
|
|
6913
|
-
*
|
|
6914
|
-
*
|
|
6915
|
-
* Returns `null` if the key contains `|`, `^`, or `,` at the top level
|
|
6916
|
-
* (making it ineligible for atom-level extraction).
|
|
6917
|
-
* Returns `[]` for the empty string (default key).
|
|
6958
|
+
* Check if supports conditions contain contradictions
|
|
6959
|
+
* e.g., @supports(display: grid) AND NOT @supports(display: grid)
|
|
6918
6960
|
*/
|
|
6919
|
-
function
|
|
6920
|
-
|
|
6921
|
-
const
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
else if (ch === ")" || ch === "]") depth--;
|
|
6927
|
-
if (depth === 0) {
|
|
6928
|
-
if (ch === "&") {
|
|
6929
|
-
const trimmed = current.trim();
|
|
6930
|
-
if (trimmed) parts.push(trimmed);
|
|
6931
|
-
current = "";
|
|
6932
|
-
continue;
|
|
6933
|
-
}
|
|
6934
|
-
if (ch === "|" || ch === "^" || ch === ",") return null;
|
|
6935
|
-
}
|
|
6936
|
-
current += ch;
|
|
6961
|
+
function hasSupportsContradiction(conditions) {
|
|
6962
|
+
const conditionMap = /* @__PURE__ */ new Map();
|
|
6963
|
+
for (const cond of conditions) {
|
|
6964
|
+
const key = `${cond.subtype}|${cond.condition}`;
|
|
6965
|
+
const existing = conditionMap.get(key);
|
|
6966
|
+
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
6967
|
+
conditionMap.set(key, !cond.negated);
|
|
6937
6968
|
}
|
|
6938
|
-
|
|
6939
|
-
if (trimmed) parts.push(trimmed);
|
|
6940
|
-
return parts;
|
|
6969
|
+
return false;
|
|
6941
6970
|
}
|
|
6942
6971
|
/**
|
|
6943
|
-
*
|
|
6944
|
-
*
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
if (withAtom.length === 0) return false;
|
|
6949
|
-
for (const wa of withAtom) {
|
|
6950
|
-
const remaining = wa.atoms.filter((a) => a !== atom);
|
|
6951
|
-
const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
|
|
6952
|
-
if (!pair) return false;
|
|
6953
|
-
if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
|
|
6954
|
-
}
|
|
6955
|
-
return true;
|
|
6956
|
-
}
|
|
6957
|
-
/**
|
|
6958
|
-
* Check if a value is a style value mapping (object with state keys)
|
|
6959
|
-
*/
|
|
6960
|
-
function isValueMapping(value) {
|
|
6961
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
6962
|
-
}
|
|
6963
|
-
/**
|
|
6964
|
-
* Expand OR conditions in parsed entries into multiple exclusive entries.
|
|
6965
|
-
*
|
|
6966
|
-
* For an entry with condition `A | B | C`, this creates 3 entries:
|
|
6967
|
-
* - condition: A
|
|
6968
|
-
* - condition: B & !A
|
|
6969
|
-
* - condition: C & !A & !B
|
|
6970
|
-
*
|
|
6971
|
-
* This ensures OR branches are mutually exclusive BEFORE the main
|
|
6972
|
-
* exclusive condition building pass.
|
|
6973
|
-
*
|
|
6974
|
-
* @param entries Parsed entries (may contain OR conditions)
|
|
6975
|
-
* @returns Expanded entries with OR branches made exclusive
|
|
6976
|
-
*/
|
|
6977
|
-
function expandOrConditions(entries) {
|
|
6978
|
-
const result = [];
|
|
6979
|
-
for (const entry of entries) {
|
|
6980
|
-
const expanded = expandSingleEntry(entry);
|
|
6981
|
-
result.push(...expanded);
|
|
6982
|
-
}
|
|
6983
|
-
return result;
|
|
6984
|
-
}
|
|
6985
|
-
/**
|
|
6986
|
-
* Expand a single entry's OR condition into multiple exclusive entries.
|
|
6987
|
-
*
|
|
6988
|
-
* Note: branches are NOT sorted by at-rule context here (unlike the
|
|
6989
|
-
* `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
|
|
6990
|
-
* the product of De Morgan negation, so each branch is expected to render
|
|
6991
|
-
* independently in its own scope and at-rule sort isn't load-bearing.
|
|
6992
|
-
* The post-build pass needs the sort because it has to preserve at-rule
|
|
6993
|
-
* wrapping across branches that came from negating a compound at-rule.
|
|
6994
|
-
*/
|
|
6995
|
-
function expandSingleEntry(entry) {
|
|
6996
|
-
const orBranches = collectOrBranches(entry.condition);
|
|
6997
|
-
if (orBranches.length <= 1) return [entry];
|
|
6998
|
-
const result = [];
|
|
6999
|
-
const priorBranches = [];
|
|
7000
|
-
for (let i = 0; i < orBranches.length; i++) {
|
|
7001
|
-
const branch = orBranches[i];
|
|
7002
|
-
let exclusiveBranch = branch;
|
|
7003
|
-
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
7004
|
-
const simplified = simplifyCondition(exclusiveBranch);
|
|
7005
|
-
if (simplified.kind === "false") {
|
|
7006
|
-
priorBranches.push(branch);
|
|
7007
|
-
continue;
|
|
7008
|
-
}
|
|
7009
|
-
result.push({
|
|
7010
|
-
...entry,
|
|
7011
|
-
stateKey: `${entry.stateKey}[${i}]`,
|
|
7012
|
-
condition: simplified
|
|
7013
|
-
});
|
|
7014
|
-
priorBranches.push(branch);
|
|
7015
|
-
}
|
|
7016
|
-
return result;
|
|
7017
|
-
}
|
|
7018
|
-
/**
|
|
7019
|
-
* Collect top-level OR branches from a condition.
|
|
7020
|
-
*
|
|
7021
|
-
* For `A | B | C`, returns [A, B, C]
|
|
7022
|
-
* For `A & B`, returns [A & B] (single branch)
|
|
7023
|
-
* For `A | (B & C)`, returns [A, B & C]
|
|
7024
|
-
*/
|
|
7025
|
-
function collectOrBranches(condition) {
|
|
7026
|
-
if (condition.kind === "true" || condition.kind === "false") return [condition];
|
|
7027
|
-
if (isCompoundCondition(condition) && condition.operator === "OR") {
|
|
7028
|
-
const branches = [];
|
|
7029
|
-
for (const child of condition.children) branches.push(...collectOrBranches(child));
|
|
7030
|
-
return branches;
|
|
7031
|
-
}
|
|
7032
|
-
return [condition];
|
|
7033
|
-
}
|
|
7034
|
-
/**
|
|
7035
|
-
* Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
|
|
7036
|
-
*
|
|
7037
|
-
* This handles ORs that arise from De Morgan expansion during negation:
|
|
7038
|
-
* !(A & B) = !A | !B
|
|
7039
|
-
*
|
|
7040
|
-
* These ORs need to be made exclusive to avoid overlapping CSS rules:
|
|
7041
|
-
* !A | !B → !A | (A & !B)
|
|
7042
|
-
*
|
|
7043
|
-
* This is logically equivalent but ensures each branch has proper context.
|
|
7044
|
-
*
|
|
7045
|
-
* Example:
|
|
7046
|
-
* Input: { "": V1, "@supports(...) & :has()": V2 }
|
|
7047
|
-
* V2's exclusive = @supports & :has
|
|
7048
|
-
* V1's exclusive = !(@supports & :has) = !@supports | !:has
|
|
7049
|
-
*
|
|
7050
|
-
* Without this fix: V1 gets two rules:
|
|
7051
|
-
* - @supports (not ...) → V1 ✓
|
|
7052
|
-
* - :not(:has()) → V1 ✗ (missing @supports context!)
|
|
7053
|
-
*
|
|
7054
|
-
* With this fix: V1 gets two exclusive rules:
|
|
7055
|
-
* - @supports (not ...) → V1 ✓
|
|
7056
|
-
* - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
|
|
7057
|
-
*/
|
|
7058
|
-
function expandExclusiveOrs(entries) {
|
|
7059
|
-
const result = [];
|
|
7060
|
-
for (const entry of entries) {
|
|
7061
|
-
const expanded = expandExclusiveConditionOrs(entry);
|
|
7062
|
-
result.push(...expanded);
|
|
7063
|
-
}
|
|
7064
|
-
return result;
|
|
7065
|
-
}
|
|
7066
|
-
/**
|
|
7067
|
-
* Check if a condition involves at-rules (media, container, supports, starting)
|
|
7068
|
-
*/
|
|
7069
|
-
function hasAtRuleContext(node) {
|
|
7070
|
-
if (node.kind === "true" || node.kind === "false") return false;
|
|
7071
|
-
if (node.kind === "state") return node.type === "media" || node.type === "container" || node.type === "supports" || node.type === "starting";
|
|
7072
|
-
if (node.kind === "compound") return node.children.some(hasAtRuleContext);
|
|
7073
|
-
return false;
|
|
7074
|
-
}
|
|
7075
|
-
/**
|
|
7076
|
-
* Sort OR branches to prioritize at-rule conditions first.
|
|
7077
|
-
*
|
|
7078
|
-
* This is critical for correct CSS generation. For `!A | !B` where A is at-rule
|
|
7079
|
-
* and B is modifier, we want:
|
|
7080
|
-
* - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
|
|
7081
|
-
* - Branch 1: A & !B (modifier negation with at-rule context)
|
|
7082
|
-
*
|
|
7083
|
-
* If we process in wrong order (!B first), we'd get:
|
|
7084
|
-
* - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
|
|
7085
|
-
* - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
|
|
7086
|
-
*/
|
|
7087
|
-
function sortOrBranchesForExpansion(branches) {
|
|
7088
|
-
return [...branches].sort((a, b) => {
|
|
7089
|
-
const aHasAtRule = hasAtRuleContext(a);
|
|
7090
|
-
const bHasAtRule = hasAtRuleContext(b);
|
|
7091
|
-
if (aHasAtRule && !bHasAtRule) return -1;
|
|
7092
|
-
if (!aHasAtRule && bHasAtRule) return 1;
|
|
7093
|
-
return 0;
|
|
7094
|
-
});
|
|
7095
|
-
}
|
|
7096
|
-
/**
|
|
7097
|
-
* Expand ORs in a single entry's exclusive condition
|
|
7098
|
-
*/
|
|
7099
|
-
function expandExclusiveConditionOrs(entry) {
|
|
7100
|
-
let orBranches = collectOrBranches(entry.exclusiveCondition);
|
|
7101
|
-
if (orBranches.length <= 1) return [entry];
|
|
7102
|
-
orBranches = sortOrBranchesForExpansion(orBranches);
|
|
7103
|
-
const result = [];
|
|
7104
|
-
const priorBranches = [];
|
|
7105
|
-
for (let i = 0; i < orBranches.length; i++) {
|
|
7106
|
-
const branch = orBranches[i];
|
|
7107
|
-
let exclusiveBranch = branch;
|
|
7108
|
-
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
7109
|
-
const simplified = simplifyCondition(exclusiveBranch);
|
|
7110
|
-
if (simplified.kind === "false") {
|
|
7111
|
-
priorBranches.push(branch);
|
|
7112
|
-
continue;
|
|
7113
|
-
}
|
|
7114
|
-
result.push({
|
|
7115
|
-
...entry,
|
|
7116
|
-
stateKey: `${entry.stateKey}[or:${i}]`,
|
|
7117
|
-
exclusiveCondition: simplified
|
|
7118
|
-
});
|
|
7119
|
-
priorBranches.push(branch);
|
|
7120
|
-
}
|
|
7121
|
-
return result;
|
|
7122
|
-
}
|
|
7123
|
-
//#endregion
|
|
7124
|
-
//#region src/pipeline/materialize-contradictions.ts
|
|
7125
|
-
/**
|
|
7126
|
-
* Generic deduplication by a key extraction function.
|
|
7127
|
-
* Preserves insertion order, keeping the first occurrence of each key.
|
|
7128
|
-
*/
|
|
7129
|
-
function dedupeByKey(items, getKey) {
|
|
7130
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7131
|
-
const result = [];
|
|
7132
|
-
for (const item of items) {
|
|
7133
|
-
const key = getKey(item);
|
|
7134
|
-
if (!seen.has(key)) {
|
|
7135
|
-
seen.add(key);
|
|
7136
|
-
result.push(item);
|
|
7137
|
-
}
|
|
7138
|
-
}
|
|
7139
|
-
return result;
|
|
7140
|
-
}
|
|
7141
|
-
function dedupeMediaConditions(conditions) {
|
|
7142
|
-
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
7143
|
-
}
|
|
7144
|
-
function dedupeContainerConditions(conditions) {
|
|
7145
|
-
return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
|
|
7146
|
-
}
|
|
7147
|
-
function dedupeSupportsConditions(conditions) {
|
|
7148
|
-
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
7149
|
-
}
|
|
7150
|
-
/**
|
|
7151
|
-
* Check if supports conditions contain contradictions
|
|
7152
|
-
* e.g., @supports(display: grid) AND NOT @supports(display: grid)
|
|
7153
|
-
*/
|
|
7154
|
-
function hasSupportsContradiction(conditions) {
|
|
7155
|
-
const conditionMap = /* @__PURE__ */ new Map();
|
|
7156
|
-
for (const cond of conditions) {
|
|
7157
|
-
const key = `${cond.subtype}|${cond.condition}`;
|
|
7158
|
-
const existing = conditionMap.get(key);
|
|
7159
|
-
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7160
|
-
conditionMap.set(key, !cond.negated);
|
|
7161
|
-
}
|
|
7162
|
-
return false;
|
|
7163
|
-
}
|
|
7164
|
-
/**
|
|
7165
|
-
* Check if a set of media conditions contains contradictions
|
|
7166
|
-
* e.g., (prefers-color-scheme: light) AND NOT (prefers-color-scheme: light)
|
|
7167
|
-
* or (width >= 900px) AND (width < 600px)
|
|
7168
|
-
*
|
|
7169
|
-
* Uses parsed media conditions for efficient analysis without regex parsing.
|
|
6972
|
+
* Check if a set of media conditions contains contradictions
|
|
6973
|
+
* e.g., (prefers-color-scheme: light) AND NOT (prefers-color-scheme: light)
|
|
6974
|
+
* or (width >= 900px) AND (width < 600px)
|
|
6975
|
+
*
|
|
6976
|
+
* Uses parsed media conditions for efficient analysis without regex parsing.
|
|
7170
6977
|
*/
|
|
7171
6978
|
function hasMediaContradiction(conditions) {
|
|
7172
6979
|
const featureConditions = /* @__PURE__ */ new Map();
|
|
@@ -7777,7 +7584,9 @@ function dedupeSelectorConditions(conditions) {
|
|
|
7777
7584
|
if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
|
|
7778
7585
|
return result.filter((c) => {
|
|
7779
7586
|
if (!("attribute" in c)) return true;
|
|
7780
|
-
|
|
7587
|
+
if (isSubsumedNegatedModifier(c, facts)) return false;
|
|
7588
|
+
if (!c.negated && c.value === void 0 && facts.positiveExactValuesByAttr.has(c.attribute)) return false;
|
|
7589
|
+
return true;
|
|
7781
7590
|
});
|
|
7782
7591
|
}
|
|
7783
7592
|
/**
|
|
@@ -8100,6 +7909,12 @@ function makeOrBranchesExclusive(children) {
|
|
|
8100
7909
|
* Check if OR branches produce different at-rule contexts when
|
|
8101
7910
|
* materialized. If so, the Cartesian product in andToCSS will
|
|
8102
7911
|
* create overlapping CSS variants that need exclusive expansion.
|
|
7912
|
+
*
|
|
7913
|
+
* Exported so Stage 2a (`expandOrConditions` in `exclusive.ts`) can
|
|
7914
|
+
* reuse the same heuristic and skip OR expansion when every branch
|
|
7915
|
+
* lives in the same at-rule/root/parent/own context — pure-selector
|
|
7916
|
+
* ORs are better collapsed into `:is(...)` at materialization time
|
|
7917
|
+
* than expanded into mutually-exclusive `A | (B & !A) | …` cascades.
|
|
8103
7918
|
*/
|
|
8104
7919
|
function branchesProduceDifferentContexts(branches) {
|
|
8105
7920
|
const contextKeys = /* @__PURE__ */ new Set();
|
|
@@ -8117,161 +7932,634 @@ function branchesProduceDifferentContexts(branches) {
|
|
|
8117
7932
|
* After deduplication, variants that differ only in their base
|
|
8118
7933
|
* modifier/pseudo conditions are merged into :is() groups.
|
|
8119
7934
|
*
|
|
8120
|
-
* Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
|
|
8121
|
-
* so here we just collect all variants. Any remaining ORs in the condition
|
|
8122
|
-
* tree (e.g., from De Morgan expansion) are handled as simple alternatives.
|
|
7935
|
+
* Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
|
|
7936
|
+
* so here we just collect all variants. Any remaining ORs in the condition
|
|
7937
|
+
* tree (e.g., from De Morgan expansion) are handled as simple alternatives.
|
|
7938
|
+
*/
|
|
7939
|
+
function orToCSS(children) {
|
|
7940
|
+
const allVariants = [];
|
|
7941
|
+
for (const child of children) {
|
|
7942
|
+
const childCSS = conditionToCSSInner(child);
|
|
7943
|
+
if (childCSS.isImpossible) continue;
|
|
7944
|
+
allVariants.push(...childCSS.variants);
|
|
7945
|
+
}
|
|
7946
|
+
if (allVariants.length === 0) return {
|
|
7947
|
+
variants: [],
|
|
7948
|
+
isImpossible: true
|
|
7949
|
+
};
|
|
7950
|
+
return {
|
|
7951
|
+
variants: dedupeVariants(allVariants),
|
|
7952
|
+
isImpossible: false
|
|
7953
|
+
};
|
|
7954
|
+
}
|
|
7955
|
+
/**
|
|
7956
|
+
* Find keys present in ALL condition arrays.
|
|
7957
|
+
*/
|
|
7958
|
+
function findCommonKeys(conditionSets, getKey) {
|
|
7959
|
+
if (conditionSets.length === 0) return /* @__PURE__ */ new Set();
|
|
7960
|
+
const common = new Set(conditionSets[0].map(getKey));
|
|
7961
|
+
for (let i = 1; i < conditionSets.length; i++) {
|
|
7962
|
+
const keys = new Set(conditionSets[i].map(getKey));
|
|
7963
|
+
for (const key of common) if (!keys.has(key)) common.delete(key);
|
|
7964
|
+
}
|
|
7965
|
+
return common;
|
|
7966
|
+
}
|
|
7967
|
+
/**
|
|
7968
|
+
* Merge OR variants that share the same "context" (at-rules, root, parent,
|
|
7969
|
+
* own, starting) into a single variant with a SelectorGroup.
|
|
7970
|
+
*
|
|
7971
|
+
* Variants with no modifier/pseudo conditions are kept separate (they match
|
|
7972
|
+
* unconditionally and can't be expressed inside :is()).
|
|
7973
|
+
*/
|
|
7974
|
+
function mergeVariantsIntoSelectorGroups(variants) {
|
|
7975
|
+
if (variants.length <= 1) return variants;
|
|
7976
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7977
|
+
for (const v of variants) {
|
|
7978
|
+
const key = getVariantContextKey(v);
|
|
7979
|
+
const group = groups.get(key);
|
|
7980
|
+
if (group) group.push(v);
|
|
7981
|
+
else groups.set(key, [v]);
|
|
7982
|
+
}
|
|
7983
|
+
const result = [];
|
|
7984
|
+
for (const group of groups.values()) {
|
|
7985
|
+
if (group.length === 1) {
|
|
7986
|
+
result.push(group[0]);
|
|
7987
|
+
continue;
|
|
7988
|
+
}
|
|
7989
|
+
const withSelectors = [];
|
|
7990
|
+
const withoutSelectors = [];
|
|
7991
|
+
for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
|
|
7992
|
+
else withSelectors.push(v);
|
|
7993
|
+
result.push(...withoutSelectors);
|
|
7994
|
+
if (withSelectors.length <= 1) {
|
|
7995
|
+
result.push(...withSelectors);
|
|
7996
|
+
continue;
|
|
7997
|
+
}
|
|
7998
|
+
result.push(factorAndGroup(withSelectors));
|
|
7999
|
+
}
|
|
8000
|
+
return result;
|
|
8001
|
+
}
|
|
8002
|
+
/**
|
|
8003
|
+
* Factor common modifier/pseudo conditions out of variants and create
|
|
8004
|
+
* a single variant with a SelectorGroup for the remaining (differing)
|
|
8005
|
+
* conditions.
|
|
8006
|
+
*
|
|
8007
|
+
* Precondition: all variants must share the same context key (identical
|
|
8008
|
+
* at-rules, root/parent/own/selector groups, startingStyle).
|
|
8009
|
+
*/
|
|
8010
|
+
function factorAndGroup(variants) {
|
|
8011
|
+
{
|
|
8012
|
+
const key0 = getVariantContextKey(variants[0]);
|
|
8013
|
+
for (let i = 1; i < variants.length; i++) {
|
|
8014
|
+
const keyI = getVariantContextKey(variants[i]);
|
|
8015
|
+
if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
|
|
8016
|
+
}
|
|
8017
|
+
}
|
|
8018
|
+
const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
|
|
8019
|
+
const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
|
|
8020
|
+
const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
|
|
8021
|
+
const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
|
|
8022
|
+
const branches = [];
|
|
8023
|
+
let hasEmptyBranch = false;
|
|
8024
|
+
for (const v of variants) {
|
|
8025
|
+
const branch = [];
|
|
8026
|
+
for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
|
|
8027
|
+
for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
|
|
8028
|
+
if (branch.length > 0) branches.push(branch);
|
|
8029
|
+
else hasEmptyBranch = true;
|
|
8030
|
+
}
|
|
8031
|
+
if (hasEmptyBranch) return {
|
|
8032
|
+
...variants[0],
|
|
8033
|
+
modifierConditions: commonModifiers,
|
|
8034
|
+
pseudoConditions: commonPseudos
|
|
8035
|
+
};
|
|
8036
|
+
const factoredGroups = tryFactorIntoDimensions(branches);
|
|
8037
|
+
if (factoredGroups) return {
|
|
8038
|
+
modifierConditions: commonModifiers,
|
|
8039
|
+
pseudoConditions: commonPseudos,
|
|
8040
|
+
selectorGroups: [...variants[0].selectorGroups, ...factoredGroups],
|
|
8041
|
+
ownGroups: [...variants[0].ownGroups],
|
|
8042
|
+
mediaConditions: [...variants[0].mediaConditions],
|
|
8043
|
+
containerConditions: [...variants[0].containerConditions],
|
|
8044
|
+
supportsConditions: [...variants[0].supportsConditions],
|
|
8045
|
+
rootGroups: [...variants[0].rootGroups],
|
|
8046
|
+
parentGroups: [...variants[0].parentGroups],
|
|
8047
|
+
startingStyle: variants[0].startingStyle
|
|
8048
|
+
};
|
|
8049
|
+
return {
|
|
8050
|
+
modifierConditions: commonModifiers,
|
|
8051
|
+
pseudoConditions: commonPseudos,
|
|
8052
|
+
selectorGroups: [...variants[0].selectorGroups, {
|
|
8053
|
+
branches,
|
|
8054
|
+
negated: false
|
|
8055
|
+
}],
|
|
8056
|
+
ownGroups: [...variants[0].ownGroups],
|
|
8057
|
+
mediaConditions: [...variants[0].mediaConditions],
|
|
8058
|
+
containerConditions: [...variants[0].containerConditions],
|
|
8059
|
+
supportsConditions: [...variants[0].supportsConditions],
|
|
8060
|
+
rootGroups: [...variants[0].rootGroups],
|
|
8061
|
+
parentGroups: [...variants[0].parentGroups],
|
|
8062
|
+
startingStyle: variants[0].startingStyle
|
|
8063
|
+
};
|
|
8064
|
+
}
|
|
8065
|
+
/**
|
|
8066
|
+
* Detect when branches form a complete Cartesian product of independent
|
|
8067
|
+
* modifier attribute dimensions and return one SelectorGroup per dimension.
|
|
8068
|
+
*
|
|
8069
|
+
* Example: 4 branches for 2 attributes × 2 values each →
|
|
8070
|
+
* :is(A1, A2):is(B1, B2) instead of :is(A1B1, A1B2, A2B1, A2B2)
|
|
8071
|
+
*/
|
|
8072
|
+
function tryFactorIntoDimensions(branches) {
|
|
8073
|
+
if (branches.length < 4) return null;
|
|
8074
|
+
const dimensions = /* @__PURE__ */ new Map();
|
|
8075
|
+
for (const branch of branches) for (const cond of branch) {
|
|
8076
|
+
if (!("attribute" in cond)) return null;
|
|
8077
|
+
if (!dimensions.has(cond.attribute)) dimensions.set(cond.attribute, /* @__PURE__ */ new Map());
|
|
8078
|
+
dimensions.get(cond.attribute).set(getModifierKey(cond), cond);
|
|
8079
|
+
}
|
|
8080
|
+
if (dimensions.size < 2) return null;
|
|
8081
|
+
for (const branch of branches) {
|
|
8082
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8083
|
+
for (const cond of branch) {
|
|
8084
|
+
const attr = cond.attribute;
|
|
8085
|
+
if (seen.has(attr)) return null;
|
|
8086
|
+
seen.add(attr);
|
|
8087
|
+
}
|
|
8088
|
+
if (seen.size !== dimensions.size) return null;
|
|
8089
|
+
}
|
|
8090
|
+
let expectedCount = 1;
|
|
8091
|
+
for (const vals of dimensions.values()) expectedCount *= vals.size;
|
|
8092
|
+
if (branches.length !== expectedCount) return null;
|
|
8093
|
+
return [...dimensions.values()].map((vals) => ({
|
|
8094
|
+
branches: [...vals.values()].map((cond) => [cond]),
|
|
8095
|
+
negated: false
|
|
8096
|
+
}));
|
|
8097
|
+
}
|
|
8098
|
+
/**
|
|
8099
|
+
* Build at-rules array from a variant
|
|
8100
|
+
*/
|
|
8101
|
+
function buildAtRulesFromVariant(variant) {
|
|
8102
|
+
const atRules = [];
|
|
8103
|
+
if (variant.mediaConditions.length > 0) {
|
|
8104
|
+
const conditionParts = variant.mediaConditions.map((c) => {
|
|
8105
|
+
if (c.subtype === "type") return c.negated ? `not ${c.condition}` : c.condition;
|
|
8106
|
+
else return c.negated ? `(not ${c.condition})` : c.condition;
|
|
8107
|
+
});
|
|
8108
|
+
atRules.push(`@media ${conditionParts.sort().join(" and ")}`);
|
|
8109
|
+
}
|
|
8110
|
+
if (variant.containerConditions.length > 0) {
|
|
8111
|
+
const byName = /* @__PURE__ */ new Map();
|
|
8112
|
+
for (const cond of variant.containerConditions) {
|
|
8113
|
+
const group = byName.get(cond.name) || [];
|
|
8114
|
+
group.push(cond);
|
|
8115
|
+
byName.set(cond.name, group);
|
|
8116
|
+
}
|
|
8117
|
+
for (const [name, conditions] of byName) {
|
|
8118
|
+
const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
|
|
8119
|
+
const namePrefix = name ? `${name} ` : "";
|
|
8120
|
+
atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
|
|
8121
|
+
}
|
|
8122
|
+
}
|
|
8123
|
+
if (variant.supportsConditions.length > 0) {
|
|
8124
|
+
const conditionParts = variant.supportsConditions.map((c) => {
|
|
8125
|
+
if (c.subtype === "selector") {
|
|
8126
|
+
const selectorCond = `selector(${c.condition})`;
|
|
8127
|
+
return c.negated ? `(not ${selectorCond})` : selectorCond;
|
|
8128
|
+
} else {
|
|
8129
|
+
const featureCond = `(${c.condition})`;
|
|
8130
|
+
return c.negated ? `(not ${featureCond})` : featureCond;
|
|
8131
|
+
}
|
|
8132
|
+
});
|
|
8133
|
+
atRules.push(`@supports ${conditionParts.join(" and ")}`);
|
|
8134
|
+
}
|
|
8135
|
+
return atRules;
|
|
8136
|
+
}
|
|
8137
|
+
//#endregion
|
|
8138
|
+
//#region src/pipeline/exclusive.ts
|
|
8139
|
+
/**
|
|
8140
|
+
* Build exclusive conditions for a list of parsed style entries.
|
|
8141
|
+
*
|
|
8142
|
+
* The entries should be ordered by priority (highest priority first).
|
|
8143
|
+
*
|
|
8144
|
+
* For each entry, we compute:
|
|
8145
|
+
* exclusiveCondition = condition & !prior[0] & !prior[1] & ...
|
|
8146
|
+
*
|
|
8147
|
+
* This ensures exactly one condition matches at any time.
|
|
8148
|
+
*
|
|
8149
|
+
* Example:
|
|
8150
|
+
* Input (ordered highest to lowest priority):
|
|
8151
|
+
* A: value1 (priority 2)
|
|
8152
|
+
* B: value2 (priority 1)
|
|
8153
|
+
* C: value3 (priority 0)
|
|
8154
|
+
*
|
|
8155
|
+
* Output:
|
|
8156
|
+
* A: A
|
|
8157
|
+
* B: B & !A
|
|
8158
|
+
* C: C & !A & !B
|
|
8159
|
+
*
|
|
8160
|
+
* @param entries Parsed style entries ordered by priority (highest first)
|
|
8161
|
+
* @returns Entries with exclusive conditions, filtered to remove impossible ones
|
|
8162
|
+
*/
|
|
8163
|
+
function buildExclusiveConditions(entries) {
|
|
8164
|
+
const result = [];
|
|
8165
|
+
const priorConditions = [];
|
|
8166
|
+
for (const entry of entries) {
|
|
8167
|
+
let exclusive = entry.condition;
|
|
8168
|
+
for (const prior of priorConditions) if (prior.kind !== "true") exclusive = and(exclusive, not(prior));
|
|
8169
|
+
const simplified = simplifyCondition(exclusive);
|
|
8170
|
+
if (simplified.kind === "false") continue;
|
|
8171
|
+
result.push({
|
|
8172
|
+
...entry,
|
|
8173
|
+
exclusiveCondition: simplified
|
|
8174
|
+
});
|
|
8175
|
+
if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
|
|
8176
|
+
}
|
|
8177
|
+
return result;
|
|
8178
|
+
}
|
|
8179
|
+
/**
|
|
8180
|
+
* Parse style entries from a value mapping object.
|
|
8181
|
+
*
|
|
8182
|
+
* @param styleKey The style key (e.g., 'padding')
|
|
8183
|
+
* @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
|
|
8184
|
+
* @param parseCondition Function to parse state keys into conditions
|
|
8185
|
+
* @returns Parsed entries ordered by priority (highest first)
|
|
8186
|
+
*/
|
|
8187
|
+
function parseStyleEntries(styleKey, valueMap, parseCondition) {
|
|
8188
|
+
const entries = [];
|
|
8189
|
+
Object.keys(valueMap).forEach((stateKey, index) => {
|
|
8190
|
+
const value = valueMap[stateKey];
|
|
8191
|
+
const condition = stateKey === "" ? trueCondition() : parseCondition(stateKey);
|
|
8192
|
+
entries.push({
|
|
8193
|
+
styleKey,
|
|
8194
|
+
stateKey,
|
|
8195
|
+
value,
|
|
8196
|
+
condition,
|
|
8197
|
+
priority: index
|
|
8198
|
+
});
|
|
8199
|
+
});
|
|
8200
|
+
entries.reverse();
|
|
8201
|
+
return entries;
|
|
8202
|
+
}
|
|
8203
|
+
/**
|
|
8204
|
+
* Merge parsed entries that share the same value.
|
|
8205
|
+
*
|
|
8206
|
+
* When multiple **non-default** state keys map to the same value, their
|
|
8207
|
+
* conditions can be combined with OR and treated as a single entry.
|
|
8208
|
+
* This must happen **before** exclusive expansion and OR branch splitting
|
|
8209
|
+
* to avoid combinatorial explosion and duplicate CSS output.
|
|
8210
|
+
*
|
|
8211
|
+
* **Merging must preserve the authored cascade.** Merging two same-value
|
|
8212
|
+
* entries with priorities `p_h > p_l` lifts the lower-priority entry up
|
|
8213
|
+
* to `p_h` and changes the "blocker" for intermediate-priority entries
|
|
8214
|
+
* from `!C_h` to `!(C_h | C_l) = !C_h & !C_l`. The added `!C_l`
|
|
8215
|
+
* constraint can incorrectly block an intermediate entry that should
|
|
8216
|
+
* have won.
|
|
8217
|
+
*
|
|
8218
|
+
* Two same-value entries with conditions `C_h` (higher priority) and
|
|
8219
|
+
* `C_l` (lower priority) are safe to merge iff for every entry
|
|
8220
|
+
* `e_m` strictly between them in priority with a different value,
|
|
8221
|
+
*
|
|
8222
|
+
* simplify(C_m & C_l & !C_h) = FALSE
|
|
8223
|
+
*
|
|
8224
|
+
* i.e. there is no scenario where the intermediate state could have
|
|
8225
|
+
* matched (`C_m`), the lower-priority same-value entry would also have
|
|
8226
|
+
* matched (`C_l`), and the higher-priority entry would not (`!C_h`).
|
|
8227
|
+
* In such scenarios the intermediate is supposed to win; the merge
|
|
8228
|
+
* would block it by introducing `!C_l`.
|
|
8229
|
+
*
|
|
8230
|
+
* Example (UNSAFE — must not merge):
|
|
8231
|
+
* `{ hovered: 'red', pressed: 'blue', disabled: 'red' }`.
|
|
8232
|
+
* C_h = disabled, C_l = hovered, C_m = pressed. `pressed & hovered &
|
|
8233
|
+
* !disabled` is satisfiable (three independent modifiers), so the
|
|
8234
|
+
* intermediate `pressed` would lose to a merged red rule when both
|
|
8235
|
+
* `pressed` and `hovered` are active — breaking the cascade
|
|
8236
|
+
* `disabled > pressed > hovered`.
|
|
8237
|
+
*
|
|
8238
|
+
* Example (SAFE — still merges):
|
|
8239
|
+
* `{ '': light, '@dark': dark, '@hc': hc, '@dark & @hc': dark }`.
|
|
8240
|
+
* C_h = `@dark & @hc`, C_l = `@dark`, C_m = `@hc`.
|
|
8241
|
+
* `@hc & @dark & !(@dark & @hc) = @hc & @dark & (!@dark | !@hc)`
|
|
8242
|
+
* simplifies to FALSE, so merging the two darks into one `@dark` rule
|
|
8243
|
+
* at the higher priority does not affect the `@hc` rule.
|
|
8244
|
+
*
|
|
8245
|
+
* Default (TRUE) entries are never merged with non-default entries.
|
|
8246
|
+
* Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
|
|
8247
|
+
* condition's participation in exclusive building. Stage 6
|
|
8248
|
+
* `mergeByValue` handles combining rules with identical CSS output
|
|
8249
|
+
* after exclusive conditions are correctly built.
|
|
8250
|
+
*
|
|
8251
|
+
* The merged entry keeps the highest priority of the merged entries.
|
|
8252
|
+
*/
|
|
8253
|
+
function mergeEntriesByValue(entries) {
|
|
8254
|
+
if (entries.length <= 1) return entries;
|
|
8255
|
+
const merged = [];
|
|
8256
|
+
for (const entry of entries) {
|
|
8257
|
+
if (entry.condition.kind === "true") {
|
|
8258
|
+
merged.push(entry);
|
|
8259
|
+
continue;
|
|
8260
|
+
}
|
|
8261
|
+
const valueKey = serializeValue(entry.value);
|
|
8262
|
+
let mergeIdx = -1;
|
|
8263
|
+
for (let j = merged.length - 1; j >= 0; j--) {
|
|
8264
|
+
const prev = merged[j];
|
|
8265
|
+
if (prev.condition.kind === "true") continue;
|
|
8266
|
+
if (serializeValue(prev.value) !== valueKey) continue;
|
|
8267
|
+
let safe = true;
|
|
8268
|
+
for (let k = j + 1; k < merged.length; k++) {
|
|
8269
|
+
const inter = merged[k];
|
|
8270
|
+
if (inter.condition.kind === "true") continue;
|
|
8271
|
+
if (serializeValue(inter.value) === valueKey) continue;
|
|
8272
|
+
if (simplifyCondition(and(inter.condition, entry.condition, not(prev.condition))).kind !== "false") {
|
|
8273
|
+
safe = false;
|
|
8274
|
+
break;
|
|
8275
|
+
}
|
|
8276
|
+
}
|
|
8277
|
+
if (safe) {
|
|
8278
|
+
mergeIdx = j;
|
|
8279
|
+
break;
|
|
8280
|
+
}
|
|
8281
|
+
}
|
|
8282
|
+
if (mergeIdx >= 0) {
|
|
8283
|
+
const prev = merged[mergeIdx];
|
|
8284
|
+
const newCondition = simplifyCondition(or(prev.condition, entry.condition));
|
|
8285
|
+
merged[mergeIdx] = {
|
|
8286
|
+
styleKey: prev.styleKey,
|
|
8287
|
+
stateKey: `${prev.stateKey} | ${entry.stateKey}`,
|
|
8288
|
+
value: prev.value,
|
|
8289
|
+
condition: newCondition,
|
|
8290
|
+
priority: Math.max(prev.priority, entry.priority)
|
|
8291
|
+
};
|
|
8292
|
+
} else merged.push(entry);
|
|
8293
|
+
}
|
|
8294
|
+
return merged;
|
|
8295
|
+
}
|
|
8296
|
+
function serializeValue(value) {
|
|
8297
|
+
if (value === null || value === void 0) return "null";
|
|
8298
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
8299
|
+
return JSON.stringify(value);
|
|
8300
|
+
}
|
|
8301
|
+
/**
|
|
8302
|
+
* Eliminate redundant state dimensions from a value map.
|
|
8303
|
+
*
|
|
8304
|
+
* When a value map contains compound AND state keys (e.g. `@dark & @hc`),
|
|
8305
|
+
* checks whether any state atom is a "don't-care" variable — i.e. the
|
|
8306
|
+
* value is the same whether that atom is present or absent. Redundant
|
|
8307
|
+
* atoms are removed from all keys and duplicate entries are collapsed.
|
|
8308
|
+
*
|
|
8309
|
+
* This runs **before** condition parsing so that downstream stages
|
|
8310
|
+
* (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
|
|
8311
|
+
* never see the irrelevant dimension, producing simpler, smaller CSS.
|
|
8312
|
+
*
|
|
8313
|
+
* Only pure top-level AND combinations are eligible. Keys that contain
|
|
8314
|
+
* `|`, `^`, or `,` at the top level are treated as opaque single atoms.
|
|
8315
|
+
*
|
|
8316
|
+
* @example
|
|
8317
|
+
* { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
|
|
8318
|
+
* // @hc is redundant → { '': A, '@dark': B }
|
|
8319
|
+
*/
|
|
8320
|
+
function extractCompoundStates(valueMap) {
|
|
8321
|
+
const keys = Object.keys(valueMap);
|
|
8322
|
+
if (keys.length < 3 || !keys.some((k) => k.includes("&"))) return valueMap;
|
|
8323
|
+
const entries = keys.map((key) => {
|
|
8324
|
+
return {
|
|
8325
|
+
atoms: splitTopLevelAnd(key) ?? [key],
|
|
8326
|
+
value: valueMap[key]
|
|
8327
|
+
};
|
|
8328
|
+
});
|
|
8329
|
+
const allAtoms = /* @__PURE__ */ new Set();
|
|
8330
|
+
for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
|
|
8331
|
+
const redundant = /* @__PURE__ */ new Set();
|
|
8332
|
+
for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
|
|
8333
|
+
if (redundant.size === 0) return valueMap;
|
|
8334
|
+
const newMap = {};
|
|
8335
|
+
for (const e of entries) {
|
|
8336
|
+
const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
|
|
8337
|
+
if (!(newKey in newMap)) newMap[newKey] = e.value;
|
|
8338
|
+
}
|
|
8339
|
+
return newMap;
|
|
8340
|
+
}
|
|
8341
|
+
/**
|
|
8342
|
+
* Split a state key by top-level `&` operators.
|
|
8343
|
+
*
|
|
8344
|
+
* Returns `null` if the key contains `|`, `^`, or `,` at the top level
|
|
8345
|
+
* (making it ineligible for atom-level extraction).
|
|
8346
|
+
* Returns `[]` for the empty string (default key).
|
|
8347
|
+
*/
|
|
8348
|
+
function splitTopLevelAnd(key) {
|
|
8349
|
+
if (key === "") return [];
|
|
8350
|
+
const parts = [];
|
|
8351
|
+
let depth = 0;
|
|
8352
|
+
let current = "";
|
|
8353
|
+
for (const ch of key) {
|
|
8354
|
+
if (ch === "(" || ch === "[") depth++;
|
|
8355
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
8356
|
+
if (depth === 0) {
|
|
8357
|
+
if (ch === "&") {
|
|
8358
|
+
const trimmed = current.trim();
|
|
8359
|
+
if (trimmed) parts.push(trimmed);
|
|
8360
|
+
current = "";
|
|
8361
|
+
continue;
|
|
8362
|
+
}
|
|
8363
|
+
if (ch === "|" || ch === "^" || ch === ",") return null;
|
|
8364
|
+
}
|
|
8365
|
+
current += ch;
|
|
8366
|
+
}
|
|
8367
|
+
const trimmed = current.trim();
|
|
8368
|
+
if (trimmed) parts.push(trimmed);
|
|
8369
|
+
return parts;
|
|
8370
|
+
}
|
|
8371
|
+
/**
|
|
8372
|
+
* An atom is redundant when every entry that contains it has a matching
|
|
8373
|
+
* partner (same remaining atoms, atom absent) with the same value.
|
|
8374
|
+
*/
|
|
8375
|
+
function isAtomRedundant(entries, atom) {
|
|
8376
|
+
const withAtom = entries.filter((e) => e.atoms.includes(atom));
|
|
8377
|
+
if (withAtom.length === 0) return false;
|
|
8378
|
+
for (const wa of withAtom) {
|
|
8379
|
+
const remaining = wa.atoms.filter((a) => a !== atom);
|
|
8380
|
+
const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
|
|
8381
|
+
if (!pair) return false;
|
|
8382
|
+
if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
|
|
8383
|
+
}
|
|
8384
|
+
return true;
|
|
8385
|
+
}
|
|
8386
|
+
/**
|
|
8387
|
+
* Check if a value is a style value mapping (object with state keys)
|
|
8388
|
+
*/
|
|
8389
|
+
function isValueMapping(value) {
|
|
8390
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
8391
|
+
}
|
|
8392
|
+
/**
|
|
8393
|
+
* Expand OR conditions in parsed entries into multiple exclusive entries.
|
|
8394
|
+
*
|
|
8395
|
+
* For an entry with condition `A | B | C`, this creates 3 entries:
|
|
8396
|
+
* - condition: A
|
|
8397
|
+
* - condition: B & !A
|
|
8398
|
+
* - condition: C & !A & !B
|
|
8399
|
+
*
|
|
8400
|
+
* This ensures OR branches are mutually exclusive BEFORE the main
|
|
8401
|
+
* exclusive condition building pass.
|
|
8402
|
+
*
|
|
8403
|
+
* @param entries Parsed entries (may contain OR conditions)
|
|
8404
|
+
* @returns Expanded entries with OR branches made exclusive
|
|
8405
|
+
*/
|
|
8406
|
+
function expandOrConditions(entries) {
|
|
8407
|
+
const result = [];
|
|
8408
|
+
for (const entry of entries) {
|
|
8409
|
+
const expanded = expandSingleEntry(entry);
|
|
8410
|
+
result.push(...expanded);
|
|
8411
|
+
}
|
|
8412
|
+
return result;
|
|
8413
|
+
}
|
|
8414
|
+
/**
|
|
8415
|
+
* Expand a single entry's OR condition into multiple exclusive entries.
|
|
8416
|
+
*
|
|
8417
|
+
* Note: branches are NOT sorted by at-rule context here (unlike the
|
|
8418
|
+
* `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
|
|
8419
|
+
* the product of De Morgan negation, so each branch is expected to render
|
|
8420
|
+
* independently in its own scope and at-rule sort isn't load-bearing.
|
|
8421
|
+
* The post-build pass needs the sort because it has to preserve at-rule
|
|
8422
|
+
* wrapping across branches that came from negating a compound at-rule.
|
|
8423
|
+
*
|
|
8424
|
+
* Skip optimisation: when every branch renders into the same at-rule /
|
|
8425
|
+
* root / parent / own context (see "Key Design Decision #2" in
|
|
8426
|
+
* `docs/pipeline.md`), forcing mutual exclusivity here produces dead
|
|
8427
|
+
* `B & !A`-style branches that materialization later folds back into
|
|
8428
|
+
* `:is(A, B)`. Bail out and let `materialize.ts` collapse the OR via
|
|
8429
|
+
* `mergeVariantsIntoSelectorGroups`. Cross-entry exclusivity is still
|
|
8430
|
+
* enforced by `buildExclusiveConditions`; the post-build `expandExclusiveOrs`
|
|
8431
|
+
* pass still handles De Morgan ORs whose branches actually differ in
|
|
8432
|
+
* context.
|
|
8123
8433
|
*/
|
|
8124
|
-
function
|
|
8125
|
-
const
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8434
|
+
function expandSingleEntry(entry) {
|
|
8435
|
+
const orBranches = collectOrBranches(entry.condition);
|
|
8436
|
+
if (orBranches.length <= 1) return [entry];
|
|
8437
|
+
if (!branchesProduceDifferentContexts(orBranches)) return [entry];
|
|
8438
|
+
const result = [];
|
|
8439
|
+
const priorBranches = [];
|
|
8440
|
+
for (let i = 0; i < orBranches.length; i++) {
|
|
8441
|
+
const branch = orBranches[i];
|
|
8442
|
+
let exclusiveBranch = branch;
|
|
8443
|
+
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
8444
|
+
const simplified = simplifyCondition(exclusiveBranch);
|
|
8445
|
+
if (simplified.kind === "false") {
|
|
8446
|
+
priorBranches.push(branch);
|
|
8447
|
+
continue;
|
|
8448
|
+
}
|
|
8449
|
+
result.push({
|
|
8450
|
+
...entry,
|
|
8451
|
+
stateKey: `${entry.stateKey}[${i}]`,
|
|
8452
|
+
condition: simplified
|
|
8453
|
+
});
|
|
8454
|
+
priorBranches.push(branch);
|
|
8130
8455
|
}
|
|
8131
|
-
|
|
8132
|
-
variants: [],
|
|
8133
|
-
isImpossible: true
|
|
8134
|
-
};
|
|
8135
|
-
return {
|
|
8136
|
-
variants: dedupeVariants(allVariants),
|
|
8137
|
-
isImpossible: false
|
|
8138
|
-
};
|
|
8456
|
+
return result;
|
|
8139
8457
|
}
|
|
8140
8458
|
/**
|
|
8141
|
-
*
|
|
8459
|
+
* Collect top-level OR branches from a condition.
|
|
8460
|
+
*
|
|
8461
|
+
* For `A | B | C`, returns [A, B, C]
|
|
8462
|
+
* For `A & B`, returns [A & B] (single branch)
|
|
8463
|
+
* For `A | (B & C)`, returns [A, B & C]
|
|
8142
8464
|
*/
|
|
8143
|
-
function
|
|
8144
|
-
if (
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
const
|
|
8148
|
-
|
|
8465
|
+
function collectOrBranches(condition) {
|
|
8466
|
+
if (condition.kind === "true" || condition.kind === "false") return [condition];
|
|
8467
|
+
if (isCompoundCondition(condition) && condition.operator === "OR") {
|
|
8468
|
+
const branches = [];
|
|
8469
|
+
for (const child of condition.children) branches.push(...collectOrBranches(child));
|
|
8470
|
+
return branches;
|
|
8149
8471
|
}
|
|
8150
|
-
return
|
|
8472
|
+
return [condition];
|
|
8151
8473
|
}
|
|
8152
8474
|
/**
|
|
8153
|
-
*
|
|
8154
|
-
* own, starting) into a single variant with a SelectorGroup.
|
|
8475
|
+
* Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
|
|
8155
8476
|
*
|
|
8156
|
-
*
|
|
8157
|
-
*
|
|
8477
|
+
* This handles ORs that arise from De Morgan expansion during negation:
|
|
8478
|
+
* !(A & B) = !A | !B
|
|
8479
|
+
*
|
|
8480
|
+
* These ORs need to be made exclusive to avoid overlapping CSS rules:
|
|
8481
|
+
* !A | !B → !A | (A & !B)
|
|
8482
|
+
*
|
|
8483
|
+
* This is logically equivalent but ensures each branch has proper context.
|
|
8484
|
+
*
|
|
8485
|
+
* Example:
|
|
8486
|
+
* Input: { "": V1, "@supports(...) & :has()": V2 }
|
|
8487
|
+
* V2's exclusive = @supports & :has
|
|
8488
|
+
* V1's exclusive = !(@supports & :has) = !@supports | !:has
|
|
8489
|
+
*
|
|
8490
|
+
* Without this fix: V1 gets two rules:
|
|
8491
|
+
* - @supports (not ...) → V1 ✓
|
|
8492
|
+
* - :not(:has()) → V1 ✗ (missing @supports context!)
|
|
8493
|
+
*
|
|
8494
|
+
* With this fix: V1 gets two exclusive rules:
|
|
8495
|
+
* - @supports (not ...) → V1 ✓
|
|
8496
|
+
* - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
|
|
8158
8497
|
*/
|
|
8159
|
-
function
|
|
8160
|
-
if (variants.length <= 1) return variants;
|
|
8161
|
-
const groups = /* @__PURE__ */ new Map();
|
|
8162
|
-
for (const v of variants) {
|
|
8163
|
-
const key = getVariantContextKey(v);
|
|
8164
|
-
const group = groups.get(key);
|
|
8165
|
-
if (group) group.push(v);
|
|
8166
|
-
else groups.set(key, [v]);
|
|
8167
|
-
}
|
|
8498
|
+
function expandExclusiveOrs(entries) {
|
|
8168
8499
|
const result = [];
|
|
8169
|
-
for (const
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
continue;
|
|
8173
|
-
}
|
|
8174
|
-
const withSelectors = [];
|
|
8175
|
-
const withoutSelectors = [];
|
|
8176
|
-
for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
|
|
8177
|
-
else withSelectors.push(v);
|
|
8178
|
-
result.push(...withoutSelectors);
|
|
8179
|
-
if (withSelectors.length <= 1) {
|
|
8180
|
-
result.push(...withSelectors);
|
|
8181
|
-
continue;
|
|
8182
|
-
}
|
|
8183
|
-
result.push(factorAndGroup(withSelectors));
|
|
8500
|
+
for (const entry of entries) {
|
|
8501
|
+
const expanded = expandExclusiveConditionOrs(entry);
|
|
8502
|
+
result.push(...expanded);
|
|
8184
8503
|
}
|
|
8185
8504
|
return result;
|
|
8186
8505
|
}
|
|
8187
8506
|
/**
|
|
8188
|
-
*
|
|
8189
|
-
|
|
8190
|
-
|
|
8507
|
+
* Check if a condition involves at-rules (media, container, supports, starting)
|
|
8508
|
+
*/
|
|
8509
|
+
function hasAtRuleContext(node) {
|
|
8510
|
+
if (node.kind === "true" || node.kind === "false") return false;
|
|
8511
|
+
if (node.kind === "state") return node.type === "media" || node.type === "container" || node.type === "supports" || node.type === "starting";
|
|
8512
|
+
if (node.kind === "compound") return node.children.some(hasAtRuleContext);
|
|
8513
|
+
return false;
|
|
8514
|
+
}
|
|
8515
|
+
/**
|
|
8516
|
+
* Sort OR branches to prioritize at-rule conditions first.
|
|
8191
8517
|
*
|
|
8192
|
-
*
|
|
8193
|
-
*
|
|
8518
|
+
* This is critical for correct CSS generation. For `!A | !B` where A is at-rule
|
|
8519
|
+
* and B is modifier, we want:
|
|
8520
|
+
* - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
|
|
8521
|
+
* - Branch 1: A & !B (modifier negation with at-rule context)
|
|
8522
|
+
*
|
|
8523
|
+
* If we process in wrong order (!B first), we'd get:
|
|
8524
|
+
* - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
|
|
8525
|
+
* - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
|
|
8194
8526
|
*/
|
|
8195
|
-
function
|
|
8196
|
-
{
|
|
8197
|
-
const
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
}
|
|
8203
|
-
const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
|
|
8204
|
-
const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
|
|
8205
|
-
const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
|
|
8206
|
-
const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
|
|
8207
|
-
const branches = [];
|
|
8208
|
-
let hasEmptyBranch = false;
|
|
8209
|
-
for (const v of variants) {
|
|
8210
|
-
const branch = [];
|
|
8211
|
-
for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
|
|
8212
|
-
for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
|
|
8213
|
-
if (branch.length > 0) branches.push(branch);
|
|
8214
|
-
else hasEmptyBranch = true;
|
|
8215
|
-
}
|
|
8216
|
-
if (hasEmptyBranch) return {
|
|
8217
|
-
...variants[0],
|
|
8218
|
-
modifierConditions: commonModifiers,
|
|
8219
|
-
pseudoConditions: commonPseudos
|
|
8220
|
-
};
|
|
8221
|
-
return {
|
|
8222
|
-
modifierConditions: commonModifiers,
|
|
8223
|
-
pseudoConditions: commonPseudos,
|
|
8224
|
-
selectorGroups: [...variants[0].selectorGroups, {
|
|
8225
|
-
branches,
|
|
8226
|
-
negated: false
|
|
8227
|
-
}],
|
|
8228
|
-
ownGroups: [...variants[0].ownGroups],
|
|
8229
|
-
mediaConditions: [...variants[0].mediaConditions],
|
|
8230
|
-
containerConditions: [...variants[0].containerConditions],
|
|
8231
|
-
supportsConditions: [...variants[0].supportsConditions],
|
|
8232
|
-
rootGroups: [...variants[0].rootGroups],
|
|
8233
|
-
parentGroups: [...variants[0].parentGroups],
|
|
8234
|
-
startingStyle: variants[0].startingStyle
|
|
8235
|
-
};
|
|
8527
|
+
function sortOrBranchesForExpansion(branches) {
|
|
8528
|
+
return [...branches].sort((a, b) => {
|
|
8529
|
+
const aHasAtRule = hasAtRuleContext(a);
|
|
8530
|
+
const bHasAtRule = hasAtRuleContext(b);
|
|
8531
|
+
if (aHasAtRule && !bHasAtRule) return -1;
|
|
8532
|
+
if (!aHasAtRule && bHasAtRule) return 1;
|
|
8533
|
+
return 0;
|
|
8534
|
+
});
|
|
8236
8535
|
}
|
|
8237
8536
|
/**
|
|
8238
|
-
*
|
|
8537
|
+
* Expand ORs in a single entry's exclusive condition
|
|
8239
8538
|
*/
|
|
8240
|
-
function
|
|
8241
|
-
|
|
8242
|
-
if (
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
const
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
}
|
|
8256
|
-
for (const [name, conditions] of byName) {
|
|
8257
|
-
const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
|
|
8258
|
-
const namePrefix = name ? `${name} ` : "";
|
|
8259
|
-
atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
|
|
8539
|
+
function expandExclusiveConditionOrs(entry) {
|
|
8540
|
+
let orBranches = collectOrBranches(entry.exclusiveCondition);
|
|
8541
|
+
if (orBranches.length <= 1) return [entry];
|
|
8542
|
+
if (!branchesProduceDifferentContexts(orBranches)) return [entry];
|
|
8543
|
+
orBranches = sortOrBranchesForExpansion(orBranches);
|
|
8544
|
+
const result = [];
|
|
8545
|
+
const priorBranches = [];
|
|
8546
|
+
for (let i = 0; i < orBranches.length; i++) {
|
|
8547
|
+
const branch = orBranches[i];
|
|
8548
|
+
let exclusiveBranch = branch;
|
|
8549
|
+
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
8550
|
+
const simplified = simplifyCondition(exclusiveBranch);
|
|
8551
|
+
if (simplified.kind === "false") {
|
|
8552
|
+
priorBranches.push(branch);
|
|
8553
|
+
continue;
|
|
8260
8554
|
}
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
const selectorCond = `selector(${c.condition})`;
|
|
8266
|
-
return c.negated ? `(not ${selectorCond})` : selectorCond;
|
|
8267
|
-
} else {
|
|
8268
|
-
const featureCond = `(${c.condition})`;
|
|
8269
|
-
return c.negated ? `(not ${featureCond})` : featureCond;
|
|
8270
|
-
}
|
|
8555
|
+
result.push({
|
|
8556
|
+
...entry,
|
|
8557
|
+
stateKey: `${entry.stateKey}[or:${i}]`,
|
|
8558
|
+
exclusiveCondition: simplified
|
|
8271
8559
|
});
|
|
8272
|
-
|
|
8560
|
+
priorBranches.push(branch);
|
|
8273
8561
|
}
|
|
8274
|
-
return
|
|
8562
|
+
return result;
|
|
8275
8563
|
}
|
|
8276
8564
|
//#endregion
|
|
8277
8565
|
//#region src/utils/case-converter.ts
|
|
@@ -8338,6 +8626,15 @@ function emitWarning(code, message) {
|
|
|
8338
8626
|
const MAX_XOR_CHAIN_LENGTH = 4;
|
|
8339
8627
|
const parseCache = new Lru(5e3);
|
|
8340
8628
|
/**
|
|
8629
|
+
* Chrome-internal pseudo-classes (e.g. `:-internal-autofill-selected`,
|
|
8630
|
+
* `:-internal-autofill-previewed`) cannot be targeted from user CSS and
|
|
8631
|
+
* may invalidate the surrounding rule in Safari even when wrapped in
|
|
8632
|
+
* forgiving `:is(...)`. The regex matches both bare uses and references
|
|
8633
|
+
* inside enhanced pseudo arguments like `:is(:-webkit-autofill,
|
|
8634
|
+
* :-internal-autofill-selected)`.
|
|
8635
|
+
*/
|
|
8636
|
+
const INTERNAL_PSEUDO_PATTERN = /:-internal-[a-z0-9-]+/g;
|
|
8637
|
+
/**
|
|
8341
8638
|
* Pattern for tokenizing state notation.
|
|
8342
8639
|
* Matches: operators, parentheses, @-prefixed states, value mods, boolean mods,
|
|
8343
8640
|
* pseudo-classes, class selectors, and attribute selectors.
|
|
@@ -8755,6 +9052,14 @@ function parseStateKey(stateKey, options = {}) {
|
|
|
8755
9052
|
const cacheKey = trimmed + "\0" + (options.isSubElement ? "1" : "0") + "\0" + localStatesKey;
|
|
8756
9053
|
const cached = parseCache.get(cacheKey);
|
|
8757
9054
|
if (cached) return cached;
|
|
9055
|
+
if (isDevEnv()) {
|
|
9056
|
+
INTERNAL_PSEUDO_PATTERN.lastIndex = 0;
|
|
9057
|
+
const internalMatches = trimmed.match(INTERNAL_PSEUDO_PATTERN);
|
|
9058
|
+
if (internalMatches && internalMatches.length > 0) {
|
|
9059
|
+
const unique = Array.from(new Set(internalMatches));
|
|
9060
|
+
emitWarning("INTERNAL_PSEUDO_USED", `State key "${trimmed}" references internal pseudo-class${unique.length > 1 ? "es" : ""} ${unique.map((p) => `\`${p}\``).join(", ")}. These are unmatchable from user CSS and can invalidate the surrounding rule in Safari (even inside \`:is(...)\`). Use \`:-webkit-autofill | :autofill\` instead for autofill states.`);
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
8758
9063
|
const result = new Parser(tokenize(trimmed), options).parse();
|
|
8759
9064
|
parseCache.set(cacheKey, result);
|
|
8760
9065
|
return result;
|
|
@@ -8824,7 +9129,15 @@ function runPipeline(styles, parserContext) {
|
|
|
8824
9129
|
function processStyles(styles, selectorSuffix, parserContext, allRules) {
|
|
8825
9130
|
const keys = Object.keys(styles);
|
|
8826
9131
|
const selectorKeys = keys.filter((key) => isSelector(key));
|
|
8827
|
-
const styleKeys =
|
|
9132
|
+
const styleKeys = [];
|
|
9133
|
+
for (const key of keys) {
|
|
9134
|
+
if (isSelector(key) || key.startsWith("@")) continue;
|
|
9135
|
+
if (key.startsWith(":")) {
|
|
9136
|
+
emitWarning("INVALID_TOP_LEVEL_PSEUDO_KEY", `Style key "${key}" starts with ':' which is not a valid Tasty style key. Use "&${key}" for nested-selector form, or move the state into a value map (e.g. \`{ color: { '${key}': value } }\`). The key has been ignored.`);
|
|
9137
|
+
continue;
|
|
9138
|
+
}
|
|
9139
|
+
styleKeys.push(key);
|
|
9140
|
+
}
|
|
8828
9141
|
processNestedSelectors(styles, selectorKeys, selectorSuffix, parserContext, allRules);
|
|
8829
9142
|
processHandlerQueue(buildHandlerQueue(styleKeys, styles), selectorSuffix, parserContext, allRules);
|
|
8830
9143
|
}
|
|
@@ -9106,14 +9419,15 @@ function validatePattern(pattern) {
|
|
|
9106
9419
|
* processSinglePattern('>Body>Row>', 'Cell')
|
|
9107
9420
|
* // → '> [data-element="Body"] > [data-element="Row"] > [data-element="Cell"]'
|
|
9108
9421
|
*
|
|
9109
|
-
* processSinglePattern('
|
|
9110
|
-
* // → '::before' (
|
|
9422
|
+
* processSinglePattern('&::before', 'Before')
|
|
9423
|
+
* // → '::before' (& attaches pseudo directly to root, no key injection)
|
|
9111
9424
|
*
|
|
9112
9425
|
* processSinglePattern('>@:hover', 'Item')
|
|
9113
9426
|
* // → '> [data-element="Item"]:hover'
|
|
9114
9427
|
*/
|
|
9115
9428
|
function processSinglePattern(pattern, key) {
|
|
9116
|
-
const
|
|
9429
|
+
const startsWithAmpersand = pattern.startsWith("&");
|
|
9430
|
+
const normalized = (startsWithAmpersand ? pattern.slice(1) : pattern).trim();
|
|
9117
9431
|
if (!normalized) return ` [data-element="${key}"]`;
|
|
9118
9432
|
const startsWithPseudo = /^::?[a-z]/.test(normalized);
|
|
9119
9433
|
let result = transformPattern(normalized);
|
|
@@ -9123,8 +9437,8 @@ function processSinglePattern(pattern, key) {
|
|
|
9123
9437
|
if (!startsWithPseudo && !result.startsWith(" ")) result = " " + result;
|
|
9124
9438
|
return result;
|
|
9125
9439
|
}
|
|
9126
|
-
if (shouldInjectKey(normalized)) result = result + ` [data-element="${key}"]`;
|
|
9127
|
-
if (!
|
|
9440
|
+
if (shouldInjectKey(normalized, key)) result = result + ` [data-element="${key}"]`;
|
|
9441
|
+
if (!startsWithAmpersand && !result.startsWith(" ")) result = " " + result;
|
|
9128
9442
|
return result;
|
|
9129
9443
|
}
|
|
9130
9444
|
/**
|
|
@@ -9262,22 +9576,28 @@ function transformPattern(pattern) {
|
|
|
9262
9576
|
* | Attribute ([type]) | No | `'[type="text"]'` | `[type="text"]` |
|
|
9263
9577
|
*
|
|
9264
9578
|
* @param pattern - The normalized pattern (after stripping &)
|
|
9579
|
+
* @param key - The sub-element key being styled. If the trailing element name
|
|
9580
|
+
* equals this key, it acts as an explicit placeholder for the key
|
|
9581
|
+
* (same role as `@`) and no auto-injection happens.
|
|
9265
9582
|
* @returns true if key should be injected, false otherwise
|
|
9266
9583
|
*
|
|
9267
9584
|
* @example
|
|
9268
|
-
* shouldInjectKey('>')
|
|
9269
|
-
* shouldInjectKey('>Body>Row')
|
|
9270
|
-
* shouldInjectKey('
|
|
9271
|
-
* shouldInjectKey('
|
|
9272
|
-
* shouldInjectKey('
|
|
9273
|
-
* shouldInjectKey('
|
|
9274
|
-
* shouldInjectKey('
|
|
9275
|
-
* shouldInjectKey('
|
|
9276
|
-
|
|
9277
|
-
|
|
9585
|
+
* shouldInjectKey('>', 'Key') // → true (trailing combinator)
|
|
9586
|
+
* shouldInjectKey('>Body>Row', 'Key') // → true (ends with different element)
|
|
9587
|
+
* shouldInjectKey('>Body>Key', 'Key') // → false (trailing name === key)
|
|
9588
|
+
* shouldInjectKey('Key', 'Key') // → false (sole element === key)
|
|
9589
|
+
* shouldInjectKey('h1', 'Key') // → false (ends with tag)
|
|
9590
|
+
* shouldInjectKey('*', 'Key') // → false (universal selector)
|
|
9591
|
+
* shouldInjectKey('::before', 'Key') // → false (ends with pseudo)
|
|
9592
|
+
* shouldInjectKey('.active', 'Key') // → false (ends with class)
|
|
9593
|
+
* shouldInjectKey('a:hover', 'Key') // → false (ends with pseudo)
|
|
9594
|
+
* shouldInjectKey('button.primary', 'Key') // → false (ends with class)
|
|
9595
|
+
*/
|
|
9596
|
+
function shouldInjectKey(pattern, key) {
|
|
9278
9597
|
const trimmed = pattern.trim();
|
|
9279
9598
|
if (/[>+~]$/.test(trimmed)) return true;
|
|
9280
|
-
|
|
9599
|
+
const trailingElement = trimmed.match(/(?:^|[\s>+~\]:])([A-Z][a-zA-Z0-9]*)$/);
|
|
9600
|
+
if (trailingElement) return trailingElement[1] !== key;
|
|
9281
9601
|
return false;
|
|
9282
9602
|
}
|
|
9283
9603
|
/**
|
|
@@ -9683,7 +10003,10 @@ function isTestEnvironment() {
|
|
|
9683
10003
|
const g = global;
|
|
9684
10004
|
if (g.vi || g.jest || g.expect || g.describe || g.it) return true;
|
|
9685
10005
|
}
|
|
9686
|
-
if (typeof window !== "undefined"
|
|
10006
|
+
if (typeof window !== "undefined") {
|
|
10007
|
+
const ua = window.navigator?.userAgent;
|
|
10008
|
+
if (ua?.includes("jsdom") || ua?.includes("HappyDOM")) return true;
|
|
10009
|
+
}
|
|
9687
10010
|
if (typeof globalThis !== "undefined") {
|
|
9688
10011
|
const gt = globalThis;
|
|
9689
10012
|
if (gt.vitest || gt.mocha) return true;
|
|
@@ -9697,7 +10020,8 @@ function createDefaultConfig(isTest) {
|
|
|
9697
10020
|
return {
|
|
9698
10021
|
maxRulesPerSheet: 8192,
|
|
9699
10022
|
forceTextInjection: isTest ?? false,
|
|
9700
|
-
devMode: isDevEnv()
|
|
10023
|
+
devMode: isDevEnv(),
|
|
10024
|
+
namePrefix: "t"
|
|
9701
10025
|
};
|
|
9702
10026
|
}
|
|
9703
10027
|
/**
|
|
@@ -9939,6 +10263,7 @@ function configure(config = {}) {
|
|
|
9939
10263
|
warnOnce("configure-after-styles", "[Tasty] Cannot call configure() after styles have been generated.\nConfiguration must be done before the first render. The configuration will be ignored.");
|
|
9940
10264
|
return;
|
|
9941
10265
|
}
|
|
10266
|
+
if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
|
|
9942
10267
|
let mergedStates = {};
|
|
9943
10268
|
let mergedUnits = {};
|
|
9944
10269
|
let mergedFuncs = {};
|
|
@@ -10090,6 +10415,17 @@ function getConfig() {
|
|
|
10090
10415
|
return currentConfig;
|
|
10091
10416
|
}
|
|
10092
10417
|
/**
|
|
10418
|
+
* Get the configured prefix used for every generated identifier
|
|
10419
|
+
* (class names, keyframe names, counter-style names).
|
|
10420
|
+
*
|
|
10421
|
+
* Falls back to the default prefix (`'t'`) when `configure()` has not
|
|
10422
|
+
* been called yet — this matches the auto-configuration behavior used
|
|
10423
|
+
* by the rest of the system.
|
|
10424
|
+
*/
|
|
10425
|
+
function getNamePrefix() {
|
|
10426
|
+
return currentConfig?.namePrefix ?? "t";
|
|
10427
|
+
}
|
|
10428
|
+
/**
|
|
10093
10429
|
* Get the global injector instance.
|
|
10094
10430
|
* Auto-configures with defaults if not already configured.
|
|
10095
10431
|
*/
|
|
@@ -10123,6 +10459,6 @@ function resetConfig() {
|
|
|
10123
10459
|
delete storage[GLOBAL_INJECTOR_KEY];
|
|
10124
10460
|
}
|
|
10125
10461
|
//#endregion
|
|
10126
|
-
export {
|
|
10462
|
+
export { parseColor as $, StyleInjector as A, strToRgb as At, styleHandlers as B, parseStateKey as C, getColorSpaceFunc as Ct, extractPredefinedStateRefs as D, getRgbValuesFromRgbaString as Dt, extractLocalPredefinedStates as E, getNamedColorHex as Et, fontFaceContentHash as F, CUSTOM_UNITS as G, warn as H, formatFontFaceRule as I, filterMods as J, DIRECTIONS as K, hasLocalFontFace as L, formatCounterStyleRule as M, hasLocalCounterStyle as N, getGlobalPredefinedStates as O, hexToRgb as Ot, extractLocalFontFace as P, normalizeColorTokenValue as Q, SheetManager as R, renderStyles as S, getColorSpaceComponents as St, createStateParserContext as T, getComponentPropertySyntax as Tt, createStyle as U, deprecationWarning as V, PropertyTypeResolver as W, getGlobalParser as X, getGlobalFuncs as Y, getGlobalPredefinedTokens as Z, markStylesGenerated as _, extractLocalProperties as _t, getGlobalCounterStyle as a, okhslPlugin as at, hasPipelineCacheEntry as b, parsePropertyToken as bt, getGlobalKeyframes as c, DEFAULT_NAME_PREFIX as ct, getNamePrefix as d, makeCounterStyleName as dt, parseStyle as et, hasGlobalKeyframes as f, makeKeyframeName as ft, isTestEnvironment as g, hashString as gt, isConfigLocked as h, isDevEnv as ht, getGlobalConfigTokens as i, okhslFunc as it, extractLocalCounterStyle as j, Lru as jt, setGlobalPredefinedStates as k, hslToRgbValues as kt, getGlobalRecipes as l, DEFAULT_ZERO_NAME_PREFIX as lt, hasStylesGenerated as m, validateNamePrefix as mt, getConfig as n, setGlobalPredefinedTokens as nt, getGlobalFontFace as o, StyleParser as ot, hasGlobalRecipes as p, tastyClassRegex as pt, customFunc as q, getEffectiveProperties as r, stringifyStyles as rt, getGlobalInjector as s, Bucket as st, configure as t, resetGlobalPredefinedTokens as tt, getGlobalStyles as u, makeClassName as ut, resetConfig as v, getEffectiveDefinition as vt, camelToKebab as w, getColorSpaceSuffix as wt, isSelector as x, colorInitialValueToComponents as xt, generateTypographyTokens as y, hasLocalProperties as yt, STYLE_HANDLER_MAP as z };
|
|
10127
10463
|
|
|
10128
|
-
//# sourceMappingURL=config-
|
|
10464
|
+
//# sourceMappingURL=config-BaxtQMS3.js.map
|