@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.
Files changed (42) hide show
  1. package/dist/{collector-DROCOiaT.js → collector-CLKusMeM.js} +3 -3
  2. package/dist/{collector-DROCOiaT.js.map → collector-CLKusMeM.js.map} +1 -1
  3. package/dist/{collector-BQHl-atL.d.ts → collector-CoababzP.d.ts} +2 -2
  4. package/dist/{config-CzzTHmtS.d.ts → config-Cp05bCvj.d.ts} +2 -2
  5. package/dist/{config-JokB1Lc8.js → config-D0ZQMdY8.js} +1278 -1229
  6. package/dist/config-D0ZQMdY8.js.map +1 -0
  7. package/dist/core/index.d.ts +4 -4
  8. package/dist/core/index.js +5 -5
  9. package/dist/{core-CW4XEUFk.js → core-DOcbMGRf.js} +5 -5
  10. package/dist/{core-CW4XEUFk.js.map → core-DOcbMGRf.js.map} +1 -1
  11. package/dist/{css-writer-Jv468wSl.js → css-writer-BZdDWI6L.js} +3 -3
  12. package/dist/{css-writer-Jv468wSl.js.map → css-writer-BZdDWI6L.js.map} +1 -1
  13. package/dist/{format-rules-B0vbh8Qz.js → format-rules-BT_JwzeT.js} +2 -2
  14. package/dist/{format-rules-B0vbh8Qz.js.map → format-rules-BT_JwzeT.js.map} +1 -1
  15. package/dist/{hydrate-BO6nlAeD.js → hydrate-BLh7OkxZ.js} +2 -2
  16. package/dist/{hydrate-BO6nlAeD.js.map → hydrate-BLh7OkxZ.js.map} +1 -1
  17. package/dist/{index-Dy74C11K.d.ts → index-B_QCrcpe.d.ts} +6 -2
  18. package/dist/{index-DMGEDjlc.d.ts → index-sk1sxVI3.d.ts} +8 -5
  19. package/dist/index.d.ts +4 -4
  20. package/dist/index.js +6 -6
  21. package/dist/{keyframes-J_JNrpdh.js → keyframes-DW6FxDsz.js} +2 -2
  22. package/dist/{keyframes-J_JNrpdh.js.map → keyframes-DW6FxDsz.js.map} +1 -1
  23. package/dist/{merge-styles-Du-eC7zp.js → merge-styles-Bv2DKtHJ.js} +2 -2
  24. package/dist/{merge-styles-Du-eC7zp.js.map → merge-styles-Bv2DKtHJ.js.map} +1 -1
  25. package/dist/{merge-styles-BS-mpcci.d.ts → merge-styles-DQO22J7_.d.ts} +2 -2
  26. package/dist/{resolve-recipes-DPRT3FMM.js → resolve-recipes-x4ElcO5U.js} +3 -3
  27. package/dist/{resolve-recipes-DPRT3FMM.js.map → resolve-recipes-x4ElcO5U.js.map} +1 -1
  28. package/dist/ssr/astro-client.js +1 -1
  29. package/dist/ssr/astro.js +3 -3
  30. package/dist/ssr/index.d.ts +1 -1
  31. package/dist/ssr/index.js +3 -3
  32. package/dist/ssr/next.d.ts +1 -1
  33. package/dist/ssr/next.js +4 -4
  34. package/dist/static/index.d.ts +2 -2
  35. package/dist/static/index.js +1 -1
  36. package/dist/zero/babel.d.ts +1 -1
  37. package/dist/zero/babel.js +4 -4
  38. package/dist/zero/index.d.ts +1 -1
  39. package/dist/zero/index.js +1 -1
  40. package/docs/styles.md +9 -7
  41. package/package.json +5 -5
  42. 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 modifier:
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 modifier = isModOnly ? nameToken : modPart?.mods[0] ?? "";
3351
- const isStrong = modifier === "strong" || modifier === "bold";
3352
- const isItalic = modifier === "italic";
3353
- const isIcon = modifier === "icon";
3354
- const isTight = modifier === "tight";
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/exclusive.ts
6898
+ //#region src/pipeline/materialize-contradictions.ts
6893
6899
  /**
6894
- * Build exclusive conditions for a list of parsed style entries.
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 buildExclusiveConditions(entries) {
6903
+ function dedupeByKey(items, getKey) {
6904
+ const seen = /* @__PURE__ */ new Set();
6918
6905
  const result = [];
