@tenphi/tasty 2.5.0 → 2.6.1
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/{collector-DROCOiaT.js → collector-CLKusMeM.js} +3 -3
- package/dist/{collector-DROCOiaT.js.map → collector-CLKusMeM.js.map} +1 -1
- package/dist/{collector-BQHl-atL.d.ts → collector-CoababzP.d.ts} +2 -2
- package/dist/{config-CzzTHmtS.d.ts → config-Cp05bCvj.d.ts} +2 -2
- package/dist/{config-JokB1Lc8.js → config-D0ZQMdY8.js} +1278 -1229
- package/dist/config-D0ZQMdY8.js.map +1 -0
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +5 -5
- package/dist/{core-CW4XEUFk.js → core-DOcbMGRf.js} +5 -5
- package/dist/{core-CW4XEUFk.js.map → core-DOcbMGRf.js.map} +1 -1
- package/dist/{css-writer-Jv468wSl.js → css-writer-BZdDWI6L.js} +3 -3
- package/dist/{css-writer-Jv468wSl.js.map → css-writer-BZdDWI6L.js.map} +1 -1
- package/dist/{format-rules-B0vbh8Qz.js → format-rules-BT_JwzeT.js} +2 -2
- package/dist/{format-rules-B0vbh8Qz.js.map → format-rules-BT_JwzeT.js.map} +1 -1
- package/dist/{hydrate-BO6nlAeD.js → hydrate-BLh7OkxZ.js} +2 -2
- package/dist/{hydrate-BO6nlAeD.js.map → hydrate-BLh7OkxZ.js.map} +1 -1
- package/dist/{index-Dy74C11K.d.ts → index-B_QCrcpe.d.ts} +6 -2
- package/dist/{index-DMGEDjlc.d.ts → index-sk1sxVI3.d.ts} +8 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.js +6 -6
- package/dist/{keyframes-J_JNrpdh.js → keyframes-DW6FxDsz.js} +2 -2
- package/dist/{keyframes-J_JNrpdh.js.map → keyframes-DW6FxDsz.js.map} +1 -1
- package/dist/{merge-styles-Du-eC7zp.js → merge-styles-Bv2DKtHJ.js} +2 -2
- package/dist/{merge-styles-Du-eC7zp.js.map → merge-styles-Bv2DKtHJ.js.map} +1 -1
- package/dist/{merge-styles-BS-mpcci.d.ts → merge-styles-DQO22J7_.d.ts} +2 -2
- package/dist/{resolve-recipes-DPRT3FMM.js → resolve-recipes-x4ElcO5U.js} +3 -3
- package/dist/{resolve-recipes-DPRT3FMM.js.map → resolve-recipes-x4ElcO5U.js.map} +1 -1
- package/dist/ssr/astro-client.js +1 -1
- package/dist/ssr/astro.js +3 -3
- 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/static/index.d.ts +2 -2
- package/dist/static/index.js +1 -1
- package/dist/zero/babel.d.ts +1 -1
- package/dist/zero/babel.js +4 -4
- package/dist/zero/index.d.ts +1 -1
- package/dist/zero/index.js +1 -1
- package/docs/styles.md +9 -7
- package/package.json +5 -5
- package/dist/config-JokB1Lc8.js.map +0 -1
|
@@ -3321,10 +3321,13 @@ function resolveFontFamily(font, fontFamily) {
|
|
|
3321
3321
|
/**
|
|
3322
3322
|
* Handles typography preset and individual font properties.
|
|
3323
3323
|
*
|
|
3324
|
-
* Preset syntax uses `/` to separate name from
|
|
3324
|
+
* Preset syntax uses `/` to separate the name from one or more
|
|
3325
|
+
* space-separated modifiers:
|
|
3325
3326
|
* - `preset="h1"` — name only
|
|
3326
3327
|
* - `preset="h2 / strong"` — name + modifier
|
|
3328
|
+
* - `preset="h2 / strong italic"` — name + multiple modifiers
|
|
3327
3329
|
* - `preset="bold"` — modifier-only shorthand (name defaults to `inherit`)
|
|
3330
|
+
* - `preset="bold italic"` — modifier-only shorthand with multiple modifiers
|
|
3328
3331
|
*
|
|
3329
3332
|
* When `preset` is defined, it sets up CSS custom properties for typography.
|
|
3330
3333
|
* Individual font props can be used with or without `preset`:
|
|
@@ -3344,14 +3347,17 @@ function presetStyle({ preset, fontSize, lineHeight, textTransform, letterSpacin
|
|
|
3344
3347
|
const { parts } = parseStyle(preset === true ? "" : String(preset)).groups[0] ?? { parts: [] };
|
|
3345
3348
|
const namePart = parts[0];
|
|
3346
3349
|
const modPart = parts[1];
|
|
3350
|
+
const nameTokens = namePart?.all ?? [];
|
|
3351
|
+
const isModOnly = nameTokens.length > 0 && nameTokens.every((t) => PRESET_MODIFIERS.has(t));
|
|
3347
3352
|
const nameToken = namePart?.mods[0] ?? namePart?.values[0] ?? "";
|
|
3348
|
-
const isModOnly = PRESET_MODIFIERS.has(nameToken);
|
|
3349
3353
|
const name = isModOnly ? "inherit" : nameToken || "inherit";
|
|
3350
|
-
const
|
|
3351
|
-
const
|
|
3352
|
-
const
|
|
3353
|
-
const
|
|
3354
|
-
const
|
|
3354
|
+
const modTokens = isModOnly ? nameTokens : modPart?.all ?? [];
|
|
3355
|
+
const activeMods = /* @__PURE__ */ new Set();
|
|
3356
|
+
for (const tok of modTokens) if (PRESET_MODIFIERS.has(tok)) activeMods.add(tok);
|
|
3357
|
+
const isStrong = activeMods.has("strong") || activeMods.has("bold");
|
|
3358
|
+
const isItalic = activeMods.has("italic");
|
|
3359
|
+
const isIcon = activeMods.has("icon");
|
|
3360
|
+
const isTight = activeMods.has("tight");
|
|
3355
3361
|
if (fontSize == null) setCSSValue(styles, "font-size", name, { cssOnly: true });
|
|
3356
3362
|
if (lineHeight == null) setCSSValue(styles, "line-height", name, { cssOnly: true });
|
|
3357
3363
|
if (letterSpacing == null) setCSSValue(styles, "letter-spacing", name, { cssOnly: true });
|
|
@@ -6889,1063 +6895,679 @@ function pruneContradictedOrBranches(terms) {
|
|
|
6889
6895
|
return flattened;
|
|
6890
6896
|
}
|
|
6891
6897
|
//#endregion
|
|
6892
|
-
//#region src/pipeline/
|
|
6898
|
+
//#region src/pipeline/materialize-contradictions.ts
|
|
6893
6899
|
/**
|
|
6894
|
-
*
|
|
6895
|
-
*
|
|
6896
|
-
* The entries should be ordered by priority (highest priority first).
|
|
6897
|
-
*
|
|
6898
|
-
* For each entry, we compute:
|
|
6899
|
-
* exclusiveCondition = condition & !prior[0] & !prior[1] & ...
|
|
6900
|
-
*
|
|
6901
|
-
* This ensures exactly one condition matches at any time.
|
|
6902
|
-
*
|
|
6903
|
-
* Example:
|
|
6904
|
-
* Input (ordered highest to lowest priority):
|
|
6905
|
-
* A: value1 (priority 2)
|
|
6906
|
-
* B: value2 (priority 1)
|
|
6907
|
-
* C: value3 (priority 0)
|
|
6908
|
-
*
|
|
6909
|
-
* Output:
|
|
6910
|
-
* A: A
|
|
6911
|
-
* B: B & !A
|
|
6912
|
-
* C: C & !A & !B
|
|
6913
|
-
*
|
|
6914
|
-
* @param entries Parsed style entries ordered by priority (highest first)
|
|
6915
|
-
* @returns Entries with exclusive conditions, filtered to remove impossible ones
|
|
6900
|
+
* Generic deduplication by a key extraction function.
|
|
6901
|
+
* Preserves insertion order, keeping the first occurrence of each key.
|
|
6916
6902
|
*/
|
|
6917
|
-
function
|
|
6903
|
+
function dedupeByKey(items, getKey) {
|
|
6904
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6918
6905
|
const result = [];
|
|
6919
|
-
const
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
result.push({
|
|
6926
|
-
...entry,
|
|
6927
|
-
exclusiveCondition: simplified
|
|
6928
|
-
});
|
|
6929
|
-
if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
|
|
6906
|
+
for (const item of items) {
|
|
6907
|
+
const key = getKey(item);
|
|
6908
|
+
if (!seen.has(key)) {
|
|
6909
|
+
seen.add(key);
|
|
6910
|
+
result.push(item);
|
|
6911
|
+
}
|
|
6930
6912
|
}
|
|
6931
6913
|
return result;
|
|
6932
6914
|
}
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
*
|
|
6936
|
-
* @param styleKey The style key (e.g., 'padding')
|
|
6937
|
-
* @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
|
|
6938
|
-
* @param parseCondition Function to parse state keys into conditions
|
|
6939
|
-
* @returns Parsed entries ordered by priority (highest first)
|
|
6940
|
-
*/
|
|
6941
|
-
function parseStyleEntries(styleKey, valueMap, parseCondition) {
|
|
6942
|
-
const entries = [];
|
|
6943
|
-
Object.keys(valueMap).forEach((stateKey, index) => {
|
|
6944
|
-
const value = valueMap[stateKey];
|
|
6945
|
-
const condition = stateKey === "" ? trueCondition() : parseCondition(stateKey);
|
|
6946
|
-
entries.push({
|
|
6947
|
-
styleKey,
|
|
6948
|
-
stateKey,
|
|
6949
|
-
value,
|
|
6950
|
-
condition,
|
|
6951
|
-
priority: index
|
|
6952
|
-
});
|
|
6953
|
-
});
|
|
6954
|
-
entries.reverse();
|
|
6955
|
-
return entries;
|
|
6915
|
+
function dedupeMediaConditions(conditions) {
|
|
6916
|
+
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
6956
6917
|
}
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
*
|
|
6960
|
-
* When multiple **non-default** state keys map to the same value, their
|
|
6961
|
-
* conditions can be combined with OR and treated as a single entry.
|
|
6962
|
-
* This must happen **before** exclusive expansion and OR branch splitting
|
|
6963
|
-
* to avoid combinatorial explosion and duplicate CSS output.
|
|
6964
|
-
*
|
|
6965
|
-
* Default (TRUE) entries are **never** merged with non-default entries.
|
|
6966
|
-
* Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
|
|
6967
|
-
* condition's participation in exclusive building. That causes
|
|
6968
|
-
* intermediate-priority states to lose their `:not(X)` negation,
|
|
6969
|
-
* breaking mutual exclusivity when X and an intermediate state are
|
|
6970
|
-
* both active. Stage 6 `mergeByValue` handles combining rules with
|
|
6971
|
-
* identical CSS output after exclusive conditions are correctly built.
|
|
6972
|
-
*
|
|
6973
|
-
* Example: `{ '@dark': 'red', '@dark & @hc': 'red' }` merges into a
|
|
6974
|
-
* single entry with condition `@dark | (@dark & @hc)` = `@dark`.
|
|
6975
|
-
*
|
|
6976
|
-
* Entries are ordered highest-priority-first. The merged entry keeps the
|
|
6977
|
-
* highest priority of the group.
|
|
6978
|
-
*/
|
|
6979
|
-
function mergeEntriesByValue(entries) {
|
|
6980
|
-
if (entries.length <= 1) return entries;
|
|
6981
|
-
const groups = /* @__PURE__ */ new Map();
|
|
6982
|
-
for (const entry of entries) {
|
|
6983
|
-
const valueKey = serializeValue(entry.value);
|
|
6984
|
-
const group = groups.get(valueKey);
|
|
6985
|
-
if (group) {
|
|
6986
|
-
group.entries.push(entry);
|
|
6987
|
-
group.maxPriority = Math.max(group.maxPriority, entry.priority);
|
|
6988
|
-
} else groups.set(valueKey, {
|
|
6989
|
-
entries: [entry],
|
|
6990
|
-
maxPriority: entry.priority
|
|
6991
|
-
});
|
|
6992
|
-
}
|
|
6993
|
-
if (groups.size === entries.length) return entries;
|
|
6994
|
-
const merged = [];
|
|
6995
|
-
for (const [, group] of groups) {
|
|
6996
|
-
if (group.entries.length === 1) {
|
|
6997
|
-
merged.push(group.entries[0]);
|
|
6998
|
-
continue;
|
|
6999
|
-
}
|
|
7000
|
-
const defaultEntries = group.entries.filter((e) => e.condition.kind === "true");
|
|
7001
|
-
const nonDefaultEntries = group.entries.filter((e) => e.condition.kind !== "true");
|
|
7002
|
-
for (const entry of defaultEntries) merged.push(entry);
|
|
7003
|
-
if (nonDefaultEntries.length === 1) merged.push(nonDefaultEntries[0]);
|
|
7004
|
-
else if (nonDefaultEntries.length >= 2) {
|
|
7005
|
-
const combinedCondition = simplifyCondition(or(...nonDefaultEntries.map((e) => e.condition)));
|
|
7006
|
-
const combinedStateKey = nonDefaultEntries.map((e) => e.stateKey).join(" | ");
|
|
7007
|
-
merged.push({
|
|
7008
|
-
styleKey: nonDefaultEntries[0].styleKey,
|
|
7009
|
-
stateKey: combinedStateKey,
|
|
7010
|
-
value: nonDefaultEntries[0].value,
|
|
7011
|
-
condition: combinedCondition,
|
|
7012
|
-
priority: group.maxPriority
|
|
7013
|
-
});
|
|
7014
|
-
}
|
|
7015
|
-
}
|
|
7016
|
-
merged.sort((a, b) => b.priority - a.priority);
|
|
7017
|
-
return merged;
|
|
6918
|
+
function dedupeContainerConditions(conditions) {
|
|
6919
|
+
return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
|
|
7018
6920
|
}
|
|
7019
|
-
function
|
|
7020
|
-
|
|
7021
|
-
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
7022
|
-
return JSON.stringify(value);
|
|
6921
|
+
function dedupeSupportsConditions(conditions) {
|
|
6922
|
+
return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
|
|
7023
6923
|
}
|
|
7024
6924
|
/**
|
|
7025
|
-
*
|
|
7026
|
-
*
|
|
7027
|
-
* When a value map contains compound AND state keys (e.g. `@dark & @hc`),
|
|
7028
|
-
* checks whether any state atom is a "don't-care" variable — i.e. the
|
|
7029
|
-
* value is the same whether that atom is present or absent. Redundant
|
|
7030
|
-
* atoms are removed from all keys and duplicate entries are collapsed.
|
|
7031
|
-
*
|
|
7032
|
-
* This runs **before** condition parsing so that downstream stages
|
|
7033
|
-
* (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
|
|
7034
|
-
* never see the irrelevant dimension, producing simpler, smaller CSS.
|
|
7035
|
-
*
|
|
7036
|
-
* Only pure top-level AND combinations are eligible. Keys that contain
|
|
7037
|
-
* `|`, `^`, or `,` at the top level are treated as opaque single atoms.
|
|
7038
|
-
*
|
|
7039
|
-
* @example
|
|
7040
|
-
* { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
|
|
7041
|
-
* // @hc is redundant → { '': A, '@dark': B }
|
|
6925
|
+
* Check if supports conditions contain contradictions
|
|
6926
|
+
* e.g., @supports(display: grid) AND NOT @supports(display: grid)
|
|
7042
6927
|
*/
|
|
7043
|
-
function
|
|
7044
|
-
const
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
};
|
|
7051
|
-
});
|
|
7052
|
-
const allAtoms = /* @__PURE__ */ new Set();
|
|
7053
|
-
for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
|
|
7054
|
-
const redundant = /* @__PURE__ */ new Set();
|
|
7055
|
-
for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
|
|
7056
|
-
if (redundant.size === 0) return valueMap;
|
|
7057
|
-
const newMap = {};
|
|
7058
|
-
for (const e of entries) {
|
|
7059
|
-
const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
|
|
7060
|
-
if (!(newKey in newMap)) newMap[newKey] = e.value;
|
|
6928
|
+
function hasSupportsContradiction(conditions) {
|
|
6929
|
+
const conditionMap = /* @__PURE__ */ new Map();
|
|
6930
|
+
for (const cond of conditions) {
|
|
6931
|
+
const key = `${cond.subtype}|${cond.condition}`;
|
|
6932
|
+
const existing = conditionMap.get(key);
|
|
6933
|
+
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
6934
|
+
conditionMap.set(key, !cond.negated);
|
|
7061
6935
|
}
|
|
7062
|
-
return
|
|
6936
|
+
return false;
|
|
7063
6937
|
}
|
|
7064
6938
|
/**
|
|
7065
|
-
*
|
|
6939
|
+
* Check if a set of media conditions contains contradictions
|
|
6940
|
+
* e.g., (prefers-color-scheme: light) AND NOT (prefers-color-scheme: light)
|
|
6941
|
+
* or (width >= 900px) AND (width < 600px)
|
|
7066
6942
|
*
|
|
7067
|
-
*
|
|
7068
|
-
* (making it ineligible for atom-level extraction).
|
|
7069
|
-
* Returns `[]` for the empty string (default key).
|
|
6943
|
+
* Uses parsed media conditions for efficient analysis without regex parsing.
|
|
7070
6944
|
*/
|
|
7071
|
-
function
|
|
7072
|
-
|
|
7073
|
-
const
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
for (const
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
if (
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
6945
|
+
function hasMediaContradiction(conditions) {
|
|
6946
|
+
const featureConditions = /* @__PURE__ */ new Map();
|
|
6947
|
+
const typeConditions = /* @__PURE__ */ new Map();
|
|
6948
|
+
const dimensionConditions = /* @__PURE__ */ new Map();
|
|
6949
|
+
const dimensionsByDim = /* @__PURE__ */ new Map();
|
|
6950
|
+
for (const cond of conditions) if (cond.subtype === "type") {
|
|
6951
|
+
const key = cond.mediaType || "all";
|
|
6952
|
+
const existing = typeConditions.get(key);
|
|
6953
|
+
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
6954
|
+
typeConditions.set(key, !cond.negated);
|
|
6955
|
+
} else if (cond.subtype === "feature") {
|
|
6956
|
+
const key = cond.condition;
|
|
6957
|
+
const existing = featureConditions.get(key);
|
|
6958
|
+
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
6959
|
+
featureConditions.set(key, !cond.negated);
|
|
6960
|
+
} else if (cond.subtype === "dimension") {
|
|
6961
|
+
const condKey = cond.condition;
|
|
6962
|
+
const existing = dimensionConditions.get(condKey);
|
|
6963
|
+
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
6964
|
+
dimensionConditions.set(condKey, !cond.negated);
|
|
6965
|
+
if (!cond.negated) {
|
|
6966
|
+
const dim = cond.dimension || "width";
|
|
6967
|
+
let bounds = dimensionsByDim.get(dim);
|
|
6968
|
+
if (!bounds) {
|
|
6969
|
+
bounds = {
|
|
6970
|
+
lowerBound: null,
|
|
6971
|
+
upperBound: null
|
|
6972
|
+
};
|
|
6973
|
+
dimensionsByDim.set(dim, bounds);
|
|
7085
6974
|
}
|
|
7086
|
-
if (
|
|
6975
|
+
if (cond.lowerBound?.valueNumeric != null) {
|
|
6976
|
+
const value = cond.lowerBound.valueNumeric;
|
|
6977
|
+
if (bounds.lowerBound === null || value > bounds.lowerBound) bounds.lowerBound = value;
|
|
6978
|
+
}
|
|
6979
|
+
if (cond.upperBound?.valueNumeric != null) {
|
|
6980
|
+
const value = cond.upperBound.valueNumeric;
|
|
6981
|
+
if (bounds.upperBound === null || value < bounds.upperBound) bounds.upperBound = value;
|
|
6982
|
+
}
|
|
6983
|
+
if (bounds.lowerBound !== null && bounds.upperBound !== null && bounds.lowerBound >= bounds.upperBound) return true;
|
|
7087
6984
|
}
|
|
7088
|
-
current += ch;
|
|
7089
6985
|
}
|
|
7090
|
-
|
|
7091
|
-
if (trimmed) parts.push(trimmed);
|
|
7092
|
-
return parts;
|
|
6986
|
+
return false;
|
|
7093
6987
|
}
|
|
7094
6988
|
/**
|
|
7095
|
-
*
|
|
7096
|
-
*
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
if (!
|
|
7105
|
-
|
|
6989
|
+
* Check if container conditions contain contradictions in style queries
|
|
6990
|
+
* e.g., style(--variant: danger) and style(--variant: success) together
|
|
6991
|
+
* Same property with different values = always false
|
|
6992
|
+
*
|
|
6993
|
+
* Uses parsed container conditions for efficient analysis without regex parsing.
|
|
6994
|
+
*/
|
|
6995
|
+
function hasContainerStyleContradiction(conditions) {
|
|
6996
|
+
const styleQueries = /* @__PURE__ */ new Map();
|
|
6997
|
+
for (const cond of conditions) {
|
|
6998
|
+
if (cond.subtype !== "style" || !cond.property) continue;
|
|
6999
|
+
const property = cond.property;
|
|
7000
|
+
const value = cond.propertyValue;
|
|
7001
|
+
if (!styleQueries.has(property)) styleQueries.set(property, {
|
|
7002
|
+
hasExistence: false,
|
|
7003
|
+
values: /* @__PURE__ */ new Set(),
|
|
7004
|
+
hasNegatedExistence: false
|
|
7005
|
+
});
|
|
7006
|
+
const entry = styleQueries.get(property);
|
|
7007
|
+
if (cond.negated) {
|
|
7008
|
+
if (value === void 0) entry.hasNegatedExistence = true;
|
|
7009
|
+
} else if (value === void 0) entry.hasExistence = true;
|
|
7010
|
+
else entry.values.add(value);
|
|
7106
7011
|
}
|
|
7107
|
-
|
|
7012
|
+
for (const [, entry] of styleQueries) {
|
|
7013
|
+
if (entry.hasExistence && entry.hasNegatedExistence) return true;
|
|
7014
|
+
if (entry.values.size > 1) return true;
|
|
7015
|
+
if (entry.hasNegatedExistence && entry.values.size > 0) return true;
|
|
7016
|
+
}
|
|
7017
|
+
return false;
|
|
7108
7018
|
}
|
|
7019
|
+
//#endregion
|
|
7020
|
+
//#region src/pipeline/materialize.ts
|
|
7109
7021
|
/**
|
|
7110
|
-
*
|
|
7022
|
+
* CSS Materialization
|
|
7023
|
+
*
|
|
7024
|
+
* Converts condition trees into CSS selectors and at-rules.
|
|
7025
|
+
* This is the final stage that produces actual CSS output.
|
|
7111
7026
|
*/
|
|
7112
|
-
|
|
7113
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
7114
|
-
}
|
|
7027
|
+
const conditionCache = new Lru(3e3);
|
|
7115
7028
|
/**
|
|
7116
|
-
*
|
|
7117
|
-
*
|
|
7118
|
-
* For an entry with condition `A | B | C`, this creates 3 entries:
|
|
7119
|
-
* - condition: A
|
|
7120
|
-
* - condition: B & !A
|
|
7121
|
-
* - condition: C & !A & !B
|
|
7122
|
-
*
|
|
7123
|
-
* This ensures OR branches are mutually exclusive BEFORE the main
|
|
7124
|
-
* exclusive condition building pass.
|
|
7125
|
-
*
|
|
7126
|
-
* @param entries Parsed entries (may contain OR conditions)
|
|
7127
|
-
* @returns Expanded entries with OR branches made exclusive
|
|
7029
|
+
* Convert a condition tree to CSS components
|
|
7128
7030
|
*/
|
|
7129
|
-
function
|
|
7130
|
-
const
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7031
|
+
function conditionToCSS(node) {
|
|
7032
|
+
const key = getConditionUniqueId(node);
|
|
7033
|
+
const cached = conditionCache.get(key);
|
|
7034
|
+
if (cached) return cached;
|
|
7035
|
+
const result = conditionToCSSInner(node);
|
|
7036
|
+
conditionCache.set(key, result);
|
|
7135
7037
|
return result;
|
|
7136
7038
|
}
|
|
7039
|
+
function emptyVariant() {
|
|
7040
|
+
return {
|
|
7041
|
+
modifierConditions: [],
|
|
7042
|
+
pseudoConditions: [],
|
|
7043
|
+
selectorGroups: [],
|
|
7044
|
+
ownGroups: [],
|
|
7045
|
+
mediaConditions: [],
|
|
7046
|
+
containerConditions: [],
|
|
7047
|
+
supportsConditions: [],
|
|
7048
|
+
rootGroups: [],
|
|
7049
|
+
parentGroups: [],
|
|
7050
|
+
startingStyle: false
|
|
7051
|
+
};
|
|
7052
|
+
}
|
|
7053
|
+
function conditionToCSSInner(node) {
|
|
7054
|
+
if (node.kind === "true") return {
|
|
7055
|
+
variants: [emptyVariant()],
|
|
7056
|
+
isImpossible: false
|
|
7057
|
+
};
|
|
7058
|
+
if (node.kind === "false") return {
|
|
7059
|
+
variants: [],
|
|
7060
|
+
isImpossible: true
|
|
7061
|
+
};
|
|
7062
|
+
if (node.kind === "state") return stateToCSS(node);
|
|
7063
|
+
if (node.kind === "compound") if (node.operator === "AND") return andToCSS(node.children);
|
|
7064
|
+
else return orToCSS(node.children);
|
|
7065
|
+
return {
|
|
7066
|
+
variants: [emptyVariant()],
|
|
7067
|
+
isImpossible: false
|
|
7068
|
+
};
|
|
7069
|
+
}
|
|
7137
7070
|
/**
|
|
7138
|
-
*
|
|
7139
|
-
*
|
|
7140
|
-
* Note: branches are NOT sorted by at-rule context here (unlike the
|
|
7141
|
-
* `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
|
|
7142
|
-
* the product of De Morgan negation, so each branch is expected to render
|
|
7143
|
-
* independently in its own scope and at-rule sort isn't load-bearing.
|
|
7144
|
-
* The post-build pass needs the sort because it has to preserve at-rule
|
|
7145
|
-
* wrapping across branches that came from negating a compound at-rule.
|
|
7071
|
+
* Convert a state condition to CSS
|
|
7146
7072
|
*/
|
|
7147
|
-
function
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7073
|
+
function stateToCSS(state) {
|
|
7074
|
+
switch (state.type) {
|
|
7075
|
+
case "media": return {
|
|
7076
|
+
variants: mediaToParsed(state).map((mediaCond) => {
|
|
7077
|
+
const v = emptyVariant();
|
|
7078
|
+
v.mediaConditions.push(mediaCond);
|
|
7079
|
+
return v;
|
|
7080
|
+
}),
|
|
7081
|
+
isImpossible: false
|
|
7082
|
+
};
|
|
7083
|
+
case "root": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "rootGroups");
|
|
7084
|
+
case "parent": return parentConditionToVariants(state.innerCondition, state.negated ?? false, state.direct);
|
|
7085
|
+
case "own": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "ownGroups");
|
|
7086
|
+
case "modifier": {
|
|
7087
|
+
const v = emptyVariant();
|
|
7088
|
+
v.modifierConditions.push(modifierToParsed(state));
|
|
7089
|
+
return {
|
|
7090
|
+
variants: [v],
|
|
7091
|
+
isImpossible: false
|
|
7092
|
+
};
|
|
7093
|
+
}
|
|
7094
|
+
case "pseudo": {
|
|
7095
|
+
const v = emptyVariant();
|
|
7096
|
+
v.pseudoConditions.push(pseudoToParsed(state));
|
|
7097
|
+
return {
|
|
7098
|
+
variants: [v],
|
|
7099
|
+
isImpossible: false
|
|
7100
|
+
};
|
|
7101
|
+
}
|
|
7102
|
+
case "container": {
|
|
7103
|
+
const v = emptyVariant();
|
|
7104
|
+
v.containerConditions.push(containerToParsed(state));
|
|
7105
|
+
return {
|
|
7106
|
+
variants: [v],
|
|
7107
|
+
isImpossible: false
|
|
7108
|
+
};
|
|
7109
|
+
}
|
|
7110
|
+
case "supports": {
|
|
7111
|
+
const v = emptyVariant();
|
|
7112
|
+
v.supportsConditions.push(supportsToParsed(state));
|
|
7113
|
+
return {
|
|
7114
|
+
variants: [v],
|
|
7115
|
+
isImpossible: false
|
|
7116
|
+
};
|
|
7117
|
+
}
|
|
7118
|
+
case "starting": {
|
|
7119
|
+
const v = emptyVariant();
|
|
7120
|
+
v.startingStyle = !state.negated;
|
|
7121
|
+
return {
|
|
7122
|
+
variants: [v],
|
|
7123
|
+
isImpossible: false
|
|
7124
|
+
};
|
|
7160
7125
|
}
|
|
7161
|
-
result.push({
|
|
7162
|
-
...entry,
|
|
7163
|
-
stateKey: `${entry.stateKey}[${i}]`,
|
|
7164
|
-
condition: simplified
|
|
7165
|
-
});
|
|
7166
|
-
priorBranches.push(branch);
|
|
7167
7126
|
}
|
|
7168
|
-
return result;
|
|
7169
7127
|
}
|
|
7170
7128
|
/**
|
|
7171
|
-
*
|
|
7172
|
-
*
|
|
7173
|
-
* For `A | B | C`, returns [A, B, C]
|
|
7174
|
-
* For `A & B`, returns [A & B] (single branch)
|
|
7175
|
-
* For `A | (B & C)`, returns [A, B & C]
|
|
7129
|
+
* Convert modifier condition to parsed structure
|
|
7176
7130
|
*/
|
|
7177
|
-
function
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
}
|
|
7184
|
-
return [condition];
|
|
7131
|
+
function modifierToParsed(state) {
|
|
7132
|
+
return {
|
|
7133
|
+
attribute: state.attribute,
|
|
7134
|
+
value: state.value,
|
|
7135
|
+
operator: state.operator,
|
|
7136
|
+
negated: state.negated ?? false
|
|
7137
|
+
};
|
|
7185
7138
|
}
|
|
7186
7139
|
/**
|
|
7187
|
-
*
|
|
7188
|
-
*
|
|
7189
|
-
* This handles ORs that arise from De Morgan expansion during negation:
|
|
7190
|
-
* !(A & B) = !A | !B
|
|
7191
|
-
*
|
|
7192
|
-
* These ORs need to be made exclusive to avoid overlapping CSS rules:
|
|
7193
|
-
* !A | !B → !A | (A & !B)
|
|
7194
|
-
*
|
|
7195
|
-
* This is logically equivalent but ensures each branch has proper context.
|
|
7196
|
-
*
|
|
7197
|
-
* Example:
|
|
7198
|
-
* Input: { "": V1, "@supports(...) & :has()": V2 }
|
|
7199
|
-
* V2's exclusive = @supports & :has
|
|
7200
|
-
* V1's exclusive = !(@supports & :has) = !@supports | !:has
|
|
7201
|
-
*
|
|
7202
|
-
* Without this fix: V1 gets two rules:
|
|
7203
|
-
* - @supports (not ...) → V1 ✓
|
|
7204
|
-
* - :not(:has()) → V1 ✗ (missing @supports context!)
|
|
7205
|
-
*
|
|
7206
|
-
* With this fix: V1 gets two exclusive rules:
|
|
7207
|
-
* - @supports (not ...) → V1 ✓
|
|
7208
|
-
* - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
|
|
7140
|
+
* Convert parsed modifier to CSS selector string (for final output)
|
|
7209
7141
|
*/
|
|
7210
|
-
function
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
const
|
|
7214
|
-
|
|
7215
|
-
}
|
|
7216
|
-
return
|
|
7142
|
+
function modifierToCSS(mod) {
|
|
7143
|
+
let selector;
|
|
7144
|
+
if (mod.value !== void 0) {
|
|
7145
|
+
const op = mod.operator || "=";
|
|
7146
|
+
selector = `[${mod.attribute}${op}"${mod.value}"]`;
|
|
7147
|
+
} else selector = `[${mod.attribute}]`;
|
|
7148
|
+
if (mod.negated) return `:not(${selector})`;
|
|
7149
|
+
return selector;
|
|
7217
7150
|
}
|
|
7218
7151
|
/**
|
|
7219
|
-
*
|
|
7152
|
+
* Convert pseudo condition to parsed structure
|
|
7220
7153
|
*/
|
|
7221
|
-
function
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7154
|
+
function pseudoToParsed(state) {
|
|
7155
|
+
return {
|
|
7156
|
+
pseudo: state.pseudo,
|
|
7157
|
+
negated: state.negated ?? false
|
|
7158
|
+
};
|
|
7226
7159
|
}
|
|
7227
7160
|
/**
|
|
7228
|
-
*
|
|
7161
|
+
* Convert parsed pseudo to CSS selector string (for final output).
|
|
7229
7162
|
*
|
|
7230
|
-
*
|
|
7231
|
-
*
|
|
7232
|
-
*
|
|
7233
|
-
*
|
|
7163
|
+
* :not() is normalized to negated :is() at parse time, so pseudo.pseudo
|
|
7164
|
+
* never starts with ':not(' here. When negated:
|
|
7165
|
+
* - :is(X) → :not(X) (unwrap :is)
|
|
7166
|
+
* - :where(X) → :not(X) (unwrap :where)
|
|
7167
|
+
* - :has(X) → :not(:has(X))
|
|
7168
|
+
* - other → :not(other)
|
|
7234
7169
|
*
|
|
7235
|
-
*
|
|
7236
|
-
*
|
|
7237
|
-
*
|
|
7170
|
+
* When not negated, single-argument :is()/:where() is unwrapped when the
|
|
7171
|
+
* inner content is a simple compound selector that can safely append to
|
|
7172
|
+
* the base selector (this happens after double-negation of :not()).
|
|
7238
7173
|
*/
|
|
7239
|
-
function
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7174
|
+
function pseudoToCSS(pseudo) {
|
|
7175
|
+
const p = pseudo.pseudo;
|
|
7176
|
+
if (pseudo.negated) {
|
|
7177
|
+
if (p.startsWith(":is(") || p.startsWith(":where(")) return `:not(${p.slice(p.indexOf("(") + 1, -1)})`;
|
|
7178
|
+
return `:not(${p})`;
|
|
7179
|
+
}
|
|
7180
|
+
if ((p.startsWith(":is(") || p.startsWith(":where(")) && !p.includes(",")) {
|
|
7181
|
+
const inner = p.slice(p.indexOf("(") + 1, -1);
|
|
7182
|
+
const ch = inner[0];
|
|
7183
|
+
if ((ch === ":" || ch === "." || ch === "[" || ch === "#") && !/\s/.test(inner)) return inner;
|
|
7184
|
+
}
|
|
7185
|
+
return p;
|
|
7247
7186
|
}
|
|
7248
7187
|
/**
|
|
7249
|
-
*
|
|
7188
|
+
* Convert media condition to parsed structure(s)
|
|
7189
|
+
* Returns an array because negated ranges produce OR branches (two separate conditions)
|
|
7250
7190
|
*/
|
|
7251
|
-
function
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
if (
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
}
|
|
7271
|
-
|
|
7272
|
-
}
|
|
7273
|
-
return result;
|
|
7191
|
+
function mediaToParsed(state) {
|
|
7192
|
+
if (state.subtype === "type") {
|
|
7193
|
+
const mediaType = state.mediaType || "all";
|
|
7194
|
+
return [{
|
|
7195
|
+
subtype: "type",
|
|
7196
|
+
negated: state.negated ?? false,
|
|
7197
|
+
condition: mediaType,
|
|
7198
|
+
mediaType: state.mediaType
|
|
7199
|
+
}];
|
|
7200
|
+
} else if (state.subtype === "feature") {
|
|
7201
|
+
let condition;
|
|
7202
|
+
if (state.featureValue) condition = `(${state.feature}: ${state.featureValue})`;
|
|
7203
|
+
else condition = `(${state.feature})`;
|
|
7204
|
+
return [{
|
|
7205
|
+
subtype: "feature",
|
|
7206
|
+
negated: state.negated ?? false,
|
|
7207
|
+
condition,
|
|
7208
|
+
feature: state.feature,
|
|
7209
|
+
featureValue: state.featureValue
|
|
7210
|
+
}];
|
|
7211
|
+
} else return dimensionToMediaParsed(state.dimension || "width", state.lowerBound, state.upperBound, state.negated ?? false);
|
|
7274
7212
|
}
|
|
7275
|
-
//#endregion
|
|
7276
|
-
//#region src/pipeline/materialize-contradictions.ts
|
|
7277
7213
|
/**
|
|
7278
|
-
*
|
|
7279
|
-
*
|
|
7214
|
+
* Convert dimension bounds to parsed media condition(s)
|
|
7215
|
+
* Uses CSS Media Queries Level 4 `not (condition)` syntax for negation.
|
|
7280
7216
|
*/
|
|
7281
|
-
function
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
const
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7217
|
+
function dimensionToMediaParsed(dimension, lowerBound, upperBound, negated) {
|
|
7218
|
+
let condition;
|
|
7219
|
+
if (lowerBound && upperBound) {
|
|
7220
|
+
const lowerOp = lowerBound.inclusive ? "<=" : "<";
|
|
7221
|
+
const upperOp = upperBound.inclusive ? "<=" : "<";
|
|
7222
|
+
condition = `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
|
|
7223
|
+
} else if (upperBound) condition = `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
|
|
7224
|
+
else if (lowerBound) condition = `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
|
|
7225
|
+
else condition = `(${dimension})`;
|
|
7226
|
+
return [{
|
|
7227
|
+
subtype: "dimension",
|
|
7228
|
+
negated: negated ?? false,
|
|
7229
|
+
condition,
|
|
7230
|
+
dimension,
|
|
7231
|
+
lowerBound,
|
|
7232
|
+
upperBound
|
|
7233
|
+
}];
|
|
7298
7234
|
}
|
|
7299
|
-
|
|
7300
|
-
|
|
7235
|
+
/**
|
|
7236
|
+
* Convert container condition to parsed structure
|
|
7237
|
+
* This enables structured analysis for contradiction detection and condition combining
|
|
7238
|
+
*/
|
|
7239
|
+
function containerToParsed(state) {
|
|
7240
|
+
let condition;
|
|
7241
|
+
if (state.subtype === "style") if (state.propertyValue) condition = `style(--${state.property}: ${state.propertyValue})`;
|
|
7242
|
+
else condition = `style(--${state.property})`;
|
|
7243
|
+
else if (state.subtype === "raw") condition = state.rawCondition;
|
|
7244
|
+
else condition = dimensionToContainerCondition(state.dimension || "width", state.lowerBound, state.upperBound);
|
|
7245
|
+
return {
|
|
7246
|
+
name: state.containerName,
|
|
7247
|
+
condition,
|
|
7248
|
+
negated: state.negated ?? false,
|
|
7249
|
+
subtype: state.subtype,
|
|
7250
|
+
property: state.property,
|
|
7251
|
+
propertyValue: state.propertyValue
|
|
7252
|
+
};
|
|
7301
7253
|
}
|
|
7302
7254
|
/**
|
|
7303
|
-
*
|
|
7304
|
-
*
|
|
7255
|
+
* Convert dimension bounds to container query condition (single string)
|
|
7256
|
+
* Container queries support "not (condition)", so no need to invert manually
|
|
7305
7257
|
*/
|
|
7306
|
-
function
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
const
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
return false;
|
|
7258
|
+
function dimensionToContainerCondition(dimension, lowerBound, upperBound) {
|
|
7259
|
+
if (lowerBound && upperBound) {
|
|
7260
|
+
const lowerOp = lowerBound.inclusive ? "<=" : "<";
|
|
7261
|
+
const upperOp = upperBound.inclusive ? "<=" : "<";
|
|
7262
|
+
return `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
|
|
7263
|
+
} else if (upperBound) return `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
|
|
7264
|
+
else if (lowerBound) return `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
|
|
7265
|
+
return "(width)";
|
|
7315
7266
|
}
|
|
7316
7267
|
/**
|
|
7317
|
-
*
|
|
7318
|
-
* e.g., (prefers-color-scheme: light) AND NOT (prefers-color-scheme: light)
|
|
7319
|
-
* or (width >= 900px) AND (width < 600px)
|
|
7320
|
-
*
|
|
7321
|
-
* Uses parsed media conditions for efficient analysis without regex parsing.
|
|
7268
|
+
* Convert supports condition to parsed structure
|
|
7322
7269
|
*/
|
|
7323
|
-
function
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
const key = cond.mediaType || "all";
|
|
7330
|
-
const existing = typeConditions.get(key);
|
|
7331
|
-
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7332
|
-
typeConditions.set(key, !cond.negated);
|
|
7333
|
-
} else if (cond.subtype === "feature") {
|
|
7334
|
-
const key = cond.condition;
|
|
7335
|
-
const existing = featureConditions.get(key);
|
|
7336
|
-
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7337
|
-
featureConditions.set(key, !cond.negated);
|
|
7338
|
-
} else if (cond.subtype === "dimension") {
|
|
7339
|
-
const condKey = cond.condition;
|
|
7340
|
-
const existing = dimensionConditions.get(condKey);
|
|
7341
|
-
if (existing !== void 0 && existing !== !cond.negated) return true;
|
|
7342
|
-
dimensionConditions.set(condKey, !cond.negated);
|
|
7343
|
-
if (!cond.negated) {
|
|
7344
|
-
const dim = cond.dimension || "width";
|
|
7345
|
-
let bounds = dimensionsByDim.get(dim);
|
|
7346
|
-
if (!bounds) {
|
|
7347
|
-
bounds = {
|
|
7348
|
-
lowerBound: null,
|
|
7349
|
-
upperBound: null
|
|
7350
|
-
};
|
|
7351
|
-
dimensionsByDim.set(dim, bounds);
|
|
7352
|
-
}
|
|
7353
|
-
if (cond.lowerBound?.valueNumeric != null) {
|
|
7354
|
-
const value = cond.lowerBound.valueNumeric;
|
|
7355
|
-
if (bounds.lowerBound === null || value > bounds.lowerBound) bounds.lowerBound = value;
|
|
7356
|
-
}
|
|
7357
|
-
if (cond.upperBound?.valueNumeric != null) {
|
|
7358
|
-
const value = cond.upperBound.valueNumeric;
|
|
7359
|
-
if (bounds.upperBound === null || value < bounds.upperBound) bounds.upperBound = value;
|
|
7360
|
-
}
|
|
7361
|
-
if (bounds.lowerBound !== null && bounds.upperBound !== null && bounds.lowerBound >= bounds.upperBound) return true;
|
|
7362
|
-
}
|
|
7363
|
-
}
|
|
7364
|
-
return false;
|
|
7270
|
+
function supportsToParsed(state) {
|
|
7271
|
+
return {
|
|
7272
|
+
subtype: state.subtype,
|
|
7273
|
+
condition: state.condition,
|
|
7274
|
+
negated: state.negated ?? false
|
|
7275
|
+
};
|
|
7365
7276
|
}
|
|
7366
7277
|
/**
|
|
7367
|
-
*
|
|
7368
|
-
* e.g., style(--variant: danger) and style(--variant: success) together
|
|
7369
|
-
* Same property with different values = always false
|
|
7370
|
-
*
|
|
7371
|
-
* Uses parsed container conditions for efficient analysis without regex parsing.
|
|
7278
|
+
* Collect all modifier and pseudo conditions from a variant as a flat array.
|
|
7372
7279
|
*/
|
|
7373
|
-
function
|
|
7374
|
-
|
|
7375
|
-
for (const cond of conditions) {
|
|
7376
|
-
if (cond.subtype !== "style" || !cond.property) continue;
|
|
7377
|
-
const property = cond.property;
|
|
7378
|
-
const value = cond.propertyValue;
|
|
7379
|
-
if (!styleQueries.has(property)) styleQueries.set(property, {
|
|
7380
|
-
hasExistence: false,
|
|
7381
|
-
values: /* @__PURE__ */ new Set(),
|
|
7382
|
-
hasNegatedExistence: false
|
|
7383
|
-
});
|
|
7384
|
-
const entry = styleQueries.get(property);
|
|
7385
|
-
if (cond.negated) {
|
|
7386
|
-
if (value === void 0) entry.hasNegatedExistence = true;
|
|
7387
|
-
} else if (value === void 0) entry.hasExistence = true;
|
|
7388
|
-
else entry.values.add(value);
|
|
7389
|
-
}
|
|
7390
|
-
for (const [, entry] of styleQueries) {
|
|
7391
|
-
if (entry.hasExistence && entry.hasNegatedExistence) return true;
|
|
7392
|
-
if (entry.values.size > 1) return true;
|
|
7393
|
-
if (entry.hasNegatedExistence && entry.values.size > 0) return true;
|
|
7394
|
-
}
|
|
7395
|
-
return false;
|
|
7280
|
+
function collectSelectorConditions(variant) {
|
|
7281
|
+
return [...variant.modifierConditions, ...variant.pseudoConditions];
|
|
7396
7282
|
}
|
|
7397
|
-
//#endregion
|
|
7398
|
-
//#region src/pipeline/materialize.ts
|
|
7399
7283
|
/**
|
|
7400
|
-
*
|
|
7284
|
+
* Convert an inner condition tree into a single SelectorVariant with
|
|
7285
|
+
* one SelectorGroup whose branches represent the inner OR alternatives.
|
|
7286
|
+
* Shared by @root() and @own().
|
|
7401
7287
|
*
|
|
7402
|
-
*
|
|
7403
|
-
*
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
*
|
|
7288
|
+
* Both positive and negated cases produce one variant with one group.
|
|
7289
|
+
* Negation simply sets the `negated` flag, which swaps :is() for :not()
|
|
7290
|
+
* in the final CSS output — no De Morgan transformation is needed.
|
|
7291
|
+
*
|
|
7292
|
+
* This mirrors parentConditionToVariants: OR branches are kept inside
|
|
7293
|
+
* a single group and rendered as comma-separated arguments in
|
|
7294
|
+
* :is()/:not(), e.g. :root:is([a], [b]) or [el]:not([a], [b]).
|
|
7408
7295
|
*/
|
|
7409
|
-
function
|
|
7410
|
-
const
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
conditionCache.set(key, result);
|
|
7415
|
-
return result;
|
|
7416
|
-
}
|
|
7417
|
-
function emptyVariant() {
|
|
7418
|
-
return {
|
|
7419
|
-
modifierConditions: [],
|
|
7420
|
-
pseudoConditions: [],
|
|
7421
|
-
selectorGroups: [],
|
|
7422
|
-
ownGroups: [],
|
|
7423
|
-
mediaConditions: [],
|
|
7424
|
-
containerConditions: [],
|
|
7425
|
-
supportsConditions: [],
|
|
7426
|
-
rootGroups: [],
|
|
7427
|
-
parentGroups: [],
|
|
7428
|
-
startingStyle: false
|
|
7296
|
+
function innerConditionToVariants(innerCondition, negated, target) {
|
|
7297
|
+
const innerCSS = conditionToCSS(innerCondition);
|
|
7298
|
+
if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
|
|
7299
|
+
variants: [],
|
|
7300
|
+
isImpossible: true
|
|
7429
7301
|
};
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7302
|
+
const branches = [];
|
|
7303
|
+
for (const innerVariant of innerCSS.variants) {
|
|
7304
|
+
const conditions = collectSelectorConditions(innerVariant);
|
|
7305
|
+
if (conditions.length > 0) branches.push(conditions);
|
|
7306
|
+
}
|
|
7307
|
+
if (branches.length === 0) return {
|
|
7433
7308
|
variants: [emptyVariant()],
|
|
7434
7309
|
isImpossible: false
|
|
7435
7310
|
};
|
|
7436
|
-
|
|
7311
|
+
const v = emptyVariant();
|
|
7312
|
+
v[target].push({
|
|
7313
|
+
branches,
|
|
7314
|
+
negated
|
|
7315
|
+
});
|
|
7316
|
+
return {
|
|
7317
|
+
variants: [v],
|
|
7318
|
+
isImpossible: false
|
|
7319
|
+
};
|
|
7320
|
+
}
|
|
7321
|
+
/**
|
|
7322
|
+
* Convert a @parent() inner condition into a single SelectorVariant with
|
|
7323
|
+
* one ParentGroup whose branches represent the inner OR alternatives.
|
|
7324
|
+
*
|
|
7325
|
+
* Both positive and negated cases produce one variant with one group.
|
|
7326
|
+
* Negation simply sets the `negated` flag, which swaps :is() for :not()
|
|
7327
|
+
* in the final CSS output — no structural transformation is needed.
|
|
7328
|
+
*/
|
|
7329
|
+
function parentConditionToVariants(innerCondition, negated, direct) {
|
|
7330
|
+
const innerCSS = conditionToCSS(innerCondition);
|
|
7331
|
+
if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
|
|
7437
7332
|
variants: [],
|
|
7438
7333
|
isImpossible: true
|
|
7439
7334
|
};
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7335
|
+
const branches = [];
|
|
7336
|
+
for (const innerVariant of innerCSS.variants) {
|
|
7337
|
+
const conditions = collectSelectorConditions(innerVariant);
|
|
7338
|
+
if (conditions.length > 0) branches.push(conditions);
|
|
7339
|
+
}
|
|
7340
|
+
if (branches.length === 0) return {
|
|
7444
7341
|
variants: [emptyVariant()],
|
|
7445
7342
|
isImpossible: false
|
|
7446
7343
|
};
|
|
7344
|
+
const v = emptyVariant();
|
|
7345
|
+
v.parentGroups.push({
|
|
7346
|
+
branches,
|
|
7347
|
+
direct,
|
|
7348
|
+
negated
|
|
7349
|
+
});
|
|
7350
|
+
return {
|
|
7351
|
+
variants: [v],
|
|
7352
|
+
isImpossible: false
|
|
7353
|
+
};
|
|
7447
7354
|
}
|
|
7448
7355
|
/**
|
|
7449
|
-
*
|
|
7356
|
+
* Sort key for canonical condition output within selectors.
|
|
7357
|
+
*
|
|
7358
|
+
* Priority order:
|
|
7359
|
+
* 0: Boolean attribute selectors ([data-hovered])
|
|
7360
|
+
* 1: Value attribute selectors ([data-size="small"])
|
|
7361
|
+
* 2: Negated boolean attributes (:not([data-disabled]))
|
|
7362
|
+
* 3: Negated value attributes (:not([data-size="small"]))
|
|
7363
|
+
* 4: Pseudo-classes (:hover, :focus)
|
|
7364
|
+
* 5: Negated pseudo-classes (:not(:disabled))
|
|
7365
|
+
*
|
|
7366
|
+
* Secondary sort: alphabetical by attribute name / pseudo string.
|
|
7450
7367
|
*/
|
|
7451
|
-
function
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
const v = emptyVariant();
|
|
7456
|
-
v.mediaConditions.push(mediaCond);
|
|
7457
|
-
return v;
|
|
7458
|
-
}),
|
|
7459
|
-
isImpossible: false
|
|
7460
|
-
};
|
|
7461
|
-
case "root": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "rootGroups");
|
|
7462
|
-
case "parent": return parentConditionToVariants(state.innerCondition, state.negated ?? false, state.direct);
|
|
7463
|
-
case "own": return innerConditionToVariants(state.innerCondition, state.negated ?? false, "ownGroups");
|
|
7464
|
-
case "modifier": {
|
|
7465
|
-
const v = emptyVariant();
|
|
7466
|
-
v.modifierConditions.push(modifierToParsed(state));
|
|
7467
|
-
return {
|
|
7468
|
-
variants: [v],
|
|
7469
|
-
isImpossible: false
|
|
7470
|
-
};
|
|
7471
|
-
}
|
|
7472
|
-
case "pseudo": {
|
|
7473
|
-
const v = emptyVariant();
|
|
7474
|
-
v.pseudoConditions.push(pseudoToParsed(state));
|
|
7475
|
-
return {
|
|
7476
|
-
variants: [v],
|
|
7477
|
-
isImpossible: false
|
|
7478
|
-
};
|
|
7479
|
-
}
|
|
7480
|
-
case "container": {
|
|
7481
|
-
const v = emptyVariant();
|
|
7482
|
-
v.containerConditions.push(containerToParsed(state));
|
|
7483
|
-
return {
|
|
7484
|
-
variants: [v],
|
|
7485
|
-
isImpossible: false
|
|
7486
|
-
};
|
|
7487
|
-
}
|
|
7488
|
-
case "supports": {
|
|
7489
|
-
const v = emptyVariant();
|
|
7490
|
-
v.supportsConditions.push(supportsToParsed(state));
|
|
7491
|
-
return {
|
|
7492
|
-
variants: [v],
|
|
7493
|
-
isImpossible: false
|
|
7494
|
-
};
|
|
7495
|
-
}
|
|
7496
|
-
case "starting": {
|
|
7497
|
-
const v = emptyVariant();
|
|
7498
|
-
v.startingStyle = !state.negated;
|
|
7499
|
-
return {
|
|
7500
|
-
variants: [v],
|
|
7501
|
-
isImpossible: false
|
|
7502
|
-
};
|
|
7503
|
-
}
|
|
7368
|
+
function conditionSortKey(cond) {
|
|
7369
|
+
if ("attribute" in cond) {
|
|
7370
|
+
const hasValue = cond.value !== void 0 ? 1 : 0;
|
|
7371
|
+
return `${(cond.negated ? 2 : 0) + hasValue}|${cond.attribute}|${cond.value ?? ""}`;
|
|
7504
7372
|
}
|
|
7373
|
+
return `${cond.negated ? 5 : 4}|${cond.pseudo}`;
|
|
7374
|
+
}
|
|
7375
|
+
function sortConditions(conditions) {
|
|
7376
|
+
return conditions.toSorted((a, b) => conditionSortKey(a).localeCompare(conditionSortKey(b)));
|
|
7377
|
+
}
|
|
7378
|
+
function branchToCSS(branch) {
|
|
7379
|
+
let parts = "";
|
|
7380
|
+
for (const cond of sortConditions(branch)) parts += selectorConditionToCSS(cond);
|
|
7381
|
+
return parts;
|
|
7505
7382
|
}
|
|
7506
7383
|
/**
|
|
7507
|
-
*
|
|
7384
|
+
* Wrap serialized selector arguments in :is() or :not().
|
|
7385
|
+
* Arguments are sorted for canonical output.
|
|
7508
7386
|
*/
|
|
7509
|
-
function
|
|
7510
|
-
return {
|
|
7511
|
-
attribute: state.attribute,
|
|
7512
|
-
value: state.value,
|
|
7513
|
-
operator: state.operator,
|
|
7514
|
-
negated: state.negated ?? false
|
|
7515
|
-
};
|
|
7387
|
+
function wrapInIsOrNot(args, negated) {
|
|
7388
|
+
return `${negated ? ":not" : ":is"}(${args.sort().join(", ")})`;
|
|
7516
7389
|
}
|
|
7517
7390
|
/**
|
|
7518
|
-
* Convert
|
|
7391
|
+
* Convert a selector group to a CSS selector fragment.
|
|
7392
|
+
*
|
|
7393
|
+
* Single-branch groups are unwrapped (no :is() wrapper).
|
|
7394
|
+
* Multi-branch groups use :is() or :not().
|
|
7395
|
+
* Negation swaps :is() for :not().
|
|
7519
7396
|
*/
|
|
7520
|
-
function
|
|
7521
|
-
|
|
7522
|
-
if (
|
|
7523
|
-
const
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
return
|
|
7397
|
+
function selectorGroupToCSS(group) {
|
|
7398
|
+
if (group.branches.length === 0) return "";
|
|
7399
|
+
if (group.branches.length === 1) {
|
|
7400
|
+
const parts = branchToCSS(group.branches[0]);
|
|
7401
|
+
if (group.negated) return `:not(${parts})`;
|
|
7402
|
+
return parts;
|
|
7403
|
+
}
|
|
7404
|
+
return wrapInIsOrNot(group.branches.map(branchToCSS), group.negated);
|
|
7528
7405
|
}
|
|
7529
7406
|
/**
|
|
7530
|
-
*
|
|
7407
|
+
* Collect facts about modifier conditions for subsumption analysis.
|
|
7408
|
+
* Tracks negated boolean attrs (:not([attr])) and positive exact values ([attr="X"]).
|
|
7531
7409
|
*/
|
|
7532
|
-
function
|
|
7410
|
+
function collectSubsumptionFacts(modifiers) {
|
|
7411
|
+
const negatedBooleanAttrs = /* @__PURE__ */ new Set();
|
|
7412
|
+
const positiveExactValuesByAttr = /* @__PURE__ */ new Map();
|
|
7413
|
+
for (const mod of modifiers) {
|
|
7414
|
+
if (mod.negated && mod.value === void 0) negatedBooleanAttrs.add(mod.attribute);
|
|
7415
|
+
if (!mod.negated && mod.value !== void 0 && (mod.operator ?? "=") === "=") {
|
|
7416
|
+
let vals = positiveExactValuesByAttr.get(mod.attribute);
|
|
7417
|
+
if (!vals) {
|
|
7418
|
+
vals = /* @__PURE__ */ new Set();
|
|
7419
|
+
positiveExactValuesByAttr.set(mod.attribute, vals);
|
|
7420
|
+
}
|
|
7421
|
+
vals.add(mod.value);
|
|
7422
|
+
}
|
|
7423
|
+
}
|
|
7533
7424
|
return {
|
|
7534
|
-
|
|
7535
|
-
|
|
7425
|
+
negatedBooleanAttrs,
|
|
7426
|
+
positiveExactValuesByAttr
|
|
7536
7427
|
};
|
|
7537
7428
|
}
|
|
7538
7429
|
/**
|
|
7539
|
-
*
|
|
7540
|
-
*
|
|
7541
|
-
* :not() is
|
|
7542
|
-
* never starts with ':not(' here. When negated:
|
|
7543
|
-
* - :is(X) → :not(X) (unwrap :is)
|
|
7544
|
-
* - :where(X) → :not(X) (unwrap :where)
|
|
7545
|
-
* - :has(X) → :not(:has(X))
|
|
7546
|
-
* - other → :not(other)
|
|
7430
|
+
* Check if a negated-value modifier is subsumed by stronger facts:
|
|
7431
|
+
* - :not([attr]) subsumes :not([attr="val"])
|
|
7432
|
+
* - [attr="X"] implies :not([attr="Y"]) is redundant (single exact value)
|
|
7547
7433
|
*
|
|
7548
|
-
*
|
|
7549
|
-
*
|
|
7550
|
-
* the base selector (this happens after double-negation of :not()).
|
|
7434
|
+
* Only applies to exact-match (=) operators; substring operators don't
|
|
7435
|
+
* imply exclusivity between values.
|
|
7551
7436
|
*/
|
|
7552
|
-
function
|
|
7553
|
-
|
|
7554
|
-
if (
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
if ((p.startsWith(":is(") || p.startsWith(":where(")) && !p.includes(",")) {
|
|
7559
|
-
const inner = p.slice(p.indexOf("(") + 1, -1);
|
|
7560
|
-
const ch = inner[0];
|
|
7561
|
-
if ((ch === ":" || ch === "." || ch === "[" || ch === "#") && !/\s/.test(inner)) return inner;
|
|
7437
|
+
function isSubsumedNegatedModifier(mod, facts) {
|
|
7438
|
+
if (!mod.negated || mod.value === void 0) return false;
|
|
7439
|
+
if (facts.negatedBooleanAttrs.has(mod.attribute)) return true;
|
|
7440
|
+
if ((mod.operator ?? "=") === "=") {
|
|
7441
|
+
const posVals = facts.positiveExactValuesByAttr.get(mod.attribute);
|
|
7442
|
+
if (posVals && posVals.size === 1 && !posVals.has(mod.value)) return true;
|
|
7562
7443
|
}
|
|
7563
|
-
return
|
|
7444
|
+
return false;
|
|
7564
7445
|
}
|
|
7565
7446
|
/**
|
|
7566
|
-
*
|
|
7567
|
-
*
|
|
7447
|
+
* Remove redundant single-condition groups that are subsumed by stronger
|
|
7448
|
+
* groups on the same attribute. O(n) — only inspects single-branch,
|
|
7449
|
+
* single-condition groups.
|
|
7568
7450
|
*/
|
|
7569
|
-
function
|
|
7570
|
-
if (
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7451
|
+
function optimizeGroups(groups) {
|
|
7452
|
+
if (groups.length <= 1) return groups;
|
|
7453
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7454
|
+
const result = [];
|
|
7455
|
+
for (const g of groups) {
|
|
7456
|
+
const key = getSelectorGroupKey(g);
|
|
7457
|
+
if (!seen.has(key)) {
|
|
7458
|
+
seen.add(key);
|
|
7459
|
+
result.push(g);
|
|
7460
|
+
}
|
|
7461
|
+
}
|
|
7462
|
+
if (result.length <= 1) return result;
|
|
7463
|
+
const effectiveModifiers = [];
|
|
7464
|
+
for (const g of result) {
|
|
7465
|
+
if (g.branches.length !== 1 || g.branches[0].length !== 1) continue;
|
|
7466
|
+
const cond = g.branches[0][0];
|
|
7467
|
+
if (!("attribute" in cond)) continue;
|
|
7468
|
+
effectiveModifiers.push({
|
|
7469
|
+
...cond,
|
|
7470
|
+
negated: g.negated !== cond.negated
|
|
7471
|
+
});
|
|
7472
|
+
}
|
|
7473
|
+
const facts = collectSubsumptionFacts(effectiveModifiers);
|
|
7474
|
+
if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
|
|
7475
|
+
return result.filter((g) => {
|
|
7476
|
+
if (g.branches.length !== 1 || g.branches[0].length !== 1) return true;
|
|
7477
|
+
const cond = g.branches[0][0];
|
|
7478
|
+
if (!("attribute" in cond) || !g.negated || cond.negated || cond.value === void 0) return true;
|
|
7479
|
+
return !isSubsumedNegatedModifier({
|
|
7480
|
+
...cond,
|
|
7481
|
+
negated: true
|
|
7482
|
+
}, facts);
|
|
7483
|
+
});
|
|
7590
7484
|
}
|
|
7591
7485
|
/**
|
|
7592
|
-
* Convert
|
|
7593
|
-
* Uses CSS Media Queries Level 4 `not (condition)` syntax for negation.
|
|
7486
|
+
* Convert root groups to CSS selector prefix (for final output)
|
|
7594
7487
|
*/
|
|
7595
|
-
function
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
else if (lowerBound) condition = `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
|
|
7603
|
-
else condition = `(${dimension})`;
|
|
7604
|
-
return [{
|
|
7605
|
-
subtype: "dimension",
|
|
7606
|
-
negated: negated ?? false,
|
|
7607
|
-
condition,
|
|
7608
|
-
dimension,
|
|
7609
|
-
lowerBound,
|
|
7610
|
-
upperBound
|
|
7611
|
-
}];
|
|
7488
|
+
function rootGroupsToCSS(groups) {
|
|
7489
|
+
if (groups.length === 0) return void 0;
|
|
7490
|
+
const optimized = optimizeGroups(groups);
|
|
7491
|
+
if (optimized.length === 0) return void 0;
|
|
7492
|
+
let prefix = ":root";
|
|
7493
|
+
for (const group of optimized) prefix += selectorGroupToCSS(group);
|
|
7494
|
+
return prefix;
|
|
7612
7495
|
}
|
|
7613
7496
|
/**
|
|
7614
|
-
* Convert
|
|
7615
|
-
*
|
|
7497
|
+
* Convert parent groups to CSS selector fragments (for final output).
|
|
7498
|
+
* Each group produces its own :is()/:not() wrapper with a combinator
|
|
7499
|
+
* suffix (` *` or ` > *`) appended to each branch.
|
|
7616
7500
|
*/
|
|
7617
|
-
function
|
|
7618
|
-
let
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
condition,
|
|
7626
|
-
negated: state.negated ?? false,
|
|
7627
|
-
subtype: state.subtype,
|
|
7628
|
-
property: state.property,
|
|
7629
|
-
propertyValue: state.propertyValue
|
|
7630
|
-
};
|
|
7501
|
+
function parentGroupsToCSS(groups) {
|
|
7502
|
+
let result = "";
|
|
7503
|
+
for (const group of groups) {
|
|
7504
|
+
const combinator = group.direct ? " > *" : " *";
|
|
7505
|
+
const args = group.branches.map((branch) => branchToCSS(branch) + combinator);
|
|
7506
|
+
result += wrapInIsOrNot(args, group.negated);
|
|
7507
|
+
}
|
|
7508
|
+
return result;
|
|
7631
7509
|
}
|
|
7632
7510
|
/**
|
|
7633
|
-
* Convert
|
|
7634
|
-
* Container queries support "not (condition)", so no need to invert manually
|
|
7511
|
+
* Convert a modifier or pseudo condition to a CSS selector fragment
|
|
7635
7512
|
*/
|
|
7636
|
-
function
|
|
7637
|
-
if (
|
|
7638
|
-
|
|
7639
|
-
const upperOp = upperBound.inclusive ? "<=" : "<";
|
|
7640
|
-
return `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
|
|
7641
|
-
} else if (upperBound) return `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
|
|
7642
|
-
else if (lowerBound) return `(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
|
|
7643
|
-
return "(width)";
|
|
7513
|
+
function selectorConditionToCSS(cond) {
|
|
7514
|
+
if ("attribute" in cond) return modifierToCSS(cond);
|
|
7515
|
+
return pseudoToCSS(cond);
|
|
7644
7516
|
}
|
|
7645
7517
|
/**
|
|
7646
|
-
*
|
|
7518
|
+
* Get unique key for a modifier condition
|
|
7647
7519
|
*/
|
|
7648
|
-
function
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
condition: state.condition,
|
|
7652
|
-
negated: state.negated ?? false
|
|
7653
|
-
};
|
|
7520
|
+
function getModifierKey(mod) {
|
|
7521
|
+
const base = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
|
|
7522
|
+
return mod.negated ? `!${base}` : base;
|
|
7654
7523
|
}
|
|
7655
7524
|
/**
|
|
7656
|
-
*
|
|
7525
|
+
* Get unique key for a pseudo condition
|
|
7657
7526
|
*/
|
|
7658
|
-
function
|
|
7659
|
-
return
|
|
7527
|
+
function getPseudoKey(pseudo) {
|
|
7528
|
+
return pseudo.negated ? `!${pseudo.pseudo}` : pseudo.pseudo;
|
|
7660
7529
|
}
|
|
7661
7530
|
/**
|
|
7662
|
-
*
|
|
7663
|
-
* one SelectorGroup whose branches represent the inner OR alternatives.
|
|
7664
|
-
* Shared by @root() and @own().
|
|
7665
|
-
*
|
|
7666
|
-
* Both positive and negated cases produce one variant with one group.
|
|
7667
|
-
* Negation simply sets the `negated` flag, which swaps :is() for :not()
|
|
7668
|
-
* in the final CSS output — no De Morgan transformation is needed.
|
|
7669
|
-
*
|
|
7670
|
-
* This mirrors parentConditionToVariants: OR branches are kept inside
|
|
7671
|
-
* a single group and rendered as comma-separated arguments in
|
|
7672
|
-
* :is()/:not(), e.g. :root:is([a], [b]) or [el]:not([a], [b]).
|
|
7531
|
+
* Get unique key for any selector condition (modifier or pseudo)
|
|
7673
7532
|
*/
|
|
7674
|
-
function
|
|
7675
|
-
|
|
7676
|
-
if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
|
|
7677
|
-
variants: [],
|
|
7678
|
-
isImpossible: true
|
|
7679
|
-
};
|
|
7680
|
-
const branches = [];
|
|
7681
|
-
for (const innerVariant of innerCSS.variants) {
|
|
7682
|
-
const conditions = collectSelectorConditions(innerVariant);
|
|
7683
|
-
if (conditions.length > 0) branches.push(conditions);
|
|
7684
|
-
}
|
|
7685
|
-
if (branches.length === 0) return {
|
|
7686
|
-
variants: [emptyVariant()],
|
|
7687
|
-
isImpossible: false
|
|
7688
|
-
};
|
|
7689
|
-
const v = emptyVariant();
|
|
7690
|
-
v[target].push({
|
|
7691
|
-
branches,
|
|
7692
|
-
negated
|
|
7693
|
-
});
|
|
7694
|
-
return {
|
|
7695
|
-
variants: [v],
|
|
7696
|
-
isImpossible: false
|
|
7697
|
-
};
|
|
7533
|
+
function getSelectorConditionKey(cond) {
|
|
7534
|
+
return "attribute" in cond ? `mod:${getModifierKey(cond)}` : `pseudo:${getPseudoKey(cond)}`;
|
|
7698
7535
|
}
|
|
7699
7536
|
/**
|
|
7700
|
-
*
|
|
7701
|
-
*
|
|
7702
|
-
*
|
|
7703
|
-
* Both positive and negated cases produce one variant with one group.
|
|
7704
|
-
* Negation simply sets the `negated` flag, which swaps :is() for :not()
|
|
7705
|
-
* in the final CSS output — no structural transformation is needed.
|
|
7537
|
+
* Deduplicate selector conditions (modifiers or pseudos).
|
|
7538
|
+
* Shared by root, parent, and own conditions.
|
|
7706
7539
|
*/
|
|
7707
|
-
function
|
|
7708
|
-
const
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
if (conditions.length > 0) branches.push(conditions);
|
|
7540
|
+
function dedupeSelectorConditions(conditions) {
|
|
7541
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7542
|
+
const result = [];
|
|
7543
|
+
for (const c of conditions) {
|
|
7544
|
+
const key = getSelectorConditionKey(c);
|
|
7545
|
+
if (!seen.has(key)) {
|
|
7546
|
+
seen.add(key);
|
|
7547
|
+
result.push(c);
|
|
7548
|
+
}
|
|
7717
7549
|
}
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
direct,
|
|
7726
|
-
negated
|
|
7550
|
+
const facts = collectSubsumptionFacts(result.filter((c) => "attribute" in c));
|
|
7551
|
+
if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
|
|
7552
|
+
return result.filter((c) => {
|
|
7553
|
+
if (!("attribute" in c)) return true;
|
|
7554
|
+
if (isSubsumedNegatedModifier(c, facts)) return false;
|
|
7555
|
+
if (!c.negated && c.value === void 0 && facts.positiveExactValuesByAttr.has(c.attribute)) return false;
|
|
7556
|
+
return true;
|
|
7727
7557
|
});
|
|
7728
|
-
return {
|
|
7729
|
-
variants: [v],
|
|
7730
|
-
isImpossible: false
|
|
7731
|
-
};
|
|
7732
7558
|
}
|
|
7733
7559
|
/**
|
|
7734
|
-
*
|
|
7735
|
-
*
|
|
7736
|
-
* Priority order:
|
|
7737
|
-
* 0: Boolean attribute selectors ([data-hovered])
|
|
7738
|
-
* 1: Value attribute selectors ([data-size="small"])
|
|
7739
|
-
* 2: Negated boolean attributes (:not([data-disabled]))
|
|
7740
|
-
* 3: Negated value attributes (:not([data-size="small"]))
|
|
7741
|
-
* 4: Pseudo-classes (:hover, :focus)
|
|
7742
|
-
* 5: Negated pseudo-classes (:not(:disabled))
|
|
7743
|
-
*
|
|
7744
|
-
* Secondary sort: alphabetical by attribute name / pseudo string.
|
|
7560
|
+
* Check for modifier contradiction: same attribute with opposite negation
|
|
7745
7561
|
*/
|
|
7746
|
-
function
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7562
|
+
function hasModifierContradiction(conditions) {
|
|
7563
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
7564
|
+
for (const mod of conditions) {
|
|
7565
|
+
const baseKey = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
|
|
7566
|
+
const existing = byKey.get(baseKey);
|
|
7567
|
+
if (existing !== void 0 && existing !== !mod.negated) return true;
|
|
7568
|
+
byKey.set(baseKey, !mod.negated);
|
|
7750
7569
|
}
|
|
7751
|
-
return
|
|
7752
|
-
}
|
|
7753
|
-
function sortConditions(conditions) {
|
|
7754
|
-
return conditions.toSorted((a, b) => conditionSortKey(a).localeCompare(conditionSortKey(b)));
|
|
7755
|
-
}
|
|
7756
|
-
function branchToCSS(branch) {
|
|
7757
|
-
let parts = "";
|
|
7758
|
-
for (const cond of sortConditions(branch)) parts += selectorConditionToCSS(cond);
|
|
7759
|
-
return parts;
|
|
7760
|
-
}
|
|
7761
|
-
/**
|
|
7762
|
-
* Wrap serialized selector arguments in :is() or :not().
|
|
7763
|
-
* Arguments are sorted for canonical output.
|
|
7764
|
-
*/
|
|
7765
|
-
function wrapInIsOrNot(args, negated) {
|
|
7766
|
-
return `${negated ? ":not" : ":is"}(${args.sort().join(", ")})`;
|
|
7767
|
-
}
|
|
7768
|
-
/**
|
|
7769
|
-
* Convert a selector group to a CSS selector fragment.
|
|
7770
|
-
*
|
|
7771
|
-
* Single-branch groups are unwrapped (no :is() wrapper).
|
|
7772
|
-
* Multi-branch groups use :is() or :not().
|
|
7773
|
-
* Negation swaps :is() for :not().
|
|
7774
|
-
*/
|
|
7775
|
-
function selectorGroupToCSS(group) {
|
|
7776
|
-
if (group.branches.length === 0) return "";
|
|
7777
|
-
if (group.branches.length === 1) {
|
|
7778
|
-
const parts = branchToCSS(group.branches[0]);
|
|
7779
|
-
if (group.negated) return `:not(${parts})`;
|
|
7780
|
-
return parts;
|
|
7781
|
-
}
|
|
7782
|
-
return wrapInIsOrNot(group.branches.map(branchToCSS), group.negated);
|
|
7783
|
-
}
|
|
7784
|
-
/**
|
|
7785
|
-
* Collect facts about modifier conditions for subsumption analysis.
|
|
7786
|
-
* Tracks negated boolean attrs (:not([attr])) and positive exact values ([attr="X"]).
|
|
7787
|
-
*/
|
|
7788
|
-
function collectSubsumptionFacts(modifiers) {
|
|
7789
|
-
const negatedBooleanAttrs = /* @__PURE__ */ new Set();
|
|
7790
|
-
const positiveExactValuesByAttr = /* @__PURE__ */ new Map();
|
|
7791
|
-
for (const mod of modifiers) {
|
|
7792
|
-
if (mod.negated && mod.value === void 0) negatedBooleanAttrs.add(mod.attribute);
|
|
7793
|
-
if (!mod.negated && mod.value !== void 0 && (mod.operator ?? "=") === "=") {
|
|
7794
|
-
let vals = positiveExactValuesByAttr.get(mod.attribute);
|
|
7795
|
-
if (!vals) {
|
|
7796
|
-
vals = /* @__PURE__ */ new Set();
|
|
7797
|
-
positiveExactValuesByAttr.set(mod.attribute, vals);
|
|
7798
|
-
}
|
|
7799
|
-
vals.add(mod.value);
|
|
7800
|
-
}
|
|
7801
|
-
}
|
|
7802
|
-
return {
|
|
7803
|
-
negatedBooleanAttrs,
|
|
7804
|
-
positiveExactValuesByAttr
|
|
7805
|
-
};
|
|
7806
|
-
}
|
|
7807
|
-
/**
|
|
7808
|
-
* Check if a negated-value modifier is subsumed by stronger facts:
|
|
7809
|
-
* - :not([attr]) subsumes :not([attr="val"])
|
|
7810
|
-
* - [attr="X"] implies :not([attr="Y"]) is redundant (single exact value)
|
|
7811
|
-
*
|
|
7812
|
-
* Only applies to exact-match (=) operators; substring operators don't
|
|
7813
|
-
* imply exclusivity between values.
|
|
7814
|
-
*/
|
|
7815
|
-
function isSubsumedNegatedModifier(mod, facts) {
|
|
7816
|
-
if (!mod.negated || mod.value === void 0) return false;
|
|
7817
|
-
if (facts.negatedBooleanAttrs.has(mod.attribute)) return true;
|
|
7818
|
-
if ((mod.operator ?? "=") === "=") {
|
|
7819
|
-
const posVals = facts.positiveExactValuesByAttr.get(mod.attribute);
|
|
7820
|
-
if (posVals && posVals.size === 1 && !posVals.has(mod.value)) return true;
|
|
7821
|
-
}
|
|
7822
|
-
return false;
|
|
7823
|
-
}
|
|
7824
|
-
/**
|
|
7825
|
-
* Remove redundant single-condition groups that are subsumed by stronger
|
|
7826
|
-
* groups on the same attribute. O(n) — only inspects single-branch,
|
|
7827
|
-
* single-condition groups.
|
|
7828
|
-
*/
|
|
7829
|
-
function optimizeGroups(groups) {
|
|
7830
|
-
if (groups.length <= 1) return groups;
|
|
7831
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7832
|
-
const result = [];
|
|
7833
|
-
for (const g of groups) {
|
|
7834
|
-
const key = getSelectorGroupKey(g);
|
|
7835
|
-
if (!seen.has(key)) {
|
|
7836
|
-
seen.add(key);
|
|
7837
|
-
result.push(g);
|
|
7838
|
-
}
|
|
7839
|
-
}
|
|
7840
|
-
if (result.length <= 1) return result;
|
|
7841
|
-
const effectiveModifiers = [];
|
|
7842
|
-
for (const g of result) {
|
|
7843
|
-
if (g.branches.length !== 1 || g.branches[0].length !== 1) continue;
|
|
7844
|
-
const cond = g.branches[0][0];
|
|
7845
|
-
if (!("attribute" in cond)) continue;
|
|
7846
|
-
effectiveModifiers.push({
|
|
7847
|
-
...cond,
|
|
7848
|
-
negated: g.negated !== cond.negated
|
|
7849
|
-
});
|
|
7850
|
-
}
|
|
7851
|
-
const facts = collectSubsumptionFacts(effectiveModifiers);
|
|
7852
|
-
if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
|
|
7853
|
-
return result.filter((g) => {
|
|
7854
|
-
if (g.branches.length !== 1 || g.branches[0].length !== 1) return true;
|
|
7855
|
-
const cond = g.branches[0][0];
|
|
7856
|
-
if (!("attribute" in cond) || !g.negated || cond.negated || cond.value === void 0) return true;
|
|
7857
|
-
return !isSubsumedNegatedModifier({
|
|
7858
|
-
...cond,
|
|
7859
|
-
negated: true
|
|
7860
|
-
}, facts);
|
|
7861
|
-
});
|
|
7862
|
-
}
|
|
7863
|
-
/**
|
|
7864
|
-
* Convert root groups to CSS selector prefix (for final output)
|
|
7865
|
-
*/
|
|
7866
|
-
function rootGroupsToCSS(groups) {
|
|
7867
|
-
if (groups.length === 0) return void 0;
|
|
7868
|
-
const optimized = optimizeGroups(groups);
|
|
7869
|
-
if (optimized.length === 0) return void 0;
|
|
7870
|
-
let prefix = ":root";
|
|
7871
|
-
for (const group of optimized) prefix += selectorGroupToCSS(group);
|
|
7872
|
-
return prefix;
|
|
7873
|
-
}
|
|
7874
|
-
/**
|
|
7875
|
-
* Convert parent groups to CSS selector fragments (for final output).
|
|
7876
|
-
* Each group produces its own :is()/:not() wrapper with a combinator
|
|
7877
|
-
* suffix (` *` or ` > *`) appended to each branch.
|
|
7878
|
-
*/
|
|
7879
|
-
function parentGroupsToCSS(groups) {
|
|
7880
|
-
let result = "";
|
|
7881
|
-
for (const group of groups) {
|
|
7882
|
-
const combinator = group.direct ? " > *" : " *";
|
|
7883
|
-
const args = group.branches.map((branch) => branchToCSS(branch) + combinator);
|
|
7884
|
-
result += wrapInIsOrNot(args, group.negated);
|
|
7885
|
-
}
|
|
7886
|
-
return result;
|
|
7887
|
-
}
|
|
7888
|
-
/**
|
|
7889
|
-
* Convert a modifier or pseudo condition to a CSS selector fragment
|
|
7890
|
-
*/
|
|
7891
|
-
function selectorConditionToCSS(cond) {
|
|
7892
|
-
if ("attribute" in cond) return modifierToCSS(cond);
|
|
7893
|
-
return pseudoToCSS(cond);
|
|
7894
|
-
}
|
|
7895
|
-
/**
|
|
7896
|
-
* Get unique key for a modifier condition
|
|
7897
|
-
*/
|
|
7898
|
-
function getModifierKey(mod) {
|
|
7899
|
-
const base = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
|
|
7900
|
-
return mod.negated ? `!${base}` : base;
|
|
7901
|
-
}
|
|
7902
|
-
/**
|
|
7903
|
-
* Get unique key for a pseudo condition
|
|
7904
|
-
*/
|
|
7905
|
-
function getPseudoKey(pseudo) {
|
|
7906
|
-
return pseudo.negated ? `!${pseudo.pseudo}` : pseudo.pseudo;
|
|
7907
|
-
}
|
|
7908
|
-
/**
|
|
7909
|
-
* Get unique key for any selector condition (modifier or pseudo)
|
|
7910
|
-
*/
|
|
7911
|
-
function getSelectorConditionKey(cond) {
|
|
7912
|
-
return "attribute" in cond ? `mod:${getModifierKey(cond)}` : `pseudo:${getPseudoKey(cond)}`;
|
|
7913
|
-
}
|
|
7914
|
-
/**
|
|
7915
|
-
* Deduplicate selector conditions (modifiers or pseudos).
|
|
7916
|
-
* Shared by root, parent, and own conditions.
|
|
7917
|
-
*/
|
|
7918
|
-
function dedupeSelectorConditions(conditions) {
|
|
7919
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7920
|
-
const result = [];
|
|
7921
|
-
for (const c of conditions) {
|
|
7922
|
-
const key = getSelectorConditionKey(c);
|
|
7923
|
-
if (!seen.has(key)) {
|
|
7924
|
-
seen.add(key);
|
|
7925
|
-
result.push(c);
|
|
7926
|
-
}
|
|
7927
|
-
}
|
|
7928
|
-
const facts = collectSubsumptionFacts(result.filter((c) => "attribute" in c));
|
|
7929
|
-
if (facts.negatedBooleanAttrs.size === 0 && facts.positiveExactValuesByAttr.size === 0) return result;
|
|
7930
|
-
return result.filter((c) => {
|
|
7931
|
-
if (!("attribute" in c)) return true;
|
|
7932
|
-
if (isSubsumedNegatedModifier(c, facts)) return false;
|
|
7933
|
-
if (!c.negated && c.value === void 0 && facts.positiveExactValuesByAttr.has(c.attribute)) return false;
|
|
7934
|
-
return true;
|
|
7935
|
-
});
|
|
7936
|
-
}
|
|
7937
|
-
/**
|
|
7938
|
-
* Check for modifier contradiction: same attribute with opposite negation
|
|
7939
|
-
*/
|
|
7940
|
-
function hasModifierContradiction(conditions) {
|
|
7941
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
7942
|
-
for (const mod of conditions) {
|
|
7943
|
-
const baseKey = mod.value ? `${mod.attribute}${mod.operator || "="}${mod.value}` : mod.attribute;
|
|
7944
|
-
const existing = byKey.get(baseKey);
|
|
7945
|
-
if (existing !== void 0 && existing !== !mod.negated) return true;
|
|
7946
|
-
byKey.set(baseKey, !mod.negated);
|
|
7947
|
-
}
|
|
7948
|
-
return false;
|
|
7570
|
+
return false;
|
|
7949
7571
|
}
|
|
7950
7572
|
/**
|
|
7951
7573
|
* Check for pseudo contradiction: same pseudo with opposite negation
|
|
@@ -8132,346 +7754,748 @@ function isModifierConditionsSuperset(a, b) {
|
|
|
8132
7754
|
function isPseudoConditionsSuperset(a, b) {
|
|
8133
7755
|
return isConditionsSuperset(a, b, getPseudoKey);
|
|
8134
7756
|
}
|
|
8135
|
-
function isSelectorGroupsSuperset(a, b) {
|
|
8136
|
-
if (a.length < b.length) return false;
|
|
8137
|
-
return isConditionsSuperset(a, b, getSelectorGroupKey);
|
|
7757
|
+
function isSelectorGroupsSuperset(a, b) {
|
|
7758
|
+
if (a.length < b.length) return false;
|
|
7759
|
+
return isConditionsSuperset(a, b, getSelectorGroupKey);
|
|
7760
|
+
}
|
|
7761
|
+
/**
|
|
7762
|
+
* Check if parent groups A is a superset of B.
|
|
7763
|
+
* Each group in B must have a matching group in A.
|
|
7764
|
+
*/
|
|
7765
|
+
function isParentGroupsSuperset(a, b) {
|
|
7766
|
+
if (a.length < b.length) return false;
|
|
7767
|
+
return isConditionsSuperset(a, b, getParentGroupKey);
|
|
7768
|
+
}
|
|
7769
|
+
function getParentGroupKey(g) {
|
|
7770
|
+
return `${g.negated ? "!" : ""}${g.direct ? ">" : ""}(${getBranchesKey(g.branches)})`;
|
|
7771
|
+
}
|
|
7772
|
+
/**
|
|
7773
|
+
* Deduplicate variants
|
|
7774
|
+
*
|
|
7775
|
+
* Removes:
|
|
7776
|
+
* 1. Exact duplicates (same key)
|
|
7777
|
+
* 2. Superset variants (more restrictive selectors that are redundant)
|
|
7778
|
+
*/
|
|
7779
|
+
function dedupeVariants(variants) {
|
|
7780
|
+
if (variants.length <= 1) return variants;
|
|
7781
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7782
|
+
const result = [];
|
|
7783
|
+
for (const v of variants) {
|
|
7784
|
+
const key = getVariantKey(v);
|
|
7785
|
+
if (!seen.has(key)) {
|
|
7786
|
+
seen.add(key);
|
|
7787
|
+
result.push(v);
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
if (result.length <= 1) return result;
|
|
7791
|
+
result.sort((a, b) => variantConditionCount(a) - variantConditionCount(b));
|
|
7792
|
+
const filtered = [];
|
|
7793
|
+
for (const candidate of result) {
|
|
7794
|
+
let isRedundant = false;
|
|
7795
|
+
for (const kept of filtered) if (isVariantSuperset(candidate, kept)) {
|
|
7796
|
+
isRedundant = true;
|
|
7797
|
+
break;
|
|
7798
|
+
}
|
|
7799
|
+
if (!isRedundant) filtered.push(candidate);
|
|
7800
|
+
}
|
|
7801
|
+
return filtered;
|
|
7802
|
+
}
|
|
7803
|
+
/**
|
|
7804
|
+
* Combine AND conditions into CSS
|
|
7805
|
+
*
|
|
7806
|
+
* AND of conditions means cartesian product of variants:
|
|
7807
|
+
* (A1 | A2) & (B1 | B2) = A1&B1 | A1&B2 | A2&B1 | A2&B2
|
|
7808
|
+
*
|
|
7809
|
+
* Variants that result in contradictions (e.g., conflicting media rules)
|
|
7810
|
+
* are filtered out.
|
|
7811
|
+
*/
|
|
7812
|
+
function andToCSS(children) {
|
|
7813
|
+
const exclusiveChildren = makeOrBranchesExclusive(children);
|
|
7814
|
+
let currentVariants = [emptyVariant()];
|
|
7815
|
+
for (const child of exclusiveChildren) {
|
|
7816
|
+
const childCSS = conditionToCSSInner(child);
|
|
7817
|
+
if (childCSS.isImpossible || childCSS.variants.length === 0) return {
|
|
7818
|
+
variants: [],
|
|
7819
|
+
isImpossible: true
|
|
7820
|
+
};
|
|
7821
|
+
const newVariants = [];
|
|
7822
|
+
for (const current of currentVariants) for (const childVariant of childCSS.variants) {
|
|
7823
|
+
const merged = mergeVariants(current, childVariant);
|
|
7824
|
+
if (merged !== null) newVariants.push(merged);
|
|
7825
|
+
}
|
|
7826
|
+
if (newVariants.length === 0) return {
|
|
7827
|
+
variants: [],
|
|
7828
|
+
isImpossible: true
|
|
7829
|
+
};
|
|
7830
|
+
currentVariants = dedupeVariants(newVariants);
|
|
7831
|
+
}
|
|
7832
|
+
return {
|
|
7833
|
+
variants: currentVariants,
|
|
7834
|
+
isImpossible: false
|
|
7835
|
+
};
|
|
7836
|
+
}
|
|
7837
|
+
/**
|
|
7838
|
+
* Make OR branches within AND children mutually exclusive.
|
|
7839
|
+
*
|
|
7840
|
+
* For an AND child that is OR(A, B), transforms it to OR(A, B & !A)
|
|
7841
|
+
* so that when andToCSS does a Cartesian product, the resulting
|
|
7842
|
+
* CSS variants don't overlap.
|
|
7843
|
+
*
|
|
7844
|
+
* Only transforms OR children whose branches actually produce
|
|
7845
|
+
* different at-rule contexts when materialized. This avoids
|
|
7846
|
+
* breaking cases where contradiction detection in the Cartesian
|
|
7847
|
+
* product naturally handles deduplication.
|
|
7848
|
+
*/
|
|
7849
|
+
function makeOrBranchesExclusive(children) {
|
|
7850
|
+
return children.map((child) => {
|
|
7851
|
+
if (!isCompoundCondition(child) || child.operator !== "OR") return child;
|
|
7852
|
+
if (child.children.length <= 1) return child;
|
|
7853
|
+
if (!branchesProduceDifferentContexts(child.children)) return child;
|
|
7854
|
+
const exclusiveBranches = [];
|
|
7855
|
+
const priorBranches = [];
|
|
7856
|
+
for (const branch of child.children) {
|
|
7857
|
+
if (priorBranches.length === 0) exclusiveBranches.push(branch);
|
|
7858
|
+
else {
|
|
7859
|
+
let exclusive = branch;
|
|
7860
|
+
for (const prior of priorBranches) exclusive = and(exclusive, not(prior));
|
|
7861
|
+
const simplified = simplifyCondition(exclusive);
|
|
7862
|
+
if (simplified.kind !== "false") exclusiveBranches.push(simplified);
|
|
7863
|
+
}
|
|
7864
|
+
priorBranches.push(branch);
|
|
7865
|
+
}
|
|
7866
|
+
if (exclusiveBranches.length === 0) return child;
|
|
7867
|
+
if (exclusiveBranches.length === 1) return exclusiveBranches[0];
|
|
7868
|
+
return {
|
|
7869
|
+
kind: "compound",
|
|
7870
|
+
operator: "OR",
|
|
7871
|
+
children: exclusiveBranches
|
|
7872
|
+
};
|
|
7873
|
+
});
|
|
7874
|
+
}
|
|
7875
|
+
/**
|
|
7876
|
+
* Check if OR branches produce different at-rule contexts when
|
|
7877
|
+
* materialized. If so, the Cartesian product in andToCSS will
|
|
7878
|
+
* create overlapping CSS variants that need exclusive expansion.
|
|
7879
|
+
*
|
|
7880
|
+
* Exported so Stage 2a (`expandOrConditions` in `exclusive.ts`) can
|
|
7881
|
+
* reuse the same heuristic and skip OR expansion when every branch
|
|
7882
|
+
* lives in the same at-rule/root/parent/own context — pure-selector
|
|
7883
|
+
* ORs are better collapsed into `:is(...)` at materialization time
|
|
7884
|
+
* than expanded into mutually-exclusive `A | (B & !A) | …` cascades.
|
|
7885
|
+
*/
|
|
7886
|
+
function branchesProduceDifferentContexts(branches) {
|
|
7887
|
+
const contextKeys = /* @__PURE__ */ new Set();
|
|
7888
|
+
for (const branch of branches) {
|
|
7889
|
+
const css = conditionToCSSInner(branch);
|
|
7890
|
+
if (css.isImpossible) continue;
|
|
7891
|
+
for (const v of css.variants) contextKeys.add(getVariantContextKey(v));
|
|
7892
|
+
}
|
|
7893
|
+
return contextKeys.size > 1;
|
|
7894
|
+
}
|
|
7895
|
+
/**
|
|
7896
|
+
* Combine OR conditions into CSS
|
|
7897
|
+
*
|
|
7898
|
+
* OR in CSS means multiple selector variants (DNF).
|
|
7899
|
+
* After deduplication, variants that differ only in their base
|
|
7900
|
+
* modifier/pseudo conditions are merged into :is() groups.
|
|
7901
|
+
*
|
|
7902
|
+
* Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
|
|
7903
|
+
* so here we just collect all variants. Any remaining ORs in the condition
|
|
7904
|
+
* tree (e.g., from De Morgan expansion) are handled as simple alternatives.
|
|
7905
|
+
*/
|
|
7906
|
+
function orToCSS(children) {
|
|
7907
|
+
const allVariants = [];
|
|
7908
|
+
for (const child of children) {
|
|
7909
|
+
const childCSS = conditionToCSSInner(child);
|
|
7910
|
+
if (childCSS.isImpossible) continue;
|
|
7911
|
+
allVariants.push(...childCSS.variants);
|
|
7912
|
+
}
|
|
7913
|
+
if (allVariants.length === 0) return {
|
|
7914
|
+
variants: [],
|
|
7915
|
+
isImpossible: true
|
|
7916
|
+
};
|
|
7917
|
+
return {
|
|
7918
|
+
variants: dedupeVariants(allVariants),
|
|
7919
|
+
isImpossible: false
|
|
7920
|
+
};
|
|
7921
|
+
}
|
|
7922
|
+
/**
|
|
7923
|
+
* Find keys present in ALL condition arrays.
|
|
7924
|
+
*/
|
|
7925
|
+
function findCommonKeys(conditionSets, getKey) {
|
|
7926
|
+
if (conditionSets.length === 0) return /* @__PURE__ */ new Set();
|
|
7927
|
+
const common = new Set(conditionSets[0].map(getKey));
|
|
7928
|
+
for (let i = 1; i < conditionSets.length; i++) {
|
|
7929
|
+
const keys = new Set(conditionSets[i].map(getKey));
|
|
7930
|
+
for (const key of common) if (!keys.has(key)) common.delete(key);
|
|
7931
|
+
}
|
|
7932
|
+
return common;
|
|
7933
|
+
}
|
|
7934
|
+
/**
|
|
7935
|
+
* Merge OR variants that share the same "context" (at-rules, root, parent,
|
|
7936
|
+
* own, starting) into a single variant with a SelectorGroup.
|
|
7937
|
+
*
|
|
7938
|
+
* Variants with no modifier/pseudo conditions are kept separate (they match
|
|
7939
|
+
* unconditionally and can't be expressed inside :is()).
|
|
7940
|
+
*/
|
|
7941
|
+
function mergeVariantsIntoSelectorGroups(variants) {
|
|
7942
|
+
if (variants.length <= 1) return variants;
|
|
7943
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7944
|
+
for (const v of variants) {
|
|
7945
|
+
const key = getVariantContextKey(v);
|
|
7946
|
+
const group = groups.get(key);
|
|
7947
|
+
if (group) group.push(v);
|
|
7948
|
+
else groups.set(key, [v]);
|
|
7949
|
+
}
|
|
7950
|
+
const result = [];
|
|
7951
|
+
for (const group of groups.values()) {
|
|
7952
|
+
if (group.length === 1) {
|
|
7953
|
+
result.push(group[0]);
|
|
7954
|
+
continue;
|
|
7955
|
+
}
|
|
7956
|
+
const withSelectors = [];
|
|
7957
|
+
const withoutSelectors = [];
|
|
7958
|
+
for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
|
|
7959
|
+
else withSelectors.push(v);
|
|
7960
|
+
result.push(...withoutSelectors);
|
|
7961
|
+
if (withSelectors.length <= 1) {
|
|
7962
|
+
result.push(...withSelectors);
|
|
7963
|
+
continue;
|
|
7964
|
+
}
|
|
7965
|
+
result.push(factorAndGroup(withSelectors));
|
|
7966
|
+
}
|
|
7967
|
+
return result;
|
|
7968
|
+
}
|
|
7969
|
+
/**
|
|
7970
|
+
* Factor common modifier/pseudo conditions out of variants and create
|
|
7971
|
+
* a single variant with a SelectorGroup for the remaining (differing)
|
|
7972
|
+
* conditions.
|
|
7973
|
+
*
|
|
7974
|
+
* Precondition: all variants must share the same context key (identical
|
|
7975
|
+
* at-rules, root/parent/own/selector groups, startingStyle).
|
|
7976
|
+
*/
|
|
7977
|
+
function factorAndGroup(variants) {
|
|
7978
|
+
{
|
|
7979
|
+
const key0 = getVariantContextKey(variants[0]);
|
|
7980
|
+
for (let i = 1; i < variants.length; i++) {
|
|
7981
|
+
const keyI = getVariantContextKey(variants[i]);
|
|
7982
|
+
if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
|
|
7986
|
+
const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
|
|
7987
|
+
const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
|
|
7988
|
+
const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
|
|
7989
|
+
const branches = [];
|
|
7990
|
+
let hasEmptyBranch = false;
|
|
7991
|
+
for (const v of variants) {
|
|
7992
|
+
const branch = [];
|
|
7993
|
+
for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
|
|
7994
|
+
for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
|
|
7995
|
+
if (branch.length > 0) branches.push(branch);
|
|
7996
|
+
else hasEmptyBranch = true;
|
|
7997
|
+
}
|
|
7998
|
+
if (hasEmptyBranch) return {
|
|
7999
|
+
...variants[0],
|
|
8000
|
+
modifierConditions: commonModifiers,
|
|
8001
|
+
pseudoConditions: commonPseudos
|
|
8002
|
+
};
|
|
8003
|
+
const factoredGroups = tryFactorIntoDimensions(branches);
|
|
8004
|
+
if (factoredGroups) return {
|
|
8005
|
+
modifierConditions: commonModifiers,
|
|
8006
|
+
pseudoConditions: commonPseudos,
|
|
8007
|
+
selectorGroups: [...variants[0].selectorGroups, ...factoredGroups],
|
|
8008
|
+
ownGroups: [...variants[0].ownGroups],
|
|
8009
|
+
mediaConditions: [...variants[0].mediaConditions],
|
|
8010
|
+
containerConditions: [...variants[0].containerConditions],
|
|
8011
|
+
supportsConditions: [...variants[0].supportsConditions],
|
|
8012
|
+
rootGroups: [...variants[0].rootGroups],
|
|
8013
|
+
parentGroups: [...variants[0].parentGroups],
|
|
8014
|
+
startingStyle: variants[0].startingStyle
|
|
8015
|
+
};
|
|
8016
|
+
return {
|
|
8017
|
+
modifierConditions: commonModifiers,
|
|
8018
|
+
pseudoConditions: commonPseudos,
|
|
8019
|
+
selectorGroups: [...variants[0].selectorGroups, {
|
|
8020
|
+
branches,
|
|
8021
|
+
negated: false
|
|
8022
|
+
}],
|
|
8023
|
+
ownGroups: [...variants[0].ownGroups],
|
|
8024
|
+
mediaConditions: [...variants[0].mediaConditions],
|
|
8025
|
+
containerConditions: [...variants[0].containerConditions],
|
|
8026
|
+
supportsConditions: [...variants[0].supportsConditions],
|
|
8027
|
+
rootGroups: [...variants[0].rootGroups],
|
|
8028
|
+
parentGroups: [...variants[0].parentGroups],
|
|
8029
|
+
startingStyle: variants[0].startingStyle
|
|
8030
|
+
};
|
|
8031
|
+
}
|
|
8032
|
+
/**
|
|
8033
|
+
* Detect when branches form a complete Cartesian product of independent
|
|
8034
|
+
* modifier attribute dimensions and return one SelectorGroup per dimension.
|
|
8035
|
+
*
|
|
8036
|
+
* Example: 4 branches for 2 attributes × 2 values each →
|
|
8037
|
+
* :is(A1, A2):is(B1, B2) instead of :is(A1B1, A1B2, A2B1, A2B2)
|
|
8038
|
+
*/
|
|
8039
|
+
function tryFactorIntoDimensions(branches) {
|
|
8040
|
+
if (branches.length < 4) return null;
|
|
8041
|
+
const dimensions = /* @__PURE__ */ new Map();
|
|
8042
|
+
for (const branch of branches) for (const cond of branch) {
|
|
8043
|
+
if (!("attribute" in cond)) return null;
|
|
8044
|
+
if (!dimensions.has(cond.attribute)) dimensions.set(cond.attribute, /* @__PURE__ */ new Map());
|
|
8045
|
+
dimensions.get(cond.attribute).set(getModifierKey(cond), cond);
|
|
8046
|
+
}
|
|
8047
|
+
if (dimensions.size < 2) return null;
|
|
8048
|
+
for (const branch of branches) {
|
|
8049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8050
|
+
for (const cond of branch) {
|
|
8051
|
+
const attr = cond.attribute;
|
|
8052
|
+
if (seen.has(attr)) return null;
|
|
8053
|
+
seen.add(attr);
|
|
8054
|
+
}
|
|
8055
|
+
if (seen.size !== dimensions.size) return null;
|
|
8056
|
+
}
|
|
8057
|
+
let expectedCount = 1;
|
|
8058
|
+
for (const vals of dimensions.values()) expectedCount *= vals.size;
|
|
8059
|
+
if (branches.length !== expectedCount) return null;
|
|
8060
|
+
return [...dimensions.values()].map((vals) => ({
|
|
8061
|
+
branches: [...vals.values()].map((cond) => [cond]),
|
|
8062
|
+
negated: false
|
|
8063
|
+
}));
|
|
8064
|
+
}
|
|
8065
|
+
/**
|
|
8066
|
+
* Build at-rules array from a variant
|
|
8067
|
+
*/
|
|
8068
|
+
function buildAtRulesFromVariant(variant) {
|
|
8069
|
+
const atRules = [];
|
|
8070
|
+
if (variant.mediaConditions.length > 0) {
|
|
8071
|
+
const conditionParts = variant.mediaConditions.map((c) => {
|
|
8072
|
+
if (c.subtype === "type") return c.negated ? `not ${c.condition}` : c.condition;
|
|
8073
|
+
else return c.negated ? `(not ${c.condition})` : c.condition;
|
|
8074
|
+
});
|
|
8075
|
+
atRules.push(`@media ${conditionParts.sort().join(" and ")}`);
|
|
8076
|
+
}
|
|
8077
|
+
if (variant.containerConditions.length > 0) {
|
|
8078
|
+
const byName = /* @__PURE__ */ new Map();
|
|
8079
|
+
for (const cond of variant.containerConditions) {
|
|
8080
|
+
const group = byName.get(cond.name) || [];
|
|
8081
|
+
group.push(cond);
|
|
8082
|
+
byName.set(cond.name, group);
|
|
8083
|
+
}
|
|
8084
|
+
for (const [name, conditions] of byName) {
|
|
8085
|
+
const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
|
|
8086
|
+
const namePrefix = name ? `${name} ` : "";
|
|
8087
|
+
atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
if (variant.supportsConditions.length > 0) {
|
|
8091
|
+
const conditionParts = variant.supportsConditions.map((c) => {
|
|
8092
|
+
if (c.subtype === "selector") {
|
|
8093
|
+
const selectorCond = `selector(${c.condition})`;
|
|
8094
|
+
return c.negated ? `(not ${selectorCond})` : selectorCond;
|
|
8095
|
+
} else {
|
|
8096
|
+
const featureCond = `(${c.condition})`;
|
|
8097
|
+
return c.negated ? `(not ${featureCond})` : featureCond;
|
|
8098
|
+
}
|
|
8099
|
+
});
|
|
8100
|
+
atRules.push(`@supports ${conditionParts.join(" and ")}`);
|
|
8101
|
+
}
|
|
8102
|
+
return atRules;
|
|
8103
|
+
}
|
|
8104
|
+
//#endregion
|
|
8105
|
+
//#region src/pipeline/exclusive.ts
|
|
8106
|
+
/**
|
|
8107
|
+
* Build exclusive conditions for a list of parsed style entries.
|
|
8108
|
+
*
|
|
8109
|
+
* The entries should be ordered by priority (highest priority first).
|
|
8110
|
+
*
|
|
8111
|
+
* For each entry, we compute:
|
|
8112
|
+
* exclusiveCondition = condition & !prior[0] & !prior[1] & ...
|
|
8113
|
+
*
|
|
8114
|
+
* This ensures exactly one condition matches at any time.
|
|
8115
|
+
*
|
|
8116
|
+
* Example:
|
|
8117
|
+
* Input (ordered highest to lowest priority):
|
|
8118
|
+
* A: value1 (priority 2)
|
|
8119
|
+
* B: value2 (priority 1)
|
|
8120
|
+
* C: value3 (priority 0)
|
|
8121
|
+
*
|
|
8122
|
+
* Output:
|
|
8123
|
+
* A: A
|
|
8124
|
+
* B: B & !A
|
|
8125
|
+
* C: C & !A & !B
|
|
8126
|
+
*
|
|
8127
|
+
* @param entries Parsed style entries ordered by priority (highest first)
|
|
8128
|
+
* @returns Entries with exclusive conditions, filtered to remove impossible ones
|
|
8129
|
+
*/
|
|
8130
|
+
function buildExclusiveConditions(entries) {
|
|
8131
|
+
const result = [];
|
|
8132
|
+
const priorConditions = [];
|
|
8133
|
+
for (const entry of entries) {
|
|
8134
|
+
let exclusive = entry.condition;
|
|
8135
|
+
for (const prior of priorConditions) if (prior.kind !== "true") exclusive = and(exclusive, not(prior));
|
|
8136
|
+
const simplified = simplifyCondition(exclusive);
|
|
8137
|
+
if (simplified.kind === "false") continue;
|
|
8138
|
+
result.push({
|
|
8139
|
+
...entry,
|
|
8140
|
+
exclusiveCondition: simplified
|
|
8141
|
+
});
|
|
8142
|
+
if (entry.condition.kind !== "true") priorConditions.push(entry.condition);
|
|
8143
|
+
}
|
|
8144
|
+
return result;
|
|
8145
|
+
}
|
|
8146
|
+
/**
|
|
8147
|
+
* Parse style entries from a value mapping object.
|
|
8148
|
+
*
|
|
8149
|
+
* @param styleKey The style key (e.g., 'padding')
|
|
8150
|
+
* @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }
|
|
8151
|
+
* @param parseCondition Function to parse state keys into conditions
|
|
8152
|
+
* @returns Parsed entries ordered by priority (highest first)
|
|
8153
|
+
*/
|
|
8154
|
+
function parseStyleEntries(styleKey, valueMap, parseCondition) {
|
|
8155
|
+
const entries = [];
|
|
8156
|
+
Object.keys(valueMap).forEach((stateKey, index) => {
|
|
8157
|
+
const value = valueMap[stateKey];
|
|
8158
|
+
const condition = stateKey === "" ? trueCondition() : parseCondition(stateKey);
|
|
8159
|
+
entries.push({
|
|
8160
|
+
styleKey,
|
|
8161
|
+
stateKey,
|
|
8162
|
+
value,
|
|
8163
|
+
condition,
|
|
8164
|
+
priority: index
|
|
8165
|
+
});
|
|
8166
|
+
});
|
|
8167
|
+
entries.reverse();
|
|
8168
|
+
return entries;
|
|
8169
|
+
}
|
|
8170
|
+
/**
|
|
8171
|
+
* Merge parsed entries that share the same value.
|
|
8172
|
+
*
|
|
8173
|
+
* When multiple **non-default** state keys map to the same value, their
|
|
8174
|
+
* conditions can be combined with OR and treated as a single entry.
|
|
8175
|
+
* This must happen **before** exclusive expansion and OR branch splitting
|
|
8176
|
+
* to avoid combinatorial explosion and duplicate CSS output.
|
|
8177
|
+
*
|
|
8178
|
+
* Default (TRUE) entries are **never** merged with non-default entries.
|
|
8179
|
+
* Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
|
|
8180
|
+
* condition's participation in exclusive building. That causes
|
|
8181
|
+
* intermediate-priority states to lose their `:not(X)` negation,
|
|
8182
|
+
* breaking mutual exclusivity when X and an intermediate state are
|
|
8183
|
+
* both active. Stage 6 `mergeByValue` handles combining rules with
|
|
8184
|
+
* identical CSS output after exclusive conditions are correctly built.
|
|
8185
|
+
*
|
|
8186
|
+
* Example: `{ '@dark': 'red', '@dark & @hc': 'red' }` merges into a
|
|
8187
|
+
* single entry with condition `@dark | (@dark & @hc)` = `@dark`.
|
|
8188
|
+
*
|
|
8189
|
+
* Entries are ordered highest-priority-first. The merged entry keeps the
|
|
8190
|
+
* highest priority of the group.
|
|
8191
|
+
*/
|
|
8192
|
+
function mergeEntriesByValue(entries) {
|
|
8193
|
+
if (entries.length <= 1) return entries;
|
|
8194
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8195
|
+
for (const entry of entries) {
|
|
8196
|
+
const valueKey = serializeValue(entry.value);
|
|
8197
|
+
const group = groups.get(valueKey);
|
|
8198
|
+
if (group) {
|
|
8199
|
+
group.entries.push(entry);
|
|
8200
|
+
group.maxPriority = Math.max(group.maxPriority, entry.priority);
|
|
8201
|
+
} else groups.set(valueKey, {
|
|
8202
|
+
entries: [entry],
|
|
8203
|
+
maxPriority: entry.priority
|
|
8204
|
+
});
|
|
8205
|
+
}
|
|
8206
|
+
if (groups.size === entries.length) return entries;
|
|
8207
|
+
const merged = [];
|
|
8208
|
+
for (const [, group] of groups) {
|
|
8209
|
+
if (group.entries.length === 1) {
|
|
8210
|
+
merged.push(group.entries[0]);
|
|
8211
|
+
continue;
|
|
8212
|
+
}
|
|
8213
|
+
const defaultEntries = group.entries.filter((e) => e.condition.kind === "true");
|
|
8214
|
+
const nonDefaultEntries = group.entries.filter((e) => e.condition.kind !== "true");
|
|
8215
|
+
for (const entry of defaultEntries) merged.push(entry);
|
|
8216
|
+
if (nonDefaultEntries.length === 1) merged.push(nonDefaultEntries[0]);
|
|
8217
|
+
else if (nonDefaultEntries.length >= 2) {
|
|
8218
|
+
const combinedCondition = simplifyCondition(or(...nonDefaultEntries.map((e) => e.condition)));
|
|
8219
|
+
const combinedStateKey = nonDefaultEntries.map((e) => e.stateKey).join(" | ");
|
|
8220
|
+
merged.push({
|
|
8221
|
+
styleKey: nonDefaultEntries[0].styleKey,
|
|
8222
|
+
stateKey: combinedStateKey,
|
|
8223
|
+
value: nonDefaultEntries[0].value,
|
|
8224
|
+
condition: combinedCondition,
|
|
8225
|
+
priority: group.maxPriority
|
|
8226
|
+
});
|
|
8227
|
+
}
|
|
8228
|
+
}
|
|
8229
|
+
merged.sort((a, b) => b.priority - a.priority);
|
|
8230
|
+
return merged;
|
|
8231
|
+
}
|
|
8232
|
+
function serializeValue(value) {
|
|
8233
|
+
if (value === null || value === void 0) return "null";
|
|
8234
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
8235
|
+
return JSON.stringify(value);
|
|
8138
8236
|
}
|
|
8139
8237
|
/**
|
|
8140
|
-
*
|
|
8141
|
-
*
|
|
8238
|
+
* Eliminate redundant state dimensions from a value map.
|
|
8239
|
+
*
|
|
8240
|
+
* When a value map contains compound AND state keys (e.g. `@dark & @hc`),
|
|
8241
|
+
* checks whether any state atom is a "don't-care" variable — i.e. the
|
|
8242
|
+
* value is the same whether that atom is present or absent. Redundant
|
|
8243
|
+
* atoms are removed from all keys and duplicate entries are collapsed.
|
|
8244
|
+
*
|
|
8245
|
+
* This runs **before** condition parsing so that downstream stages
|
|
8246
|
+
* (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
|
|
8247
|
+
* never see the irrelevant dimension, producing simpler, smaller CSS.
|
|
8248
|
+
*
|
|
8249
|
+
* Only pure top-level AND combinations are eligible. Keys that contain
|
|
8250
|
+
* `|`, `^`, or `,` at the top level are treated as opaque single atoms.
|
|
8251
|
+
*
|
|
8252
|
+
* @example
|
|
8253
|
+
* { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
|
|
8254
|
+
* // @hc is redundant → { '': A, '@dark': B }
|
|
8142
8255
|
*/
|
|
8143
|
-
function
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8256
|
+
function extractCompoundStates(valueMap) {
|
|
8257
|
+
const keys = Object.keys(valueMap);
|
|
8258
|
+
if (keys.length < 3 || !keys.some((k) => k.includes("&"))) return valueMap;
|
|
8259
|
+
const entries = keys.map((key) => {
|
|
8260
|
+
return {
|
|
8261
|
+
atoms: splitTopLevelAnd(key) ?? [key],
|
|
8262
|
+
value: valueMap[key]
|
|
8263
|
+
};
|
|
8264
|
+
});
|
|
8265
|
+
const allAtoms = /* @__PURE__ */ new Set();
|
|
8266
|
+
for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
|
|
8267
|
+
const redundant = /* @__PURE__ */ new Set();
|
|
8268
|
+
for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
|
|
8269
|
+
if (redundant.size === 0) return valueMap;
|
|
8270
|
+
const newMap = {};
|
|
8271
|
+
for (const e of entries) {
|
|
8272
|
+
const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
|
|
8273
|
+
if (!(newKey in newMap)) newMap[newKey] = e.value;
|
|
8274
|
+
}
|
|
8275
|
+
return newMap;
|
|
8149
8276
|
}
|
|
8150
8277
|
/**
|
|
8151
|
-
*
|
|
8278
|
+
* Split a state key by top-level `&` operators.
|
|
8152
8279
|
*
|
|
8153
|
-
*
|
|
8154
|
-
*
|
|
8155
|
-
*
|
|
8280
|
+
* Returns `null` if the key contains `|`, `^`, or `,` at the top level
|
|
8281
|
+
* (making it ineligible for atom-level extraction).
|
|
8282
|
+
* Returns `[]` for the empty string (default key).
|
|
8156
8283
|
*/
|
|
8157
|
-
function
|
|
8158
|
-
if (
|
|
8159
|
-
const
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
if (
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
for (const kept of filtered) if (isVariantSuperset(candidate, kept)) {
|
|
8174
|
-
isRedundant = true;
|
|
8175
|
-
break;
|
|
8284
|
+
function splitTopLevelAnd(key) {
|
|
8285
|
+
if (key === "") return [];
|
|
8286
|
+
const parts = [];
|
|
8287
|
+
let depth = 0;
|
|
8288
|
+
let current = "";
|
|
8289
|
+
for (const ch of key) {
|
|
8290
|
+
if (ch === "(" || ch === "[") depth++;
|
|
8291
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
8292
|
+
if (depth === 0) {
|
|
8293
|
+
if (ch === "&") {
|
|
8294
|
+
const trimmed = current.trim();
|
|
8295
|
+
if (trimmed) parts.push(trimmed);
|
|
8296
|
+
current = "";
|
|
8297
|
+
continue;
|
|
8298
|
+
}
|
|
8299
|
+
if (ch === "|" || ch === "^" || ch === ",") return null;
|
|
8176
8300
|
}
|
|
8177
|
-
|
|
8301
|
+
current += ch;
|
|
8178
8302
|
}
|
|
8179
|
-
|
|
8303
|
+
const trimmed = current.trim();
|
|
8304
|
+
if (trimmed) parts.push(trimmed);
|
|
8305
|
+
return parts;
|
|
8180
8306
|
}
|
|
8181
8307
|
/**
|
|
8182
|
-
*
|
|
8183
|
-
*
|
|
8184
|
-
* AND of conditions means cartesian product of variants:
|
|
8185
|
-
* (A1 | A2) & (B1 | B2) = A1&B1 | A1&B2 | A2&B1 | A2&B2
|
|
8186
|
-
*
|
|
8187
|
-
* Variants that result in contradictions (e.g., conflicting media rules)
|
|
8188
|
-
* are filtered out.
|
|
8308
|
+
* An atom is redundant when every entry that contains it has a matching
|
|
8309
|
+
* partner (same remaining atoms, atom absent) with the same value.
|
|
8189
8310
|
*/
|
|
8190
|
-
function
|
|
8191
|
-
const
|
|
8192
|
-
|
|
8193
|
-
for (const
|
|
8194
|
-
const
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
};
|
|
8199
|
-
const newVariants = [];
|
|
8200
|
-
for (const current of currentVariants) for (const childVariant of childCSS.variants) {
|
|
8201
|
-
const merged = mergeVariants(current, childVariant);
|
|
8202
|
-
if (merged !== null) newVariants.push(merged);
|
|
8203
|
-
}
|
|
8204
|
-
if (newVariants.length === 0) return {
|
|
8205
|
-
variants: [],
|
|
8206
|
-
isImpossible: true
|
|
8207
|
-
};
|
|
8208
|
-
currentVariants = dedupeVariants(newVariants);
|
|
8311
|
+
function isAtomRedundant(entries, atom) {
|
|
8312
|
+
const withAtom = entries.filter((e) => e.atoms.includes(atom));
|
|
8313
|
+
if (withAtom.length === 0) return false;
|
|
8314
|
+
for (const wa of withAtom) {
|
|
8315
|
+
const remaining = wa.atoms.filter((a) => a !== atom);
|
|
8316
|
+
const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
|
|
8317
|
+
if (!pair) return false;
|
|
8318
|
+
if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
|
|
8209
8319
|
}
|
|
8210
|
-
return
|
|
8211
|
-
variants: currentVariants,
|
|
8212
|
-
isImpossible: false
|
|
8213
|
-
};
|
|
8320
|
+
return true;
|
|
8214
8321
|
}
|
|
8215
8322
|
/**
|
|
8216
|
-
*
|
|
8217
|
-
*
|
|
8218
|
-
* For an AND child that is OR(A, B), transforms it to OR(A, B & !A)
|
|
8219
|
-
* so that when andToCSS does a Cartesian product, the resulting
|
|
8220
|
-
* CSS variants don't overlap.
|
|
8221
|
-
*
|
|
8222
|
-
* Only transforms OR children whose branches actually produce
|
|
8223
|
-
* different at-rule contexts when materialized. This avoids
|
|
8224
|
-
* breaking cases where contradiction detection in the Cartesian
|
|
8225
|
-
* product naturally handles deduplication.
|
|
8323
|
+
* Check if a value is a style value mapping (object with state keys)
|
|
8226
8324
|
*/
|
|
8227
|
-
function
|
|
8228
|
-
return
|
|
8229
|
-
if (!isCompoundCondition(child) || child.operator !== "OR") return child;
|
|
8230
|
-
if (child.children.length <= 1) return child;
|
|
8231
|
-
if (!branchesProduceDifferentContexts(child.children)) return child;
|
|
8232
|
-
const exclusiveBranches = [];
|
|
8233
|
-
const priorBranches = [];
|
|
8234
|
-
for (const branch of child.children) {
|
|
8235
|
-
if (priorBranches.length === 0) exclusiveBranches.push(branch);
|
|
8236
|
-
else {
|
|
8237
|
-
let exclusive = branch;
|
|
8238
|
-
for (const prior of priorBranches) exclusive = and(exclusive, not(prior));
|
|
8239
|
-
const simplified = simplifyCondition(exclusive);
|
|
8240
|
-
if (simplified.kind !== "false") exclusiveBranches.push(simplified);
|
|
8241
|
-
}
|
|
8242
|
-
priorBranches.push(branch);
|
|
8243
|
-
}
|
|
8244
|
-
if (exclusiveBranches.length === 0) return child;
|
|
8245
|
-
if (exclusiveBranches.length === 1) return exclusiveBranches[0];
|
|
8246
|
-
return {
|
|
8247
|
-
kind: "compound",
|
|
8248
|
-
operator: "OR",
|
|
8249
|
-
children: exclusiveBranches
|
|
8250
|
-
};
|
|
8251
|
-
});
|
|
8325
|
+
function isValueMapping(value) {
|
|
8326
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
8252
8327
|
}
|
|
8253
8328
|
/**
|
|
8254
|
-
*
|
|
8255
|
-
*
|
|
8256
|
-
*
|
|
8329
|
+
* Expand OR conditions in parsed entries into multiple exclusive entries.
|
|
8330
|
+
*
|
|
8331
|
+
* For an entry with condition `A | B | C`, this creates 3 entries:
|
|
8332
|
+
* - condition: A
|
|
8333
|
+
* - condition: B & !A
|
|
8334
|
+
* - condition: C & !A & !B
|
|
8335
|
+
*
|
|
8336
|
+
* This ensures OR branches are mutually exclusive BEFORE the main
|
|
8337
|
+
* exclusive condition building pass.
|
|
8338
|
+
*
|
|
8339
|
+
* @param entries Parsed entries (may contain OR conditions)
|
|
8340
|
+
* @returns Expanded entries with OR branches made exclusive
|
|
8257
8341
|
*/
|
|
8258
|
-
function
|
|
8259
|
-
const
|
|
8260
|
-
for (const
|
|
8261
|
-
const
|
|
8262
|
-
|
|
8263
|
-
for (const v of css.variants) contextKeys.add(getVariantContextKey(v));
|
|
8342
|
+
function expandOrConditions(entries) {
|
|
8343
|
+
const result = [];
|
|
8344
|
+
for (const entry of entries) {
|
|
8345
|
+
const expanded = expandSingleEntry(entry);
|
|
8346
|
+
result.push(...expanded);
|
|
8264
8347
|
}
|
|
8265
|
-
return
|
|
8348
|
+
return result;
|
|
8266
8349
|
}
|
|
8267
8350
|
/**
|
|
8268
|
-
*
|
|
8351
|
+
* Expand a single entry's OR condition into multiple exclusive entries.
|
|
8269
8352
|
*
|
|
8270
|
-
*
|
|
8271
|
-
*
|
|
8272
|
-
*
|
|
8353
|
+
* Note: branches are NOT sorted by at-rule context here (unlike the
|
|
8354
|
+
* `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
|
|
8355
|
+
* the product of De Morgan negation, so each branch is expected to render
|
|
8356
|
+
* independently in its own scope and at-rule sort isn't load-bearing.
|
|
8357
|
+
* The post-build pass needs the sort because it has to preserve at-rule
|
|
8358
|
+
* wrapping across branches that came from negating a compound at-rule.
|
|
8273
8359
|
*
|
|
8274
|
-
*
|
|
8275
|
-
*
|
|
8276
|
-
*
|
|
8360
|
+
* Skip optimisation: when every branch renders into the same at-rule /
|
|
8361
|
+
* root / parent / own context (see "Key Design Decision #2" in
|
|
8362
|
+
* `docs/pipeline.md`), forcing mutual exclusivity here produces dead
|
|
8363
|
+
* `B & !A`-style branches that materialization later folds back into
|
|
8364
|
+
* `:is(A, B)`. Bail out and let `materialize.ts` collapse the OR via
|
|
8365
|
+
* `mergeVariantsIntoSelectorGroups`. Cross-entry exclusivity is still
|
|
8366
|
+
* enforced by `buildExclusiveConditions`; the post-build `expandExclusiveOrs`
|
|
8367
|
+
* pass still handles De Morgan ORs whose branches actually differ in
|
|
8368
|
+
* context.
|
|
8277
8369
|
*/
|
|
8278
|
-
function
|
|
8279
|
-
const
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8370
|
+
function expandSingleEntry(entry) {
|
|
8371
|
+
const orBranches = collectOrBranches(entry.condition);
|
|
8372
|
+
if (orBranches.length <= 1) return [entry];
|
|
8373
|
+
if (!branchesProduceDifferentContexts(orBranches)) return [entry];
|
|
8374
|
+
const result = [];
|
|
8375
|
+
const priorBranches = [];
|
|
8376
|
+
for (let i = 0; i < orBranches.length; i++) {
|
|
8377
|
+
const branch = orBranches[i];
|
|
8378
|
+
let exclusiveBranch = branch;
|
|
8379
|
+
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
8380
|
+
const simplified = simplifyCondition(exclusiveBranch);
|
|
8381
|
+
if (simplified.kind === "false") {
|
|
8382
|
+
priorBranches.push(branch);
|
|
8383
|
+
continue;
|
|
8384
|
+
}
|
|
8385
|
+
result.push({
|
|
8386
|
+
...entry,
|
|
8387
|
+
stateKey: `${entry.stateKey}[${i}]`,
|
|
8388
|
+
condition: simplified
|
|
8389
|
+
});
|
|
8390
|
+
priorBranches.push(branch);
|
|
8284
8391
|
}
|
|
8285
|
-
|
|
8286
|
-
variants: [],
|
|
8287
|
-
isImpossible: true
|
|
8288
|
-
};
|
|
8289
|
-
return {
|
|
8290
|
-
variants: dedupeVariants(allVariants),
|
|
8291
|
-
isImpossible: false
|
|
8292
|
-
};
|
|
8392
|
+
return result;
|
|
8293
8393
|
}
|
|
8294
8394
|
/**
|
|
8295
|
-
*
|
|
8395
|
+
* Collect top-level OR branches from a condition.
|
|
8396
|
+
*
|
|
8397
|
+
* For `A | B | C`, returns [A, B, C]
|
|
8398
|
+
* For `A & B`, returns [A & B] (single branch)
|
|
8399
|
+
* For `A | (B & C)`, returns [A, B & C]
|
|
8296
8400
|
*/
|
|
8297
|
-
function
|
|
8298
|
-
if (
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
const
|
|
8302
|
-
|
|
8401
|
+
function collectOrBranches(condition) {
|
|
8402
|
+
if (condition.kind === "true" || condition.kind === "false") return [condition];
|
|
8403
|
+
if (isCompoundCondition(condition) && condition.operator === "OR") {
|
|
8404
|
+
const branches = [];
|
|
8405
|
+
for (const child of condition.children) branches.push(...collectOrBranches(child));
|
|
8406
|
+
return branches;
|
|
8303
8407
|
}
|
|
8304
|
-
return
|
|
8408
|
+
return [condition];
|
|
8305
8409
|
}
|
|
8306
8410
|
/**
|
|
8307
|
-
*
|
|
8308
|
-
* own, starting) into a single variant with a SelectorGroup.
|
|
8411
|
+
* Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
|
|
8309
8412
|
*
|
|
8310
|
-
*
|
|
8311
|
-
*
|
|
8413
|
+
* This handles ORs that arise from De Morgan expansion during negation:
|
|
8414
|
+
* !(A & B) = !A | !B
|
|
8415
|
+
*
|
|
8416
|
+
* These ORs need to be made exclusive to avoid overlapping CSS rules:
|
|
8417
|
+
* !A | !B → !A | (A & !B)
|
|
8418
|
+
*
|
|
8419
|
+
* This is logically equivalent but ensures each branch has proper context.
|
|
8420
|
+
*
|
|
8421
|
+
* Example:
|
|
8422
|
+
* Input: { "": V1, "@supports(...) & :has()": V2 }
|
|
8423
|
+
* V2's exclusive = @supports & :has
|
|
8424
|
+
* V1's exclusive = !(@supports & :has) = !@supports | !:has
|
|
8425
|
+
*
|
|
8426
|
+
* Without this fix: V1 gets two rules:
|
|
8427
|
+
* - @supports (not ...) → V1 ✓
|
|
8428
|
+
* - :not(:has()) → V1 ✗ (missing @supports context!)
|
|
8429
|
+
*
|
|
8430
|
+
* With this fix: V1 gets two exclusive rules:
|
|
8431
|
+
* - @supports (not ...) → V1 ✓
|
|
8432
|
+
* - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)
|
|
8312
8433
|
*/
|
|
8313
|
-
function
|
|
8314
|
-
if (variants.length <= 1) return variants;
|
|
8315
|
-
const groups = /* @__PURE__ */ new Map();
|
|
8316
|
-
for (const v of variants) {
|
|
8317
|
-
const key = getVariantContextKey(v);
|
|
8318
|
-
const group = groups.get(key);
|
|
8319
|
-
if (group) group.push(v);
|
|
8320
|
-
else groups.set(key, [v]);
|
|
8321
|
-
}
|
|
8434
|
+
function expandExclusiveOrs(entries) {
|
|
8322
8435
|
const result = [];
|
|
8323
|
-
for (const
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
continue;
|
|
8327
|
-
}
|
|
8328
|
-
const withSelectors = [];
|
|
8329
|
-
const withoutSelectors = [];
|
|
8330
|
-
for (const v of group) if (v.modifierConditions.length === 0 && v.pseudoConditions.length === 0) withoutSelectors.push(v);
|
|
8331
|
-
else withSelectors.push(v);
|
|
8332
|
-
result.push(...withoutSelectors);
|
|
8333
|
-
if (withSelectors.length <= 1) {
|
|
8334
|
-
result.push(...withSelectors);
|
|
8335
|
-
continue;
|
|
8336
|
-
}
|
|
8337
|
-
result.push(factorAndGroup(withSelectors));
|
|
8436
|
+
for (const entry of entries) {
|
|
8437
|
+
const expanded = expandExclusiveConditionOrs(entry);
|
|
8438
|
+
result.push(...expanded);
|
|
8338
8439
|
}
|
|
8339
8440
|
return result;
|
|
8340
8441
|
}
|
|
8341
8442
|
/**
|
|
8342
|
-
*
|
|
8343
|
-
* a single variant with a SelectorGroup for the remaining (differing)
|
|
8344
|
-
* conditions.
|
|
8345
|
-
*
|
|
8346
|
-
* Precondition: all variants must share the same context key (identical
|
|
8347
|
-
* at-rules, root/parent/own/selector groups, startingStyle).
|
|
8443
|
+
* Check if a condition involves at-rules (media, container, supports, starting)
|
|
8348
8444
|
*/
|
|
8349
|
-
function
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
if (keyI !== key0) throw new Error(`factorAndGroup: context key mismatch at index ${i}.\n expected: ${key0}\n got: ${keyI}`);
|
|
8355
|
-
}
|
|
8356
|
-
}
|
|
8357
|
-
const commonModKeys = findCommonKeys(variants.map((v) => v.modifierConditions), getModifierKey);
|
|
8358
|
-
const commonPseudoKeys = findCommonKeys(variants.map((v) => v.pseudoConditions), getPseudoKey);
|
|
8359
|
-
const commonModifiers = variants[0].modifierConditions.filter((m) => commonModKeys.has(getModifierKey(m)));
|
|
8360
|
-
const commonPseudos = variants[0].pseudoConditions.filter((p) => commonPseudoKeys.has(getPseudoKey(p)));
|
|
8361
|
-
const branches = [];
|
|
8362
|
-
let hasEmptyBranch = false;
|
|
8363
|
-
for (const v of variants) {
|
|
8364
|
-
const branch = [];
|
|
8365
|
-
for (const mod of v.modifierConditions) if (!commonModKeys.has(getModifierKey(mod))) branch.push(mod);
|
|
8366
|
-
for (const pseudo of v.pseudoConditions) if (!commonPseudoKeys.has(getPseudoKey(pseudo))) branch.push(pseudo);
|
|
8367
|
-
if (branch.length > 0) branches.push(branch);
|
|
8368
|
-
else hasEmptyBranch = true;
|
|
8369
|
-
}
|
|
8370
|
-
if (hasEmptyBranch) return {
|
|
8371
|
-
...variants[0],
|
|
8372
|
-
modifierConditions: commonModifiers,
|
|
8373
|
-
pseudoConditions: commonPseudos
|
|
8374
|
-
};
|
|
8375
|
-
const factoredGroups = tryFactorIntoDimensions(branches);
|
|
8376
|
-
if (factoredGroups) return {
|
|
8377
|
-
modifierConditions: commonModifiers,
|
|
8378
|
-
pseudoConditions: commonPseudos,
|
|
8379
|
-
selectorGroups: [...variants[0].selectorGroups, ...factoredGroups],
|
|
8380
|
-
ownGroups: [...variants[0].ownGroups],
|
|
8381
|
-
mediaConditions: [...variants[0].mediaConditions],
|
|
8382
|
-
containerConditions: [...variants[0].containerConditions],
|
|
8383
|
-
supportsConditions: [...variants[0].supportsConditions],
|
|
8384
|
-
rootGroups: [...variants[0].rootGroups],
|
|
8385
|
-
parentGroups: [...variants[0].parentGroups],
|
|
8386
|
-
startingStyle: variants[0].startingStyle
|
|
8387
|
-
};
|
|
8388
|
-
return {
|
|
8389
|
-
modifierConditions: commonModifiers,
|
|
8390
|
-
pseudoConditions: commonPseudos,
|
|
8391
|
-
selectorGroups: [...variants[0].selectorGroups, {
|
|
8392
|
-
branches,
|
|
8393
|
-
negated: false
|
|
8394
|
-
}],
|
|
8395
|
-
ownGroups: [...variants[0].ownGroups],
|
|
8396
|
-
mediaConditions: [...variants[0].mediaConditions],
|
|
8397
|
-
containerConditions: [...variants[0].containerConditions],
|
|
8398
|
-
supportsConditions: [...variants[0].supportsConditions],
|
|
8399
|
-
rootGroups: [...variants[0].rootGroups],
|
|
8400
|
-
parentGroups: [...variants[0].parentGroups],
|
|
8401
|
-
startingStyle: variants[0].startingStyle
|
|
8402
|
-
};
|
|
8445
|
+
function hasAtRuleContext(node) {
|
|
8446
|
+
if (node.kind === "true" || node.kind === "false") return false;
|
|
8447
|
+
if (node.kind === "state") return node.type === "media" || node.type === "container" || node.type === "supports" || node.type === "starting";
|
|
8448
|
+
if (node.kind === "compound") return node.children.some(hasAtRuleContext);
|
|
8449
|
+
return false;
|
|
8403
8450
|
}
|
|
8404
8451
|
/**
|
|
8405
|
-
*
|
|
8406
|
-
* modifier attribute dimensions and return one SelectorGroup per dimension.
|
|
8452
|
+
* Sort OR branches to prioritize at-rule conditions first.
|
|
8407
8453
|
*
|
|
8408
|
-
*
|
|
8409
|
-
*
|
|
8454
|
+
* This is critical for correct CSS generation. For `!A | !B` where A is at-rule
|
|
8455
|
+
* and B is modifier, we want:
|
|
8456
|
+
* - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
|
|
8457
|
+
* - Branch 1: A & !B (modifier negation with at-rule context)
|
|
8458
|
+
*
|
|
8459
|
+
* If we process in wrong order (!B first), we'd get:
|
|
8460
|
+
* - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
|
|
8461
|
+
* - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
|
|
8410
8462
|
*/
|
|
8411
|
-
function
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
if (
|
|
8416
|
-
if (!
|
|
8417
|
-
|
|
8418
|
-
}
|
|
8419
|
-
if (dimensions.size < 2) return null;
|
|
8420
|
-
for (const branch of branches) {
|
|
8421
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8422
|
-
for (const cond of branch) {
|
|
8423
|
-
const attr = cond.attribute;
|
|
8424
|
-
if (seen.has(attr)) return null;
|
|
8425
|
-
seen.add(attr);
|
|
8426
|
-
}
|
|
8427
|
-
if (seen.size !== dimensions.size) return null;
|
|
8428
|
-
}
|
|
8429
|
-
let expectedCount = 1;
|
|
8430
|
-
for (const vals of dimensions.values()) expectedCount *= vals.size;
|
|
8431
|
-
if (branches.length !== expectedCount) return null;
|
|
8432
|
-
return [...dimensions.values()].map((vals) => ({
|
|
8433
|
-
branches: [...vals.values()].map((cond) => [cond]),
|
|
8434
|
-
negated: false
|
|
8435
|
-
}));
|
|
8463
|
+
function sortOrBranchesForExpansion(branches) {
|
|
8464
|
+
return [...branches].sort((a, b) => {
|
|
8465
|
+
const aHasAtRule = hasAtRuleContext(a);
|
|
8466
|
+
const bHasAtRule = hasAtRuleContext(b);
|
|
8467
|
+
if (aHasAtRule && !bHasAtRule) return -1;
|
|
8468
|
+
if (!aHasAtRule && bHasAtRule) return 1;
|
|
8469
|
+
return 0;
|
|
8470
|
+
});
|
|
8436
8471
|
}
|
|
8437
8472
|
/**
|
|
8438
|
-
*
|
|
8473
|
+
* Expand ORs in a single entry's exclusive condition
|
|
8439
8474
|
*/
|
|
8440
|
-
function
|
|
8441
|
-
|
|
8442
|
-
if (
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
const
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
}
|
|
8456
|
-
for (const [name, conditions] of byName) {
|
|
8457
|
-
const conditionParts = conditions.map((c) => c.negated ? `(not ${c.condition})` : c.condition);
|
|
8458
|
-
const namePrefix = name ? `${name} ` : "";
|
|
8459
|
-
atRules.push(`@container ${namePrefix}${conditionParts.join(" and ")}`);
|
|
8475
|
+
function expandExclusiveConditionOrs(entry) {
|
|
8476
|
+
let orBranches = collectOrBranches(entry.exclusiveCondition);
|
|
8477
|
+
if (orBranches.length <= 1) return [entry];
|
|
8478
|
+
if (!branchesProduceDifferentContexts(orBranches)) return [entry];
|
|
8479
|
+
orBranches = sortOrBranchesForExpansion(orBranches);
|
|
8480
|
+
const result = [];
|
|
8481
|
+
const priorBranches = [];
|
|
8482
|
+
for (let i = 0; i < orBranches.length; i++) {
|
|
8483
|
+
const branch = orBranches[i];
|
|
8484
|
+
let exclusiveBranch = branch;
|
|
8485
|
+
for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
|
|
8486
|
+
const simplified = simplifyCondition(exclusiveBranch);
|
|
8487
|
+
if (simplified.kind === "false") {
|
|
8488
|
+
priorBranches.push(branch);
|
|
8489
|
+
continue;
|
|
8460
8490
|
}
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
const selectorCond = `selector(${c.condition})`;
|
|
8466
|
-
return c.negated ? `(not ${selectorCond})` : selectorCond;
|
|
8467
|
-
} else {
|
|
8468
|
-
const featureCond = `(${c.condition})`;
|
|
8469
|
-
return c.negated ? `(not ${featureCond})` : featureCond;
|
|
8470
|
-
}
|
|
8491
|
+
result.push({
|
|
8492
|
+
...entry,
|
|
8493
|
+
stateKey: `${entry.stateKey}[or:${i}]`,
|
|
8494
|
+
exclusiveCondition: simplified
|
|
8471
8495
|
});
|
|
8472
|
-
|
|
8496
|
+
priorBranches.push(branch);
|
|
8473
8497
|
}
|
|
8474
|
-
return
|
|
8498
|
+
return result;
|
|
8475
8499
|
}
|
|
8476
8500
|
//#endregion
|
|
8477
8501
|
//#region src/utils/case-converter.ts
|
|
@@ -8538,6 +8562,15 @@ function emitWarning(code, message) {
|
|
|
8538
8562
|
const MAX_XOR_CHAIN_LENGTH = 4;
|
|
8539
8563
|
const parseCache = new Lru(5e3);
|
|
8540
8564
|
/**
|
|
8565
|
+
* Chrome-internal pseudo-classes (e.g. `:-internal-autofill-selected`,
|
|
8566
|
+
* `:-internal-autofill-previewed`) cannot be targeted from user CSS and
|
|
8567
|
+
* may invalidate the surrounding rule in Safari even when wrapped in
|
|
8568
|
+
* forgiving `:is(...)`. The regex matches both bare uses and references
|
|
8569
|
+
* inside enhanced pseudo arguments like `:is(:-webkit-autofill,
|
|
8570
|
+
* :-internal-autofill-selected)`.
|
|
8571
|
+
*/
|
|
8572
|
+
const INTERNAL_PSEUDO_PATTERN = /:-internal-[a-z0-9-]+/g;
|
|
8573
|
+
/**
|
|
8541
8574
|
* Pattern for tokenizing state notation.
|
|
8542
8575
|
* Matches: operators, parentheses, @-prefixed states, value mods, boolean mods,
|
|
8543
8576
|
* pseudo-classes, class selectors, and attribute selectors.
|
|
@@ -8955,6 +8988,14 @@ function parseStateKey(stateKey, options = {}) {
|
|
|
8955
8988
|
const cacheKey = trimmed + "\0" + (options.isSubElement ? "1" : "0") + "\0" + localStatesKey;
|
|
8956
8989
|
const cached = parseCache.get(cacheKey);
|
|
8957
8990
|
if (cached) return cached;
|
|
8991
|
+
if (isDevEnv()) {
|
|
8992
|
+
INTERNAL_PSEUDO_PATTERN.lastIndex = 0;
|
|
8993
|
+
const internalMatches = trimmed.match(INTERNAL_PSEUDO_PATTERN);
|
|
8994
|
+
if (internalMatches && internalMatches.length > 0) {
|
|
8995
|
+
const unique = Array.from(new Set(internalMatches));
|
|
8996
|
+
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.`);
|
|
8997
|
+
}
|
|
8998
|
+
}
|
|
8958
8999
|
const result = new Parser(tokenize(trimmed), options).parse();
|
|
8959
9000
|
parseCache.set(cacheKey, result);
|
|
8960
9001
|
return result;
|
|
@@ -9024,7 +9065,15 @@ function runPipeline(styles, parserContext) {
|
|
|
9024
9065
|
function processStyles(styles, selectorSuffix, parserContext, allRules) {
|
|
9025
9066
|
const keys = Object.keys(styles);
|
|
9026
9067
|
const selectorKeys = keys.filter((key) => isSelector(key));
|
|
9027
|
-
const styleKeys =
|
|
9068
|
+
const styleKeys = [];
|
|
9069
|
+
for (const key of keys) {
|
|
9070
|
+
if (isSelector(key) || key.startsWith("@")) continue;
|
|
9071
|
+
if (key.startsWith(":")) {
|
|
9072
|
+
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.`);
|
|
9073
|
+
continue;
|
|
9074
|
+
}
|
|
9075
|
+
styleKeys.push(key);
|
|
9076
|
+
}
|
|
9028
9077
|
processNestedSelectors(styles, selectorKeys, selectorSuffix, parserContext, allRules);
|
|
9029
9078
|
processHandlerQueue(buildHandlerQueue(styleKeys, styles), selectorSuffix, parserContext, allRules);
|
|
9030
9079
|
}
|
|
@@ -10348,4 +10397,4 @@ function resetConfig() {
|
|
|
10348
10397
|
//#endregion
|
|
10349
10398
|
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 };
|
|
10350
10399
|
|
|
10351
|
-
//# sourceMappingURL=config-
|
|
10400
|
+
//# sourceMappingURL=config-D0ZQMdY8.js.map
|