@tenphi/tasty 0.0.0-snapshot.167ce4e → 0.0.0-snapshot.17c4f74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/async-storage-B7_o6FKt.js.map +1 -1
- package/dist/{collector-WUJKpM4q.js → collector-C-keQH9m.js} +24 -11
- package/dist/collector-C-keQH9m.js.map +1 -0
- package/dist/{collector-LuU1vZ68.d.ts → collector-osfWTeRd.d.ts} +12 -2
- package/dist/{config-raGoEeGs.js → config-BBiyxMCe.js} +1588 -1256
- package/dist/config-BBiyxMCe.js.map +1 -0
- package/dist/{config-vuCRkBWX.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-CrPLif0D.js → core-BO4319td.js} +18 -12
- package/dist/core-BO4319td.js.map +1 -0
- package/dist/{css-writer-BaR8ywQm.js → css-writer-BWvwQzz0.js} +28 -6
- package/dist/css-writer-BWvwQzz0.js.map +1 -0
- package/dist/format-global-rules-Dbc_1tc3.js.map +1 -1
- package/dist/{format-rules-B_Cuw1ZS.js → format-rules-BSjeH4Z7.js} +2 -2
- package/dist/{format-rules-B_Cuw1ZS.js.map → format-rules-BSjeH4Z7.js.map} +1 -1
- package/dist/{hydrate-DmVyww8Y.js → hydrate-CcvrP4qJ.js} +2 -2
- package/dist/{hydrate-DmVyww8Y.js.map → hydrate-CcvrP4qJ.js.map} +1 -1
- package/dist/{index-ZRxZWzlj.d.ts → index-B_k47mc_.d.ts} +74 -21
- package/dist/{index-dUtwpOux.d.ts → index-tcHuMPFt.d.ts} +22 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +10 -10
- package/dist/index.js.map +1 -1
- package/dist/{keyframes-C9OD_9bX.js → keyframes-BUQhdOSJ.js} +2 -2
- package/dist/{keyframes-C9OD_9bX.js.map → keyframes-BUQhdOSJ.js.map} +1 -1
- package/dist/{merge-styles-CtDJMhpJ.d.ts → merge-styles-BMWcH6MF.d.ts} +2 -2
- package/dist/{merge-styles-B57eQpFZ.js → merge-styles-Cd2vBl9b.js} +2 -2
- package/dist/{merge-styles-B57eQpFZ.js.map → merge-styles-Cd2vBl9b.js.map} +1 -1
- package/dist/{resolve-recipes-J9mdpVSZ.js → resolve-recipes-C1nrvnYh.js} +3 -3
- package/dist/{resolve-recipes-J9mdpVSZ.js.map → resolve-recipes-C1nrvnYh.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.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/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 +8 -8
- package/dist/collector-WUJKpM4q.js.map +0 -1
- package/dist/config-raGoEeGs.js.map +0 -1
- package/dist/core-CrPLif0D.js.map +0 -1
- package/dist/css-writer-BaR8ywQm.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";
|
|
@@ -1788,6 +1876,7 @@ function scan(src, cb) {
|
|
|
1788
1876
|
//#endregion
|
|
1789
1877
|
//#region src/parser/parser.ts
|
|
1790
1878
|
var StyleParser = class {
|
|
1879
|
+
opts;
|
|
1791
1880
|
cache;
|
|
1792
1881
|
constructor(opts = {}) {
|
|
1793
1882
|
this.opts = opts;
|
|
@@ -3234,10 +3323,13 @@ function resolveFontFamily(font, fontFamily) {
|
|
|
3234
3323
|
/**
|
|
3235
3324
|
* Handles typography preset and individual font properties.
|
|
3236
3325
|
*
|
|
3237
|
-
* Preset syntax uses `/` to separate name from
|
|
3326
|
+
* Preset syntax uses `/` to separate the name from one or more
|
|
3327
|
+
* space-separated modifiers:
|
|
3238
3328
|
* - `preset="h1"` — name only
|
|
3239
3329
|
* - `preset="h2 / strong"` — name + modifier
|
|
3330
|
+
* - `preset="h2 / strong italic"` — name + multiple modifiers
|
|
3240
3331
|
* - `preset="bold"` — modifier-only shorthand (name defaults to `inherit`)
|
|
3332
|
+
* - `preset="bold italic"` — modifier-only shorthand with multiple modifiers
|
|
3241
3333
|
*
|
|
3242
3334
|
* When `preset` is defined, it sets up CSS custom properties for typography.
|
|
3243
3335
|
* Individual font props can be used with or without `preset`:
|
|
@@ -3257,14 +3349,17 @@ function presetStyle({ preset, fontSize, lineHeight, textTransform, letterSpacin
|
|
|
3257
3349
|
const { parts } = parseStyle(preset === true ? "" : String(preset)).groups[0] ?? { parts: [] };
|
|
3258
3350
|
const namePart = parts[0];
|
|
3259
3351
|
const modPart = parts[1];
|
|
3352
|
+
const nameTokens = namePart?.all ?? [];
|
|
3353
|
+
const isModOnly = nameTokens.length > 0 && nameTokens.every((t) => PRESET_MODIFIERS.has(t));
|
|
3260
3354
|
const nameToken = namePart?.mods[0] ?? namePart?.values[0] ?? "";
|
|
3261
|
-
const isModOnly = PRESET_MODIFIERS.has(nameToken);
|
|
3262
3355
|
const name = isModOnly ? "inherit" : nameToken || "inherit";
|
|
3263
|
-
const
|
|
3264
|
-
const
|
|
3265
|
-
const
|
|
3266
|
-
const
|
|
3267
|
-
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");
|
|
3268
3363
|
if (fontSize == null) setCSSValue(styles, "font-size", name, { cssOnly: true });
|
|
3269
3364
|
if (lineHeight == null) setCSSValue(styles, "line-height", name, { cssOnly: true });
|
|
3270
3365
|
if (letterSpacing == null) setCSSValue(styles, "letter-spacing", name, { cssOnly: true });
|
|
@@ -4129,7 +4224,7 @@ var SheetManager = class {
|
|
|
4129
4224
|
}
|
|
4130
4225
|
}
|
|
4131
4226
|
if (!anyInserted) {}
|
|
4132
|
-
} 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);
|
|
4133
4228
|
}
|
|
4134
4229
|
} else if (styleElement) {
|
|
4135
4230
|
const atomicRuleIndex = this.findAvailableRuleIndex(targetSheet);
|
|
@@ -4281,6 +4376,37 @@ var SheetManager = class {
|
|
|
4281
4376
|
return sheet.ruleCount;
|
|
4282
4377
|
}
|
|
4283
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
|
+
/**
|
|
4284
4410
|
* Force cleanup of unused styles
|
|
4285
4411
|
*/
|
|
4286
4412
|
forceCleanup(registry) {
|
|
@@ -4801,18 +4927,10 @@ function formatCounterStyleRule(name, descriptors) {
|
|
|
4801
4927
|
//#endregion
|
|
4802
4928
|
//#region src/injector/injector.ts
|
|
4803
4929
|
/**
|
|
4804
|
-
* Generate a deterministic class name from a cache key using content hash.
|
|
4805
|
-
* The same cache key always produces the same class name across environments.
|
|
4806
|
-
*/
|
|
4807
|
-
function generateClassName(cacheKey) {
|
|
4808
|
-
return `t${hashString(cacheKey)}`;
|
|
4809
|
-
}
|
|
4810
|
-
const RSC_CLASS_RE = /\.(t[a-z0-9]+)\.\1/g;
|
|
4811
|
-
/**
|
|
4812
4930
|
* Extract class names from `<style data-tasty-rsc>` tags.
|
|
4813
4931
|
* The doubled-specificity pattern `.tXXX.tXXX` makes extraction reliable.
|
|
4814
4932
|
*/
|
|
4815
|
-
function extractRSCClassNames() {
|
|
4933
|
+
function extractRSCClassNames(rscClassRegex) {
|
|
4816
4934
|
if (typeof document === "undefined") return [];
|
|
4817
4935
|
const styles = document.querySelectorAll("style[data-tasty-rsc]");
|
|
4818
4936
|
if (styles.length === 0) return [];
|
|
@@ -4821,8 +4939,8 @@ function extractRSCClassNames() {
|
|
|
4821
4939
|
const text = style.textContent;
|
|
4822
4940
|
if (!text) continue;
|
|
4823
4941
|
let match;
|
|
4824
|
-
|
|
4825
|
-
while ((match =
|
|
4942
|
+
rscClassRegex.lastIndex = 0;
|
|
4943
|
+
while ((match = rscClassRegex.exec(text)) !== null) classSet.add(match[1]);
|
|
4826
4944
|
}
|
|
4827
4945
|
return Array.from(classSet);
|
|
4828
4946
|
}
|
|
@@ -4836,7 +4954,7 @@ function extractRSCClassNames() {
|
|
|
4836
4954
|
* Called inside `inject()` / `allocateClassName()` to pick up
|
|
4837
4955
|
* class names rendered on the server (including during SPA navigation).
|
|
4838
4956
|
*/
|
|
4839
|
-
function syncServerClasses(registry) {
|
|
4957
|
+
function syncServerClasses(registry, rscClassRegex) {
|
|
4840
4958
|
if (typeof window === "undefined") return;
|
|
4841
4959
|
const classes = window.__TASTY__;
|
|
4842
4960
|
if (classes && classes.length > registry.serverClassSyncIndex) {
|
|
@@ -4845,7 +4963,7 @@ function syncServerClasses(registry) {
|
|
|
4845
4963
|
}
|
|
4846
4964
|
if (!registry.rscStylesScanned) {
|
|
4847
4965
|
registry.rscStylesScanned = true;
|
|
4848
|
-
for (const cls of extractRSCClassNames()) registerHydratedClass(registry, cls);
|
|
4966
|
+
for (const cls of extractRSCClassNames(rscClassRegex)) registerHydratedClass(registry, cls);
|
|
4849
4967
|
}
|
|
4850
4968
|
}
|
|
4851
4969
|
function registerHydratedClass(registry, className) {
|
|
@@ -4857,25 +4975,40 @@ function registerHydratedClass(registry, className) {
|
|
|
4857
4975
|
});
|
|
4858
4976
|
registry.refCounts.set(className, 0);
|
|
4859
4977
|
}
|
|
4860
|
-
var StyleInjector = class
|
|
4978
|
+
var StyleInjector = class {
|
|
4861
4979
|
sheetManager;
|
|
4862
4980
|
config;
|
|
4863
4981
|
globalRuleCounter = 0;
|
|
4864
4982
|
pendingGCHandle = null;
|
|
4983
|
+
namePrefix;
|
|
4984
|
+
classRegex;
|
|
4985
|
+
rscClassRegex;
|
|
4865
4986
|
/** @internal — exposed for debug utilities only */
|
|
4866
4987
|
get _sheetManager() {
|
|
4867
4988
|
return this.sheetManager;
|
|
4868
4989
|
}
|
|
4869
4990
|
constructor(config = {}) {
|
|
4991
|
+
if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
|
|
4870
4992
|
this.config = config;
|
|
4871
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));
|
|
4872
5005
|
}
|
|
4873
5006
|
/**
|
|
4874
5007
|
* Check if `className` was hydrated from server-rendered styles and,
|
|
4875
5008
|
* if so, wire the cacheKey mapping. Returns true on hit.
|
|
4876
5009
|
*/
|
|
4877
5010
|
tryHydratedHit(registry, cacheKey, className) {
|
|
4878
|
-
syncServerClasses(registry);
|
|
5011
|
+
syncServerClasses(registry, this.rscClassRegex);
|
|
4879
5012
|
const rule = registry.rules.get(className);
|
|
4880
5013
|
if (rule && rule.ruleIndex === -2 && rule.sheetIndex === -2) {
|
|
4881
5014
|
registry.cacheKeyToClassName.set(cacheKey, className);
|
|
@@ -4894,7 +5027,7 @@ var StyleInjector = class StyleInjector {
|
|
|
4894
5027
|
className: registry.cacheKeyToClassName.get(cacheKey),
|
|
4895
5028
|
isNewAllocation: false
|
|
4896
5029
|
};
|
|
4897
|
-
const className = generateClassName(cacheKey);
|
|
5030
|
+
const className = this.generateClassName(cacheKey);
|
|
4898
5031
|
if (this.tryHydratedHit(registry, cacheKey, className)) return {
|
|
4899
5032
|
className,
|
|
4900
5033
|
isNewAllocation: false
|
|
@@ -4946,7 +5079,7 @@ var StyleInjector = class StyleInjector {
|
|
|
4946
5079
|
};
|
|
4947
5080
|
}
|
|
4948
5081
|
} else if (cacheKey) {
|
|
4949
|
-
className = generateClassName(cacheKey);
|
|
5082
|
+
className = this.generateClassName(cacheKey);
|
|
4950
5083
|
if (this.tryHydratedHit(registry, cacheKey, className)) {
|
|
4951
5084
|
registry.refCounts.set(className, (registry.refCounts.get(className) || 0) + 1);
|
|
4952
5085
|
if (registry.metrics) registry.metrics.hits++;
|
|
@@ -4955,7 +5088,10 @@ var StyleInjector = class StyleInjector {
|
|
|
4955
5088
|
dispose: () => this.dispose(className, registry)
|
|
4956
5089
|
};
|
|
4957
5090
|
}
|
|
4958
|
-
} else
|
|
5091
|
+
} else {
|
|
5092
|
+
const parts = rules.map((r) => `${r.selector}\0${r.declarations}`);
|
|
5093
|
+
className = makeClassName(this.namePrefix, hashString(parts.join("\n")));
|
|
5094
|
+
}
|
|
4959
5095
|
const rulesToInsert = rules.map((rule) => {
|
|
4960
5096
|
let newSelector = rule.selector;
|
|
4961
5097
|
if (rule.needsClassName) {
|
|
@@ -5173,8 +5309,23 @@ var StyleInjector = class StyleInjector {
|
|
|
5173
5309
|
}
|
|
5174
5310
|
const cssName = effectiveResult.cssName;
|
|
5175
5311
|
const definition = effectiveResult.definition;
|
|
5176
|
-
|
|
5177
|
-
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;
|
|
5178
5329
|
const parts = [];
|
|
5179
5330
|
if (definition.syntax != null) {
|
|
5180
5331
|
let syntax = String(definition.syntax).trim();
|
|
@@ -5194,8 +5345,8 @@ var StyleInjector = class StyleInjector {
|
|
|
5194
5345
|
selector: `@property ${cssName}`,
|
|
5195
5346
|
declarations
|
|
5196
5347
|
};
|
|
5197
|
-
|
|
5198
|
-
|
|
5348
|
+
registry.injectedProperties.set(cssName, normalizePropertyDefinition(definition));
|
|
5349
|
+
this.sheetManager.insertGlobalRule(registry, [rule], `property:${cacheKey}`, root);
|
|
5199
5350
|
}
|
|
5200
5351
|
/**
|
|
5201
5352
|
* Check whether a given @property name was already injected by this injector.
|
|
@@ -5275,12 +5426,12 @@ var StyleInjector = class StyleInjector {
|
|
|
5275
5426
|
let actualName;
|
|
5276
5427
|
if (providedName) {
|
|
5277
5428
|
const existingContentForName = registry.keyframesNameToContent.get(providedName);
|
|
5278
|
-
if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}
|
|
5429
|
+
if (existingContentForName && existingContentForName !== contentHash) actualName = `${providedName}-${makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++))}`;
|
|
5279
5430
|
else {
|
|
5280
5431
|
actualName = providedName;
|
|
5281
5432
|
registry.keyframesNameToContent.set(providedName, contentHash);
|
|
5282
5433
|
}
|
|
5283
|
-
} else actualName =
|
|
5434
|
+
} else actualName = makeKeyframeName(this.namePrefix, String(registry.keyframesCounter++));
|
|
5284
5435
|
const result = this.sheetManager.insertKeyframes(registry, steps, actualName, root);
|
|
5285
5436
|
if (!result) return {
|
|
5286
5437
|
toString: () => "",
|
|
@@ -5329,7 +5480,6 @@ var StyleInjector = class StyleInjector {
|
|
|
5329
5480
|
}
|
|
5330
5481
|
}
|
|
5331
5482
|
}
|
|
5332
|
-
static TASTY_CLASS_RE = /^t[a-z0-9]+$/;
|
|
5333
5483
|
/**
|
|
5334
5484
|
* Record a render-time usage hit for one or more classNames.
|
|
5335
5485
|
* Handles space-separated multi-chunk classNames.
|
|
@@ -5345,7 +5495,7 @@ var StyleInjector = class StyleInjector {
|
|
|
5345
5495
|
const now = Date.now();
|
|
5346
5496
|
const parts = className.indexOf(" ") === -1 ? [className] : className.split(" ");
|
|
5347
5497
|
for (const cls of parts) {
|
|
5348
|
-
if (!
|
|
5498
|
+
if (!this.classRegex.test(cls)) continue;
|
|
5349
5499
|
if (!registry.rules.has(cls)) continue;
|
|
5350
5500
|
const entry = registry.usageMap.get(cls);
|
|
5351
5501
|
if (entry) entry.lastTouchedAt = now;
|
|
@@ -5398,7 +5548,7 @@ var StyleInjector = class StyleInjector {
|
|
|
5398
5548
|
if (registry.usageMap.size - activeCount <= capacity) return 0;
|
|
5399
5549
|
}
|
|
5400
5550
|
const liveClasses = /* @__PURE__ */ new Set();
|
|
5401
|
-
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);
|
|
5402
5552
|
let swept = 0;
|
|
5403
5553
|
if (force) for (const [className] of registry.usageMap) {
|
|
5404
5554
|
if (liveClasses.has(className)) continue;
|
|
@@ -6055,9 +6205,13 @@ function simplifyInner(node) {
|
|
|
6055
6205
|
return node;
|
|
6056
6206
|
}
|
|
6057
6207
|
if (node.kind === "compound") {
|
|
6208
|
+
const key = getConditionUniqueId(node);
|
|
6209
|
+
const cached = simplifyCache.get(key);
|
|
6210
|
+
if (cached) return cached;
|
|
6058
6211
|
const simplifiedChildren = node.children.map((c) => simplifyInner(c));
|
|
6059
|
-
|
|
6060
|
-
|
|
6212
|
+
const result = node.operator === "AND" ? simplifyAnd(simplifiedChildren) : simplifyOr(simplifiedChildren);
|
|
6213
|
+
simplifyCache.set(key, result);
|
|
6214
|
+
return result;
|
|
6061
6215
|
}
|
|
6062
6216
|
return node;
|
|
6063
6217
|
}
|
|
@@ -6075,13 +6229,23 @@ function simplifyAnd(children) {
|
|
|
6075
6229
|
if (hasRangeContradiction(terms)) return falseCondition();
|
|
6076
6230
|
if (hasAttributeConflict(terms)) return falseCondition();
|
|
6077
6231
|
if (hasContainerStyleConflict(terms)) return falseCondition();
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6232
|
+
const MAX_SIMPLIFY_PASSES = 4;
|
|
6233
|
+
for (let pass = 0; pass < MAX_SIMPLIFY_PASSES; pass++) {
|
|
6234
|
+
const lengthBefore = terms.length;
|
|
6235
|
+
const keyBefore = simplifyTermsKey(terms);
|
|
6236
|
+
terms = removeImpliedNegations(terms);
|
|
6237
|
+
terms = deduplicateTerms(terms);
|
|
6238
|
+
terms = mergeRanges(terms);
|
|
6239
|
+
terms = sortTerms(terms);
|
|
6240
|
+
terms = applyAbsorptionAnd(terms);
|
|
6241
|
+
terms = applyConsensusAnd(terms);
|
|
6242
|
+
terms = pruneContradictedOrBranches(terms);
|
|
6243
|
+
if (terms.length === 0) break;
|
|
6244
|
+
if (terms.length === 1) break;
|
|
6245
|
+
if (hasContradiction(terms)) return falseCondition();
|
|
6246
|
+
if (hasAttributeConflict(terms)) return falseCondition();
|
|
6247
|
+
if (terms.length === lengthBefore && simplifyTermsKey(terms) === keyBefore) break;
|
|
6248
|
+
}
|
|
6085
6249
|
if (terms.length === 0) return trueCondition();
|
|
6086
6250
|
if (terms.length === 1) return terms[0];
|
|
6087
6251
|
return {
|
|
@@ -6396,6 +6560,13 @@ function removeImpliedNegations(terms) {
|
|
|
6396
6560
|
const isImplied = buildImpliedNegationCheck(terms);
|
|
6397
6561
|
return terms.filter((t) => !isImplied(t));
|
|
6398
6562
|
}
|
|
6563
|
+
/**
|
|
6564
|
+
* Build a stable, order-insensitive key for a list of AND terms.
|
|
6565
|
+
* Used to detect fixpoint convergence in `simplifyAnd`.
|
|
6566
|
+
*/
|
|
6567
|
+
function simplifyTermsKey(terms) {
|
|
6568
|
+
return terms.map((t) => getConditionUniqueId(t)).sort().join("&");
|
|
6569
|
+
}
|
|
6399
6570
|
function deduplicateTerms(terms) {
|
|
6400
6571
|
const seen = /* @__PURE__ */ new Set();
|
|
6401
6572
|
const result = [];
|
|
@@ -6778,1061 +6949,677 @@ function pruneContradictedOrBranches(terms) {
|
|
|
6778
6949
|
return flattened;
|
|
6779
6950
|
}
|
|
6780
6951
|
//#endregion
|
|
6781
|
-
//#region src/pipeline/
|
|
6952
|
+
//#region src/pipeline/materialize-contradictions.ts
|
|
6782
6953
|
/**
|
|
6783
|
-
*
|
|
6784
|
-
*
|
|
6785
|
-
* The entries should be ordered by priority (highest priority first).
|
|
6786
|
-
*
|
|
6787
|
-
* For each entry, we compute:
|
|
6788
|
-
* exclusiveCondition = condition & !prior[0] & !prior[1] & ...
|
|
6789
|
-
*
|
|
6790
|
-
* This ensures exactly one condition matches at any time.
|
|
6791
|
-
*
|
|
6792
|
-
* Example:
|
|
6793
|
-
* Input (ordered highest to lowest priority):
|
|
6794
|
-
* A: value1 (priority 2)
|
|
6795
|
-
* B: value2 (priority 1)
|
|
6796
|
-
* C: value3 (priority 0)
|
|
6797
|
-
*
|
|
6798
|
-
* Output:
|
|
6799
|
-
* A: A
|
|
6800
|
-
* B: B & !A
|
|
6801
|
-
* C: C & !A & !B
|
|
6802
|
-
*
|
|
6803
|
-
* @param entries Parsed style entries ordered by priority (highest first)
|
|
6804
|
-
* @returns Entries with exclusive conditions, filtered to remove impossible ones
|
|
6954
|
+
* Generic deduplication by a key extraction function.
|
|
6955
|
+
* Preserves insertion order, keeping the first occurrence of each key.
|
|
6805
6956
|
*/
|
|
6806
|
-
function
|
|
6957
|
+
function dedupeByKey(items, getKey) {
|
|
6958
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6807
6959
|
const result = [];
|
|
6808
|
-
const
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
result.push({
|
|
6815
|
-
...entry,
|
|
6816
|
-
exclusiveCondition: simplified
|
|
6817
|
-
});
|
|
6818
|
-
if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
|
|
6960
|
+
for (const item of items) {
|
|
6961
|
+
const key = getKey(item);
|
|
6962
|
+
if (!seen.has(key)) {
|
|
6963
|
+
seen.add(key);
|
|
6964
|
+
result.push(item);
|
|
6965
|
+
}
|
|
6819
6966
|
}
|
|
6820
6967
|
return result;
|
|
6821
6968
|
}
|
|
6969
|
+
function dedupeMediaConditions(conditions) {
|
|
6970
|
+
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
6971
|
+
}
|
|
6972
|
+
function dedupeContainerConditions(conditions) {
|
|
6973
|
+
return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
|
|
6974
|
+
}
|
|
6975
|
+
function dedupeSupportsConditions(conditions) {
|
|
6976
|
+
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
6977
|
+
}
|
|
6822
6978
|
/**
|
|
6823
|
-
*
|
|
6824
|
-
*
|
|
6825
|
-
* @param styleKey The style key (e.g., 'padding')
|
|
6826
|
-
* @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
|
|
6827
|
-
* @param parseCondition Function to parse state keys into conditions
|
|
6828
|
-
* @returns Parsed entries ordered by priority (highest first)
|
|
6979
|
+
* Check if supports conditions contain contradictions
|
|
6980
|
+
* e.g., @supports(display: grid) AND NOT @supports(display: grid)
|
|
6829
6981
|
*/
|
|
6830
|
-
function
|
|
6831
|
-
const
|
|
6832
|
-
|
|
6833
|
-
const
|
|
6834
|
-
const
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
condition,
|
|
6840
|
-
priority: index
|
|
6841
|
-
});
|
|
6842
|
-
});
|
|
6843
|
-
entries.reverse();
|
|
6844
|
-
return entries;
|
|
6982
|
+
function hasSupportsContradiction(conditions) {
|
|
6983
|
+
const conditionMap = /* @__PURE__ */ new Map();
|
|
6984
|
+
for (const cond of conditions) {
|
|
6985
|
+
const key = `${cond.subtype}|${cond.condition}`;
|
|
6986
|
+
const existing = conditionMap.get(key);
|
|
6987
|
+
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
6988
|
+
conditionMap.set(key, !cond.negated);
|
|
6989
|
+
}
|
|
6990
|
+
return false;
|
|
6845
6991
|
}
|
|
6846
6992
|
/**
|
|
6847
|
-
*
|
|
6848
|
-
*
|
|
6849
|
-
*
|
|
6850
|
-
* conditions can be combined with OR and treated as a single entry.
|
|
6851
|
-
* This must happen **before** exclusive expansion and OR branch splitting
|
|
6852
|
-
* to avoid combinatorial explosion and duplicate CSS output.
|
|
6853
|
-
*
|
|
6854
|
-
* Default (TRUE) entries are **never** merged with non-default entries.
|
|
6855
|
-
* Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
|
|
6856
|
-
* condition's participation in exclusive building. That causes
|
|
6857
|
-
* intermediate-priority states to lose their `:not(X)` negation,
|
|
6858
|
-
* breaking mutual exclusivity when X and an intermediate state are
|
|
6859
|
-
* both active. Stage 6 `mergeByValue` handles combining rules with
|
|
6860
|
-
* identical CSS output after exclusive conditions are correctly built.
|
|
6993
|
+
* Check if a set of media conditions contains contradictions
|
|
6994
|
+
* e.g., (prefers-color-scheme: light) AND NOT (prefers-color-scheme: light)
|
|
6995
|
+
* or (width >= 900px) AND (width < 600px)
|
|
6861
6996
|
*
|
|
6862
|
-
*
|
|
6863
|
-
|
|
6997
|
+
* Uses parsed media conditions for efficient analysis without regex parsing.
|
|
6998
|
+
*/
|
|
6999
|
+
function hasMediaContradiction(conditions) {
|
|
7000
|
+
const featureConditions = /* @__PURE__ */ new Map();
|
|
7001
|
+
const typeConditions = /* @__PURE__ */ new Map();
|
|
7002
|
+
const dimensionConditions = /* @__PURE__ */ new Map();
|
|
7003
|
+
const dimensionsByDim = /* @__PURE__ */ new Map();
|
|
7004
|
+
for (const cond of conditions) if (cond.subtype === "type") {
|
|
7005
|
+
const key = cond.mediaType || "all";
|
|
7006
|
+
const existing = typeConditions.get(key);
|
|
7007
|
+
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7008
|
+
typeConditions.set(key, !cond.negated);
|
|
7009
|
+
} else if (cond.subtype === "feature") {
|
|
7010
|
+
const key = cond.condition;
|
|
7011
|
+
const existing = featureConditions.get(key);
|
|
7012
|
+
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7013
|
+
featureConditions.set(key, !cond.negated);
|
|
7014
|
+
} else if (cond.subtype === "dimension") {
|
|
7015
|
+
const condKey = cond.condition;
|
|
7016
|
+
const existing = dimensionConditions.get(condKey);
|
|
7017
|
+
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7018
|
+
dimensionConditions.set(condKey, !cond.negated);
|
|
7019
|
+
if (!cond.negated) {
|
|
7020
|
+
const dim = cond.dimension || "width";
|
|
7021
|
+
let bounds = dimensionsByDim.get(dim);
|
|
7022
|
+
if (!bounds) {
|
|
7023
|
+
bounds = {
|
|
7024
|
+
lowerBound: null,
|
|
7025
|
+
upperBound: null
|
|
7026
|
+
};
|
|
7027
|
+
dimensionsByDim.set(dim, bounds);
|
|
7028
|
+
}
|
|
7029
|
+
if (cond.lowerBound?.valueNumeric != null) {
|
|
7030
|
+
const value = cond.lowerBound.valueNumeric;
|
|
7031
|
+
if (bounds.lowerBound === null || value > bounds.lowerBound) bounds.lowerBound = value;
|
|
7032
|
+
}
|
|
7033
|
+
if (cond.upperBound?.valueNumeric != null) {
|
|
7034
|
+
const value = cond.upperBound.valueNumeric;
|
|
7035
|
+
if (bounds.upperBound === null || value < bounds.upperBound) bounds.upperBound = value;
|
|
7036
|
+
}
|
|
7037
|
+
if (bounds.lowerBound !== null && bounds.upperBound !== null && bounds.lowerBound >= bounds.upperBound) return true;
|
|
7038
|
+
}
|
|
7039
|
+
}
|
|
7040
|
+
return false;
|
|
7041
|
+
}
|
|
7042
|
+
/**
|
|
7043
|
+
* Check if container conditions contain contradictions in style queries
|
|
7044
|
+
* e.g., style(--variant: danger) and style(--variant: success) together
|
|
7045
|
+
* Same property with different values = always false
|
|
6864
7046
|
*
|
|
6865
|
-
*
|
|
6866
|
-
* highest priority of the group.
|
|
7047
|
+
* Uses parsed container conditions for efficient analysis without regex parsing.
|
|
6867
7048
|
*/
|
|
6868
|
-
function
|
|
6869
|
-
|
|
6870
|
-
const
|
|
6871
|
-
|
|
6872
|
-
const
|
|
6873
|
-
const
|
|
6874
|
-
if (
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
entries: [entry],
|
|
6879
|
-
maxPriority: entry.priority
|
|
7049
|
+
function hasContainerStyleContradiction(conditions) {
|
|
7050
|
+
const styleQueries = /* @__PURE__ */ new Map();
|
|
7051
|
+
for (const cond of conditions) {
|
|
7052
|
+
if (cond.subtype !== "style" || !cond.property) continue;
|
|
7053
|
+
const property = cond.property;
|
|
7054
|
+
const value = cond.propertyValue;
|
|
7055
|
+
if (!styleQueries.has(property)) styleQueries.set(property, {
|
|
7056
|
+
hasExistence: false,
|
|
7057
|
+
values: /* @__PURE__ */ new Set(),
|
|
7058
|
+
hasNegatedExistence: false
|
|
6880
7059
|
});
|
|
7060
|
+
const entry = styleQueries.get(property);
|
|
7061
|
+
if (cond.negated) {
|
|
7062
|
+
if (value === void 0) entry.hasNegatedExistence = true;
|
|
7063
|
+
} else if (value === void 0) entry.hasExistence = true;
|
|
7064
|
+
else entry.values.add(value);
|
|
6881
7065
|
}
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
if (
|
|
6886
|
-
merged.push(group.entries[0]);
|
|
6887
|
-
continue;
|
|
6888
|
-
}
|
|
6889
|
-
const defaultEntries = group.entries.filter((e) => e.condition.kind === "true");
|
|
6890
|
-
const nonDefaultEntries = group.entries.filter((e) => e.condition.kind !== "true");
|
|
6891
|
-
for (const entry of defaultEntries) merged.push(entry);
|
|
6892
|
-
if (nonDefaultEntries.length === 1) merged.push(nonDefaultEntries[0]);
|
|
6893
|
-
else if (nonDefaultEntries.length >= 2) {
|
|
6894
|
-
const combinedCondition = simplifyCondition(or(...nonDefaultEntries.map((e) => e.condition)));
|
|
6895
|
-
const combinedStateKey = nonDefaultEntries.map((e) => e.stateKey).join(" | ");
|
|
6896
|
-
merged.push({
|
|
6897
|
-
styleKey: nonDefaultEntries[0].styleKey,
|
|
6898
|
-
stateKey: combinedStateKey,
|
|
6899
|
-
value: nonDefaultEntries[0].value,
|
|
6900
|
-
condition: combinedCondition,
|
|
6901
|
-
priority: group.maxPriority
|
|
6902
|
-
});
|
|
6903
|
-
}
|
|
7066
|
+
for (const [, entry] of styleQueries) {
|
|
7067
|
+
if (entry.hasExistence && entry.hasNegatedExistence) return true;
|
|
7068
|
+
if (entry.values.size > 1) return true;
|
|
7069
|
+
if (entry.hasNegatedExistence && entry.values.size > 0) return true;
|
|
6904
7070
|
}
|
|
6905
|
-
|
|
6906
|
-
return merged;
|
|
6907
|
-
}
|
|
6908
|
-
function serializeValue(value) {
|
|
6909
|
-
if (value === null || value === void 0) return "null";
|
|
6910
|
-
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
6911
|
-
return JSON.stringify(value);
|
|
7071
|
+
return false;
|
|
6912
7072
|
}
|
|
7073
|
+
//#endregion
|
|
7074
|
+
//#region src/pipeline/materialize.ts
|
|
6913
7075
|
/**
|
|
6914
|
-
*
|
|
6915
|
-
*
|
|
6916
|
-
* When a value map contains compound AND state keys (e.g. `@dark & @hc`),
|
|
6917
|
-
* checks whether any state atom is a "don't-care" variable — i.e. the
|
|
6918
|
-
* value is the same whether that atom is present or absent. Redundant
|
|
6919
|
-
* atoms are removed from all keys and duplicate entries are collapsed.
|
|
6920
|
-
*
|
|
6921
|
-
* This runs **before** condition parsing so that downstream stages
|
|
6922
|
-
* (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
|
|
6923
|
-
* never see the irrelevant dimension, producing simpler, smaller CSS.
|
|
6924
|
-
*
|
|
6925
|
-
* Only pure top-level AND combinations are eligible. Keys that contain
|
|
6926
|
-
* `|`, `^`, or `,` at the top level are treated as opaque single atoms.
|
|
7076
|
+
* CSS Materialization
|
|
6927
7077
|
*
|
|
6928
|
-
*
|
|
6929
|
-
*
|
|
6930
|
-
* // @hc is redundant → { '': A, '@dark': B }
|
|
7078
|
+
* Converts condition trees into CSS selectors and at-rules.
|
|
7079
|
+
* This is the final stage that produces actual CSS output.
|
|
6931
7080
|
*/
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
7081
|
+
const conditionCache = new Lru(3e3);
|
|
7082
|
+
/**
|
|
7083
|
+
* Convert a condition tree to CSS components
|
|
7084
|
+
*/
|
|
7085
|
+
function conditionToCSS(node) {
|
|
7086
|
+
const key = getConditionUniqueId(node);
|
|
7087
|
+
const cached = conditionCache.get(key);
|
|
7088
|
+
if (cached) return cached;
|
|
7089
|
+
const result = conditionToCSSInner(node);
|
|
7090
|
+
conditionCache.set(key, result);
|
|
7091
|
+
return result;
|
|
7092
|
+
}
|
|
7093
|
+
function emptyVariant() {
|
|
7094
|
+
return {
|
|
7095
|
+
modifierConditions: [],
|
|
7096
|
+
pseudoConditions: [],
|
|
7097
|
+
selectorGroups: [],
|
|
7098
|
+
ownGroups: [],
|
|
7099
|
+
mediaConditions: [],
|
|
7100
|
+
containerConditions: [],
|
|
7101
|
+
supportsConditions: [],
|
|
7102
|
+
rootGroups: [],
|
|
7103
|
+
parentGroups: [],
|
|
7104
|
+
startingStyle: false
|
|
7105
|
+
};
|
|
7106
|
+
}
|
|
7107
|
+
function conditionToCSSInner(node) {
|
|
7108
|
+
if (node.kind === "true") return {
|
|
7109
|
+
variants: [emptyVariant()],
|
|
7110
|
+
isImpossible: false
|
|
7111
|
+
};
|
|
7112
|
+
if (node.kind === "false") return {
|
|
7113
|
+
variants: [],
|
|
7114
|
+
isImpossible: true
|
|
7115
|
+
};
|
|
7116
|
+
if (node.kind === "state") return stateToCSS(node);
|
|
7117
|
+
if (node.kind === "compound") if (node.operator === "AND") return andToCSS(node.children);
|
|
7118
|
+
else return orToCSS(node.children);
|
|
7119
|
+
return {
|
|
7120
|
+
variants: [emptyVariant()],
|
|
7121
|
+
isImpossible: false
|
|
7122
|
+
};
|
|
6952
7123
|
}
|
|
6953
7124
|
/**
|
|
6954
|
-
*
|
|
6955
|
-
*
|
|
6956
|
-
* Returns `null` if the key contains `|`, `^`, or `,` at the top level
|
|
6957
|
-
* (making it ineligible for atom-level extraction).
|
|
6958
|
-
* Returns `[]` for the empty string (default key).
|
|
7125
|
+
* Convert a state condition to CSS
|
|
6959
7126
|
*/
|
|
6960
|
-
function
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
7127
|
+
function stateToCSS(state) {
|
|
7128
|
+
switch (state.type) {
|
|
7129
|
+
case "media": return {
|
|
7130
|
+
variants: mediaToParsed(state).map((mediaCond) => {
|
|
7131
|
+
const v = emptyVariant();
|
|
7132
|
+
v.mediaConditions.push(mediaCond);
|
|
7133
|
+
return v;
|
|
7134
|
+
}),
|
|
7135
|
+
isImpossible: false
|
|
7136
|
+
};
|
|
7137
|
+
case "root": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "rootGroups");
|
|
7138
|
+
case "parent": return parentConditionToVariants(state.innerCondition, state.negated ?? false, state.direct);
|
|
7139
|
+
case "own": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "ownGroups");
|
|
7140
|
+
case "modifier": {
|
|
7141
|
+
const v = emptyVariant();
|
|
7142
|
+
v.modifierConditions.push(modifierToParsed(state));
|
|
7143
|
+
return {
|
|
7144
|
+
variants: [v],
|
|
7145
|
+
isImpossible: false
|
|
7146
|
+
};
|
|
7147
|
+
}
|
|
7148
|
+
case "pseudo": {
|
|
7149
|
+
const v = emptyVariant();
|
|
7150
|
+
v.pseudoConditions.push(pseudoToParsed(state));
|
|
7151
|
+
return {
|
|
7152
|
+
variants: [v],
|
|
7153
|
+
isImpossible: false
|
|
7154
|
+
};
|
|
7155
|
+
}
|
|
7156
|
+
case "container": {
|
|
7157
|
+
const v = emptyVariant();
|
|
7158
|
+
v.containerConditions.push(containerToParsed(state));
|
|
7159
|
+
return {
|
|
7160
|
+
variants: [v],
|
|
7161
|
+
isImpossible: false
|
|
7162
|
+
};
|
|
7163
|
+
}
|
|
7164
|
+
case "supports": {
|
|
7165
|
+
const v = emptyVariant();
|
|
7166
|
+
v.supportsConditions.push(supportsToParsed(state));
|
|
7167
|
+
return {
|
|
7168
|
+
variants: [v],
|
|
7169
|
+
isImpossible: false
|
|
7170
|
+
};
|
|
7171
|
+
}
|
|
7172
|
+
case "starting": {
|
|
7173
|
+
const v = emptyVariant();
|
|
7174
|
+
v.startingStyle = !state.negated;
|
|
7175
|
+
return {
|
|
7176
|
+
variants: [v],
|
|
7177
|
+
isImpossible: false
|
|
7178
|
+
};
|
|
6976
7179
|
}
|
|
6977
|
-
current += ch;
|
|
6978
7180
|
}
|
|
6979
|
-
const trimmed = current.trim();
|
|
6980
|
-
if (trimmed) parts.push(trimmed);
|
|
6981
|
-
return parts;
|
|
6982
7181
|
}
|
|
6983
7182
|
/**
|
|
6984
|
-
*
|
|
6985
|
-
* partner (same remaining atoms, atom absent) with the same value.
|
|
7183
|
+
* Convert modifier condition to parsed structure
|
|
6986
7184
|
*/
|
|
6987
|
-
function
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
|
|
6995
|
-
}
|
|
6996
|
-
return true;
|
|
7185
|
+
function modifierToParsed(state) {
|
|
7186
|
+
return {
|
|
7187
|
+
attribute: state.attribute,
|
|
7188
|
+
value: state.value,
|
|
7189
|
+
operator: state.operator,
|
|
7190
|
+
negated: state.negated ?? false
|
|
7191
|
+
};
|
|
6997
7192
|
}
|
|
6998
7193
|
/**
|
|
6999
|
-
*
|
|
7194
|
+
* Convert parsed modifier to CSS selector string (for final output)
|
|
7000
7195
|
*/
|
|
7001
|
-
function
|
|
7002
|
-
|
|
7196
|
+
function modifierToCSS(mod) {
|
|
7197
|
+
let selector;
|
|
7198
|
+
if (mod.value !== void 0) {
|
|
7199
|
+
const op = mod.operator || "=";
|
|
7200
|
+
selector = `[${mod.attribute}${op}"${mod.value}"]`;
|
|
7201
|
+
} else selector = `[${mod.attribute}]`;
|
|
7202
|
+
if (mod.negated) return `:not(${selector})`;
|
|
7203
|
+
return selector;
|
|
7003
7204
|
}
|
|
7004
7205
|
/**
|
|
7005
|
-
*
|
|
7006
|
-
*
|
|
7007
|
-
* For an entry with condition `A | B | C`, this creates 3 entries:
|
|
7008
|
-
* - condition: A
|
|
7009
|
-
* - condition: B & !A
|
|
7010
|
-
* - condition: C & !A & !B
|
|
7011
|
-
*
|
|
7012
|
-
* This ensures OR branches are mutually exclusive BEFORE the main
|
|
7013
|
-
* exclusive condition building pass.
|
|
7014
|
-
*
|
|
7015
|
-
* @param entries Parsed entries (may contain OR conditions)
|
|
7016
|
-
* @returns Expanded entries with OR branches made exclusive
|
|
7206
|
+
* Convert pseudo condition to parsed structure
|
|
7017
7207
|
*/
|
|
7018
|
-
function
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
}
|
|
7024
|
-
return result;
|
|
7208
|
+
function pseudoToParsed(state) {
|
|
7209
|
+
return {
|
|
7210
|
+
pseudo: state.pseudo,
|
|
7211
|
+
negated: state.negated ?? false
|
|
7212
|
+
};
|
|
7025
7213
|
}
|
|
7026
7214
|
/**
|
|
7027
|
-
*
|
|
7215
|
+
* Convert parsed pseudo to CSS selector string (for final output).
|
|
7028
7216
|
*
|
|
7029
|
-
*
|
|
7030
|
-
*
|
|
7031
|
-
*
|
|
7032
|
-
*
|
|
7033
|
-
*
|
|
7034
|
-
*
|
|
7217
|
+
* :not() is normalized to negated :is() at parse time, so pseudo.pseudo
|
|
7218
|
+
* never starts with ':not(' here. When negated:
|
|
7219
|
+
* - :is(X) → :not(X) (unwrap :is)
|
|
7220
|
+
* - :where(X) → :not(X) (unwrap :where)
|
|
7221
|
+
* - :has(X) → :not(:has(X))
|
|
7222
|
+
* - other → :not(other)
|
|
7223
|
+
*
|
|
7224
|
+
* When not negated, single-argument :is()/:where() is unwrapped when the
|
|
7225
|
+
* inner content is a simple compound selector that can safely append to
|
|
7226
|
+
* the base selector (this happens after double-negation of :not()).
|
|
7035
7227
|
*/
|
|
7036
|
-
function
|
|
7037
|
-
const
|
|
7038
|
-
if (
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
for (let i = 0; i < orBranches.length; i++) {
|
|
7042
|
-
const branch = orBranches[i];
|
|
7043
|
-
let exclusiveBranch = branch;
|
|
7044
|
-
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
7045
|
-
const simplified = simplifyCondition(exclusiveBranch);
|
|
7046
|
-
if (simplified.kind === "false") {
|
|
7047
|
-
priorBranches.push(branch);
|
|
7048
|
-
continue;
|
|
7049
|
-
}
|
|
7050
|
-
result.push({
|
|
7051
|
-
...entry,
|
|
7052
|
-
stateKey: `${entry.stateKey}[${i}]`,
|
|
7053
|
-
condition: simplified
|
|
7054
|
-
});
|
|
7055
|
-
priorBranches.push(branch);
|
|
7228
|
+
function pseudoToCSS(pseudo) {
|
|
7229
|
+
const p = pseudo.pseudo;
|
|
7230
|
+
if (pseudo.negated) {
|
|
7231
|
+
if (p.startsWith(":is(") || p.startsWith(":where(")) return `:not(${p.slice(p.indexOf("(") + 1, -1)})`;
|
|
7232
|
+
return `:not(${p})`;
|
|
7056
7233
|
}
|
|
7057
|
-
|
|
7234
|
+
if ((p.startsWith(":is(") || p.startsWith(":where(")) && !p.includes(",")) {
|
|
7235
|
+
const inner = p.slice(p.indexOf("(") + 1, -1);
|
|
7236
|
+
const ch = inner[0];
|
|
7237
|
+
if ((ch === ":" || ch === "." || ch === "[" || ch === "#") && !/\s/.test(inner)) return inner;
|
|
7238
|
+
}
|
|
7239
|
+
return p;
|
|
7058
7240
|
}
|
|
7059
7241
|
/**
|
|
7060
|
-
*
|
|
7061
|
-
*
|
|
7062
|
-
* For `A | B | C`, returns [A, B, C]
|
|
7063
|
-
* For `A & B`, returns [A & B] (single branch)
|
|
7064
|
-
* For `A | (B & C)`, returns [A, B & C]
|
|
7242
|
+
* Convert media condition to parsed structure(s)
|
|
7243
|
+
* Returns an array because negated ranges produce OR branches (two separate conditions)
|
|
7065
7244
|
*/
|
|
7066
|
-
function
|
|
7067
|
-
if (
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7245
|
+
function mediaToParsed(state) {
|
|
7246
|
+
if (state.subtype === "type") {
|
|
7247
|
+
const mediaType = state.mediaType || "all";
|
|
7248
|
+
return [{
|
|
7249
|
+
subtype: "type",
|
|
7250
|
+
negated: state.negated ?? false,
|
|
7251
|
+
condition: mediaType,
|
|
7252
|
+
mediaType: state.mediaType
|
|
7253
|
+
}];
|
|
7254
|
+
} else if (state.subtype === "feature") {
|
|
7255
|
+
let condition;
|
|
7256
|
+
if (state.featureValue) condition = `(${state.feature}: ${state.featureValue})`;
|
|
7257
|
+
else condition = `(${state.feature})`;
|
|
7258
|
+
return [{
|
|
7259
|
+
subtype: "feature",
|
|
7260
|
+
negated: state.negated ?? false,
|
|
7261
|
+
condition,
|
|
7262
|
+
feature: state.feature,
|
|
7263
|
+
featureValue: state.featureValue
|
|
7264
|
+
}];
|
|
7265
|
+
} else return dimensionToMediaParsed(state.dimension || "width", state.lowerBound, state.upperBound, state.negated ?? false);
|
|
7074
7266
|
}
|
|
7075
7267
|
/**
|
|
7076
|
-
*
|
|
7077
|
-
*
|
|
7078
|
-
* This handles ORs that arise from De Morgan expansion during negation:
|
|
7079
|
-
* !(A & B) = !A | !B
|
|
7080
|
-
*
|
|
7081
|
-
* These ORs need to be made exclusive to avoid overlapping CSS rules:
|
|
7082
|
-
* !A | !B → !A | (A & !B)
|
|
7083
|
-
*
|
|
7084
|
-
* This is logically equivalent but ensures each branch has proper context.
|
|
7085
|
-
*
|
|
7086
|
-
* Example:
|
|
7087
|
-
* Input: { "": V1, "@supports(...) & :has()": V2 }
|
|
7088
|
-
* V2's exclusive = @supports & :has
|
|
7089
|
-
* V1's exclusive = !(@supports & :has) = !@supports | !:has
|
|
7090
|
-
*
|
|
7091
|
-
* Without this fix: V1 gets two rules:
|
|
7092
|
-
* - @supports (not ...) → V1 ✓
|
|
7093
|
-
* - :not(:has()) → V1 ✗ (missing @supports context!)
|
|
7094
|
-
*
|
|
7095
|
-
* With this fix: V1 gets two exclusive rules:
|
|
7096
|
-
* - @supports (not ...) → V1 ✓
|
|
7097
|
-
* - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
|
|
7268
|
+
* Convert dimension bounds to parsed media condition(s)
|
|
7269
|
+
* Uses CSS Media Queries Level 4 `not (condition)` syntax for negation.
|
|
7098
7270
|
*/
|
|
7099
|
-
function
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
const
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7271
|
+
function dimensionToMediaParsed(dimension, lowerBound, upperBound, negated) {
|
|
7272
|
+
let condition;
|
|
7273
|
+
if (lowerBound && upperBound) {
|
|
7274
|
+
const lowerOp = lowerBound.inclusive ? "<=" : "<";
|
|
7275
|
+
const upperOp = upperBound.inclusive ? "<=" : "<";
|
|
7276
|
+
condition = `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
|
|
7277
|
+
} else if (upperBound) condition = `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
|
|
7278
|
+
else if (lowerBound) condition = `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
|
|
7279
|
+
else condition = `(${dimension})`;
|
|
7280
|
+
return [{
|
|
7281
|
+
subtype: "dimension",
|
|
7282
|
+
negated: negated ?? false,
|
|
7283
|
+
condition,
|
|
7284
|
+
dimension,
|
|
7285
|
+
lowerBound,
|
|
7286
|
+
upperBound
|
|
7287
|
+
}];
|
|
7106
7288
|
}
|
|
7107
7289
|
/**
|
|
7108
|
-
*
|
|
7290
|
+
* Convert container condition to parsed structure
|
|
7291
|
+
* This enables structured analysis for contradiction detection and condition combining
|
|
7109
7292
|
*/
|
|
7110
|
-
function
|
|
7111
|
-
|
|
7112
|
-
if (
|
|
7113
|
-
|
|
7114
|
-
|
|
7293
|
+
function containerToParsed(state) {
|
|
7294
|
+
let condition;
|
|
7295
|
+
if (state.subtype === "style") if (state.propertyValue) condition = `style(--${state.property}: ${state.propertyValue})`;
|
|
7296
|
+
else condition = `style(--${state.property})`;
|
|
7297
|
+
else if (state.subtype === "raw") condition = state.rawCondition;
|
|
7298
|
+
else condition = dimensionToContainerCondition(state.dimension || "width", state.lowerBound, state.upperBound);
|
|
7299
|
+
return {
|
|
7300
|
+
name: state.containerName,
|
|
7301
|
+
condition,
|
|
7302
|
+
negated: state.negated ?? false,
|
|
7303
|
+
subtype: state.subtype,
|
|
7304
|
+
property: state.property,
|
|
7305
|
+
propertyValue: state.propertyValue
|
|
7306
|
+
};
|
|
7115
7307
|
}
|
|
7116
7308
|
/**
|
|
7117
|
-
*
|
|
7118
|
-
*
|
|
7119
|
-
* This is critical for correct CSS generation. For `!A | !B` where A is at-rule
|
|
7120
|
-
* and B is modifier, we want:
|
|
7121
|
-
* - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
|
|
7122
|
-
* - Branch 1: A & !B (modifier negation with at-rule context)
|
|
7123
|
-
*
|
|
7124
|
-
* If we process in wrong order (!B first), we'd get:
|
|
7125
|
-
* - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
|
|
7126
|
-
* - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
|
|
7309
|
+
* Convert dimension bounds to container query condition (single string)
|
|
7310
|
+
* Container queries support "not (condition)", so no need to invert manually
|
|
7127
7311
|
*/
|
|
7128
|
-
function
|
|
7129
|
-
|
|
7130
|
-
const
|
|
7131
|
-
const
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7312
|
+
function dimensionToContainerCondition(dimension, lowerBound, upperBound) {
|
|
7313
|
+
if (lowerBound && upperBound) {
|
|
7314
|
+
const lowerOp = lowerBound.inclusive ? "<=" : "<";
|
|
7315
|
+
const upperOp = upperBound.inclusive ? "<=" : "<";
|
|
7316
|
+
return `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
|
|
7317
|
+
} else if (upperBound) return `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
|
|
7318
|
+
else if (lowerBound) return `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
|
|
7319
|
+
return "(width)";
|
|
7136
7320
|
}
|
|
7137
7321
|
/**
|
|
7138
|
-
*
|
|
7322
|
+
* Convert supports condition to parsed structure
|
|
7139
7323
|
*/
|
|
7140
|
-
function
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
for (let i = 0; i < orBranches.length; i++) {
|
|
7147
|
-
const branch = orBranches[i];
|
|
7148
|
-
let exclusiveBranch = branch;
|
|
7149
|
-
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
7150
|
-
const simplified = simplifyCondition(exclusiveBranch);
|
|
7151
|
-
if (simplified.kind === "false") {
|
|
7152
|
-
priorBranches.push(branch);
|
|
7153
|
-
continue;
|
|
7154
|
-
}
|
|
7155
|
-
result.push({
|
|
7156
|
-
...entry,
|
|
7157
|
-
stateKey: `${entry.stateKey}[or:${i}]`,
|
|
7158
|
-
exclusiveCondition: simplified
|
|
7159
|
-
});
|
|
7160
|
-
priorBranches.push(branch);
|
|
7161
|
-
}
|
|
7162
|
-
return result;
|
|
7324
|
+
function supportsToParsed(state) {
|
|
7325
|
+
return {
|
|
7326
|
+
subtype: state.subtype,
|
|
7327
|
+
condition: state.condition,
|
|
7328
|
+
negated: state.negated ?? false
|
|
7329
|
+
};
|
|
7163
7330
|
}
|
|
7164
|
-
//#endregion
|
|
7165
|
-
//#region src/pipeline/materialize-contradictions.ts
|
|
7166
7331
|
/**
|
|
7167
|
-
*
|
|
7168
|
-
* Preserves insertion order, keeping the first occurrence of each key.
|
|
7332
|
+
* Collect all modifier and pseudo conditions from a variant as a flat array.
|
|
7169
7333
|
*/
|
|
7170
|
-
function
|
|
7171
|
-
|
|
7172
|
-
const result = [];
|
|
7173
|
-
for (const item of items) {
|
|
7174
|
-
const key = getKey(item);
|
|
7175
|
-
if (!seen.has(key)) {
|
|
7176
|
-
seen.add(key);
|
|
7177
|
-
result.push(item);
|
|
7178
|
-
}
|
|
7179
|
-
}
|
|
7180
|
-
return result;
|
|
7181
|
-
}
|
|
7182
|
-
function dedupeMediaConditions(conditions) {
|
|
7183
|
-
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
7184
|
-
}
|
|
7185
|
-
function dedupeContainerConditions(conditions) {
|
|
7186
|
-
return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
|
|
7187
|
-
}
|
|
7188
|
-
function dedupeSupportsConditions(conditions) {
|
|
7189
|
-
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
7334
|
+
function collectSelectorConditions(variant) {
|
|
7335
|
+
return [...variant.modifierConditions, ...variant.pseudoConditions];
|
|
7190
7336
|
}
|
|
7191
7337
|
/**
|
|
7192
|
-
*
|
|
7193
|
-
*
|
|
7338
|
+
* Convert an inner condition tree into a single SelectorVariant with
|
|
7339
|
+
* one SelectorGroup whose branches represent the inner OR alternatives.
|
|
7340
|
+
* Shared by @root() and @own().
|
|
7341
|
+
*
|
|
7342
|
+
* Both positive and negated cases produce one variant with one group.
|
|
7343
|
+
* Negation simply sets the `negated` flag, which swaps :is() for :not()
|
|
7344
|
+
* in the final CSS output — no De Morgan transformation is needed.
|
|
7345
|
+
*
|
|
7346
|
+
* This mirrors parentConditionToVariants: OR branches are kept inside
|
|
7347
|
+
* a single group and rendered as comma-separated arguments in
|
|
7348
|
+
* :is()/:not(), e.g. :root:is([a], [b]) or [el]:not([a], [b]).
|
|
7194
7349
|
*/
|
|
7195
|
-
function
|
|
7196
|
-
const
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7350
|
+
function innerConditionToVariants(innerCondition, negated, target) {
|
|
7351
|
+
const innerCSS = conditionToCSS(innerCondition);
|
|
7352
|
+
if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
|
|
7353
|
+
variants: [],
|
|
7354
|
+
isImpossible: true
|
|
7355
|
+
};
|
|
7356
|
+
const branches = [];
|
|
7357
|
+
for (const innerVariant of innerCSS.variants) {
|
|
7358
|
+
const conditions = collectSelectorConditions(innerVariant);
|
|
7359
|
+
if (conditions.length > 0) branches.push(conditions);
|
|
7202
7360
|
}
|
|
7203
|
-
return
|
|
7361
|
+
if (branches.length === 0) return {
|
|
7362
|
+
variants: [emptyVariant()],
|
|
7363
|
+
isImpossible: false
|
|
7364
|
+
};
|
|
7365
|
+
const v = emptyVariant();
|
|
7366
|
+
v[target].push({
|
|
7367
|
+
branches,
|
|
7368
|
+
negated
|
|
7369
|
+
});
|
|
7370
|
+
return {
|
|
7371
|
+
variants: [v],
|
|
7372
|
+
isImpossible: false
|
|
7373
|
+
};
|
|
7204
7374
|
}
|
|
7205
7375
|
/**
|
|
7206
|
-
*
|
|
7207
|
-
*
|
|
7208
|
-
* or (width >= 900px) AND (width < 600px)
|
|
7376
|
+
* Convert a @parent() inner condition into a single SelectorVariant with
|
|
7377
|
+
* one ParentGroup whose branches represent the inner OR alternatives.
|
|
7209
7378
|
*
|
|
7210
|
-
*
|
|
7379
|
+
* Both positive and negated cases produce one variant with one group.
|
|
7380
|
+
* Negation simply sets the `negated` flag, which swaps :is() for :not()
|
|
7381
|
+
* in the final CSS output — no structural transformation is needed.
|
|
7211
7382
|
*/
|
|
7212
|
-
function
|
|
7213
|
-
const
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
for (const cond of conditions) if (cond.subtype === "type") {
|
|
7218
|
-
const key = cond.mediaType || "all";
|
|
7219
|
-
const existing = typeConditions.get(key);
|
|
7220
|
-
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7221
|
-
typeConditions.set(key, !cond.negated);
|
|
7222
|
-
} else if (cond.subtype === "feature") {
|
|
7223
|
-
const key = cond.condition;
|
|
7224
|
-
const existing = featureConditions.get(key);
|
|
7225
|
-
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7226
|
-
featureConditions.set(key, !cond.negated);
|
|
7227
|
-
} else if (cond.subtype === "dimension") {
|
|
7228
|
-
const condKey = cond.condition;
|
|
7229
|
-
const existing = dimensionConditions.get(condKey);
|
|
7230
|
-
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7231
|
-
dimensionConditions.set(condKey, !cond.negated);
|
|
7232
|
-
if (!cond.negated) {
|
|
7233
|
-
const dim = cond.dimension || "width";
|
|
7234
|
-
let bounds = dimensionsByDim.get(dim);
|
|
7235
|
-
if (!bounds) {
|
|
7236
|
-
bounds = {
|
|
7237
|
-
lowerBound: null,
|
|
7238
|
-
upperBound: null
|
|
7239
|
-
};
|
|
7240
|
-
dimensionsByDim.set(dim, bounds);
|
|
7241
|
-
}
|
|
7242
|
-
if (cond.lowerBound?.valueNumeric != null) {
|
|
7243
|
-
const value = cond.lowerBound.valueNumeric;
|
|
7244
|
-
if (bounds.lowerBound === null || value > bounds.lowerBound) bounds.lowerBound = value;
|
|
7245
|
-
}
|
|
7246
|
-
if (cond.upperBound?.valueNumeric != null) {
|
|
7247
|
-
const value = cond.upperBound.valueNumeric;
|
|
7248
|
-
if (bounds.upperBound === null || value < bounds.upperBound) bounds.upperBound = value;
|
|
7249
|
-
}
|
|
7250
|
-
if (bounds.lowerBound !== null && bounds.upperBound !== null && bounds.lowerBound >= bounds.upperBound) return true;
|
|
7251
|
-
}
|
|
7252
|
-
}
|
|
7253
|
-
return false;
|
|
7254
|
-
}
|
|
7255
|
-
/**
|
|
7256
|
-
* Check if container conditions contain contradictions in style queries
|
|
7257
|
-
* e.g., style(--variant: danger) and style(--variant: success) together
|
|
7258
|
-
* Same property with different values = always false
|
|
7259
|
-
*
|
|
7260
|
-
* Uses parsed container conditions for efficient analysis without regex parsing.
|
|
7261
|
-
*/
|
|
7262
|
-
function hasContainerStyleContradiction(conditions) {
|
|
7263
|
-
const styleQueries = /* @__PURE__ */ new Map();
|
|
7264
|
-
for (const cond of conditions) {
|
|
7265
|
-
if (cond.subtype !== "style" || !cond.property) continue;
|
|
7266
|
-
const property = cond.property;
|
|
7267
|
-
const value = cond.propertyValue;
|
|
7268
|
-
if (!styleQueries.has(property)) styleQueries.set(property, {
|
|
7269
|
-
hasExistence: false,
|
|
7270
|
-
values: /* @__PURE__ */ new Set(),
|
|
7271
|
-
hasNegatedExistence: false
|
|
7272
|
-
});
|
|
7273
|
-
const entry = styleQueries.get(property);
|
|
7274
|
-
if (cond.negated) {
|
|
7275
|
-
if (value === void 0) entry.hasNegatedExistence = true;
|
|
7276
|
-
} else if (value === void 0) entry.hasExistence = true;
|
|
7277
|
-
else entry.values.add(value);
|
|
7278
|
-
}
|
|
7279
|
-
for (const [, entry] of styleQueries) {
|
|
7280
|
-
if (entry.hasExistence && entry.hasNegatedExistence) return true;
|
|
7281
|
-
if (entry.values.size > 1) return true;
|
|
7282
|
-
if (entry.hasNegatedExistence && entry.values.size > 0) return true;
|
|
7283
|
-
}
|
|
7284
|
-
return false;
|
|
7285
|
-
}
|
|
7286
|
-
//#endregion
|
|
7287
|
-
//#region src/pipeline/materialize.ts
|
|
7288
|
-
/**
|
|
7289
|
-
* CSS Materialization
|
|
7290
|
-
*
|
|
7291
|
-
* Converts condition trees into CSS selectors and at-rules.
|
|
7292
|
-
* This is the final stage that produces actual CSS output.
|
|
7293
|
-
*/
|
|
7294
|
-
const conditionCache = new Lru(3e3);
|
|
7295
|
-
/**
|
|
7296
|
-
* Convert a condition tree to CSS components
|
|
7297
|
-
*/
|
|
7298
|
-
function conditionToCSS(node) {
|
|
7299
|
-
const key = getConditionUniqueId(node);
|
|
7300
|
-
const cached = conditionCache.get(key);
|
|
7301
|
-
if (cached) return cached;
|
|
7302
|
-
const result = conditionToCSSInner(node);
|
|
7303
|
-
conditionCache.set(key, result);
|
|
7304
|
-
return result;
|
|
7305
|
-
}
|
|
7306
|
-
function emptyVariant() {
|
|
7307
|
-
return {
|
|
7308
|
-
modifierConditions: [],
|
|
7309
|
-
pseudoConditions: [],
|
|
7310
|
-
selectorGroups: [],
|
|
7311
|
-
ownGroups: [],
|
|
7312
|
-
mediaConditions: [],
|
|
7313
|
-
containerConditions: [],
|
|
7314
|
-
supportsConditions: [],
|
|
7315
|
-
rootGroups: [],
|
|
7316
|
-
parentGroups: [],
|
|
7317
|
-
startingStyle: false
|
|
7383
|
+
function parentConditionToVariants(innerCondition, negated, direct) {
|
|
7384
|
+
const innerCSS = conditionToCSS(innerCondition);
|
|
7385
|
+
if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
|
|
7386
|
+
variants: [],
|
|
7387
|
+
isImpossible: true
|
|
7318
7388
|
};
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7389
|
+
const branches = [];
|
|
7390
|
+
for (const innerVariant of innerCSS.variants) {
|
|
7391
|
+
const conditions = collectSelectorConditions(innerVariant);
|
|
7392
|
+
if (conditions.length > 0) branches.push(conditions);
|
|
7393
|
+
}
|
|
7394
|
+
if (branches.length === 0) return {
|
|
7322
7395
|
variants: [emptyVariant()],
|
|
7323
7396
|
isImpossible: false
|
|
7324
7397
|
};
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
else return orToCSS(node.children);
|
|
7398
|
+
const v = emptyVariant();
|
|
7399
|
+
v.parentGroups.push({
|
|
7400
|
+
branches,
|
|
7401
|
+
direct,
|
|
7402
|
+
negated
|
|
7403
|
+
});
|
|
7332
7404
|
return {
|
|
7333
|
-
variants: [
|
|
7405
|
+
variants: [v],
|
|
7334
7406
|
isImpossible: false
|
|
7335
7407
|
};
|
|
7336
7408
|
}
|
|
7337
7409
|
/**
|
|
7338
|
-
*
|
|
7410
|
+
* Sort key for canonical condition output within selectors.
|
|
7411
|
+
*
|
|
7412
|
+
* Priority order:
|
|
7413
|
+
* 0: Boolean attribute selectors ([data-hovered])
|
|
7414
|
+
* 1: Value attribute selectors ([data-size="small"])
|
|
7415
|
+
* 2: Negated boolean attributes (:not([data-disabled]))
|
|
7416
|
+
* 3: Negated value attributes (:not([data-size="small"]))
|
|
7417
|
+
* 4: Pseudo-classes (:hover, :focus)
|
|
7418
|
+
* 5: Negated pseudo-classes (:not(:disabled))
|
|
7419
|
+
*
|
|
7420
|
+
* Secondary sort: alphabetical by attribute name / pseudo string.
|
|
7339
7421
|
*/
|
|
7340
|
-
function
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
const v = emptyVariant();
|
|
7345
|
-
v.mediaConditions.push(mediaCond);
|
|
7346
|
-
return v;
|
|
7347
|
-
}),
|
|
7348
|
-
isImpossible: false
|
|
7349
|
-
};
|
|
7350
|
-
case "root": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "rootGroups");
|
|
7351
|
-
case "parent": return parentConditionToVariants(state.innerCondition, state.negated ?? false, state.direct);
|
|
7352
|
-
case "own": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "ownGroups");
|
|
7353
|
-
case "modifier": {
|
|
7354
|
-
const v = emptyVariant();
|
|
7355
|
-
v.modifierConditions.push(modifierToParsed(state));
|
|
7356
|
-
return {
|
|
7357
|
-
variants: [v],
|
|
7358
|
-
isImpossible: false
|
|
7359
|
-
};
|
|
7360
|
-
}
|
|
7361
|
-
case "pseudo": {
|
|
7362
|
-
const v = emptyVariant();
|
|
7363
|
-
v.pseudoConditions.push(pseudoToParsed(state));
|
|
7364
|
-
return {
|
|
7365
|
-
variants: [v],
|
|
7366
|
-
isImpossible: false
|
|
7367
|
-
};
|
|
7368
|
-
}
|
|
7369
|
-
case "container": {
|
|
7370
|
-
const v = emptyVariant();
|
|
7371
|
-
v.containerConditions.push(containerToParsed(state));
|
|
7372
|
-
return {
|
|
7373
|
-
variants: [v],
|
|
7374
|
-
isImpossible: false
|
|
7375
|
-
};
|
|
7376
|
-
}
|
|
7377
|
-
case "supports": {
|
|
7378
|
-
const v = emptyVariant();
|
|
7379
|
-
v.supportsConditions.push(supportsToParsed(state));
|
|
7380
|
-
return {
|
|
7381
|
-
variants: [v],
|
|
7382
|
-
isImpossible: false
|
|
7383
|
-
};
|
|
7384
|
-
}
|
|
7385
|
-
case "starting": {
|
|
7386
|
-
const v = emptyVariant();
|
|
7387
|
-
v.startingStyle = !state.negated;
|
|
7388
|
-
return {
|
|
7389
|
-
variants: [v],
|
|
7390
|
-
isImpossible: false
|
|
7391
|
-
};
|
|
7392
|
-
}
|
|
7422
|
+
function conditionSortKey(cond) {
|
|
7423
|
+
if ("attribute" in cond) {
|
|
7424
|
+
const hasValue = cond.value !== void 0 ? 1 : 0;
|
|
7425
|
+
return `${(cond.negated ? 2 : 0) + hasValue}|${cond.attribute}|${cond.value ?? ""}`;
|
|
7393
7426
|
}
|
|
7427
|
+
return `${cond.negated ? 5 : 4}|${cond.pseudo}`;
|
|
7428
|
+
}
|
|
7429
|
+
function sortConditions(conditions) {
|
|
7430
|
+
return conditions.toSorted((a, b) => conditionSortKey(a).localeCompare(conditionSortKey(b)));
|
|
7431
|
+
}
|
|
7432
|
+
function branchToCSS(branch) {
|
|
7433
|
+
let parts = "";
|
|
7434
|
+
for (const cond of sortConditions(branch)) parts += selectorConditionToCSS(cond);
|
|
7435
|
+
return parts;
|
|
7394
7436
|
}
|
|
7395
7437
|
/**
|
|
7396
|
-
*
|
|
7438
|
+
* Wrap serialized selector arguments in :is() or :not().
|
|
7439
|
+
* Arguments are sorted for canonical output.
|
|
7397
7440
|
*/
|
|
7398
|
-
function
|
|
7399
|
-
return {
|
|
7400
|
-
attribute: state.attribute,
|
|
7401
|
-
value: state.value,
|
|
7402
|
-
operator: state.operator,
|
|
7403
|
-
negated: state.negated ?? false
|
|
7404
|
-
};
|
|
7441
|
+
function wrapInIsOrNot(args, negated) {
|
|
7442
|
+
return `${negated ? ":not" : ":is"}(${args.sort().join(", ")})`;
|
|
7405
7443
|
}
|
|
7406
7444
|
/**
|
|
7407
|
-
* Convert
|
|
7445
|
+
* Convert a selector group to a CSS selector fragment.
|
|
7446
|
+
*
|
|
7447
|
+
* Single-branch groups are unwrapped (no :is() wrapper).
|
|
7448
|
+
* Multi-branch groups use :is() or :not().
|
|
7449
|
+
* Negation swaps :is() for :not().
|
|
7408
7450
|
*/
|
|
7409
|
-
function
|
|
7410
|
-
|
|
7411
|
-
if (
|
|
7412
|
-
const
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
return
|
|
7451
|
+
function selectorGroupToCSS(group) {
|
|
7452
|
+
if (group.branches.length === 0) return "";
|
|
7453
|
+
if (group.branches.length === 1) {
|
|
7454
|
+
const parts = branchToCSS(group.branches[0]);
|
|
7455
|
+
if (group.negated) return `:not(${parts})`;
|
|
7456
|
+
return parts;
|
|
7457
|
+
}
|
|
7458
|
+
return wrapInIsOrNot(group.branches.map(branchToCSS), group.negated);
|
|
7417
7459
|
}
|
|
7418
7460
|
/**
|
|
7419
|
-
*
|
|
7461
|
+
* Collect facts about modifier conditions for subsumption analysis.
|
|
7462
|
+
* Tracks negated boolean attrs (:not([attr])) and positive exact values ([attr="X"]).
|
|
7420
7463
|
*/
|
|
7421
|
-
function
|
|
7464
|
+
function collectSubsumptionFacts(modifiers) {
|
|
7465
|
+
const negatedBooleanAttrs = /* @__PURE__ */ new Set();
|
|
7466
|
+
const positiveExactValuesByAttr = /* @__PURE__ */ new Map();
|
|
7467
|
+
for (const mod of modifiers) {
|
|
7468
|
+
if (mod.negated && mod.value === void 0) negatedBooleanAttrs.add(mod.attribute);
|
|
7469
|
+
if (!mod.negated && mod.value !== void 0 && (mod.operator ?? "=") === "=") {
|
|
7470
|
+
let vals = positiveExactValuesByAttr.get(mod.attribute);
|
|
7471
|
+
if (!vals) {
|
|
7472
|
+
vals = /* @__PURE__ */ new Set();
|
|
7473
|
+
positiveExactValuesByAttr.set(mod.attribute, vals);
|
|
7474
|
+
}
|
|
7475
|
+
vals.add(mod.value);
|
|
7476
|
+
}
|
|
7477
|
+
}
|
|
7422
7478
|
return {
|
|
7423
|
-
|
|
7424
|
-
|
|
7479
|
+
negatedBooleanAttrs,
|
|
7480
|
+
positiveExactValuesByAttr
|
|
7425
7481
|
};
|
|
7426
7482
|
}
|
|
7427
7483
|
/**
|
|
7428
|
-
*
|
|
7429
|
-
*
|
|
7430
|
-
* :not() is
|
|
7431
|
-
* never starts with ':not(' here. When negated:
|
|
7432
|
-
* - :is(X) → :not(X) (unwrap :is)
|
|
7433
|
-
* - :where(X) → :not(X) (unwrap :where)
|
|
7434
|
-
* - :has(X) → :not(:has(X))
|
|
7435
|
-
* - other → :not(other)
|
|
7484
|
+
* Check if a negated-value modifier is subsumed by stronger facts:
|
|
7485
|
+
* - :not([attr]) subsumes :not([attr="val"])
|
|
7486
|
+
* - [attr="X"] implies :not([attr="Y"]) is redundant (single exact value)
|
|
7436
7487
|
*
|
|
7437
|
-
*
|
|
7438
|
-
*
|
|
7439
|
-
* the base selector (this happens after double-negation of :not()).
|
|
7488
|
+
* Only applies to exact-match (=) operators; substring operators don't
|
|
7489
|
+
* imply exclusivity between values.
|
|
7440
7490
|
*/
|
|
7441
|
-
function
|
|
7442
|
-
|
|
7443
|
-
if (
|
|
7444
|
-
|
|
7445
|
-
|
|
7491
|
+
function isSubsumedNegatedModifier(mod, facts) {
|
|
7492
|
+
if (!mod.negated || mod.value === void 0) return false;
|
|
7493
|
+
if (facts.negatedBooleanAttrs.has(mod.attribute)) return true;
|
|
7494
|
+
if ((mod.operator ?? "=") === "=") {
|
|
7495
|
+
const posVals = facts.positiveExactValuesByAttr.get(mod.attribute);
|
|
7496
|
+
if (posVals && posVals.size === 1 && !posVals.has(mod.value)) return true;
|
|
7446
7497
|
}
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7498
|
+
return false;
|
|
7499
|
+
}
|
|
7500
|
+
/**
|
|
7501
|
+
* Remove redundant single-condition groups that are subsumed by stronger
|
|
7502
|
+
* groups on the same attribute. O(n) — only inspects single-branch,
|
|
7503
|
+
* single-condition groups.
|
|
7504
|
+
*/
|
|
7505
|
+
function optimizeGroups(groups) {
|
|
7506
|
+
if (groups.length <= 1) return groups;
|
|
7507
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7508
|
+
const result = [];
|
|
7509
|
+
for (const g of groups) {
|
|
7510
|
+
const key = getSelectorGroupKey(g);
|
|
7511
|
+
if (!seen.has(key)) {
|
|
7512
|
+
seen.add(key);
|
|
7513
|
+
result.push(g);
|
|
7514
|
+
}
|
|
7451
7515
|
}
|
|
7452
|
-
return
|
|
7516
|
+
if (result.length <= 1) return result;
|
|
7517
|
+
const effectiveModifiers = [];
|
|
7518
|
+
for (const g of result) {
|
|
7519
|
+
if (g.branches.length !== 1 || g.branches[0].length !== 1) continue;
|
|
7520
|
+
const cond = g.branches[0][0];
|
|
7521
|
+
if (!("attribute" in cond)) continue;
|
|
7522
|
+
effectiveModifiers.push({
|
|
7523
|
+
...cond,
|
|
7524
|
+
negated: g.negated !== cond.negated
|
|
7525
|
+
});
|
|
7526
|
+
}
|
|
7527
|
+
const facts = collectSubsumptionFacts(effectiveModifiers);
|
|
7528
|
+
if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
|
|
7529
|
+
return result.filter((g) => {
|
|
7530
|
+
if (g.branches.length !== 1 || g.branches[0].length !== 1) return true;
|
|
7531
|
+
const cond = g.branches[0][0];
|
|
7532
|
+
if (!("attribute" in cond) || !g.negated || cond.negated || cond.value === void 0) return true;
|
|
7533
|
+
return !isSubsumedNegatedModifier({
|
|
7534
|
+
...cond,
|
|
7535
|
+
negated: true
|
|
7536
|
+
}, facts);
|
|
7537
|
+
});
|
|
7453
7538
|
}
|
|
7454
7539
|
/**
|
|
7455
|
-
* Convert
|
|
7456
|
-
* Returns an array because negated ranges produce OR branches (two separate conditions)
|
|
7540
|
+
* Convert root groups to CSS selector prefix (for final output)
|
|
7457
7541
|
*/
|
|
7458
|
-
function
|
|
7459
|
-
if (
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
mediaType: state.mediaType
|
|
7466
|
-
}];
|
|
7467
|
-
} else if (state.subtype === "feature") {
|
|
7468
|
-
let condition;
|
|
7469
|
-
if (state.featureValue) condition = `(${state.feature}: ${state.featureValue})`;
|
|
7470
|
-
else condition = `(${state.feature})`;
|
|
7471
|
-
return [{
|
|
7472
|
-
subtype: "feature",
|
|
7473
|
-
negated: state.negated ?? false,
|
|
7474
|
-
condition,
|
|
7475
|
-
feature: state.feature,
|
|
7476
|
-
featureValue: state.featureValue
|
|
7477
|
-
}];
|
|
7478
|
-
} else return dimensionToMediaParsed(state.dimension || "width", state.lowerBound, state.upperBound, state.negated ?? false);
|
|
7542
|
+
function rootGroupsToCSS(groups) {
|
|
7543
|
+
if (groups.length === 0) return void 0;
|
|
7544
|
+
const optimized = optimizeGroups(groups);
|
|
7545
|
+
if (optimized.length === 0) return void 0;
|
|
7546
|
+
let prefix = ":root";
|
|
7547
|
+
for (const group of optimized) prefix += selectorGroupToCSS(group);
|
|
7548
|
+
return prefix;
|
|
7479
7549
|
}
|
|
7480
7550
|
/**
|
|
7481
|
-
* Convert
|
|
7482
|
-
*
|
|
7551
|
+
* Convert parent groups to CSS selector fragments (for final output).
|
|
7552
|
+
* Each group produces its own :is()/:not() wrapper with a combinator
|
|
7553
|
+
* suffix (` *` or ` > *`) appended to each branch.
|
|
7483
7554
|
*/
|
|
7484
|
-
function
|
|
7485
|
-
let
|
|
7486
|
-
|
|
7487
|
-
const
|
|
7488
|
-
const
|
|
7489
|
-
|
|
7490
|
-
}
|
|
7491
|
-
|
|
7492
|
-
else condition = `(${dimension})`;
|
|
7493
|
-
return [{
|
|
7494
|
-
subtype: "dimension",
|
|
7495
|
-
negated: negated ?? false,
|
|
7496
|
-
condition,
|
|
7497
|
-
dimension,
|
|
7498
|
-
lowerBound,
|
|
7499
|
-
upperBound
|
|
7500
|
-
}];
|
|
7555
|
+
function parentGroupsToCSS(groups) {
|
|
7556
|
+
let result = "";
|
|
7557
|
+
for (const group of groups) {
|
|
7558
|
+
const combinator = group.direct ? " > *" : " *";
|
|
7559
|
+
const args = group.branches.map((branch) => branchToCSS(branch) + combinator);
|
|
7560
|
+
result += wrapInIsOrNot(args, group.negated);
|
|
7561
|
+
}
|
|
7562
|
+
return result;
|
|
7501
7563
|
}
|
|
7502
7564
|
/**
|
|
7503
|
-
* Convert
|
|
7504
|
-
* This enables structured analysis for contradiction detection and condition combining
|
|
7565
|
+
* Convert a modifier or pseudo condition to a CSS selector fragment
|
|
7505
7566
|
*/
|
|
7506
|
-
function
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
else condition = `style(--${state.property})`;
|
|
7510
|
-
else if (state.subtype === "raw") condition = state.rawCondition;
|
|
7511
|
-
else condition = dimensionToContainerCondition(state.dimension || "width", state.lowerBound, state.upperBound);
|
|
7512
|
-
return {
|
|
7513
|
-
name: state.containerName,
|
|
7514
|
-
condition,
|
|
7515
|
-
negated: state.negated ?? false,
|
|
7516
|
-
subtype: state.subtype,
|
|
7517
|
-
property: state.property,
|
|
7518
|
-
propertyValue: state.propertyValue
|
|
7519
|
-
};
|
|
7567
|
+
function selectorConditionToCSS(cond) {
|
|
7568
|
+
if ("attribute" in cond) return modifierToCSS(cond);
|
|
7569
|
+
return pseudoToCSS(cond);
|
|
7520
7570
|
}
|
|
7521
7571
|
/**
|
|
7522
|
-
*
|
|
7523
|
-
* Container queries support "not (condition)", so no need to invert manually
|
|
7572
|
+
* Get unique key for a modifier condition
|
|
7524
7573
|
*/
|
|
7525
|
-
function
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
const upperOp = upperBound.inclusive ? "<=" : "<";
|
|
7529
|
-
return `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
|
|
7530
|
-
} else if (upperBound) return `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
|
|
7531
|
-
else if (lowerBound) return `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
|
|
7532
|
-
return "(width)";
|
|
7574
|
+
function getModifierKey(mod) {
|
|
7575
|
+
const base = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
|
|
7576
|
+
return mod.negated ? `!${base}` : base;
|
|
7533
7577
|
}
|
|
7534
7578
|
/**
|
|
7535
|
-
*
|
|
7579
|
+
* Get unique key for a pseudo condition
|
|
7536
7580
|
*/
|
|
7537
|
-
function
|
|
7538
|
-
return {
|
|
7539
|
-
subtype: state.subtype,
|
|
7540
|
-
condition: state.condition,
|
|
7541
|
-
negated: state.negated ?? false
|
|
7542
|
-
};
|
|
7581
|
+
function getPseudoKey(pseudo) {
|
|
7582
|
+
return pseudo.negated ? `!${pseudo.pseudo}` : pseudo.pseudo;
|
|
7543
7583
|
}
|
|
7544
7584
|
/**
|
|
7545
|
-
*
|
|
7585
|
+
* Get unique key for any selector condition (modifier or pseudo)
|
|
7546
7586
|
*/
|
|
7547
|
-
function
|
|
7548
|
-
return
|
|
7587
|
+
function getSelectorConditionKey(cond) {
|
|
7588
|
+
return "attribute" in cond ? `mod:${getModifierKey(cond)}` : `pseudo:${getPseudoKey(cond)}`;
|
|
7549
7589
|
}
|
|
7550
7590
|
/**
|
|
7551
|
-
*
|
|
7552
|
-
*
|
|
7553
|
-
* Shared by @root() and @own().
|
|
7554
|
-
*
|
|
7555
|
-
* Both positive and negated cases produce one variant with one group.
|
|
7556
|
-
* Negation simply sets the `negated` flag, which swaps :is() for :not()
|
|
7557
|
-
* in the final CSS output — no De Morgan transformation is needed.
|
|
7558
|
-
*
|
|
7559
|
-
* This mirrors parentConditionToVariants: OR branches are kept inside
|
|
7560
|
-
* a single group and rendered as comma-separated arguments in
|
|
7561
|
-
* :is()/:not(), e.g. :root:is([a], [b]) or [el]:not([a], [b]).
|
|
7591
|
+
* Deduplicate selector conditions (modifiers or pseudos).
|
|
7592
|
+
* Shared by root, parent, and own conditions.
|
|
7562
7593
|
*/
|
|
7563
|
-
function
|
|
7564
|
-
const
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
if (conditions.length > 0) branches.push(conditions);
|
|
7594
|
+
function dedupeSelectorConditions(conditions) {
|
|
7595
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7596
|
+
const result = [];
|
|
7597
|
+
for (const c of conditions) {
|
|
7598
|
+
const key = getSelectorConditionKey(c);
|
|
7599
|
+
if (!seen.has(key)) {
|
|
7600
|
+
seen.add(key);
|
|
7601
|
+
result.push(c);
|
|
7602
|
+
}
|
|
7573
7603
|
}
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
negated
|
|
7604
|
+
const facts = collectSubsumptionFacts(result.filter((c) => "attribute" in c));
|
|
7605
|
+
if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
|
|
7606
|
+
return result.filter((c) => {
|
|
7607
|
+
if (!("attribute" in c)) return true;
|
|
7608
|
+
if (isSubsumedNegatedModifier(c, facts)) return false;
|
|
7609
|
+
if (!c.negated && c.value === void 0 && facts.positiveExactValuesByAttr.has(c.attribute)) return false;
|
|
7610
|
+
return true;
|
|
7582
7611
|
});
|
|
7583
|
-
return {
|
|
7584
|
-
variants: [v],
|
|
7585
|
-
isImpossible: false
|
|
7586
|
-
};
|
|
7587
7612
|
}
|
|
7588
7613
|
/**
|
|
7589
|
-
*
|
|
7590
|
-
* one ParentGroup whose branches represent the inner OR alternatives.
|
|
7591
|
-
*
|
|
7592
|
-
* Both positive and negated cases produce one variant with one group.
|
|
7593
|
-
* Negation simply sets the `negated` flag, which swaps :is() for :not()
|
|
7594
|
-
* in the final CSS output — no structural transformation is needed.
|
|
7614
|
+
* Check for modifier contradiction: same attribute with opposite negation
|
|
7595
7615
|
*/
|
|
7596
|
-
function
|
|
7597
|
-
const
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
for (const innerVariant of innerCSS.variants) {
|
|
7604
|
-
const conditions = collectSelectorConditions(innerVariant);
|
|
7605
|
-
if (conditions.length > 0) branches.push(conditions);
|
|
7606
|
-
}
|
|
7607
|
-
if (branches.length === 0) return {
|
|
7608
|
-
variants: [emptyVariant()],
|
|
7609
|
-
isImpossible: false
|
|
7610
|
-
};
|
|
7611
|
-
const v = emptyVariant();
|
|
7612
|
-
v.parentGroups.push({
|
|
7613
|
-
branches,
|
|
7614
|
-
direct,
|
|
7615
|
-
negated
|
|
7616
|
-
});
|
|
7617
|
-
return {
|
|
7618
|
-
variants: [v],
|
|
7619
|
-
isImpossible: false
|
|
7620
|
-
};
|
|
7621
|
-
}
|
|
7622
|
-
/**
|
|
7623
|
-
* Sort key for canonical condition output within selectors.
|
|
7624
|
-
*
|
|
7625
|
-
* Priority order:
|
|
7626
|
-
* 0: Boolean attribute selectors ([data-hovered])
|
|
7627
|
-
* 1: Value attribute selectors ([data-size="small"])
|
|
7628
|
-
* 2: Negated boolean attributes (:not([data-disabled]))
|
|
7629
|
-
* 3: Negated value attributes (:not([data-size="small"]))
|
|
7630
|
-
* 4: Pseudo-classes (:hover, :focus)
|
|
7631
|
-
* 5: Negated pseudo-classes (:not(:disabled))
|
|
7632
|
-
*
|
|
7633
|
-
* Secondary sort: alphabetical by attribute name / pseudo string.
|
|
7634
|
-
*/
|
|
7635
|
-
function conditionSortKey(cond) {
|
|
7636
|
-
if ("attribute" in cond) {
|
|
7637
|
-
const hasValue = cond.value !== void 0 ? 1 : 0;
|
|
7638
|
-
return `${(cond.negated ? 2 : 0) + hasValue}|${cond.attribute}|${cond.value ?? ""}`;
|
|
7639
|
-
}
|
|
7640
|
-
return `${cond.negated ? 5 : 4}|${cond.pseudo}`;
|
|
7641
|
-
}
|
|
7642
|
-
function sortConditions(conditions) {
|
|
7643
|
-
return conditions.toSorted((a, b) => conditionSortKey(a).localeCompare(conditionSortKey(b)));
|
|
7644
|
-
}
|
|
7645
|
-
function branchToCSS(branch) {
|
|
7646
|
-
let parts = "";
|
|
7647
|
-
for (const cond of sortConditions(branch)) parts += selectorConditionToCSS(cond);
|
|
7648
|
-
return parts;
|
|
7649
|
-
}
|
|
7650
|
-
/**
|
|
7651
|
-
* Wrap serialized selector arguments in :is() or :not().
|
|
7652
|
-
* Arguments are sorted for canonical output.
|
|
7653
|
-
*/
|
|
7654
|
-
function wrapInIsOrNot(args, negated) {
|
|
7655
|
-
return `${negated ? ":not" : ":is"}(${args.sort().join(", ")})`;
|
|
7656
|
-
}
|
|
7657
|
-
/**
|
|
7658
|
-
* Convert a selector group to a CSS selector fragment.
|
|
7659
|
-
*
|
|
7660
|
-
* Single-branch groups are unwrapped (no :is() wrapper).
|
|
7661
|
-
* Multi-branch groups use :is() or :not().
|
|
7662
|
-
* Negation swaps :is() for :not().
|
|
7663
|
-
*/
|
|
7664
|
-
function selectorGroupToCSS(group) {
|
|
7665
|
-
if (group.branches.length === 0) return "";
|
|
7666
|
-
if (group.branches.length === 1) {
|
|
7667
|
-
const parts = branchToCSS(group.branches[0]);
|
|
7668
|
-
if (group.negated) return `:not(${parts})`;
|
|
7669
|
-
return parts;
|
|
7670
|
-
}
|
|
7671
|
-
return wrapInIsOrNot(group.branches.map(branchToCSS), group.negated);
|
|
7672
|
-
}
|
|
7673
|
-
/**
|
|
7674
|
-
* Collect facts about modifier conditions for subsumption analysis.
|
|
7675
|
-
* Tracks negated boolean attrs (:not([attr])) and positive exact values ([attr="X"]).
|
|
7676
|
-
*/
|
|
7677
|
-
function collectSubsumptionFacts(modifiers) {
|
|
7678
|
-
const negatedBooleanAttrs = /* @__PURE__ */ new Set();
|
|
7679
|
-
const positiveExactValuesByAttr = /* @__PURE__ */ new Map();
|
|
7680
|
-
for (const mod of modifiers) {
|
|
7681
|
-
if (mod.negated && mod.value === void 0) negatedBooleanAttrs.add(mod.attribute);
|
|
7682
|
-
if (!mod.negated && mod.value !== void 0 && (mod.operator ?? "=") === "=") {
|
|
7683
|
-
let vals = positiveExactValuesByAttr.get(mod.attribute);
|
|
7684
|
-
if (!vals) {
|
|
7685
|
-
vals = /* @__PURE__ */ new Set();
|
|
7686
|
-
positiveExactValuesByAttr.set(mod.attribute, vals);
|
|
7687
|
-
}
|
|
7688
|
-
vals.add(mod.value);
|
|
7689
|
-
}
|
|
7690
|
-
}
|
|
7691
|
-
return {
|
|
7692
|
-
negatedBooleanAttrs,
|
|
7693
|
-
positiveExactValuesByAttr
|
|
7694
|
-
};
|
|
7695
|
-
}
|
|
7696
|
-
/**
|
|
7697
|
-
* Check if a negated-value modifier is subsumed by stronger facts:
|
|
7698
|
-
* - :not([attr]) subsumes :not([attr="val"])
|
|
7699
|
-
* - [attr="X"] implies :not([attr="Y"]) is redundant (single exact value)
|
|
7700
|
-
*
|
|
7701
|
-
* Only applies to exact-match (=) operators; substring operators don't
|
|
7702
|
-
* imply exclusivity between values.
|
|
7703
|
-
*/
|
|
7704
|
-
function isSubsumedNegatedModifier(mod, facts) {
|
|
7705
|
-
if (!mod.negated || mod.value === void 0) return false;
|
|
7706
|
-
if (facts.negatedBooleanAttrs.has(mod.attribute)) return true;
|
|
7707
|
-
if ((mod.operator ?? "=") === "=") {
|
|
7708
|
-
const posVals = facts.positiveExactValuesByAttr.get(mod.attribute);
|
|
7709
|
-
if (posVals && posVals.size === 1 && !posVals.has(mod.value)) return true;
|
|
7710
|
-
}
|
|
7711
|
-
return false;
|
|
7712
|
-
}
|
|
7713
|
-
/**
|
|
7714
|
-
* Remove redundant single-condition groups that are subsumed by stronger
|
|
7715
|
-
* groups on the same attribute. O(n) — only inspects single-branch,
|
|
7716
|
-
* single-condition groups.
|
|
7717
|
-
*/
|
|
7718
|
-
function optimizeGroups(groups) {
|
|
7719
|
-
if (groups.length <= 1) return groups;
|
|
7720
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7721
|
-
const result = [];
|
|
7722
|
-
for (const g of groups) {
|
|
7723
|
-
const key = getSelectorGroupKey(g);
|
|
7724
|
-
if (!seen.has(key)) {
|
|
7725
|
-
seen.add(key);
|
|
7726
|
-
result.push(g);
|
|
7727
|
-
}
|
|
7728
|
-
}
|
|
7729
|
-
if (result.length <= 1) return result;
|
|
7730
|
-
const effectiveModifiers = [];
|
|
7731
|
-
for (const g of result) {
|
|
7732
|
-
if (g.branches.length !== 1 || g.branches[0].length !== 1) continue;
|
|
7733
|
-
const cond = g.branches[0][0];
|
|
7734
|
-
if (!("attribute" in cond)) continue;
|
|
7735
|
-
effectiveModifiers.push({
|
|
7736
|
-
...cond,
|
|
7737
|
-
negated: g.negated !== cond.negated
|
|
7738
|
-
});
|
|
7739
|
-
}
|
|
7740
|
-
const facts = collectSubsumptionFacts(effectiveModifiers);
|
|
7741
|
-
if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
|
|
7742
|
-
return result.filter((g) => {
|
|
7743
|
-
if (g.branches.length !== 1 || g.branches[0].length !== 1) return true;
|
|
7744
|
-
const cond = g.branches[0][0];
|
|
7745
|
-
if (!("attribute" in cond) || !g.negated || cond.negated || cond.value === void 0) return true;
|
|
7746
|
-
return !isSubsumedNegatedModifier({
|
|
7747
|
-
...cond,
|
|
7748
|
-
negated: true
|
|
7749
|
-
}, facts);
|
|
7750
|
-
});
|
|
7751
|
-
}
|
|
7752
|
-
/**
|
|
7753
|
-
* Convert root groups to CSS selector prefix (for final output)
|
|
7754
|
-
*/
|
|
7755
|
-
function rootGroupsToCSS(groups) {
|
|
7756
|
-
if (groups.length === 0) return void 0;
|
|
7757
|
-
const optimized = optimizeGroups(groups);
|
|
7758
|
-
if (optimized.length === 0) return void 0;
|
|
7759
|
-
let prefix = ":root";
|
|
7760
|
-
for (const group of optimized) prefix += selectorGroupToCSS(group);
|
|
7761
|
-
return prefix;
|
|
7762
|
-
}
|
|
7763
|
-
/**
|
|
7764
|
-
* Convert parent groups to CSS selector fragments (for final output).
|
|
7765
|
-
* Each group produces its own :is()/:not() wrapper with a combinator
|
|
7766
|
-
* suffix (` *` or ` > *`) appended to each branch.
|
|
7767
|
-
*/
|
|
7768
|
-
function parentGroupsToCSS(groups) {
|
|
7769
|
-
let result = "";
|
|
7770
|
-
for (const group of groups) {
|
|
7771
|
-
const combinator = group.direct ? " > *" : " *";
|
|
7772
|
-
const args = group.branches.map((branch) => branchToCSS(branch) + combinator);
|
|
7773
|
-
result += wrapInIsOrNot(args, group.negated);
|
|
7774
|
-
}
|
|
7775
|
-
return result;
|
|
7776
|
-
}
|
|
7777
|
-
/**
|
|
7778
|
-
* Convert a modifier or pseudo condition to a CSS selector fragment
|
|
7779
|
-
*/
|
|
7780
|
-
function selectorConditionToCSS(cond) {
|
|
7781
|
-
if ("attribute" in cond) return modifierToCSS(cond);
|
|
7782
|
-
return pseudoToCSS(cond);
|
|
7783
|
-
}
|
|
7784
|
-
/**
|
|
7785
|
-
* Get unique key for a modifier condition
|
|
7786
|
-
*/
|
|
7787
|
-
function getModifierKey(mod) {
|
|
7788
|
-
const base = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
|
|
7789
|
-
return mod.negated ? `!${base}` : base;
|
|
7790
|
-
}
|
|
7791
|
-
/**
|
|
7792
|
-
* Get unique key for a pseudo condition
|
|
7793
|
-
*/
|
|
7794
|
-
function getPseudoKey(pseudo) {
|
|
7795
|
-
return pseudo.negated ? `!${pseudo.pseudo}` : pseudo.pseudo;
|
|
7796
|
-
}
|
|
7797
|
-
/**
|
|
7798
|
-
* Get unique key for any selector condition (modifier or pseudo)
|
|
7799
|
-
*/
|
|
7800
|
-
function getSelectorConditionKey(cond) {
|
|
7801
|
-
return "attribute" in cond ? `mod:${getModifierKey(cond)}` : `pseudo:${getPseudoKey(cond)}`;
|
|
7802
|
-
}
|
|
7803
|
-
/**
|
|
7804
|
-
* Deduplicate selector conditions (modifiers or pseudos).
|
|
7805
|
-
* Shared by root, parent, and own conditions.
|
|
7806
|
-
*/
|
|
7807
|
-
function dedupeSelectorConditions(conditions) {
|
|
7808
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7809
|
-
const result = [];
|
|
7810
|
-
for (const c of conditions) {
|
|
7811
|
-
const key = getSelectorConditionKey(c);
|
|
7812
|
-
if (!seen.has(key)) {
|
|
7813
|
-
seen.add(key);
|
|
7814
|
-
result.push(c);
|
|
7815
|
-
}
|
|
7816
|
-
}
|
|
7817
|
-
const facts = collectSubsumptionFacts(result.filter((c) => "attribute" in c));
|
|
7818
|
-
if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
|
|
7819
|
-
return result.filter((c) => {
|
|
7820
|
-
if (!("attribute" in c)) return true;
|
|
7821
|
-
if (isSubsumedNegatedModifier(c, facts)) return false;
|
|
7822
|
-
if (!c.negated && c.value === void 0 && facts.positiveExactValuesByAttr.has(c.attribute)) return false;
|
|
7823
|
-
return true;
|
|
7824
|
-
});
|
|
7825
|
-
}
|
|
7826
|
-
/**
|
|
7827
|
-
* Check for modifier contradiction: same attribute with opposite negation
|
|
7828
|
-
*/
|
|
7829
|
-
function hasModifierContradiction(conditions) {
|
|
7830
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
7831
|
-
for (const mod of conditions) {
|
|
7832
|
-
const baseKey = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
|
|
7833
|
-
const existing = byKey.get(baseKey);
|
|
7834
|
-
if (existing !== void 0 && existing !== !mod.negated) return true;
|
|
7835
|
-
byKey.set(baseKey, !mod.negated);
|
|
7616
|
+
function hasModifierContradiction(conditions) {
|
|
7617
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
7618
|
+
for (const mod of conditions) {
|
|
7619
|
+
const baseKey = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
|
|
7620
|
+
const existing = byKey.get(baseKey);
|
|
7621
|
+
if (existing !== void 0 && existing !== !mod.negated) return true;
|
|
7622
|
+
byKey.set(baseKey, !mod.negated);
|
|
7836
7623
|
}
|
|
7837
7624
|
return false;
|
|
7838
7625
|
}
|
|
@@ -8039,328 +7826,827 @@ function getParentGroupKey(g) {
|
|
|
8039
7826
|
/**
|
|
8040
7827
|
* Deduplicate variants
|
|
8041
7828
|
*
|
|
8042
|
-
* Removes:
|
|
8043
|
-
* 1. Exact duplicates (same key)
|
|
8044
|
-
* 2. Superset variants (more restrictive selectors that are redundant)
|
|
7829
|
+
* Removes:
|
|
7830
|
+
* 1. Exact duplicates (same key)
|
|
7831
|
+
* 2. Superset variants (more restrictive selectors that are redundant)
|
|
7832
|
+
*/
|
|
7833
|
+
function dedupeVariants(variants) {
|
|
7834
|
+
if (variants.length <= 1) return variants;
|
|
7835
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7836
|
+
const result = [];
|
|
7837
|
+
for (const v of variants) {
|
|
7838
|
+
const key = getVariantKey(v);
|
|
7839
|
+
if (!seen.has(key)) {
|
|
7840
|
+
seen.add(key);
|
|
7841
|
+
result.push(v);
|
|
7842
|
+
}
|
|
7843
|
+
}
|
|
7844
|
+
if (result.length <= 1) return result;
|
|
7845
|
+
result.sort((a, b) => variantConditionCount(a) - variantConditionCount(b));
|
|
7846
|
+
const filtered = [];
|
|
7847
|
+
for (const candidate of result) {
|
|
7848
|
+
let isRedundant = false;
|
|
7849
|
+
for (const kept of filtered) if (isVariantSuperset(candidate, kept)) {
|
|
7850
|
+
isRedundant = true;
|
|
7851
|
+
break;
|
|
7852
|
+
}
|
|
7853
|
+
if (!isRedundant) filtered.push(candidate);
|
|
7854
|
+
}
|
|
7855
|
+
return filtered;
|
|
7856
|
+
}
|
|
7857
|
+
/**
|
|
7858
|
+
* Combine AND conditions into CSS
|
|
7859
|
+
*
|
|
7860
|
+
* AND of conditions means cartesian product of variants:
|
|
7861
|
+
* (A1 | A2) & (B1 | B2) = A1&B1 | A1&B2 | A2&B1 | A2&B2
|
|
7862
|
+
*
|
|
7863
|
+
* Variants that result in contradictions (e.g., conflicting media rules)
|
|
7864
|
+
* are filtered out.
|
|
7865
|
+
*/
|
|
7866
|
+
function andToCSS(children) {
|
|
7867
|
+
const exclusiveChildren = makeOrBranchesExclusive(children);
|
|
7868
|
+
let currentVariants = [emptyVariant()];
|
|
7869
|
+
for (const child of exclusiveChildren) {
|
|
7870
|
+
const childCSS = conditionToCSSInner(child);
|
|
7871
|
+
if (childCSS.isImpossible || childCSS.variants.length === 0) return {
|
|
7872
|
+
variants: [],
|
|
7873
|
+
isImpossible: true
|
|
7874
|
+
};
|
|
7875
|
+
const newVariants = [];
|
|
7876
|
+
for (const current of currentVariants) for (const childVariant of childCSS.variants) {
|
|
7877
|
+
const merged = mergeVariants(current, childVariant);
|
|
7878
|
+
if (merged !== null) newVariants.push(merged);
|
|
7879
|
+
}
|
|
7880
|
+
if (newVariants.length === 0) return {
|
|
7881
|
+
variants: [],
|
|
7882
|
+
isImpossible: true
|
|
7883
|
+
};
|
|
7884
|
+
currentVariants = dedupeVariants(newVariants);
|
|
7885
|
+
}
|
|
7886
|
+
return {
|
|
7887
|
+
variants: currentVariants,
|
|
7888
|
+
isImpossible: false
|
|
7889
|
+
};
|
|
7890
|
+
}
|
|
7891
|
+
/**
|
|
7892
|
+
* Make OR branches within AND children mutually exclusive.
|
|
7893
|
+
*
|
|
7894
|
+
* For an AND child that is OR(A, B), transforms it to OR(A, B & !A)
|
|
7895
|
+
* so that when andToCSS does a Cartesian product, the resulting
|
|
7896
|
+
* CSS variants don't overlap.
|
|
7897
|
+
*
|
|
7898
|
+
* Only transforms OR children whose branches actually produce
|
|
7899
|
+
* different at-rule contexts when materialized. This avoids
|
|
7900
|
+
* breaking cases where contradiction detection in the Cartesian
|
|
7901
|
+
* product naturally handles deduplication.
|
|
7902
|
+
*/
|
|
7903
|
+
function makeOrBranchesExclusive(children) {
|
|
7904
|
+
return children.map((child) => {
|
|
7905
|
+
if (!isCompoundCondition(child) || child.operator !== "OR") return child;
|
|
7906
|
+
if (child.children.length <= 1) return child;
|
|
7907
|
+
if (!branchesProduceDifferentContexts(child.children)) return child;
|
|
7908
|
+
const exclusiveBranches = [];
|
|
7909
|
+
const priorBranches = [];
|
|
7910
|
+
for (const branch of child.children) {
|
|
7911
|
+
if (priorBranches.length === 0) exclusiveBranches.push(branch);
|
|
7912
|
+
else {
|
|
7913
|
+
let exclusive = branch;
|
|
7914
|
+
for (const prior of priorBranches) exclusive = and(exclusive, not(prior));
|
|
7915
|
+
const simplified = simplifyCondition(exclusive);
|
|
7916
|
+
if (simplified.kind !== "false") exclusiveBranches.push(simplified);
|
|
7917
|
+
}
|
|
7918
|
+
priorBranches.push(branch);
|
|
7919
|
+
}
|
|
7920
|
+
if (exclusiveBranches.length === 0) return child;
|
|
7921
|
+
if (exclusiveBranches.length === 1) return exclusiveBranches[0];
|
|
7922
|
+
return {
|
|
7923
|
+
kind: "compound",
|
|
7924
|
+
operator: "OR",
|
|
7925
|
+
children: exclusiveBranches
|
|
7926
|
+
};
|
|
7927
|
+
});
|
|
7928
|
+
}
|
|
7929
|
+
/**
|
|
7930
|
+
* Check if OR branches produce different at-rule contexts when
|
|
7931
|
+
* materialized. If so, the Cartesian product in andToCSS will
|
|
7932
|
+
* create overlapping CSS variants that need exclusive expansion.
|
|
7933
|
+
*
|
|
7934
|
+
* Exported so Stage 2a (`expandOrConditions` in `exclusive.ts`) can
|
|
7935
|
+
* reuse the same heuristic and skip OR expansion when every branch
|
|
7936
|
+
* lives in the same at-rule/root/parent/own context — pure-selector
|
|
7937
|
+
* ORs are better collapsed into `:is(...)` at materialization time
|
|
7938
|
+
* than expanded into mutually-exclusive `A | (B & !A) | …` cascades.
|
|
7939
|
+
*/
|
|
7940
|
+
function branchesProduceDifferentContexts(branches) {
|
|
7941
|
+
const contextKeys = /* @__PURE__ */ new Set();
|
|
7942
|
+
for (const branch of branches) {
|
|
7943
|
+
const css = conditionToCSSInner(branch);
|
|
7944
|
+
if (css.isImpossible) continue;
|
|
7945
|
+
for (const v of css.variants) contextKeys.add(getVariantContextKey(v));
|
|
7946
|
+
}
|
|
7947
|
+
return contextKeys.size > 1;
|
|
7948
|
+
}
|
|
7949
|
+
/**
|
|
7950
|
+
* Combine OR conditions into CSS
|
|
7951
|
+
*
|
|
7952
|
+
* OR in CSS means multiple selector variants (DNF).
|
|
7953
|
+
* After deduplication, variants that differ only in their base
|
|
7954
|
+
* modifier/pseudo conditions are merged into :is() groups.
|
|
7955
|
+
*
|
|
7956
|
+
* Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
|
|
7957
|
+
* so here we just collect all variants. Any remaining ORs in the condition
|
|
7958
|
+
* tree (e.g., from De Morgan expansion) are handled as simple alternatives.
|
|
7959
|
+
*/
|
|
7960
|
+
/**
|
|
7961
|
+
* Serialize a single negated own-element selector leaf (modifier or pseudo)
|
|
7962
|
+
* to its *positive* selector string, for use inside a combined `:not(...)`.
|
|
7963
|
+
*
|
|
7964
|
+
* Returns `null` when the node is not a negated, own-element selector leaf
|
|
7965
|
+
* (e.g. positive, or a media/container/parent/root/own/supports/starting
|
|
7966
|
+
* wrapper, or a compound), which means it cannot participate in De Morgan
|
|
7967
|
+
* recombination.
|
|
7968
|
+
*/
|
|
7969
|
+
function negatedSelectorLeafToPositiveSelector(node) {
|
|
7970
|
+
if (node.kind !== "state" || !node.negated) return null;
|
|
7971
|
+
if (node.type === "modifier") return modifierToCSS({
|
|
7972
|
+
attribute: node.attribute,
|
|
7973
|
+
value: node.value,
|
|
7974
|
+
operator: node.operator,
|
|
7975
|
+
negated: false
|
|
7976
|
+
});
|
|
7977
|
+
if (node.type === "pseudo") {
|
|
7978
|
+
const p = node.pseudo;
|
|
7979
|
+
if ((p.startsWith(":is(") || p.startsWith(":where(")) && !p.includes(",")) {
|
|
7980
|
+
const inner = p.slice(p.indexOf("(") + 1, -1);
|
|
7981
|
+
if (!/\s/.test(inner)) return inner;
|
|
7982
|
+
return `:is(${inner})`;
|
|
7983
|
+
}
|
|
7984
|
+
return p;
|
|
7985
|
+
}
|
|
7986
|
+
return null;
|
|
7987
|
+
}
|
|
7988
|
+
/**
|
|
7989
|
+
* De Morgan recombination for an OR whose every branch is a negated
|
|
7990
|
+
* own-element selector leaf:
|
|
7991
|
+
*
|
|
7992
|
+
* OR(¬a, ¬b, ¬c) ≡ ¬(a ∧ b ∧ c) → single `:not(a b c)`
|
|
7993
|
+
*
|
|
7994
|
+
* This keeps the catch-all/default exclusive condition (which has no
|
|
7995
|
+
* positive terms to prune against) from exploding into a Cartesian product
|
|
7996
|
+
* of OR branches at `andToCSS`. Returns `null` when recombination does not
|
|
7997
|
+
* apply, so genuine unions (e.g. `:hover | :focus`) fall through to the
|
|
7998
|
+
* normal per-branch materialization.
|
|
7999
|
+
*/
|
|
8000
|
+
function tryRecombineNegatedSelectorOr(children) {
|
|
8001
|
+
if (children.length < 2) return null;
|
|
8002
|
+
const positiveSelectors = [];
|
|
8003
|
+
for (const child of children) {
|
|
8004
|
+
const sel = negatedSelectorLeafToPositiveSelector(child);
|
|
8005
|
+
if (sel === null) return null;
|
|
8006
|
+
positiveSelectors.push(sel);
|
|
8007
|
+
}
|
|
8008
|
+
positiveSelectors.sort();
|
|
8009
|
+
const compound = positiveSelectors.join("");
|
|
8010
|
+
const v = emptyVariant();
|
|
8011
|
+
v.pseudoConditions.push({
|
|
8012
|
+
pseudo: `:is(${compound})`,
|
|
8013
|
+
negated: true
|
|
8014
|
+
});
|
|
8015
|
+
return {
|
|
8016
|
+
variants: [v],
|
|
8017
|
+
isImpossible: false
|
|
8018
|
+
};
|
|
8019
|
+
}
|
|
8020
|
+
function orToCSS(children) {
|
|
8021
|
+
const recombined = tryRecombineNegatedSelectorOr(children);
|
|
8022
|
+
if (recombined !== null) return recombined;
|
|
8023
|
+
const allVariants = [];
|
|
8024
|
+
for (const child of children) {
|
|
8025
|
+
const childCSS = conditionToCSSInner(child);
|
|
8026
|
+
if (childCSS.isImpossible) continue;
|
|
8027
|
+
allVariants.push(...childCSS.variants);
|
|
8028
|
+
}
|
|
8029
|
+
if (allVariants.length === 0) return {
|
|
8030
|
+
variants: [],
|
|
8031
|
+
isImpossible: true
|
|
8032
|
+
};
|
|
8033
|
+
return {
|
|
8034
|
+
variants: dedupeVariants(allVariants),
|
|
8035
|
+
isImpossible: false
|
|
8036
|
+
};
|
|
8037
|
+
}
|
|
8038
|
+
/**
|
|
8039
|
+
* Find keys present in ALL condition arrays.
|
|
8040
|
+
*/
|
|
8041
|
+
function findCommonKeys(conditionSets, getKey) {
|
|
8042
|
+
if (conditionSets.length === 0) return /* @__PURE__ */ new Set();
|
|
8043
|
+
const common = new Set(conditionSets[0].map(getKey));
|
|
8044
|
+
for (let i = 1; i < conditionSets.length; i++) {
|
|
8045
|
+
const keys = new Set(conditionSets[i].map(getKey));
|
|
8046
|
+
for (const key of common) if (!keys.has(key)) common.delete(key);
|
|
8047
|
+
}
|
|
8048
|
+
return common;
|
|
8049
|
+
}
|
|
8050
|
+
/**
|
|
8051
|
+
* Merge OR variants that share the same "context" (at-rules, root, parent,
|
|
8052
|
+
* own, starting) into a single variant with a SelectorGroup.
|
|
8053
|
+
*
|
|
8054
|
+
* Variants with no modifier/pseudo conditions are kept separate (they match
|
|
8055
|
+
* unconditionally and can't be expressed inside :is()).
|
|
8056
|
+
*/
|
|
8057
|
+
function mergeVariantsIntoSelectorGroups(variants) {
|
|
8058
|
+
if (variants.length <= 1) return variants;
|
|
8059
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8060
|
+
for (const v of variants) {
|
|
8061
|
+
const key = getVariantContextKey(v);
|
|
8062
|
+
const group = groups.get(key);
|
|
8063
|
+
if (group) group.push(v);
|
|
8064
|
+
else groups.set(key, [v]);
|
|
8065
|
+
}
|
|
8066
|
+
const result = [];
|
|
8067
|
+
for (const group of groups.values()) {
|
|
8068
|
+
if (group.length === 1) {
|
|
8069
|
+
result.push(group[0]);
|
|
8070
|
+
continue;
|
|
8071
|
+
}
|
|
8072
|
+
const withSelectors = [];
|
|
8073
|
+
const withoutSelectors = [];
|
|
8074
|
+
for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
|
|
8075
|
+
else withSelectors.push(v);
|
|
8076
|
+
result.push(...withoutSelectors);
|
|
8077
|
+
if (withSelectors.length <= 1) {
|
|
8078
|
+
result.push(...withSelectors);
|
|
8079
|
+
continue;
|
|
8080
|
+
}
|
|
8081
|
+
result.push(factorAndGroup(withSelectors));
|
|
8082
|
+
}
|
|
8083
|
+
return result;
|
|
8084
|
+
}
|
|
8085
|
+
/**
|
|
8086
|
+
* Factor common modifier/pseudo conditions out of variants and create
|
|
8087
|
+
* a single variant with a SelectorGroup for the remaining (differing)
|
|
8088
|
+
* conditions.
|
|
8089
|
+
*
|
|
8090
|
+
* Precondition: all variants must share the same context key (identical
|
|
8091
|
+
* at-rules, root/parent/own/selector groups, startingStyle).
|
|
8092
|
+
*/
|
|
8093
|
+
function factorAndGroup(variants) {
|
|
8094
|
+
{
|
|
8095
|
+
const key0 = getVariantContextKey(variants[0]);
|
|
8096
|
+
for (let i = 1; i < variants.length; i++) {
|
|
8097
|
+
const keyI = getVariantContextKey(variants[i]);
|
|
8098
|
+
if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
|
|
8102
|
+
const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
|
|
8103
|
+
const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
|
|
8104
|
+
const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
|
|
8105
|
+
const branches = [];
|
|
8106
|
+
let hasEmptyBranch = false;
|
|
8107
|
+
for (const v of variants) {
|
|
8108
|
+
const branch = [];
|
|
8109
|
+
for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
|
|
8110
|
+
for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
|
|
8111
|
+
if (branch.length > 0) branches.push(branch);
|
|
8112
|
+
else hasEmptyBranch = true;
|
|
8113
|
+
}
|
|
8114
|
+
if (hasEmptyBranch) return {
|
|
8115
|
+
...variants[0],
|
|
8116
|
+
modifierConditions: commonModifiers,
|
|
8117
|
+
pseudoConditions: commonPseudos
|
|
8118
|
+
};
|
|
8119
|
+
const factoredGroups = tryFactorIntoDimensions(branches);
|
|
8120
|
+
if (factoredGroups) return {
|
|
8121
|
+
modifierConditions: commonModifiers,
|
|
8122
|
+
pseudoConditions: commonPseudos,
|
|
8123
|
+
selectorGroups: [...variants[0].selectorGroups, ...factoredGroups],
|
|
8124
|
+
ownGroups: [...variants[0].ownGroups],
|
|
8125
|
+
mediaConditions: [...variants[0].mediaConditions],
|
|
8126
|
+
containerConditions: [...variants[0].containerConditions],
|
|
8127
|
+
supportsConditions: [...variants[0].supportsConditions],
|
|
8128
|
+
rootGroups: [...variants[0].rootGroups],
|
|
8129
|
+
parentGroups: [...variants[0].parentGroups],
|
|
8130
|
+
startingStyle: variants[0].startingStyle
|
|
8131
|
+
};
|
|
8132
|
+
return {
|
|
8133
|
+
modifierConditions: commonModifiers,
|
|
8134
|
+
pseudoConditions: commonPseudos,
|
|
8135
|
+
selectorGroups: [...variants[0].selectorGroups, {
|
|
8136
|
+
branches,
|
|
8137
|
+
negated: false
|
|
8138
|
+
}],
|
|
8139
|
+
ownGroups: [...variants[0].ownGroups],
|
|
8140
|
+
mediaConditions: [...variants[0].mediaConditions],
|
|
8141
|
+
containerConditions: [...variants[0].containerConditions],
|
|
8142
|
+
supportsConditions: [...variants[0].supportsConditions],
|
|
8143
|
+
rootGroups: [...variants[0].rootGroups],
|
|
8144
|
+
parentGroups: [...variants[0].parentGroups],
|
|
8145
|
+
startingStyle: variants[0].startingStyle
|
|
8146
|
+
};
|
|
8147
|
+
}
|
|
8148
|
+
/**
|
|
8149
|
+
* Detect when branches form a complete Cartesian product of independent
|
|
8150
|
+
* modifier attribute dimensions and return one SelectorGroup per dimension.
|
|
8151
|
+
*
|
|
8152
|
+
* Example: 4 branches for 2 attributes × 2 values each →
|
|
8153
|
+
* :is(A1, A2):is(B1, B2) instead of :is(A1B1, A1B2, A2B1, A2B2)
|
|
8154
|
+
*/
|
|
8155
|
+
function tryFactorIntoDimensions(branches) {
|
|
8156
|
+
if (branches.length < 4) return null;
|
|
8157
|
+
const dimensions = /* @__PURE__ */ new Map();
|
|
8158
|
+
for (const branch of branches) for (const cond of branch) {
|
|
8159
|
+
if (!("attribute" in cond)) return null;
|
|
8160
|
+
if (!dimensions.has(cond.attribute)) dimensions.set(cond.attribute, /* @__PURE__ */ new Map());
|
|
8161
|
+
dimensions.get(cond.attribute).set(getModifierKey(cond), cond);
|
|
8162
|
+
}
|
|
8163
|
+
if (dimensions.size < 2) return null;
|
|
8164
|
+
for (const branch of branches) {
|
|
8165
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8166
|
+
for (const cond of branch) {
|
|
8167
|
+
const attr = cond.attribute;
|
|
8168
|
+
if (seen.has(attr)) return null;
|
|
8169
|
+
seen.add(attr);
|
|
8170
|
+
}
|
|
8171
|
+
if (seen.size !== dimensions.size) return null;
|
|
8172
|
+
}
|
|
8173
|
+
let expectedCount = 1;
|
|
8174
|
+
for (const vals of dimensions.values()) expectedCount *= vals.size;
|
|
8175
|
+
if (branches.length !== expectedCount) return null;
|
|
8176
|
+
return [...dimensions.values()].map((vals) => ({
|
|
8177
|
+
branches: [...vals.values()].map((cond) => [cond]),
|
|
8178
|
+
negated: false
|
|
8179
|
+
}));
|
|
8180
|
+
}
|
|
8181
|
+
/**
|
|
8182
|
+
* Build at-rules array from a variant
|
|
8183
|
+
*/
|
|
8184
|
+
function buildAtRulesFromVariant(variant) {
|
|
8185
|
+
const atRules = [];
|
|
8186
|
+
if (variant.mediaConditions.length > 0) {
|
|
8187
|
+
const conditionParts = variant.mediaConditions.map((c) => {
|
|
8188
|
+
if (c.subtype === "type") return c.negated ? `not ${c.condition}` : c.condition;
|
|
8189
|
+
else return c.negated ? `(not ${c.condition})` : c.condition;
|
|
8190
|
+
});
|
|
8191
|
+
atRules.push(`@media ${conditionParts.sort().join(" and ")}`);
|
|
8192
|
+
}
|
|
8193
|
+
if (variant.containerConditions.length > 0) {
|
|
8194
|
+
const byName = /* @__PURE__ */ new Map();
|
|
8195
|
+
for (const cond of variant.containerConditions) {
|
|
8196
|
+
const group = byName.get(cond.name) || [];
|
|
8197
|
+
group.push(cond);
|
|
8198
|
+
byName.set(cond.name, group);
|
|
8199
|
+
}
|
|
8200
|
+
for (const [name, conditions] of byName) {
|
|
8201
|
+
const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
|
|
8202
|
+
const namePrefix = name ? `${name} ` : "";
|
|
8203
|
+
atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
8206
|
+
if (variant.supportsConditions.length > 0) {
|
|
8207
|
+
const conditionParts = variant.supportsConditions.map((c) => {
|
|
8208
|
+
if (c.subtype === "selector") {
|
|
8209
|
+
const selectorCond = `selector(${c.condition})`;
|
|
8210
|
+
return c.negated ? `(not ${selectorCond})` : selectorCond;
|
|
8211
|
+
} else {
|
|
8212
|
+
const featureCond = `(${c.condition})`;
|
|
8213
|
+
return c.negated ? `(not ${featureCond})` : featureCond;
|
|
8214
|
+
}
|
|
8215
|
+
});
|
|
8216
|
+
atRules.push(`@supports ${conditionParts.join(" and ")}`);
|
|
8217
|
+
}
|
|
8218
|
+
return atRules;
|
|
8219
|
+
}
|
|
8220
|
+
//#endregion
|
|
8221
|
+
//#region src/pipeline/exclusive.ts
|
|
8222
|
+
/**
|
|
8223
|
+
* Build exclusive conditions for a list of parsed style entries.
|
|
8224
|
+
*
|
|
8225
|
+
* The entries should be ordered by priority (highest priority first).
|
|
8226
|
+
*
|
|
8227
|
+
* For each entry, we compute:
|
|
8228
|
+
* exclusiveCondition = condition & !prior[0] & !prior[1] & ...
|
|
8229
|
+
*
|
|
8230
|
+
* This ensures exactly one condition matches at any time.
|
|
8231
|
+
*
|
|
8232
|
+
* Example:
|
|
8233
|
+
* Input (ordered highest to lowest priority):
|
|
8234
|
+
* A: value1 (priority 2)
|
|
8235
|
+
* B: value2 (priority 1)
|
|
8236
|
+
* C: value3 (priority 0)
|
|
8237
|
+
*
|
|
8238
|
+
* Output:
|
|
8239
|
+
* A: A
|
|
8240
|
+
* B: B & !A
|
|
8241
|
+
* C: C & !A & !B
|
|
8242
|
+
*
|
|
8243
|
+
* @param entries Parsed style entries ordered by priority (highest first)
|
|
8244
|
+
* @returns Entries with exclusive conditions, filtered to remove impossible ones
|
|
8245
|
+
*/
|
|
8246
|
+
function buildExclusiveConditions(entries) {
|
|
8247
|
+
const result = [];
|
|
8248
|
+
const priorConditions = [];
|
|
8249
|
+
for (const entry of entries) {
|
|
8250
|
+
let exclusive = entry.condition;
|
|
8251
|
+
for (const prior of priorConditions) {
|
|
8252
|
+
if (prior.kind === "true") continue;
|
|
8253
|
+
if (entry.condition.kind !== "true" && simplifyCondition(and(entry.condition, prior)).kind === "false") continue;
|
|
8254
|
+
exclusive = and(exclusive, not(prior));
|
|
8255
|
+
}
|
|
8256
|
+
const simplified = simplifyCondition(exclusive);
|
|
8257
|
+
if (simplified.kind === "false") continue;
|
|
8258
|
+
result.push({
|
|
8259
|
+
...entry,
|
|
8260
|
+
exclusiveCondition: simplified
|
|
8261
|
+
});
|
|
8262
|
+
if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
|
|
8263
|
+
}
|
|
8264
|
+
return result;
|
|
8265
|
+
}
|
|
8266
|
+
/**
|
|
8267
|
+
* Parse style entries from a value mapping object.
|
|
8268
|
+
*
|
|
8269
|
+
* @param styleKey The style key (e.g., 'padding')
|
|
8270
|
+
* @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
|
|
8271
|
+
* @param parseCondition Function to parse state keys into conditions
|
|
8272
|
+
* @returns Parsed entries ordered by priority (highest first)
|
|
8273
|
+
*/
|
|
8274
|
+
function parseStyleEntries(styleKey, valueMap, parseCondition) {
|
|
8275
|
+
const entries = [];
|
|
8276
|
+
Object.keys(valueMap).forEach((stateKey, index) => {
|
|
8277
|
+
const value = valueMap[stateKey];
|
|
8278
|
+
const condition = stateKey === "" ? trueCondition() : parseCondition(stateKey);
|
|
8279
|
+
entries.push({
|
|
8280
|
+
styleKey,
|
|
8281
|
+
stateKey,
|
|
8282
|
+
value,
|
|
8283
|
+
condition,
|
|
8284
|
+
priority: index
|
|
8285
|
+
});
|
|
8286
|
+
});
|
|
8287
|
+
entries.reverse();
|
|
8288
|
+
return entries;
|
|
8289
|
+
}
|
|
8290
|
+
/**
|
|
8291
|
+
* Merge parsed entries that share the same value.
|
|
8292
|
+
*
|
|
8293
|
+
* When multiple **non-default** state keys map to the same value, their
|
|
8294
|
+
* conditions can be combined with OR and treated as a single entry.
|
|
8295
|
+
* This must happen **before** exclusive expansion and OR branch splitting
|
|
8296
|
+
* to avoid combinatorial explosion and duplicate CSS output.
|
|
8297
|
+
*
|
|
8298
|
+
* **Merging must preserve the authored cascade.** Merging two same-value
|
|
8299
|
+
* entries with priorities `p_h > p_l` lifts the lower-priority entry up
|
|
8300
|
+
* to `p_h` and changes the "blocker" for intermediate-priority entries
|
|
8301
|
+
* from `!C_h` to `!(C_h | C_l) = !C_h & !C_l`. The added `!C_l`
|
|
8302
|
+
* constraint can incorrectly block an intermediate entry that should
|
|
8303
|
+
* have won.
|
|
8304
|
+
*
|
|
8305
|
+
* Two same-value entries with conditions `C_h` (higher priority) and
|
|
8306
|
+
* `C_l` (lower priority) are safe to merge iff for every entry
|
|
8307
|
+
* `e_m` strictly between them in priority with a different value,
|
|
8308
|
+
*
|
|
8309
|
+
* simplify(C_m & C_l & !C_h) = FALSE
|
|
8310
|
+
*
|
|
8311
|
+
* i.e. there is no scenario where the intermediate state could have
|
|
8312
|
+
* matched (`C_m`), the lower-priority same-value entry would also have
|
|
8313
|
+
* matched (`C_l`), and the higher-priority entry would not (`!C_h`).
|
|
8314
|
+
* In such scenarios the intermediate is supposed to win; the merge
|
|
8315
|
+
* would block it by introducing `!C_l`.
|
|
8316
|
+
*
|
|
8317
|
+
* Example (UNSAFE — must not merge):
|
|
8318
|
+
* `{ hovered: 'red', pressed: 'blue', disabled: 'red' }`.
|
|
8319
|
+
* C_h = disabled, C_l = hovered, C_m = pressed. `pressed & hovered &
|
|
8320
|
+
* !disabled` is satisfiable (three independent modifiers), so the
|
|
8321
|
+
* intermediate `pressed` would lose to a merged red rule when both
|
|
8322
|
+
* `pressed` and `hovered` are active — breaking the cascade
|
|
8323
|
+
* `disabled > pressed > hovered`.
|
|
8324
|
+
*
|
|
8325
|
+
* Example (SAFE — still merges):
|
|
8326
|
+
* `{ '': light, '@dark': dark, '@hc': hc, '@dark & @hc': dark }`.
|
|
8327
|
+
* C_h = `@dark & @hc`, C_l = `@dark`, C_m = `@hc`.
|
|
8328
|
+
* `@hc & @dark & !(@dark & @hc) = @hc & @dark & (!@dark | !@hc)`
|
|
8329
|
+
* simplifies to FALSE, so merging the two darks into one `@dark` rule
|
|
8330
|
+
* at the higher priority does not affect the `@hc` rule.
|
|
8331
|
+
*
|
|
8332
|
+
* Default (TRUE) entries are never merged with non-default entries.
|
|
8333
|
+
* Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
|
|
8334
|
+
* condition's participation in exclusive building. Stage 6
|
|
8335
|
+
* `mergeByValue` handles combining rules with identical CSS output
|
|
8336
|
+
* after exclusive conditions are correctly built.
|
|
8337
|
+
*
|
|
8338
|
+
* The merged entry keeps the highest priority of the merged entries.
|
|
8045
8339
|
*/
|
|
8046
|
-
function
|
|
8047
|
-
if (
|
|
8048
|
-
const
|
|
8049
|
-
const
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
seen.add(key);
|
|
8054
|
-
result.push(v);
|
|
8340
|
+
function mergeEntriesByValue(entries) {
|
|
8341
|
+
if (entries.length <= 1) return entries;
|
|
8342
|
+
const merged = [];
|
|
8343
|
+
for (const entry of entries) {
|
|
8344
|
+
if (entry.condition.kind === "true") {
|
|
8345
|
+
merged.push(entry);
|
|
8346
|
+
continue;
|
|
8055
8347
|
}
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8348
|
+
const valueKey = serializeValue(entry.value);
|
|
8349
|
+
let mergeIdx = -1;
|
|
8350
|
+
for (let j = merged.length - 1; j >= 0; j--) {
|
|
8351
|
+
const prev = merged[j];
|
|
8352
|
+
if (prev.condition.kind === "true") continue;
|
|
8353
|
+
if (serializeValue(prev.value) !== valueKey) continue;
|
|
8354
|
+
let safe = true;
|
|
8355
|
+
for (let k = j + 1; k < merged.length; k++) {
|
|
8356
|
+
const inter = merged[k];
|
|
8357
|
+
if (inter.condition.kind === "true") continue;
|
|
8358
|
+
if (serializeValue(inter.value) === valueKey) continue;
|
|
8359
|
+
if (simplifyCondition(and(inter.condition, entry.condition, not(prev.condition))).kind !== "false") {
|
|
8360
|
+
safe = false;
|
|
8361
|
+
break;
|
|
8362
|
+
}
|
|
8363
|
+
}
|
|
8364
|
+
if (safe) {
|
|
8365
|
+
mergeIdx = j;
|
|
8366
|
+
break;
|
|
8367
|
+
}
|
|
8065
8368
|
}
|
|
8066
|
-
if (
|
|
8369
|
+
if (mergeIdx >= 0) {
|
|
8370
|
+
const prev = merged[mergeIdx];
|
|
8371
|
+
const newCondition = simplifyCondition(or(prev.condition, entry.condition));
|
|
8372
|
+
merged[mergeIdx] = {
|
|
8373
|
+
styleKey: prev.styleKey,
|
|
8374
|
+
stateKey: `${prev.stateKey} | ${entry.stateKey}`,
|
|
8375
|
+
value: prev.value,
|
|
8376
|
+
condition: newCondition,
|
|
8377
|
+
priority: Math.max(prev.priority, entry.priority)
|
|
8378
|
+
};
|
|
8379
|
+
} else merged.push(entry);
|
|
8067
8380
|
}
|
|
8068
|
-
return
|
|
8381
|
+
return merged;
|
|
8382
|
+
}
|
|
8383
|
+
function serializeValue(value) {
|
|
8384
|
+
if (value === null || value === void 0) return "null";
|
|
8385
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
8386
|
+
return JSON.stringify(value);
|
|
8069
8387
|
}
|
|
8070
8388
|
/**
|
|
8071
|
-
*
|
|
8389
|
+
* Eliminate redundant state dimensions from a value map.
|
|
8072
8390
|
*
|
|
8073
|
-
* AND
|
|
8074
|
-
*
|
|
8391
|
+
* When a value map contains compound AND state keys (e.g. `@dark & @hc`),
|
|
8392
|
+
* checks whether any state atom is a "don't-care" variable — i.e. the
|
|
8393
|
+
* value is the same whether that atom is present or absent. Redundant
|
|
8394
|
+
* atoms are removed from all keys and duplicate entries are collapsed.
|
|
8075
8395
|
*
|
|
8076
|
-
*
|
|
8077
|
-
*
|
|
8396
|
+
* This runs **before** condition parsing so that downstream stages
|
|
8397
|
+
* (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
|
|
8398
|
+
* never see the irrelevant dimension, producing simpler, smaller CSS.
|
|
8399
|
+
*
|
|
8400
|
+
* Only pure top-level AND combinations are eligible. Keys that contain
|
|
8401
|
+
* `|`, `^`, or `,` at the top level are treated as opaque single atoms.
|
|
8402
|
+
*
|
|
8403
|
+
* @example
|
|
8404
|
+
* { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
|
|
8405
|
+
* // @hc is redundant → { '': A, '@dark': B }
|
|
8078
8406
|
*/
|
|
8079
|
-
function
|
|
8080
|
-
const
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
isImpossible: true
|
|
8087
|
-
};
|
|
8088
|
-
const newVariants = [];
|
|
8089
|
-
for (const current of currentVariants) for (const childVariant of childCSS.variants) {
|
|
8090
|
-
const merged = mergeVariants(current, childVariant);
|
|
8091
|
-
if (merged !== null) newVariants.push(merged);
|
|
8092
|
-
}
|
|
8093
|
-
if (newVariants.length === 0) return {
|
|
8094
|
-
variants: [],
|
|
8095
|
-
isImpossible: true
|
|
8407
|
+
function extractCompoundStates(valueMap) {
|
|
8408
|
+
const keys = Object.keys(valueMap);
|
|
8409
|
+
if (keys.length < 3 || !keys.some((k) => k.includes("&"))) return valueMap;
|
|
8410
|
+
const entries = keys.map((key) => {
|
|
8411
|
+
return {
|
|
8412
|
+
atoms: splitTopLevelAnd(key) ?? [key],
|
|
8413
|
+
value: valueMap[key]
|
|
8096
8414
|
};
|
|
8097
|
-
|
|
8415
|
+
});
|
|
8416
|
+
const allAtoms = /* @__PURE__ */ new Set();
|
|
8417
|
+
for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
|
|
8418
|
+
const redundant = /* @__PURE__ */ new Set();
|
|
8419
|
+
for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
|
|
8420
|
+
if (redundant.size === 0) return valueMap;
|
|
8421
|
+
const newMap = {};
|
|
8422
|
+
for (const e of entries) {
|
|
8423
|
+
const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
|
|
8424
|
+
if (!(newKey in newMap)) newMap[newKey] = e.value;
|
|
8098
8425
|
}
|
|
8099
|
-
return
|
|
8100
|
-
variants: currentVariants,
|
|
8101
|
-
isImpossible: false
|
|
8102
|
-
};
|
|
8426
|
+
return newMap;
|
|
8103
8427
|
}
|
|
8104
8428
|
/**
|
|
8105
|
-
*
|
|
8106
|
-
*
|
|
8107
|
-
* For an AND child that is OR(A, B), transforms it to OR(A, B & !A)
|
|
8108
|
-
* so that when andToCSS does a Cartesian product, the resulting
|
|
8109
|
-
* CSS variants don't overlap.
|
|
8429
|
+
* Split a state key by top-level `&` operators.
|
|
8110
8430
|
*
|
|
8111
|
-
*
|
|
8112
|
-
*
|
|
8113
|
-
*
|
|
8114
|
-
* product naturally handles deduplication.
|
|
8431
|
+
* Returns `null` if the key contains `|`, `^`, or `,` at the top level
|
|
8432
|
+
* (making it ineligible for atom-level extraction).
|
|
8433
|
+
* Returns `[]` for the empty string (default key).
|
|
8115
8434
|
*/
|
|
8116
|
-
function
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8435
|
+
function splitTopLevelAnd(key) {
|
|
8436
|
+
if (key === "") return [];
|
|
8437
|
+
const parts = [];
|
|
8438
|
+
let depth = 0;
|
|
8439
|
+
let current = "";
|
|
8440
|
+
for (const ch of key) {
|
|
8441
|
+
if (ch === "(" || ch === "[") depth++;
|
|
8442
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
8443
|
+
if (depth === 0) {
|
|
8444
|
+
if (ch === "&") {
|
|
8445
|
+
const trimmed = current.trim();
|
|
8446
|
+
if (trimmed) parts.push(trimmed);
|
|
8447
|
+
current = "";
|
|
8448
|
+
continue;
|
|
8130
8449
|
}
|
|
8131
|
-
|
|
8450
|
+
if (ch === "|" || ch === "^" || ch === ",") return null;
|
|
8132
8451
|
}
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
children: exclusiveBranches
|
|
8139
|
-
};
|
|
8140
|
-
});
|
|
8452
|
+
current += ch;
|
|
8453
|
+
}
|
|
8454
|
+
const trimmed = current.trim();
|
|
8455
|
+
if (trimmed) parts.push(trimmed);
|
|
8456
|
+
return parts;
|
|
8141
8457
|
}
|
|
8142
8458
|
/**
|
|
8143
|
-
*
|
|
8144
|
-
*
|
|
8145
|
-
* create overlapping CSS variants that need exclusive expansion.
|
|
8459
|
+
* An atom is redundant when every entry that contains it has a matching
|
|
8460
|
+
* partner (same remaining atoms, atom absent) with the same value.
|
|
8146
8461
|
*/
|
|
8147
|
-
function
|
|
8148
|
-
const
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8462
|
+
function isAtomRedundant(entries, atom) {
|
|
8463
|
+
const withAtom = entries.filter((e) => e.atoms.includes(atom));
|
|
8464
|
+
if (withAtom.length === 0) return false;
|
|
8465
|
+
for (const wa of withAtom) {
|
|
8466
|
+
const remaining = wa.atoms.filter((a) => a !== atom);
|
|
8467
|
+
const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
|
|
8468
|
+
if (!pair) return false;
|
|
8469
|
+
if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
|
|
8153
8470
|
}
|
|
8154
|
-
return
|
|
8471
|
+
return true;
|
|
8155
8472
|
}
|
|
8156
8473
|
/**
|
|
8157
|
-
*
|
|
8474
|
+
* Check if a value is a style value mapping (object with state keys)
|
|
8475
|
+
*/
|
|
8476
|
+
function isValueMapping(value) {
|
|
8477
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
8478
|
+
}
|
|
8479
|
+
/**
|
|
8480
|
+
* Expand OR conditions in parsed entries into multiple exclusive entries.
|
|
8158
8481
|
*
|
|
8159
|
-
*
|
|
8160
|
-
*
|
|
8161
|
-
*
|
|
8482
|
+
* For an entry with condition `A | B | C`, this creates 3 entries:
|
|
8483
|
+
* - condition: A
|
|
8484
|
+
* - condition: B & !A
|
|
8485
|
+
* - condition: C & !A & !B
|
|
8162
8486
|
*
|
|
8163
|
-
*
|
|
8164
|
-
*
|
|
8165
|
-
*
|
|
8487
|
+
* This ensures OR branches are mutually exclusive BEFORE the main
|
|
8488
|
+
* exclusive condition building pass.
|
|
8489
|
+
*
|
|
8490
|
+
* @param entries Parsed entries (may contain OR conditions)
|
|
8491
|
+
* @returns Expanded entries with OR branches made exclusive
|
|
8166
8492
|
*/
|
|
8167
|
-
function
|
|
8168
|
-
const
|
|
8169
|
-
for (const
|
|
8170
|
-
const
|
|
8171
|
-
|
|
8172
|
-
allVariants.push(...childCSS.variants);
|
|
8493
|
+
function expandOrConditions(entries) {
|
|
8494
|
+
const result = [];
|
|
8495
|
+
for (const entry of entries) {
|
|
8496
|
+
const expanded = expandSingleEntry(entry);
|
|
8497
|
+
result.push(...expanded);
|
|
8173
8498
|
}
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8499
|
+
return result;
|
|
8500
|
+
}
|
|
8501
|
+
/**
|
|
8502
|
+
* Expand a single entry's OR condition into multiple exclusive entries.
|
|
8503
|
+
*
|
|
8504
|
+
* Note: branches are NOT sorted by at-rule context here (unlike the
|
|
8505
|
+
* `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
|
|
8506
|
+
* the product of De Morgan negation, so each branch is expected to render
|
|
8507
|
+
* independently in its own scope and at-rule sort isn't load-bearing.
|
|
8508
|
+
* The post-build pass needs the sort because it has to preserve at-rule
|
|
8509
|
+
* wrapping across branches that came from negating a compound at-rule.
|
|
8510
|
+
*
|
|
8511
|
+
* Skip optimisation: when every branch renders into the same at-rule /
|
|
8512
|
+
* root / parent / own context (see "Key Design Decision #2" in
|
|
8513
|
+
* `docs/pipeline.md`), forcing mutual exclusivity here produces dead
|
|
8514
|
+
* `B & !A`-style branches that materialization later folds back into
|
|
8515
|
+
* `:is(A, B)`. Bail out and let `materialize.ts` collapse the OR via
|
|
8516
|
+
* `mergeVariantsIntoSelectorGroups`. Cross-entry exclusivity is still
|
|
8517
|
+
* enforced by `buildExclusiveConditions`; the post-build `expandExclusiveOrs`
|
|
8518
|
+
* pass still handles De Morgan ORs whose branches actually differ in
|
|
8519
|
+
* context.
|
|
8520
|
+
*/
|
|
8521
|
+
function expandSingleEntry(entry) {
|
|
8522
|
+
const orBranches = collectOrBranches(entry.condition);
|
|
8523
|
+
if (orBranches.length <= 1) return [entry];
|
|
8524
|
+
if (!branchesProduceDifferentContexts(orBranches)) return [entry];
|
|
8525
|
+
const result = [];
|
|
8526
|
+
const priorBranches = [];
|
|
8527
|
+
for (let i = 0; i < orBranches.length; i++) {
|
|
8528
|
+
const branch = orBranches[i];
|
|
8529
|
+
let exclusiveBranch = branch;
|
|
8530
|
+
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
8531
|
+
const simplified = simplifyCondition(exclusiveBranch);
|
|
8532
|
+
if (simplified.kind === "false") {
|
|
8533
|
+
priorBranches.push(branch);
|
|
8534
|
+
continue;
|
|
8535
|
+
}
|
|
8536
|
+
result.push({
|
|
8537
|
+
...entry,
|
|
8538
|
+
stateKey: `${entry.stateKey}[${i}]`,
|
|
8539
|
+
condition: simplified
|
|
8540
|
+
});
|
|
8541
|
+
priorBranches.push(branch);
|
|
8542
|
+
}
|
|
8543
|
+
return result;
|
|
8182
8544
|
}
|
|
8183
8545
|
/**
|
|
8184
|
-
*
|
|
8546
|
+
* Collect top-level OR branches from a condition.
|
|
8547
|
+
*
|
|
8548
|
+
* For `A | B | C`, returns [A, B, C]
|
|
8549
|
+
* For `A & B`, returns [A & B] (single branch)
|
|
8550
|
+
* For `A | (B & C)`, returns [A, B & C]
|
|
8185
8551
|
*/
|
|
8186
|
-
function
|
|
8187
|
-
if (
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
const
|
|
8191
|
-
|
|
8552
|
+
function collectOrBranches(condition) {
|
|
8553
|
+
if (condition.kind === "true" || condition.kind === "false") return [condition];
|
|
8554
|
+
if (isCompoundCondition(condition) && condition.operator === "OR") {
|
|
8555
|
+
const branches = [];
|
|
8556
|
+
for (const child of condition.children) branches.push(...collectOrBranches(child));
|
|
8557
|
+
return branches;
|
|
8192
8558
|
}
|
|
8193
|
-
return
|
|
8559
|
+
return [condition];
|
|
8194
8560
|
}
|
|
8195
8561
|
/**
|
|
8196
|
-
*
|
|
8197
|
-
* own, starting) into a single variant with a SelectorGroup.
|
|
8562
|
+
* Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
|
|
8198
8563
|
*
|
|
8199
|
-
*
|
|
8200
|
-
*
|
|
8564
|
+
* This handles ORs that arise from De Morgan expansion during negation:
|
|
8565
|
+
* !(A & B) = !A | !B
|
|
8566
|
+
*
|
|
8567
|
+
* These ORs need to be made exclusive to avoid overlapping CSS rules:
|
|
8568
|
+
* !A | !B → !A | (A & !B)
|
|
8569
|
+
*
|
|
8570
|
+
* This is logically equivalent but ensures each branch has proper context.
|
|
8571
|
+
*
|
|
8572
|
+
* Example:
|
|
8573
|
+
* Input: { "": V1, "@supports(...) & :has()": V2 }
|
|
8574
|
+
* V2's exclusive = @supports & :has
|
|
8575
|
+
* V1's exclusive = !(@supports & :has) = !@supports | !:has
|
|
8576
|
+
*
|
|
8577
|
+
* Without this fix: V1 gets two rules:
|
|
8578
|
+
* - @supports (not ...) → V1 ✓
|
|
8579
|
+
* - :not(:has()) → V1 ✗ (missing @supports context!)
|
|
8580
|
+
*
|
|
8581
|
+
* With this fix: V1 gets two exclusive rules:
|
|
8582
|
+
* - @supports (not ...) → V1 ✓
|
|
8583
|
+
* - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
|
|
8201
8584
|
*/
|
|
8202
|
-
function
|
|
8203
|
-
if (variants.length <= 1) return variants;
|
|
8204
|
-
const groups = /* @__PURE__ */ new Map();
|
|
8205
|
-
for (const v of variants) {
|
|
8206
|
-
const key = getVariantContextKey(v);
|
|
8207
|
-
const group = groups.get(key);
|
|
8208
|
-
if (group) group.push(v);
|
|
8209
|
-
else groups.set(key, [v]);
|
|
8210
|
-
}
|
|
8585
|
+
function expandExclusiveOrs(entries) {
|
|
8211
8586
|
const result = [];
|
|
8212
|
-
for (const
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
continue;
|
|
8216
|
-
}
|
|
8217
|
-
const withSelectors = [];
|
|
8218
|
-
const withoutSelectors = [];
|
|
8219
|
-
for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
|
|
8220
|
-
else withSelectors.push(v);
|
|
8221
|
-
result.push(...withoutSelectors);
|
|
8222
|
-
if (withSelectors.length <= 1) {
|
|
8223
|
-
result.push(...withSelectors);
|
|
8224
|
-
continue;
|
|
8225
|
-
}
|
|
8226
|
-
result.push(factorAndGroup(withSelectors));
|
|
8587
|
+
for (const entry of entries) {
|
|
8588
|
+
const expanded = expandExclusiveConditionOrs(entry);
|
|
8589
|
+
result.push(...expanded);
|
|
8227
8590
|
}
|
|
8228
8591
|
return result;
|
|
8229
8592
|
}
|
|
8230
8593
|
/**
|
|
8231
|
-
*
|
|
8232
|
-
* a single variant with a SelectorGroup for the remaining (differing)
|
|
8233
|
-
* conditions.
|
|
8234
|
-
*
|
|
8235
|
-
* Precondition: all variants must share the same context key (identical
|
|
8236
|
-
* at-rules, root/parent/own/selector groups, startingStyle).
|
|
8594
|
+
* Check if a condition involves at-rules (media, container, supports, starting)
|
|
8237
8595
|
*/
|
|
8238
|
-
function
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
|
|
8244
|
-
}
|
|
8245
|
-
}
|
|
8246
|
-
const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
|
|
8247
|
-
const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
|
|
8248
|
-
const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
|
|
8249
|
-
const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
|
|
8250
|
-
const branches = [];
|
|
8251
|
-
let hasEmptyBranch = false;
|
|
8252
|
-
for (const v of variants) {
|
|
8253
|
-
const branch = [];
|
|
8254
|
-
for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
|
|
8255
|
-
for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
|
|
8256
|
-
if (branch.length > 0) branches.push(branch);
|
|
8257
|
-
else hasEmptyBranch = true;
|
|
8258
|
-
}
|
|
8259
|
-
if (hasEmptyBranch) return {
|
|
8260
|
-
...variants[0],
|
|
8261
|
-
modifierConditions: commonModifiers,
|
|
8262
|
-
pseudoConditions: commonPseudos
|
|
8263
|
-
};
|
|
8264
|
-
const factoredGroups = tryFactorIntoDimensions(branches);
|
|
8265
|
-
if (factoredGroups) return {
|
|
8266
|
-
modifierConditions: commonModifiers,
|
|
8267
|
-
pseudoConditions: commonPseudos,
|
|
8268
|
-
selectorGroups: [...variants[0].selectorGroups, ...factoredGroups],
|
|
8269
|
-
ownGroups: [...variants[0].ownGroups],
|
|
8270
|
-
mediaConditions: [...variants[0].mediaConditions],
|
|
8271
|
-
containerConditions: [...variants[0].containerConditions],
|
|
8272
|
-
supportsConditions: [...variants[0].supportsConditions],
|
|
8273
|
-
rootGroups: [...variants[0].rootGroups],
|
|
8274
|
-
parentGroups: [...variants[0].parentGroups],
|
|
8275
|
-
startingStyle: variants[0].startingStyle
|
|
8276
|
-
};
|
|
8277
|
-
return {
|
|
8278
|
-
modifierConditions: commonModifiers,
|
|
8279
|
-
pseudoConditions: commonPseudos,
|
|
8280
|
-
selectorGroups: [...variants[0].selectorGroups, {
|
|
8281
|
-
branches,
|
|
8282
|
-
negated: false
|
|
8283
|
-
}],
|
|
8284
|
-
ownGroups: [...variants[0].ownGroups],
|
|
8285
|
-
mediaConditions: [...variants[0].mediaConditions],
|
|
8286
|
-
containerConditions: [...variants[0].containerConditions],
|
|
8287
|
-
supportsConditions: [...variants[0].supportsConditions],
|
|
8288
|
-
rootGroups: [...variants[0].rootGroups],
|
|
8289
|
-
parentGroups: [...variants[0].parentGroups],
|
|
8290
|
-
startingStyle: variants[0].startingStyle
|
|
8291
|
-
};
|
|
8596
|
+
function hasAtRuleContext(node) {
|
|
8597
|
+
if (node.kind === "true" || node.kind === "false") return false;
|
|
8598
|
+
if (node.kind === "state") return node.type === "media" || node.type === "container" || node.type === "supports" || node.type === "starting";
|
|
8599
|
+
if (node.kind === "compound") return node.children.some(hasAtRuleContext);
|
|
8600
|
+
return false;
|
|
8292
8601
|
}
|
|
8293
8602
|
/**
|
|
8294
|
-
*
|
|
8295
|
-
* modifier attribute dimensions and return one SelectorGroup per dimension.
|
|
8603
|
+
* Sort OR branches to prioritize at-rule conditions first.
|
|
8296
8604
|
*
|
|
8297
|
-
*
|
|
8298
|
-
*
|
|
8605
|
+
* This is critical for correct CSS generation. For `!A | !B` where A is at-rule
|
|
8606
|
+
* and B is modifier, we want:
|
|
8607
|
+
* - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
|
|
8608
|
+
* - Branch 1: A & !B (modifier negation with at-rule context)
|
|
8609
|
+
*
|
|
8610
|
+
* If we process in wrong order (!B first), we'd get:
|
|
8611
|
+
* - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
|
|
8612
|
+
* - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
|
|
8299
8613
|
*/
|
|
8300
|
-
function
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
if (
|
|
8305
|
-
if (!
|
|
8306
|
-
|
|
8307
|
-
}
|
|
8308
|
-
if (dimensions.size < 2) return null;
|
|
8309
|
-
for (const branch of branches) {
|
|
8310
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8311
|
-
for (const cond of branch) {
|
|
8312
|
-
const attr = cond.attribute;
|
|
8313
|
-
if (seen.has(attr)) return null;
|
|
8314
|
-
seen.add(attr);
|
|
8315
|
-
}
|
|
8316
|
-
if (seen.size !== dimensions.size) return null;
|
|
8317
|
-
}
|
|
8318
|
-
let expectedCount = 1;
|
|
8319
|
-
for (const vals of dimensions.values()) expectedCount *= vals.size;
|
|
8320
|
-
if (branches.length !== expectedCount) return null;
|
|
8321
|
-
return [...dimensions.values()].map((vals) => ({
|
|
8322
|
-
branches: [...vals.values()].map((cond) => [cond]),
|
|
8323
|
-
negated: false
|
|
8324
|
-
}));
|
|
8614
|
+
function sortOrBranchesForExpansion(branches) {
|
|
8615
|
+
return [...branches].sort((a, b) => {
|
|
8616
|
+
const aHasAtRule = hasAtRuleContext(a);
|
|
8617
|
+
const bHasAtRule = hasAtRuleContext(b);
|
|
8618
|
+
if (aHasAtRule && !bHasAtRule) return -1;
|
|
8619
|
+
if (!aHasAtRule && bHasAtRule) return 1;
|
|
8620
|
+
return 0;
|
|
8621
|
+
});
|
|
8325
8622
|
}
|
|
8326
8623
|
/**
|
|
8327
|
-
*
|
|
8624
|
+
* Expand ORs in a single entry's exclusive condition
|
|
8328
8625
|
*/
|
|
8329
|
-
function
|
|
8330
|
-
|
|
8331
|
-
if (
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
const
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
}
|
|
8345
|
-
for (const [name, conditions] of byName) {
|
|
8346
|
-
const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
|
|
8347
|
-
const namePrefix = name ? `${name} ` : "";
|
|
8348
|
-
atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
|
|
8626
|
+
function expandExclusiveConditionOrs(entry) {
|
|
8627
|
+
let orBranches = collectOrBranches(entry.exclusiveCondition);
|
|
8628
|
+
if (orBranches.length <= 1) return [entry];
|
|
8629
|
+
if (!branchesProduceDifferentContexts(orBranches)) return [entry];
|
|
8630
|
+
orBranches = sortOrBranchesForExpansion(orBranches);
|
|
8631
|
+
const result = [];
|
|
8632
|
+
const priorBranches = [];
|
|
8633
|
+
for (let i = 0; i < orBranches.length; i++) {
|
|
8634
|
+
const branch = orBranches[i];
|
|
8635
|
+
let exclusiveBranch = branch;
|
|
8636
|
+
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
8637
|
+
const simplified = simplifyCondition(exclusiveBranch);
|
|
8638
|
+
if (simplified.kind === "false") {
|
|
8639
|
+
priorBranches.push(branch);
|
|
8640
|
+
continue;
|
|
8349
8641
|
}
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
const selectorCond = `selector(${c.condition})`;
|
|
8355
|
-
return c.negated ? `(not ${selectorCond})` : selectorCond;
|
|
8356
|
-
} else {
|
|
8357
|
-
const featureCond = `(${c.condition})`;
|
|
8358
|
-
return c.negated ? `(not ${featureCond})` : featureCond;
|
|
8359
|
-
}
|
|
8642
|
+
result.push({
|
|
8643
|
+
...entry,
|
|
8644
|
+
stateKey: `${entry.stateKey}[or:${i}]`,
|
|
8645
|
+
exclusiveCondition: simplified
|
|
8360
8646
|
});
|
|
8361
|
-
|
|
8647
|
+
priorBranches.push(branch);
|
|
8362
8648
|
}
|
|
8363
|
-
return
|
|
8649
|
+
return result;
|
|
8364
8650
|
}
|
|
8365
8651
|
//#endregion
|
|
8366
8652
|
//#region src/utils/case-converter.ts
|
|
@@ -8427,6 +8713,15 @@ function emitWarning(code, message) {
|
|
|
8427
8713
|
const MAX_XOR_CHAIN_LENGTH = 4;
|
|
8428
8714
|
const parseCache = new Lru(5e3);
|
|
8429
8715
|
/**
|
|
8716
|
+
* Chrome-internal pseudo-classes (e.g. `:-internal-autofill-selected`,
|
|
8717
|
+
* `:-internal-autofill-previewed`) cannot be targeted from user CSS and
|
|
8718
|
+
* may invalidate the surrounding rule in Safari even when wrapped in
|
|
8719
|
+
* forgiving `:is(...)`. The regex matches both bare uses and references
|
|
8720
|
+
* inside enhanced pseudo arguments like `:is(:-webkit-autofill,
|
|
8721
|
+
* :-internal-autofill-selected)`.
|
|
8722
|
+
*/
|
|
8723
|
+
const INTERNAL_PSEUDO_PATTERN = /:-internal-[a-z0-9-]+/g;
|
|
8724
|
+
/**
|
|
8430
8725
|
* Pattern for tokenizing state notation.
|
|
8431
8726
|
* Matches: operators, parentheses, @-prefixed states, value mods, boolean mods,
|
|
8432
8727
|
* pseudo-classes, class selectors, and attribute selectors.
|
|
@@ -8619,7 +8914,15 @@ var Parser = class {
|
|
|
8619
8914
|
return createPseudoCondition(value, false, value);
|
|
8620
8915
|
}
|
|
8621
8916
|
if (value.startsWith(".")) return createPseudoCondition(value, false, value);
|
|
8622
|
-
if (value.startsWith("["))
|
|
8917
|
+
if (value.startsWith("[")) {
|
|
8918
|
+
const attrMatch = /^\[\s*([a-zA-Z_][\w-]*)\s*(?:(=|\^=|\$=|\*=)\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+)))?\s*\]$/.exec(value);
|
|
8919
|
+
if (attrMatch) {
|
|
8920
|
+
const [, attribute, operator, dq, sq, bare] = attrMatch;
|
|
8921
|
+
if (operator === void 0) return createModifierCondition(attribute, void 0, "=", false, value);
|
|
8922
|
+
return createModifierCondition(attribute, dq ?? sq ?? bare, operator, false, value);
|
|
8923
|
+
}
|
|
8924
|
+
return createPseudoCondition(value, false, value);
|
|
8925
|
+
}
|
|
8623
8926
|
if (value.includes("=")) return this.parseValueModifier(value);
|
|
8624
8927
|
return this.parseBooleanModifier(value);
|
|
8625
8928
|
}
|
|
@@ -8844,6 +9147,14 @@ function parseStateKey(stateKey, options = {}) {
|
|
|
8844
9147
|
const cacheKey = trimmed + "\0" + (options.isSubElement ? "1" : "0") + "\0" + localStatesKey;
|
|
8845
9148
|
const cached = parseCache.get(cacheKey);
|
|
8846
9149
|
if (cached) return cached;
|
|
9150
|
+
if (isDevEnv()) {
|
|
9151
|
+
INTERNAL_PSEUDO_PATTERN.lastIndex = 0;
|
|
9152
|
+
const internalMatches = trimmed.match(INTERNAL_PSEUDO_PATTERN);
|
|
9153
|
+
if (internalMatches && internalMatches.length > 0) {
|
|
9154
|
+
const unique = Array.from(new Set(internalMatches));
|
|
9155
|
+
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.`);
|
|
9156
|
+
}
|
|
9157
|
+
}
|
|
8847
9158
|
const result = new Parser(tokenize(trimmed), options).parse();
|
|
8848
9159
|
parseCache.set(cacheKey, result);
|
|
8849
9160
|
return result;
|
|
@@ -8913,7 +9224,15 @@ function runPipeline(styles, parserContext) {
|
|
|
8913
9224
|
function processStyles(styles, selectorSuffix, parserContext, allRules) {
|
|
8914
9225
|
const keys = Object.keys(styles);
|
|
8915
9226
|
const selectorKeys = keys.filter((key) => isSelector(key));
|
|
8916
|
-
const styleKeys =
|
|
9227
|
+
const styleKeys = [];
|
|
9228
|
+
for (const key of keys) {
|
|
9229
|
+
if (isSelector(key) || key.startsWith("@")) continue;
|
|
9230
|
+
if (key.startsWith(":")) {
|
|
9231
|
+
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.`);
|
|
9232
|
+
continue;
|
|
9233
|
+
}
|
|
9234
|
+
styleKeys.push(key);
|
|
9235
|
+
}
|
|
8917
9236
|
processNestedSelectors(styles, selectorKeys, selectorSuffix, parserContext, allRules);
|
|
8918
9237
|
processHandlerQueue(buildHandlerQueue(styleKeys, styles), selectorSuffix, parserContext, allRules);
|
|
8919
9238
|
}
|
|
@@ -9796,7 +10115,8 @@ function createDefaultConfig(isTest) {
|
|
|
9796
10115
|
return {
|
|
9797
10116
|
maxRulesPerSheet: 8192,
|
|
9798
10117
|
forceTextInjection: isTest ?? false,
|
|
9799
|
-
devMode: isDevEnv()
|
|
10118
|
+
devMode: isDevEnv(),
|
|
10119
|
+
namePrefix: "t"
|
|
9800
10120
|
};
|
|
9801
10121
|
}
|
|
9802
10122
|
/**
|
|
@@ -10038,6 +10358,7 @@ function configure(config = {}) {
|
|
|
10038
10358
|
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.");
|
|
10039
10359
|
return;
|
|
10040
10360
|
}
|
|
10361
|
+
if (config.namePrefix !== void 0) validateNamePrefix(config.namePrefix);
|
|
10041
10362
|
let mergedStates = {};
|
|
10042
10363
|
let mergedUnits = {};
|
|
10043
10364
|
let mergedFuncs = {};
|
|
@@ -10189,6 +10510,17 @@ function getConfig() {
|
|
|
10189
10510
|
return currentConfig;
|
|
10190
10511
|
}
|
|
10191
10512
|
/**
|
|
10513
|
+
* Get the configured prefix used for every generated identifier
|
|
10514
|
+
* (class names, keyframe names, counter-style names).
|
|
10515
|
+
*
|
|
10516
|
+
* Falls back to the default prefix (`'t'`) when `configure()` has not
|
|
10517
|
+
* been called yet — this matches the auto-configuration behavior used
|
|
10518
|
+
* by the rest of the system.
|
|
10519
|
+
*/
|
|
10520
|
+
function getNamePrefix() {
|
|
10521
|
+
return currentConfig?.namePrefix ?? "t";
|
|
10522
|
+
}
|
|
10523
|
+
/**
|
|
10192
10524
|
* Get the global injector instance.
|
|
10193
10525
|
* Auto-configures with defaults if not already configured.
|
|
10194
10526
|
*/
|
|
@@ -10222,6 +10554,6 @@ function resetConfig() {
|
|
|
10222
10554
|
delete storage[GLOBAL_INJECTOR_KEY];
|
|
10223
10555
|
}
|
|
10224
10556
|
//#endregion
|
|
10225
|
-
export {
|
|
10557
|
+
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 };
|
|
10226
10558
|
|
|
10227
|
-
//# sourceMappingURL=config-
|
|
10559
|
+
//# sourceMappingURL=config-BBiyxMCe.js.map
|