6919
- const priorConditions = [];
6920
- for (const entry of entries) {
6921
- let exclusive = entry.condition;
6922
- for (const prior of priorConditions) if (prior.kind !== "true") exclusive = and(exclusive, not(prior));
6923
- const simplified = simplifyCondition(exclusive);
6924
- if (simplified.kind === "false") continue;
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
- * Parse style entries from a value mapping object.
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
- * Merge parsed entries that share the same value.
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 serializeValue(value) {
7020
- if (value === null || value === void 0) return "null";
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
- * Eliminate redundant state dimensions from a value map.
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 extractCompoundStates(valueMap) {
7044
- const keys = Object.keys(valueMap);
7045
- if (keys.length < 3 || !keys.some((k) => k.includes("&"))) return valueMap;
7046
- const entries = keys.map((key) => {
7047
- return {
7048
- atoms: splitTopLevelAnd(key) ?? [key],
7049
- value: valueMap[key]
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 newMap;
6936
+ return false;
7063
6937
  }
7064
6938
  /**
7065
- * Split a state key by top-level `&` operators.
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
- * Returns `null` if the key contains `|`, `^`, or `,` at the top level
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 splitTopLevelAnd(key) {
7072
- if (key === "") return [];
7073
- const parts = [];
7074
- let depth = 0;
7075
- let current = "";
7076
- for (const ch of key) {
7077
- if (ch === "(" || ch === "[") depth++;
7078
- else if (ch === ")" || ch === "]") depth--;
7079
- if (depth === 0) {
7080
- if (ch === "&") {
7081
- const trimmed = current.trim();
7082
- if (trimmed) parts.push(trimmed);
7083
- current = "";
7084
- continue;
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 (ch === "|" || ch === "^" || ch === ",") return null;
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
- const trimmed = current.trim();
7091
- if (trimmed) parts.push(trimmed);
7092
- return parts;
6986
+ return false;
7093
6987
  }
7094
6988
  /**
7095
- * An atom is redundant when every entry that contains it has a matching
7096
- * partner (same remaining atoms, atom absent) with the same value.
7097
- */
7098
- function isAtomRedundant(entries, atom) {
7099
- const withAtom = entries.filter((e) => e.atoms.includes(atom));
7100
- if (withAtom.length === 0) return false;
7101
- for (const wa of withAtom) {
7102
- const remaining = wa.atoms.filter((a) => a !== atom);
7103
- const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
7104
- if (!pair) return false;
7105
- if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
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
- return true;
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
- * Check if a value is a style value mapping (object with state keys)
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
- function isValueMapping(value) {
7113
- return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
7114
- }
7027
+ const conditionCache = new Lru(3e3);
7115
7028
  /**
7116
- * Expand OR conditions in parsed entries into multiple exclusive entries.
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 expandOrConditions(entries) {
7130
- const result = [];
7131
- for (const entry of entries) {
7132
- const expanded = expandSingleEntry(entry);
7133
- result.push(...expanded);
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
- * Expand a single entry's OR condition into multiple exclusive entries.
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 expandSingleEntry(entry) {
7148
- const orBranches = collectOrBranches(entry.condition);
7149
- if (orBranches.length <= 1) return [entry];
7150
- const result = [];
7151
- const priorBranches = [];
7152
- for (let i = 0; i < orBranches.length; i++) {
7153
- const branch = orBranches[i];
7154
- let exclusiveBranch = branch;
7155
- for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
7156
- const simplified = simplifyCondition(exclusiveBranch);
7157
- if (simplified.kind === "false") {
7158
- priorBranches.push(branch);
7159
- continue;
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
- * Collect top-level OR branches from a condition.
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 collectOrBranches(condition) {
7178
- if (condition.kind === "true" || condition.kind === "false") return [condition];
7179
- if (isCompoundCondition(condition) && condition.operator === "OR") {
7180
- const branches = [];
7181
- for (const child of condition.children) branches.push(...collectOrBranches(child));
7182
- return branches;
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
- * Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
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 expandExclusiveOrs(entries) {
7211
- const result = [];
7212
- for (const entry of entries) {
7213
- const expanded = expandExclusiveConditionOrs(entry);
7214
- result.push(...expanded);
7215
- }
7216
- return result;
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
- * Check if a condition involves at-rules (media, container, supports, starting)
7152
+ * Convert pseudo condition to parsed structure
7220
7153
  */
7221
- function hasAtRuleContext(node) {
7222
- if (node.kind === "true" || node.kind === "false") return false;
7223
- if (node.kind === "state") return node.type === "media" || node.type === "container" || node.type === "supports" || node.type === "starting";
7224
- if (node.kind === "compound") return node.children.some(hasAtRuleContext);
7225
- return false;
7154
+ function pseudoToParsed(state) {
7155
+ return {
7156
+ pseudo: state.pseudo,
7157
+ negated: state.negated ?? false
7158
+ };
7226
7159
  }
7227
7160
  /**
7228
- * Sort OR branches to prioritize at-rule conditions first.
7161
+ * Convert parsed pseudo to CSS selector string (for final output).
7229
7162
  *
7230
- * This is critical for correct CSS generation. For `!A | !B` where A is at-rule
7231
- * and B is modifier, we want:
7232
- * - Branch 0: !A (at-rule negation - covers "no @supports/media" case)
7233
- * - Branch 1: A & !B (modifier negation with at-rule context)
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
- * If we process in wrong order (!B first), we'd get:
7236
- * - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)
7237
- * - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)
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 sortOrBranchesForExpansion(branches) {
7240
- return [...branches].sort((a, b) => {
7241
- const aHasAtRule = hasAtRuleContext(a);
7242
- const bHasAtRule = hasAtRuleContext(b);
7243
- if (aHasAtRule && !bHasAtRule) return -1;
7244
- if (!aHasAtRule && bHasAtRule) return 1;
7245
- return 0;
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
- * Expand ORs in a single entry's exclusive condition
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 expandExclusiveConditionOrs(entry) {
7252
- let orBranches = collectOrBranches(entry.exclusiveCondition);
7253
- if (orBranches.length <= 1) return [entry];
7254
- orBranches = sortOrBranchesForExpansion(orBranches);
7255
- const result = [];
7256
- const priorBranches = [];
7257
- for (let i = 0; i < orBranches.length; i++) {
7258
- const branch = orBranches[i];
7259
- let exclusiveBranch = branch;
7260
- for (const prior of priorBranches) exclusiveBranch = and(exclusiveBranch, not(prior));
7261
- const simplified = simplifyCondition(exclusiveBranch);
7262
- if (simplified.kind === "false") {
7263
- priorBranches.push(branch);
7264
- continue;
7265
- }
7266
- result.push({
7267
- ...entry,
7268
- stateKey: `${entry.stateKey}[or:${i}]`,
7269
- exclusiveCondition: simplified
7270
- });
7271
- priorBranches.push(branch);
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
- * Generic deduplication by a key extraction function.
7279
- * Preserves insertion order, keeping the first occurrence of each key.
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 dedupeByKey(items, getKey) {
7282
- const seen = /* @__PURE__ */ new Set();
7283
- const result = [];
7284
- for (const item of items) {
7285
- const key = getKey(item);
7286
- if (!seen.has(key)) {
7287
- seen.add(key);
7288
- result.push(item);
7289
- }
7290
- }
7291
- return result;
7292
- }
7293
- function dedupeMediaConditions(conditions) {
7294
- return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
7295
- }
7296
- function dedupeContainerConditions(conditions) {
7297
- return dedupeByKey(conditions, (c) => `${c.name ?? ""}|${c.condition}|${c.negated}`);
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
- function dedupeSupportsConditions(conditions) {
7300
- return dedupeByKey(conditions, (c) => `${c.subtype}|${c.condition}|${c.negated}`);
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
- * Check if supports conditions contain contradictions
7304
- * e.g., @supports(display: grid) AND NOT @supports(display: grid)
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 hasSupportsContradiction(conditions) {
7307
- const conditionMap = /* @__PURE__ */ new Map();
7308
- for (const cond of conditions) {
7309
- const key = `${cond.subtype}|${cond.condition}`;
7310
- const existing = conditionMap.get(key);
7311
- if (existing !== void 0 && existing !== !cond.negated) return true;
7312
- conditionMap.set(key, !cond.negated);
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
- * Check if a set of media conditions contains contradictions
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 hasMediaContradiction(conditions) {
7324
- const featureConditions = /* @__PURE__ */ new Map();
7325
- const typeConditions = /* @__PURE__ */ new Map();
7326
- const dimensionConditions = /* @__PURE__ */ new Map();
7327
- const dimensionsByDim = /* @__PURE__ */ new Map();
7328
- for (const cond of conditions) if (cond.subtype === "type") {
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
- * Check if container conditions contain contradictions in style queries
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 hasContainerStyleContradiction(conditions) {
7374
- const styleQueries = /* @__PURE__ */ new Map();
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
- * CSS Materialization
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
- * Converts condition trees into CSS selectors and at-rules.
7403
- * This is the final stage that produces actual CSS output.
7404
- */
7405
- const conditionCache = new Lru(3e3);
7406
- /**
7407
- * Convert a condition tree to CSS components
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 conditionToCSS(node) {
7410
- const key = getConditionUniqueId(node);
7411
- const cached = conditionCache.get(key);
7412
- if (cached) return cached;
7413
- const result = conditionToCSSInner(node);
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
- function conditionToCSSInner(node) {
7432
- if (node.kind === "true") return {
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
- if (node.kind === "false") return {
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
- if (node.kind === "state") return stateToCSS(node);
7441
- if (node.kind === "compound") if (node.operator === "AND") return andToCSS(node.children);
7442
- else return orToCSS(node.children);
7443
- return {
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
- * Convert a state condition to CSS
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 stateToCSS(state) {
7452
- switch (state.type) {
7453
- case "media": return {
7454
- variants: mediaToParsed(state).map((mediaCond) => {
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
- * Convert modifier condition to parsed structure
7384
+ * Wrap serialized selector arguments in :is() or :not().
7385
+ * Arguments are sorted for canonical output.
7508
7386
  */
7509
- function modifierToParsed(state) {
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 parsed modifier to CSS selector string (for final output)
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 modifierToCSS(mod) {
7521
- let selector;
7522
- if (mod.value !== void 0) {
7523
- const op = mod.operator || "=";
7524
- selector = `[${mod.attribute}${op}"${mod.value}"]`;
7525
- } else selector = `[${mod.attribute}]`;
7526
- if (mod.negated) return `:not(${selector})`;
7527
- return selector;
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
- * Convert pseudo condition to parsed structure
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 pseudoToParsed(state) {
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
- pseudo: state.pseudo,
7535
- negated: state.negated ?? false
7425
+ negatedBooleanAttrs,
7426
+ positiveExactValuesByAttr
7536
7427
  };
7537
7428
  }
7538
7429
  /**
7539
- * Convert parsed pseudo to CSS selector string (for final output).
7540
- *
7541
- * :not() is normalized to negated :is() at parse time, so pseudo.pseudo
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
- * When not negated, single-argument :is()/:where() is unwrapped when the
7549
- * inner content is a simple compound selector that can safely append to
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 pseudoToCSS(pseudo) {
7553
- const p = pseudo.pseudo;
7554
- if (pseudo.negated) {
7555
- if (p.startsWith(":is(") || p.startsWith(":where(")) return `:not(${p.slice(p.indexOf("(") + 1, -1)})`;
7556
- return `:not(${p})`;
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 p;
7444
+ return false;
7564
7445
  }
7565
7446
  /**
7566
- * Convert media condition to parsed structure(s)
7567
- * Returns an array because negated ranges produce OR branches (two separate conditions)
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 mediaToParsed(state) {
7570
- if (state.subtype === "type") {
7571
- const mediaType = state.mediaType || "all";
7572
- return [{
7573
- subtype: "type",
7574
- negated: state.negated ?? false,
7575
- condition: mediaType,
7576
- mediaType: state.mediaType
7577
- }];
7578
- } else if (state.subtype === "feature") {
7579
- let condition;
7580
- if (state.featureValue) condition = `(${state.feature}: ${state.featureValue})`;
7581
- else condition = `(${state.feature})`;
7582
- return [{
7583
- subtype: "feature",
7584
- negated: state.negated ?? false,
7585
- condition,
7586
- feature: state.feature,
7587
- featureValue: state.featureValue
7588
- }];
7589
- } else return dimensionToMediaParsed(state.dimension || "width", state.lowerBound, state.upperBound, state.negated ?? false);
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 dimension bounds to parsed media condition(s)
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 dimensionToMediaParsed(dimension, lowerBound, upperBound, negated) {
7596
- let condition;
7597
- if (lowerBound && upperBound) {
7598
- const lowerOp = lowerBound.inclusive ? "<=" : "<";
7599
- const upperOp = upperBound.inclusive ? "<=" : "<";
7600
- condition = `(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
7601
- } else if (upperBound) condition = `(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
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 container condition to parsed structure
7615
- * This enables structured analysis for contradiction detection and condition combining
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 containerToParsed(state) {
7618
- let condition;
7619
- if (state.subtype === "style") if (state.propertyValue) condition = `style(--${state.property}: ${state.propertyValue})`;
7620
- else condition = `style(--${state.property})`;
7621
- else if (state.subtype === "raw") condition = state.rawCondition;
7622
- else condition = dimensionToContainerCondition(state.dimension || "width", state.lowerBound, state.upperBound);
7623
- return {
7624
- name: state.containerName,
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 dimension bounds to container query condition (single string)
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 dimensionToContainerCondition(dimension, lowerBound, upperBound) {
7637
- if (lowerBound && upperBound) {
7638
- const lowerOp = lowerBound.inclusive ? "<=" : "<";
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
- * Convert supports condition to parsed structure
7518
+ * Get unique key for a modifier condition
7647
7519
  */
7648
- function supportsToParsed(state) {
7649
- return {
7650
- subtype: state.subtype,
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
- * Collect all modifier and pseudo conditions from a variant as a flat array.
7525
+ * Get unique key for a pseudo condition
7657
7526
  */
7658
- function collectSelectorConditions(variant) {
7659
- return [...variant.modifierConditions, ...variant.pseudoConditions];
7527
+ function getPseudoKey(pseudo) {
7528
+ return pseudo.negated ? `!${pseudo.pseudo}` : pseudo.pseudo;
7660
7529
  }
7661
7530
  /**
7662
- * Convert an inner condition tree into a single SelectorVariant with
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 innerConditionToVariants(innerCondition, negated, target) {
7675
- const innerCSS = conditionToCSS(innerCondition);
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
- * Convert a @parent() inner condition into a single SelectorVariant with
7701
- * one ParentGroup whose branches represent the inner OR alternatives.
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 parentConditionToVariants(innerCondition, negated, direct) {
7708
- const innerCSS = conditionToCSS(innerCondition);
7709
- if (innerCSS.isImpossible || innerCSS.variants.length === 0) return {
7710
- variants: [],
7711
- isImpossible: true
7712
- };
7713
- const branches = [];
7714
- for (const innerVariant of innerCSS.variants) {
7715
- const conditions = collectSelectorConditions(innerVariant);
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
- if (branches.length === 0) return {
7719
- variants: [emptyVariant()],
7720
- isImpossible: false
7721
- };
7722
- const v = emptyVariant();
7723
- v.parentGroups.push({
7724
- branches,
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
- * Sort key for canonical condition output within selectors.
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 conditionSortKey(cond) {
7747
- if ("attribute" in cond) {
7748
- const hasValue = cond.value !== void 0 ? 1 : 0;
7749
- return `${(cond.negated ? 2 : 0) + hasValue}|${cond.attribute}|${cond.value ?? ""}`;
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 `${cond.negated ? 5 : 4}|${cond.pseudo}`;
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
- * Check if parent groups A is a superset of B.
8141
- * Each group in B must have a matching group in A.
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 isParentGroupsSuperset(a, b) {
8144
- if (a.length < b.length) return false;
8145
- return isConditionsSuperset(a, b, getParentGroupKey);
8146
- }
8147
- function getParentGroupKey(g) {
8148
- return `${g.negated ? "!" : ""}${g.direct ? ">" : ""}(${getBranchesKey(g.branches)})`;
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
- * Deduplicate variants
8278
+ * Split a state key by top-level `&` operators.
8152
8279
  *
8153
- * Removes:
8154
- * 1. Exact duplicates (same key)
8155
- * 2. Superset variants (more restrictive selectors that are redundant)
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 dedupeVariants(variants) {
8158
- if (variants.length <= 1) return variants;
8159
- const seen = /* @__PURE__ */ new Set();
8160
- const result = [];
8161
- for (const v of variants) {
8162
- const key = getVariantKey(v);
8163
- if (!seen.has(key)) {
8164
- seen.add(key);
8165
- result.push(v);
8166
- }
8167
- }
8168
- if (result.length <= 1) return result;
8169
- result.sort((a, b) => variantConditionCount(a) - variantConditionCount(b));
8170
- const filtered = [];
8171
- for (const candidate of result) {
8172
- let isRedundant = false;
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
- if (!isRedundant) filtered.push(candidate);
8301
+ current += ch;
8178
8302
  }
8179
- return filtered;
8303
+ const trimmed = current.trim();
8304
+ if (trimmed) parts.push(trimmed);
8305
+ return parts;
8180
8306
  }
8181
8307
  /**
8182
- * Combine AND conditions into CSS
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 andToCSS(children) {
8191
- const exclusiveChildren = makeOrBranchesExclusive(children);
8192
- let currentVariants = [emptyVariant()];
8193
- for (const child of exclusiveChildren) {
8194
- const childCSS = conditionToCSSInner(child);
8195
- if (childCSS.isImpossible || childCSS.variants.length === 0) return {
8196
- variants: [],
8197
- isImpossible: true
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
- * Make OR branches within AND children mutually exclusive.
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 makeOrBranchesExclusive(children) {
8228
- return children.map((child) => {
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
- * Check if OR branches produce different at-rule contexts when
8255
- * materialized. If so, the Cartesian product in andToCSS will
8256
- * create overlapping CSS variants that need exclusive expansion.
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 branchesProduceDifferentContexts(branches) {
8259
- const contextKeys = /* @__PURE__ */ new Set();
8260
- for (const branch of branches) {
8261
- const css = conditionToCSSInner(branch);
8262
- if (css.isImpossible) continue;
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 contextKeys.size > 1;
8348
+ return result;
8266
8349
  }
8267
8350
  /**
8268
- * Combine OR conditions into CSS
8351
+ * Expand a single entry's OR condition into multiple exclusive entries.
8269
8352
  *
8270
- * OR in CSS means multiple selector variants (DNF).
8271
- * After deduplication, variants that differ only in their base
8272
- * modifier/pseudo conditions are merged into :is() groups.
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
- * Note: OR exclusivity is handled at the pipeline level (expandOrConditions),
8275
- * so here we just collect all variants. Any remaining ORs in the condition
8276
- * tree (e.g., from De Morgan expansion) are handled as simple alternatives.
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 orToCSS(children) {
8279
- const allVariants = [];
8280
- for (const child of children) {
8281
- const childCSS = conditionToCSSInner(child);
8282
- if (childCSS.isImpossible) continue;
8283
- allVariants.push(...childCSS.variants);
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
- if (allVariants.length === 0) return {
8286
- variants: [],
8287
- isImpossible: true
8288
- };
8289
- return {
8290
- variants: dedupeVariants(allVariants),
8291
- isImpossible: false
8292
- };
8392
+ return result;
8293
8393
  }
8294
8394
  /**
8295
- * Find keys present in ALL condition arrays.
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 findCommonKeys(conditionSets, getKey) {
8298
- if (conditionSets.length === 0) return /* @__PURE__ */ new Set();
8299
- const common = new Set(conditionSets[0].map(getKey));
8300
- for (let i = 1; i < conditionSets.length; i++) {
8301
- const keys = new Set(conditionSets[i].map(getKey));
8302
- for (const key of common) if (!keys.has(key)) common.delete(key);
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 common;
8408
+ return [condition];
8305
8409
  }
8306
8410
  /**
8307
- * Merge OR variants that share the same "context" (at-rules, root, parent,
8308
- * own, starting) into a single variant with a SelectorGroup.
8411
+ * Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.
8309
8412
  *
8310
- * Variants with no modifier/pseudo conditions are kept separate (they match
8311
- * unconditionally and can't be expressed inside :is()).
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 mergeVariantsIntoSelectorGroups(variants) {
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 group of groups.values()) {
8324
- if (group.length === 1) {
8325
- result.push(group[0]);
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
- * Factor common modifier/pseudo conditions out of variants and create
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 factorAndGroup(variants) {
8350
- {
8351
- const key0 = getVariantContextKey(variants[0]);
8352
- for (let i = 1; i < variants.length; i++) {
8353
- const keyI = getVariantContextKey(variants[i]);
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
- * Detect when branches form a complete Cartesian product of independent
8406
- * modifier attribute dimensions and return one SelectorGroup per dimension.
8452
+ * Sort OR branches to prioritize at-rule conditions first.
8407
8453
  *
8408
- * Example: 4 branches for 2 attributes × 2 values each
8409
- * :is(A1, A2):is(B1, B2) instead of :is(A1B1, A1B2, A2B1, A2B2)
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 tryFactorIntoDimensions(branches) {
8412
- if (branches.length < 4) return null;
8413
- const dimensions = /* @__PURE__ */ new Map();
8414
- for (const branch of branches) for (const cond of branch) {
8415
- if (!("attribute" in cond)) return null;
8416
- if (!dimensions.has(cond.attribute)) dimensions.set(cond.attribute, /* @__PURE__ */ new Map());
8417
- dimensions.get(cond.attribute).set(getModifierKey(cond), cond);
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
- * Build at-rules array from a variant
8473
+ * Expand ORs in a single entry's exclusive condition
8439
8474
  */
8440
- function buildAtRulesFromVariant(variant) {
8441
- const atRules = [];
8442
- if (variant.mediaConditions.length > 0) {
8443
- const conditionParts = variant.mediaConditions.map((c) => {
8444
- if (c.subtype === "type") return c.negated ? `not ${c.condition}` : c.condition;
8445
- else return c.negated ? `(not ${c.condition})` : c.condition;
8446
- });
8447
- atRules.push(`@media ${conditionParts.sort().join(" and ")}`);
8448
- }
8449
- if (variant.containerConditions.length > 0) {
8450
- const byName = /* @__PURE__ */ new Map();
8451
- for (const cond of variant.containerConditions) {
8452
- const group = byName.get(cond.name) || [];
8453
- group.push(cond);
8454
- byName.set(cond.name, group);
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
- if (variant.supportsConditions.length > 0) {
8463
- const conditionParts = variant.supportsConditions.map((c) => {
8464
- if (c.subtype === "selector") {
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
- atRules.push(`@supports ${conditionParts.join(" and ")}`);
8496
+ priorBranches.push(branch);
8473
8497
  }
8474
- return atRules;
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 = keys.filter((key) => !isSelector(key) && !key.startsWith("@"));
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-JokB1Lc8.js.map
10400
+ //# sourceMappingURL=config-D0ZQMdY8.js.map