@tenphi/tasty 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pipeline/exclusive.js +121 -17
- package/dist/pipeline/exclusive.js.map +1 -1
- package/dist/pipeline/index.js +92 -51
- package/dist/pipeline/index.js.map +1 -1
- package/dist/pipeline/materialize-contradictions.js +125 -0
- package/dist/pipeline/materialize-contradictions.js.map +1 -0
- package/dist/pipeline/materialize.js +1 -120
- package/dist/pipeline/materialize.js.map +1 -1
- package/dist/pipeline/simplify.js +38 -3
- package/dist/pipeline/simplify.js.map +1 -1
- package/dist/tasty.d.ts +1 -1
- package/docs/pipeline.md +204 -50
- package/package.json +1 -1
|
@@ -68,10 +68,18 @@ function parseStyleEntries(styleKey, valueMap, parseCondition) {
|
|
|
68
68
|
/**
|
|
69
69
|
* Merge parsed entries that share the same value.
|
|
70
70
|
*
|
|
71
|
-
* When multiple state keys map to the same value, their
|
|
72
|
-
* be combined with OR and treated as a single entry.
|
|
73
|
-
* **before** exclusive expansion and OR branch splitting
|
|
74
|
-
* combinatorial explosion and duplicate CSS output.
|
|
71
|
+
* When multiple **non-default** state keys map to the same value, their
|
|
72
|
+
* conditions can be combined with OR and treated as a single entry.
|
|
73
|
+
* This must happen **before** exclusive expansion and OR branch splitting
|
|
74
|
+
* to avoid combinatorial explosion and duplicate CSS output.
|
|
75
|
+
*
|
|
76
|
+
* Default (TRUE) entries are **never** merged with non-default entries.
|
|
77
|
+
* Merging `TRUE | X` collapses to `TRUE`, destroying the non-default
|
|
78
|
+
* condition's participation in exclusive building. That causes
|
|
79
|
+
* intermediate-priority states to lose their `:not(X)` negation,
|
|
80
|
+
* breaking mutual exclusivity when X and an intermediate state are
|
|
81
|
+
* both active. Stage 6 `mergeByValue` handles combining rules with
|
|
82
|
+
* identical CSS output after exclusive conditions are correctly built.
|
|
75
83
|
*
|
|
76
84
|
* Example: `{ '@dark': 'red', '@dark & @hc': 'red' }` merges into a
|
|
77
85
|
* single entry with condition `@dark | (@dark & @hc)` = `@dark`.
|
|
@@ -100,17 +108,21 @@ function mergeEntriesByValue(entries) {
|
|
|
100
108
|
merged.push(group.entries[0]);
|
|
101
109
|
continue;
|
|
102
110
|
}
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
stateKey
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
const defaultEntries = group.entries.filter((e) => e.condition.kind === "true");
|
|
112
|
+
const nonDefaultEntries = group.entries.filter((e) => e.condition.kind !== "true");
|
|
113
|
+
for (const entry of defaultEntries) merged.push(entry);
|
|
114
|
+
if (nonDefaultEntries.length === 1) merged.push(nonDefaultEntries[0]);
|
|
115
|
+
else if (nonDefaultEntries.length >= 2) {
|
|
116
|
+
const combinedCondition = simplifyCondition(or(...nonDefaultEntries.map((e) => e.condition)));
|
|
117
|
+
const combinedStateKey = nonDefaultEntries.map((e) => e.stateKey).join(" | ");
|
|
118
|
+
merged.push({
|
|
119
|
+
styleKey: nonDefaultEntries[0].styleKey,
|
|
120
|
+
stateKey: combinedStateKey,
|
|
121
|
+
value: nonDefaultEntries[0].value,
|
|
122
|
+
condition: combinedCondition,
|
|
123
|
+
priority: group.maxPriority
|
|
124
|
+
});
|
|
125
|
+
}
|
|
114
126
|
}
|
|
115
127
|
merged.sort((a, b) => b.priority - a.priority);
|
|
116
128
|
return merged;
|
|
@@ -121,6 +133,91 @@ function serializeValue(value) {
|
|
|
121
133
|
return JSON.stringify(value);
|
|
122
134
|
}
|
|
123
135
|
/**
|
|
136
|
+
* Eliminate redundant state dimensions from a value map.
|
|
137
|
+
*
|
|
138
|
+
* When a value map contains compound AND state keys (e.g. `@dark & @hc`),
|
|
139
|
+
* checks whether any state atom is a "don't-care" variable — i.e. the
|
|
140
|
+
* value is the same whether that atom is present or absent. Redundant
|
|
141
|
+
* atoms are removed from all keys and duplicate entries are collapsed.
|
|
142
|
+
*
|
|
143
|
+
* This runs **before** condition parsing so that downstream stages
|
|
144
|
+
* (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)
|
|
145
|
+
* never see the irrelevant dimension, producing simpler, smaller CSS.
|
|
146
|
+
*
|
|
147
|
+
* Only pure top-level AND combinations are eligible. Keys that contain
|
|
148
|
+
* `|`, `^`, or `,` at the top level are treated as opaque single atoms.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }
|
|
152
|
+
* // @hc is redundant → { '': A, '@dark': B }
|
|
153
|
+
*/
|
|
154
|
+
function extractCompoundStates(valueMap) {
|
|
155
|
+
const keys = Object.keys(valueMap);
|
|
156
|
+
if (keys.length < 3 || !keys.some((k) => k.includes("&"))) return valueMap;
|
|
157
|
+
const entries = keys.map((key) => {
|
|
158
|
+
return {
|
|
159
|
+
atoms: splitTopLevelAnd(key) ?? [key],
|
|
160
|
+
value: valueMap[key]
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
const allAtoms = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const e of entries) for (const a of e.atoms) allAtoms.add(a);
|
|
165
|
+
const redundant = /* @__PURE__ */ new Set();
|
|
166
|
+
for (const atom of allAtoms) if (isAtomRedundant(entries, atom)) redundant.add(atom);
|
|
167
|
+
if (redundant.size === 0) return valueMap;
|
|
168
|
+
const newMap = {};
|
|
169
|
+
for (const e of entries) {
|
|
170
|
+
const newKey = e.atoms.filter((a) => !redundant.has(a)).join(" & ");
|
|
171
|
+
if (!(newKey in newMap)) newMap[newKey] = e.value;
|
|
172
|
+
}
|
|
173
|
+
return newMap;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Split a state key by top-level `&` operators.
|
|
177
|
+
*
|
|
178
|
+
* Returns `null` if the key contains `|`, `^`, or `,` at the top level
|
|
179
|
+
* (making it ineligible for atom-level extraction).
|
|
180
|
+
* Returns `[]` for the empty string (default key).
|
|
181
|
+
*/
|
|
182
|
+
function splitTopLevelAnd(key) {
|
|
183
|
+
if (key === "") return [];
|
|
184
|
+
const parts = [];
|
|
185
|
+
let depth = 0;
|
|
186
|
+
let current = "";
|
|
187
|
+
for (const ch of key) {
|
|
188
|
+
if (ch === "(" || ch === "[") depth++;
|
|
189
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
190
|
+
if (depth === 0) {
|
|
191
|
+
if (ch === "&") {
|
|
192
|
+
const trimmed = current.trim();
|
|
193
|
+
if (trimmed) parts.push(trimmed);
|
|
194
|
+
current = "";
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (ch === "|" || ch === "^" || ch === ",") return null;
|
|
198
|
+
}
|
|
199
|
+
current += ch;
|
|
200
|
+
}
|
|
201
|
+
const trimmed = current.trim();
|
|
202
|
+
if (trimmed) parts.push(trimmed);
|
|
203
|
+
return parts;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* An atom is redundant when every entry that contains it has a matching
|
|
207
|
+
* partner (same remaining atoms, atom absent) with the same value.
|
|
208
|
+
*/
|
|
209
|
+
function isAtomRedundant(entries, atom) {
|
|
210
|
+
const withAtom = entries.filter((e) => e.atoms.includes(atom));
|
|
211
|
+
if (withAtom.length === 0) return false;
|
|
212
|
+
for (const wa of withAtom) {
|
|
213
|
+
const remaining = wa.atoms.filter((a) => a !== atom);
|
|
214
|
+
const pair = entries.find((e) => !e.atoms.includes(atom) && e.atoms.length === remaining.length && remaining.every((r) => e.atoms.includes(r)));
|
|
215
|
+
if (!pair) return false;
|
|
216
|
+
if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
124
221
|
* Check if a value is a style value mapping (object with state keys)
|
|
125
222
|
*/
|
|
126
223
|
function isValueMapping(value) {
|
|
@@ -149,7 +246,14 @@ function expandOrConditions(entries) {
|
|
|
149
246
|
return result;
|
|
150
247
|
}
|
|
151
248
|
/**
|
|
152
|
-
* Expand a single entry's OR condition into multiple exclusive entries
|
|
249
|
+
* Expand a single entry's OR condition into multiple exclusive entries.
|
|
250
|
+
*
|
|
251
|
+
* Note: branches are NOT sorted by at-rule context here (unlike the
|
|
252
|
+
* `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't
|
|
253
|
+
* the product of De Morgan negation, so each branch is expected to render
|
|
254
|
+
* independently in its own scope and at-rule sort isn't load-bearing.
|
|
255
|
+
* The post-build pass needs the sort because it has to preserve at-rule
|
|
256
|
+
* wrapping across branches that came from negating a compound at-rule.
|
|
153
257
|
*/
|
|
154
258
|
function expandSingleEntry(entry) {
|
|
155
259
|
const orBranches = collectOrBranches(entry.condition);
|
|
@@ -280,6 +384,6 @@ function expandExclusiveConditionOrs(entry) {
|
|
|
280
384
|
return result;
|
|
281
385
|
}
|
|
282
386
|
//#endregion
|
|
283
|
-
export { buildExclusiveConditions, expandExclusiveOrs, expandOrConditions, isValueMapping, mergeEntriesByValue, parseStyleEntries };
|
|
387
|
+
export { buildExclusiveConditions, expandExclusiveOrs, expandOrConditions, extractCompoundStates, isValueMapping, mergeEntriesByValue, parseStyleEntries };
|
|
284
388
|
|
|
285
389
|
//# sourceMappingURL=exclusive.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exclusive.js","names":[],"sources":["../../src/pipeline/exclusive.ts"],"sourcesContent":["/**\n * Exclusive Condition Builder\n *\n * Transforms parsed style entries into exclusive conditions.\n * Each entry's condition is ANDed with the negation of all higher-priority conditions,\n * ensuring exactly one condition matches at any given time.\n */\n\nimport type { StyleValue } from '../utils/styles';\n\nimport type { ConditionNode } from './conditions';\nimport { and, isCompoundCondition, not, or, trueCondition } from './conditions';\nimport { simplifyCondition } from './simplify';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Parsed style entry with condition\n */\nexport interface ParsedStyleEntry {\n styleKey: string; // e.g., 'padding', 'fill'\n stateKey: string; // Original key: '', 'compact', '@media(w < 768px)'\n value: StyleValue; // The style value (before handler processing)\n condition: ConditionNode; // Parsed condition tree\n priority: number; // Order in original object (higher = higher priority)\n}\n\n/**\n * Style entry with exclusive condition\n */\nexport interface ExclusiveStyleEntry extends ParsedStyleEntry {\n exclusiveCondition: ConditionNode; // condition & !higherPriorityConditions\n}\n\n// ============================================================================\n// Main Function\n// ============================================================================\n\n/**\n * Build exclusive conditions for a list of parsed style entries.\n *\n * The entries should be ordered by priority (highest priority first).\n *\n * For each entry, we compute:\n * exclusiveCondition = condition & !prior[0] & !prior[1] & ...\n *\n * This ensures exactly one condition matches at any time.\n *\n * Example:\n * Input (ordered highest to lowest priority):\n * A: value1 (priority 2)\n * B: value2 (priority 1)\n * C: value3 (priority 0)\n *\n * Output:\n * A: A\n * B: B & !A\n * C: C & !A & !B\n *\n * @param entries Parsed style entries ordered by priority (highest first)\n * @returns Entries with exclusive conditions, filtered to remove impossible ones\n */\nexport function buildExclusiveConditions(\n entries: ParsedStyleEntry[],\n): ExclusiveStyleEntry[] {\n const result: ExclusiveStyleEntry[] = [];\n const priorConditions: ConditionNode[] = [];\n\n for (const entry of entries) {\n // Build: condition & !prior[0] & !prior[1] & ...\n let exclusive: ConditionNode = entry.condition;\n\n for (const prior of priorConditions) {\n // Skip negating \"always true\" (default state) - it would become \"always false\"\n if (prior.kind !== 'true') {\n exclusive = and(exclusive, not(prior));\n }\n }\n\n // Simplify the exclusive condition\n const simplified = simplifyCondition(exclusive);\n\n // Skip impossible conditions (simplified to FALSE)\n if (simplified.kind === 'false') {\n continue;\n }\n\n result.push({\n ...entry,\n exclusiveCondition: simplified,\n });\n\n // Add non-default conditions to prior list for subsequent entries\n if (entry.condition.kind !== 'true') {\n priorConditions.push(entry.condition);\n }\n }\n\n return result;\n}\n\n/**\n * Parse style entries from a value mapping object.\n *\n * @param styleKey The style key (e.g., 'padding')\n * @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }\n * @param parseCondition Function to parse state keys into conditions\n * @returns Parsed entries ordered by priority (highest first)\n */\nexport function parseStyleEntries(\n styleKey: string,\n valueMap: Record<string, StyleValue>,\n parseCondition: (stateKey: string) => ConditionNode,\n): ParsedStyleEntry[] {\n const entries: ParsedStyleEntry[] = [];\n const keys = Object.keys(valueMap);\n\n keys.forEach((stateKey, index) => {\n const value = valueMap[stateKey];\n const condition =\n stateKey === '' ? trueCondition() : parseCondition(stateKey);\n\n entries.push({\n styleKey,\n stateKey,\n value,\n condition,\n priority: index,\n });\n });\n\n // Reverse so highest priority (last in object) comes first for exclusive building\n // buildExclusiveConditions expects highest priority first\n entries.reverse();\n\n return entries;\n}\n\n/**\n * Merge parsed entries that share the same value.\n *\n * When multiple state keys map to the same value, their conditions can\n * be combined with OR and treated as a single entry. This must happen\n * **before** exclusive expansion and OR branch splitting to avoid\n * combinatorial explosion and duplicate CSS output.\n *\n * Example: `{ '@dark': 'red', '@dark & @hc': 'red' }` merges into a\n * single entry with condition `@dark | (@dark & @hc)` = `@dark`.\n *\n * Entries are ordered highest-priority-first. The merged entry keeps the\n * highest priority of the group.\n */\nexport function mergeEntriesByValue(\n entries: ParsedStyleEntry[],\n): ParsedStyleEntry[] {\n if (entries.length <= 1) return entries;\n\n const groups = new Map<\n string,\n { entries: ParsedStyleEntry[]; maxPriority: number }\n >();\n\n for (const entry of entries) {\n const valueKey = serializeValue(entry.value);\n const group = groups.get(valueKey);\n if (group) {\n group.entries.push(entry);\n group.maxPriority = Math.max(group.maxPriority, entry.priority);\n } else {\n groups.set(valueKey, { entries: [entry], maxPriority: entry.priority });\n }\n }\n\n // If no merges possible, return as-is\n if (groups.size === entries.length) return entries;\n\n const merged: ParsedStyleEntry[] = [];\n for (const [, group] of groups) {\n if (group.entries.length === 1) {\n merged.push(group.entries[0]);\n continue;\n }\n\n // Combine conditions with OR, then simplify\n const combinedCondition = simplifyCondition(\n or(...group.entries.map((e) => e.condition)),\n );\n\n // Combine state keys for debugging\n const combinedStateKey = group.entries.map((e) => e.stateKey).join(' | ');\n\n // When a group contains the default (true) entry, use minPriority.\n // true|X = true, so the merged condition is unconditional; at elevated\n // priority it would shadow entries between the default and the\n // highest-priority member, breaking exclusivity.\n const hasDefault = group.entries.some((e) => e.condition.kind === 'true');\n const minPriority = Math.min(...group.entries.map((e) => e.priority));\n\n merged.push({\n styleKey: group.entries[0].styleKey,\n stateKey: combinedStateKey,\n value: group.entries[0].value,\n condition: combinedCondition,\n priority: hasDefault ? minPriority : group.maxPriority,\n });\n }\n\n // Re-sort by priority (highest first)\n merged.sort((a, b) => b.priority - a.priority);\n\n return merged;\n}\n\nfunction serializeValue(value: StyleValue): string {\n if (value === null || value === undefined) return 'null';\n if (typeof value === 'string' || typeof value === 'number') {\n return String(value);\n }\n return JSON.stringify(value);\n}\n\n/**\n * Check if a value is a style value mapping (object with state keys)\n */\nexport function isValueMapping(\n value: StyleValue | Record<string, StyleValue>,\n): value is Record<string, StyleValue> {\n return (\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\n// ============================================================================\n// OR Expansion\n// ============================================================================\n\n/**\n * Expand OR conditions in parsed entries into multiple exclusive entries.\n *\n * For an entry with condition `A | B | C`, this creates 3 entries:\n * - condition: A\n * - condition: B & !A\n * - condition: C & !A & !B\n *\n * This ensures OR branches are mutually exclusive BEFORE the main\n * exclusive condition building pass.\n *\n * @param entries Parsed entries (may contain OR conditions)\n * @returns Expanded entries with OR branches made exclusive\n */\nexport function expandOrConditions(\n entries: ParsedStyleEntry[],\n): ParsedStyleEntry[] {\n const result: ParsedStyleEntry[] = [];\n\n for (const entry of entries) {\n const expanded = expandSingleEntry(entry);\n result.push(...expanded);\n }\n\n return result;\n}\n\n/**\n * Expand a single entry's OR condition into multiple exclusive entries\n */\nfunction expandSingleEntry(entry: ParsedStyleEntry): ParsedStyleEntry[] {\n const orBranches = collectOrBranches(entry.condition);\n\n // If no OR (single branch), return as-is\n if (orBranches.length <= 1) {\n return [entry];\n }\n\n // Make each OR branch exclusive from prior branches\n const result: ParsedStyleEntry[] = [];\n const priorBranches: ConditionNode[] = [];\n\n for (let i = 0; i < orBranches.length; i++) {\n const branch = orBranches[i];\n\n // Build: branch & !prior[0] & !prior[1] & ...\n let exclusiveBranch: ConditionNode = branch;\n for (const prior of priorBranches) {\n exclusiveBranch = and(exclusiveBranch, not(prior));\n }\n\n // Simplify to detect impossible combinations\n const simplified = simplifyCondition(exclusiveBranch);\n\n // Skip impossible branches\n if (simplified.kind === 'false') {\n priorBranches.push(branch);\n continue;\n }\n\n result.push({\n ...entry,\n stateKey: `${entry.stateKey}[${i}]`, // Mark as expanded branch\n condition: simplified,\n // Keep same priority - all branches from same entry have same priority\n });\n\n priorBranches.push(branch);\n }\n\n return result;\n}\n\n/**\n * Collect top-level OR branches from a condition.\n *\n * For `A | B | C`, returns [A, B, C]\n * For `A & B`, returns [A & B] (single branch)\n * For `A | (B & C)`, returns [A, B & C]\n */\nfunction collectOrBranches(condition: ConditionNode): ConditionNode[] {\n if (condition.kind === 'true' || condition.kind === 'false') {\n return [condition];\n }\n\n if (isCompoundCondition(condition) && condition.operator === 'OR') {\n // Flatten nested ORs\n const branches: ConditionNode[] = [];\n for (const child of condition.children) {\n branches.push(...collectOrBranches(child));\n }\n return branches;\n }\n\n // Not an OR - return as single branch\n return [condition];\n}\n\n// ============================================================================\n// Post-Build OR Expansion (for De Morgan ORs)\n// ============================================================================\n\n/**\n * Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.\n *\n * This handles ORs that arise from De Morgan expansion during negation:\n * !(A & B) = !A | !B\n *\n * These ORs need to be made exclusive to avoid overlapping CSS rules:\n * !A | !B → !A | (A & !B)\n *\n * This is logically equivalent but ensures each branch has proper context.\n *\n * Example:\n * Input: { \"\": V1, \"@supports(...) & :has()\": V2 }\n * V2's exclusive = @supports & :has\n * V1's exclusive = !(@supports & :has) = !@supports | !:has\n *\n * Without this fix: V1 gets two rules:\n * - @supports (not ...) → V1 ✓\n * - :not(:has()) → V1 ✗ (missing @supports context!)\n *\n * With this fix: V1 gets two exclusive rules:\n * - @supports (not ...) → V1 ✓\n * - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)\n */\nexport function expandExclusiveOrs(\n entries: ExclusiveStyleEntry[],\n): ExclusiveStyleEntry[] {\n const result: ExclusiveStyleEntry[] = [];\n\n for (const entry of entries) {\n const expanded = expandExclusiveConditionOrs(entry);\n result.push(...expanded);\n }\n\n return result;\n}\n\n/**\n * Check if a condition involves at-rules (media, container, supports, starting)\n */\nfunction hasAtRuleContext(node: ConditionNode): boolean {\n if (node.kind === 'true' || node.kind === 'false') {\n return false;\n }\n\n if (node.kind === 'state') {\n // These condition types generate at-rules\n return (\n node.type === 'media' ||\n node.type === 'container' ||\n node.type === 'supports' ||\n node.type === 'starting'\n );\n }\n\n if (node.kind === 'compound') {\n return node.children.some(hasAtRuleContext);\n }\n\n return false;\n}\n\n/**\n * Sort OR branches to prioritize at-rule conditions first.\n *\n * This is critical for correct CSS generation. For `!A | !B` where A is at-rule\n * and B is modifier, we want:\n * - Branch 0: !A (at-rule negation - covers \"no @supports/media\" case)\n * - Branch 1: A & !B (modifier negation with at-rule context)\n *\n * If we process in wrong order (!B first), we'd get:\n * - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)\n * - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)\n */\nfunction sortOrBranchesForExpansion(\n branches: ConditionNode[],\n): ConditionNode[] {\n return [...branches].sort((a, b) => {\n const aHasAtRule = hasAtRuleContext(a);\n const bHasAtRule = hasAtRuleContext(b);\n\n // At-rule conditions come first\n if (aHasAtRule && !bHasAtRule) return -1;\n if (!aHasAtRule && bHasAtRule) return 1;\n\n // Same type - keep original order (stable sort)\n return 0;\n });\n}\n\n/**\n * Expand ORs in a single entry's exclusive condition\n */\nfunction expandExclusiveConditionOrs(\n entry: ExclusiveStyleEntry,\n): ExclusiveStyleEntry[] {\n let orBranches = collectOrBranches(entry.exclusiveCondition);\n\n // If no OR (single branch), return as-is\n if (orBranches.length <= 1) {\n return [entry];\n }\n\n // Sort branches so at-rule conditions come first\n // This ensures proper context inheritance during expansion\n orBranches = sortOrBranchesForExpansion(orBranches);\n\n // Make each OR branch exclusive from prior branches\n const result: ExclusiveStyleEntry[] = [];\n const priorBranches: ConditionNode[] = [];\n\n for (let i = 0; i < orBranches.length; i++) {\n const branch = orBranches[i];\n\n // Build: branch & !prior[0] & !prior[1] & ...\n // This transforms: !A | !B → !A, !B & !!A = !A, (A & !B)\n let exclusiveBranch: ConditionNode = branch;\n for (const prior of priorBranches) {\n exclusiveBranch = and(exclusiveBranch, not(prior));\n }\n\n // Simplify to detect impossible combinations and clean up double negations\n const simplified = simplifyCondition(exclusiveBranch);\n\n // Skip impossible branches\n if (simplified.kind === 'false') {\n priorBranches.push(branch);\n continue;\n }\n\n result.push({\n ...entry,\n stateKey: `${entry.stateKey}[or:${i}]`, // Mark as expanded OR branch\n exclusiveCondition: simplified,\n });\n\n priorBranches.push(branch);\n }\n\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"exclusive.js","names":[],"sources":["../../src/pipeline/exclusive.ts"],"sourcesContent":["/**\n * Exclusive Condition Builder\n *\n * Transforms parsed style entries into exclusive conditions.\n * Each entry's condition is ANDed with the negation of all higher-priority conditions,\n * ensuring exactly one condition matches at any given time.\n */\n\nimport type { StyleValue } from '../utils/styles';\n\nimport type { ConditionNode } from './conditions';\nimport { and, isCompoundCondition, not, or, trueCondition } from './conditions';\nimport { simplifyCondition } from './simplify';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Parsed style entry with condition\n */\nexport interface ParsedStyleEntry {\n styleKey: string; // e.g., 'padding', 'fill'\n stateKey: string; // Original key: '', 'compact', '@media(w < 768px)'\n value: StyleValue; // The style value (before handler processing)\n condition: ConditionNode; // Parsed condition tree\n priority: number; // Order in original object (higher = higher priority)\n}\n\n/**\n * Style entry with exclusive condition\n */\nexport interface ExclusiveStyleEntry extends ParsedStyleEntry {\n exclusiveCondition: ConditionNode; // condition & !higherPriorityConditions\n}\n\n// ============================================================================\n// Main Function\n// ============================================================================\n\n/**\n * Build exclusive conditions for a list of parsed style entries.\n *\n * The entries should be ordered by priority (highest priority first).\n *\n * For each entry, we compute:\n * exclusiveCondition = condition & !prior[0] & !prior[1] & ...\n *\n * This ensures exactly one condition matches at any time.\n *\n * Example:\n * Input (ordered highest to lowest priority):\n * A: value1 (priority 2)\n * B: value2 (priority 1)\n * C: value3 (priority 0)\n *\n * Output:\n * A: A\n * B: B & !A\n * C: C & !A & !B\n *\n * @param entries Parsed style entries ordered by priority (highest first)\n * @returns Entries with exclusive conditions, filtered to remove impossible ones\n */\nexport function buildExclusiveConditions(\n entries: ParsedStyleEntry[],\n): ExclusiveStyleEntry[] {\n const result: ExclusiveStyleEntry[] = [];\n const priorConditions: ConditionNode[] = [];\n\n for (const entry of entries) {\n // Build: condition & !prior[0] & !prior[1] & ...\n let exclusive: ConditionNode = entry.condition;\n\n for (const prior of priorConditions) {\n // Skip negating \"always true\" (default state) - it would become \"always false\"\n if (prior.kind !== 'true') {\n exclusive = and(exclusive, not(prior));\n }\n }\n\n // Simplify the exclusive condition\n const simplified = simplifyCondition(exclusive);\n\n // Skip impossible conditions (simplified to FALSE)\n if (simplified.kind === 'false') {\n continue;\n }\n\n result.push({\n ...entry,\n exclusiveCondition: simplified,\n });\n\n // Add non-default conditions to prior list for subsequent entries\n if (entry.condition.kind !== 'true') {\n priorConditions.push(entry.condition);\n }\n }\n\n return result;\n}\n\n/**\n * Parse style entries from a value mapping object.\n *\n * @param styleKey The style key (e.g., 'padding')\n * @param valueMap The value mapping { '': '2x', 'compact': '1x', '@media(w < 768px)': '0.5x' }\n * @param parseCondition Function to parse state keys into conditions\n * @returns Parsed entries ordered by priority (highest first)\n */\nexport function parseStyleEntries(\n styleKey: string,\n valueMap: Record<string, StyleValue>,\n parseCondition: (stateKey: string) => ConditionNode,\n): ParsedStyleEntry[] {\n const entries: ParsedStyleEntry[] = [];\n const keys = Object.keys(valueMap);\n\n keys.forEach((stateKey, index) => {\n const value = valueMap[stateKey];\n const condition =\n stateKey === '' ? trueCondition() : parseCondition(stateKey);\n\n entries.push({\n styleKey,\n stateKey,\n value,\n condition,\n priority: index,\n });\n });\n\n // Reverse so highest priority (last in object) comes first for exclusive building\n // buildExclusiveConditions expects highest priority first\n entries.reverse();\n\n return entries;\n}\n\n/**\n * Merge parsed entries that share the same value.\n *\n * When multiple **non-default** state keys map to the same value, their\n * conditions can be combined with OR and treated as a single entry.\n * This must happen **before** exclusive expansion and OR branch splitting\n * to avoid combinatorial explosion and duplicate CSS output.\n *\n * Default (TRUE) entries are **never** merged with non-default entries.\n * Merging `TRUE | X` collapses to `TRUE`, destroying the non-default\n * condition's participation in exclusive building. That causes\n * intermediate-priority states to lose their `:not(X)` negation,\n * breaking mutual exclusivity when X and an intermediate state are\n * both active. Stage 6 `mergeByValue` handles combining rules with\n * identical CSS output after exclusive conditions are correctly built.\n *\n * Example: `{ '@dark': 'red', '@dark & @hc': 'red' }` merges into a\n * single entry with condition `@dark | (@dark & @hc)` = `@dark`.\n *\n * Entries are ordered highest-priority-first. The merged entry keeps the\n * highest priority of the group.\n */\nexport function mergeEntriesByValue(\n entries: ParsedStyleEntry[],\n): ParsedStyleEntry[] {\n if (entries.length <= 1) return entries;\n\n const groups = new Map<\n string,\n { entries: ParsedStyleEntry[]; maxPriority: number }\n >();\n\n for (const entry of entries) {\n const valueKey = serializeValue(entry.value);\n const group = groups.get(valueKey);\n if (group) {\n group.entries.push(entry);\n group.maxPriority = Math.max(group.maxPriority, entry.priority);\n } else {\n groups.set(valueKey, { entries: [entry], maxPriority: entry.priority });\n }\n }\n\n // If no merges possible, return as-is\n if (groups.size === entries.length) return entries;\n\n const merged: ParsedStyleEntry[] = [];\n for (const [, group] of groups) {\n if (group.entries.length === 1) {\n merged.push(group.entries[0]);\n continue;\n }\n\n // Separate default (TRUE) entries from non-default entries.\n // Default entries must stay separate so that non-default conditions\n // participate in exclusive building and block intermediate states.\n const defaultEntries = group.entries.filter(\n (e) => e.condition.kind === 'true',\n );\n const nonDefaultEntries = group.entries.filter(\n (e) => e.condition.kind !== 'true',\n );\n\n // Keep default entries as-is\n for (const entry of defaultEntries) {\n merged.push(entry);\n }\n\n // Merge only non-default entries\n if (nonDefaultEntries.length === 1) {\n merged.push(nonDefaultEntries[0]);\n } else if (nonDefaultEntries.length >= 2) {\n const combinedCondition = simplifyCondition(\n or(...nonDefaultEntries.map((e) => e.condition)),\n );\n\n const combinedStateKey = nonDefaultEntries\n .map((e) => e.stateKey)\n .join(' | ');\n\n merged.push({\n styleKey: nonDefaultEntries[0].styleKey,\n stateKey: combinedStateKey,\n value: nonDefaultEntries[0].value,\n condition: combinedCondition,\n priority: group.maxPriority,\n });\n }\n }\n\n // Re-sort by priority (highest first)\n merged.sort((a, b) => b.priority - a.priority);\n\n return merged;\n}\n\nfunction serializeValue(value: StyleValue): string {\n if (value === null || value === undefined) return 'null';\n if (typeof value === 'string' || typeof value === 'number') {\n return String(value);\n }\n return JSON.stringify(value);\n}\n\n// ============================================================================\n// Compound State Extraction\n// ============================================================================\n\n/**\n * Eliminate redundant state dimensions from a value map.\n *\n * When a value map contains compound AND state keys (e.g. `@dark & @hc`),\n * checks whether any state atom is a \"don't-care\" variable — i.e. the\n * value is the same whether that atom is present or absent. Redundant\n * atoms are removed from all keys and duplicate entries are collapsed.\n *\n * This runs **before** condition parsing so that downstream stages\n * (`mergeEntriesByValue`, `buildExclusiveConditions`, materialization)\n * never see the irrelevant dimension, producing simpler, smaller CSS.\n *\n * Only pure top-level AND combinations are eligible. Keys that contain\n * `|`, `^`, or `,` at the top level are treated as opaque single atoms.\n *\n * @example\n * { '': A, '@dark': B, '@hc': A, '@dark & @hc': B }\n * // @hc is redundant → { '': A, '@dark': B }\n */\nexport function extractCompoundStates(\n valueMap: Record<string, StyleValue>,\n): Record<string, StyleValue> {\n const keys = Object.keys(valueMap);\n\n if (keys.length < 3 || !keys.some((k) => k.includes('&'))) {\n return valueMap;\n }\n\n const entries = keys.map((key) => {\n const atoms = splitTopLevelAnd(key);\n return {\n // null means the key has non-AND operators; treat the whole key\n // as a single opaque atom so it never matches partial pairs.\n atoms: atoms ?? [key],\n value: valueMap[key],\n };\n });\n\n const allAtoms = new Set<string>();\n for (const e of entries) {\n for (const a of e.atoms) allAtoms.add(a);\n }\n\n const redundant = new Set<string>();\n for (const atom of allAtoms) {\n if (isAtomRedundant(entries, atom)) {\n redundant.add(atom);\n }\n }\n\n if (redundant.size === 0) return valueMap;\n\n const newMap: Record<string, StyleValue> = {};\n for (const e of entries) {\n const filtered = e.atoms.filter((a) => !redundant.has(a));\n const newKey = filtered.join(' & ');\n if (!(newKey in newMap)) {\n newMap[newKey] = e.value;\n }\n }\n\n return newMap;\n}\n\n/**\n * Split a state key by top-level `&` operators.\n *\n * Returns `null` if the key contains `|`, `^`, or `,` at the top level\n * (making it ineligible for atom-level extraction).\n * Returns `[]` for the empty string (default key).\n */\nfunction splitTopLevelAnd(key: string): string[] | null {\n if (key === '') return [];\n\n const parts: string[] = [];\n let depth = 0;\n let current = '';\n\n for (const ch of key) {\n if (ch === '(' || ch === '[') depth++;\n else if (ch === ')' || ch === ']') depth--;\n\n if (depth === 0) {\n if (ch === '&') {\n const trimmed = current.trim();\n if (trimmed) parts.push(trimmed);\n current = '';\n continue;\n }\n if (ch === '|' || ch === '^' || ch === ',') {\n return null;\n }\n }\n\n current += ch;\n }\n\n const trimmed = current.trim();\n if (trimmed) parts.push(trimmed);\n\n return parts;\n}\n\n/**\n * An atom is redundant when every entry that contains it has a matching\n * partner (same remaining atoms, atom absent) with the same value.\n */\nfunction isAtomRedundant(\n entries: { atoms: string[]; value: StyleValue }[],\n atom: string,\n): boolean {\n const withAtom = entries.filter((e) => e.atoms.includes(atom));\n if (withAtom.length === 0) return false;\n\n for (const wa of withAtom) {\n const remaining = wa.atoms.filter((a) => a !== atom);\n\n const pair = entries.find(\n (e) =>\n !e.atoms.includes(atom) &&\n e.atoms.length === remaining.length &&\n remaining.every((r) => e.atoms.includes(r)),\n );\n\n if (!pair) return false;\n if (serializeValue(wa.value) !== serializeValue(pair.value)) return false;\n }\n\n return true;\n}\n\n/**\n * Check if a value is a style value mapping (object with state keys)\n */\nexport function isValueMapping(\n value: StyleValue | Record<string, StyleValue>,\n): value is Record<string, StyleValue> {\n return (\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\n// ============================================================================\n// OR Expansion\n// ============================================================================\n\n/**\n * Expand OR conditions in parsed entries into multiple exclusive entries.\n *\n * For an entry with condition `A | B | C`, this creates 3 entries:\n * - condition: A\n * - condition: B & !A\n * - condition: C & !A & !B\n *\n * This ensures OR branches are mutually exclusive BEFORE the main\n * exclusive condition building pass.\n *\n * @param entries Parsed entries (may contain OR conditions)\n * @returns Expanded entries with OR branches made exclusive\n */\nexport function expandOrConditions(\n entries: ParsedStyleEntry[],\n): ParsedStyleEntry[] {\n const result: ParsedStyleEntry[] = [];\n\n for (const entry of entries) {\n const expanded = expandSingleEntry(entry);\n result.push(...expanded);\n }\n\n return result;\n}\n\n/**\n * Expand a single entry's OR condition into multiple exclusive entries.\n *\n * Note: branches are NOT sorted by at-rule context here (unlike the\n * `expandExclusiveOrs` pass below). User-authored ORs in state keys aren't\n * the product of De Morgan negation, so each branch is expected to render\n * independently in its own scope and at-rule sort isn't load-bearing.\n * The post-build pass needs the sort because it has to preserve at-rule\n * wrapping across branches that came from negating a compound at-rule.\n */\nfunction expandSingleEntry(entry: ParsedStyleEntry): ParsedStyleEntry[] {\n const orBranches = collectOrBranches(entry.condition);\n\n // If no OR (single branch), return as-is\n if (orBranches.length <= 1) {\n return [entry];\n }\n\n // Make each OR branch exclusive from prior branches\n const result: ParsedStyleEntry[] = [];\n const priorBranches: ConditionNode[] = [];\n\n for (let i = 0; i < orBranches.length; i++) {\n const branch = orBranches[i];\n\n // Build: branch & !prior[0] & !prior[1] & ...\n let exclusiveBranch: ConditionNode = branch;\n for (const prior of priorBranches) {\n exclusiveBranch = and(exclusiveBranch, not(prior));\n }\n\n // Simplify to detect impossible combinations\n const simplified = simplifyCondition(exclusiveBranch);\n\n // Skip impossible branches\n if (simplified.kind === 'false') {\n priorBranches.push(branch);\n continue;\n }\n\n result.push({\n ...entry,\n stateKey: `${entry.stateKey}[${i}]`, // Mark as expanded branch\n condition: simplified,\n // Keep same priority - all branches from same entry have same priority\n });\n\n priorBranches.push(branch);\n }\n\n return result;\n}\n\n/**\n * Collect top-level OR branches from a condition.\n *\n * For `A | B | C`, returns [A, B, C]\n * For `A & B`, returns [A & B] (single branch)\n * For `A | (B & C)`, returns [A, B & C]\n */\nfunction collectOrBranches(condition: ConditionNode): ConditionNode[] {\n if (condition.kind === 'true' || condition.kind === 'false') {\n return [condition];\n }\n\n if (isCompoundCondition(condition) && condition.operator === 'OR') {\n // Flatten nested ORs\n const branches: ConditionNode[] = [];\n for (const child of condition.children) {\n branches.push(...collectOrBranches(child));\n }\n return branches;\n }\n\n // Not an OR - return as single branch\n return [condition];\n}\n\n// ============================================================================\n// Post-Build OR Expansion (for De Morgan ORs)\n// ============================================================================\n\n/**\n * Expand OR conditions in exclusive entries AFTER buildExclusiveConditions.\n *\n * This handles ORs that arise from De Morgan expansion during negation:\n * !(A & B) = !A | !B\n *\n * These ORs need to be made exclusive to avoid overlapping CSS rules:\n * !A | !B → !A | (A & !B)\n *\n * This is logically equivalent but ensures each branch has proper context.\n *\n * Example:\n * Input: { \"\": V1, \"@supports(...) & :has()\": V2 }\n * V2's exclusive = @supports & :has\n * V1's exclusive = !(@supports & :has) = !@supports | !:has\n *\n * Without this fix: V1 gets two rules:\n * - @supports (not ...) → V1 ✓\n * - :not(:has()) → V1 ✗ (missing @supports context!)\n *\n * With this fix: V1 gets two exclusive rules:\n * - @supports (not ...) → V1 ✓\n * - @supports (...) { :not(:has()) } → V1 ✓ (proper context!)\n */\nexport function expandExclusiveOrs(\n entries: ExclusiveStyleEntry[],\n): ExclusiveStyleEntry[] {\n const result: ExclusiveStyleEntry[] = [];\n\n for (const entry of entries) {\n const expanded = expandExclusiveConditionOrs(entry);\n result.push(...expanded);\n }\n\n return result;\n}\n\n/**\n * Check if a condition involves at-rules (media, container, supports, starting)\n */\nfunction hasAtRuleContext(node: ConditionNode): boolean {\n if (node.kind === 'true' || node.kind === 'false') {\n return false;\n }\n\n if (node.kind === 'state') {\n // These condition types generate at-rules\n return (\n node.type === 'media' ||\n node.type === 'container' ||\n node.type === 'supports' ||\n node.type === 'starting'\n );\n }\n\n if (node.kind === 'compound') {\n return node.children.some(hasAtRuleContext);\n }\n\n return false;\n}\n\n/**\n * Sort OR branches to prioritize at-rule conditions first.\n *\n * This is critical for correct CSS generation. For `!A | !B` where A is at-rule\n * and B is modifier, we want:\n * - Branch 0: !A (at-rule negation - covers \"no @supports/media\" case)\n * - Branch 1: A & !B (modifier negation with at-rule context)\n *\n * If we process in wrong order (!B first), we'd get:\n * - Branch 0: !B (modifier negation WITHOUT at-rule context - WRONG!)\n * - Branch 1: B & !A (at-rule negation with modifier - incomplete coverage)\n */\nfunction sortOrBranchesForExpansion(\n branches: ConditionNode[],\n): ConditionNode[] {\n return [...branches].sort((a, b) => {\n const aHasAtRule = hasAtRuleContext(a);\n const bHasAtRule = hasAtRuleContext(b);\n\n // At-rule conditions come first\n if (aHasAtRule && !bHasAtRule) return -1;\n if (!aHasAtRule && bHasAtRule) return 1;\n\n // Same type - keep original order (stable sort)\n return 0;\n });\n}\n\n/**\n * Expand ORs in a single entry's exclusive condition\n */\nfunction expandExclusiveConditionOrs(\n entry: ExclusiveStyleEntry,\n): ExclusiveStyleEntry[] {\n let orBranches = collectOrBranches(entry.exclusiveCondition);\n\n // If no OR (single branch), return as-is\n if (orBranches.length <= 1) {\n return [entry];\n }\n\n // Sort branches so at-rule conditions come first\n // This ensures proper context inheritance during expansion\n orBranches = sortOrBranchesForExpansion(orBranches);\n\n // Make each OR branch exclusive from prior branches\n const result: ExclusiveStyleEntry[] = [];\n const priorBranches: ConditionNode[] = [];\n\n for (let i = 0; i < orBranches.length; i++) {\n const branch = orBranches[i];\n\n // Build: branch & !prior[0] & !prior[1] & ...\n // This transforms: !A | !B → !A, !B & !!A = !A, (A & !B)\n let exclusiveBranch: ConditionNode = branch;\n for (const prior of priorBranches) {\n exclusiveBranch = and(exclusiveBranch, not(prior));\n }\n\n // Simplify to detect impossible combinations and clean up double negations\n const simplified = simplifyCondition(exclusiveBranch);\n\n // Skip impossible branches\n if (simplified.kind === 'false') {\n priorBranches.push(branch);\n continue;\n }\n\n result.push({\n ...entry,\n stateKey: `${entry.stateKey}[or:${i}]`, // Mark as expanded OR branch\n exclusiveCondition: simplified,\n });\n\n priorBranches.push(branch);\n }\n\n return result;\n}\n"],"mappings":"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"}
|
package/dist/pipeline/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { STYLE_HANDLER_MAP } from "../styles/index.js";
|
|
|
5
5
|
import { createStateParserContext, extractLocalPredefinedStates } from "../states/index.js";
|
|
6
6
|
import { and, or, trueCondition } from "./conditions.js";
|
|
7
7
|
import { simplifyCondition } from "./simplify.js";
|
|
8
|
-
import { buildExclusiveConditions, expandExclusiveOrs, expandOrConditions, isValueMapping, mergeEntriesByValue, parseStyleEntries } from "./exclusive.js";
|
|
8
|
+
import { buildExclusiveConditions, expandExclusiveOrs, expandOrConditions, extractCompoundStates, isValueMapping, mergeEntriesByValue, parseStyleEntries } from "./exclusive.js";
|
|
9
9
|
import { branchToCSS, buildAtRulesFromVariant, conditionToCSS, mergeVariantsIntoSelectorGroups, optimizeGroups, parentGroupsToCSS, rootGroupsToCSS, selectorGroupToCSS } from "./materialize.js";
|
|
10
10
|
import { emitWarning } from "./warnings.js";
|
|
11
11
|
import { parseStateKey } from "./parseStateKey.js";
|
|
@@ -13,17 +13,25 @@ import { parseStateKey } from "./parseStateKey.js";
|
|
|
13
13
|
/**
|
|
14
14
|
* Tasty Style Rendering Pipeline
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
16
|
+
* Main entrypoint for the style rendering pipeline. Transforms a `Styles`
|
|
17
|
+
* object into an array of `CSSRule` objects ready for DOM injection.
|
|
18
|
+
*
|
|
19
|
+
* Per-handler stages (see docs/pipeline.md for full detail):
|
|
20
|
+
* 0. PRE-PARSE NORMALIZATION - extractCompoundStates (exclusive.ts)
|
|
21
|
+
* 1. PARSE CONDITIONS - parseStyleEntries + parseStateKey
|
|
22
|
+
* 1b. MERGE ENTRIES BY VALUE - mergeEntriesByValue (exclusive.ts)
|
|
23
|
+
* 2a. EXPAND USER OR BRANCHES - expandOrConditions (exclusive.ts)
|
|
24
|
+
* 2b. BUILD EXCLUSIVE CONDITIONS - buildExclusiveConditions
|
|
25
|
+
* 3. EXPAND DE MORGAN ORs - expandExclusiveOrs (exclusive.ts)
|
|
26
|
+
* 4. COMPUTE STATE COMBINATIONS - computeStateCombinations
|
|
27
|
+
* 5. CALL HANDLERS - run style handlers for each snapshot
|
|
28
|
+
* 6. MERGE BY VALUE - mergeByValue (index.ts)
|
|
29
|
+
* 7. MATERIALIZE CSS - conditionToCSS + materializeComputedRule
|
|
30
|
+
*
|
|
31
|
+
* Simplification (`simplifyCondition`) runs inside most stages; calls are
|
|
32
|
+
* memoized by condition unique-id. The post-pass in `runPipeline` dedupes
|
|
33
|
+
* identical rules and emits all `@starting-style` rules last so they win
|
|
34
|
+
* the cascade over their equal-specificity normal counterparts.
|
|
27
35
|
*/
|
|
28
36
|
const pipelineCache = new Lru(5e3);
|
|
29
37
|
/**
|
|
@@ -56,12 +64,25 @@ function runPipeline(styles, parserContext) {
|
|
|
56
64
|
return normal.concat(starting);
|
|
57
65
|
}
|
|
58
66
|
/**
|
|
59
|
-
* Process styles at a given nesting level
|
|
67
|
+
* Process styles at a given nesting level.
|
|
68
|
+
*
|
|
69
|
+
* Splits keys into nested-selector keys and style-handler keys, recurses
|
|
70
|
+
* into nested selectors, then runs the per-handler stages 1–7 over the
|
|
71
|
+
* style keys.
|
|
60
72
|
*/
|
|
61
73
|
function processStyles(styles, selectorSuffix, parserContext, allRules) {
|
|
62
74
|
const keys = Object.keys(styles);
|
|
63
75
|
const selectorKeys = keys.filter((key) => isSelector(key));
|
|
64
76
|
const styleKeys = keys.filter((key) => !isSelector(key) && !key.startsWith("@"));
|
|
77
|
+
processNestedSelectors(styles, selectorKeys, selectorSuffix, parserContext, allRules);
|
|
78
|
+
processHandlerQueue(buildHandlerQueue(styleKeys, styles), selectorSuffix, parserContext, allRules);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Recurse into nested selector keys. Each nested key may expand into multiple
|
|
82
|
+
* suffixes (comma-separated patterns); each suffix is processed independently
|
|
83
|
+
* with the parent's parser context augmented for sub-element scope.
|
|
84
|
+
*/
|
|
85
|
+
function processNestedSelectors(styles, selectorKeys, selectorSuffix, parserContext, allRules) {
|
|
65
86
|
for (const key of selectorKeys) {
|
|
66
87
|
const nestedStyles = styles[key];
|
|
67
88
|
if (!nestedStyles || typeof nestedStyles !== "object") continue;
|
|
@@ -80,46 +101,15 @@ function processStyles(styles, selectorSuffix, parserContext, allRules) {
|
|
|
80
101
|
};
|
|
81
102
|
for (const suffix of suffixes) processStyles(cleanedStyles, selectorSuffix + suffix, subContext, allRules);
|
|
82
103
|
}
|
|
83
|
-
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Run the per-handler pipeline (stages 1–7) over a handler queue and append
|
|
107
|
+
* the resulting CSS rules to `allRules`.
|
|
108
|
+
*/
|
|
109
|
+
function processHandlerQueue(handlerQueue, selectorSuffix, parserContext, allRules) {
|
|
84
110
|
for (const { handler, styleMap } of handlerQueue) {
|
|
85
111
|
const lookupStyles = handler.__lookupStyles;
|
|
86
|
-
const
|
|
87
|
-
for (const styleName of lookupStyles) {
|
|
88
|
-
const value = styleMap[styleName];
|
|
89
|
-
if (value === void 0) continue;
|
|
90
|
-
if (isValueMapping(value)) {
|
|
91
|
-
const fullyExpanded = expandExclusiveOrs(buildExclusiveConditions(expandOrConditions(mergeEntriesByValue(parseStyleEntries(styleName, value, (stateKey) => parseStateKey(stateKey, { context: parserContext }))))));
|
|
92
|
-
exclusiveByStyle.set(styleName, fullyExpanded);
|
|
93
|
-
} else exclusiveByStyle.set(styleName, [{
|
|
94
|
-
styleKey: styleName,
|
|
95
|
-
stateKey: "",
|
|
96
|
-
value,
|
|
97
|
-
condition: trueCondition(),
|
|
98
|
-
priority: 0,
|
|
99
|
-
exclusiveCondition: trueCondition()
|
|
100
|
-
}]);
|
|
101
|
-
}
|
|
102
|
-
const stateSnapshots = computeStateCombinations(exclusiveByStyle, lookupStyles);
|
|
103
|
-
const computedRules = [];
|
|
104
|
-
for (const snapshot of stateSnapshots) {
|
|
105
|
-
const result = handler(snapshot.values);
|
|
106
|
-
if (!result) continue;
|
|
107
|
-
const results = Array.isArray(result) ? result : [result];
|
|
108
|
-
for (const r of results) {
|
|
109
|
-
if (!r || typeof r !== "object") continue;
|
|
110
|
-
const { $, ...styleProps } = r;
|
|
111
|
-
const declarations = {};
|
|
112
|
-
for (const [prop, val] of Object.entries(styleProps)) if (val != null && val !== "") declarations[prop] = String(val);
|
|
113
|
-
if (Object.keys(declarations).length === 0) continue;
|
|
114
|
-
const suffixes = $ ? (Array.isArray($) ? $ : [$]).map((s) => selectorSuffix + normalizeSelectorSuffix(String(s))) : [selectorSuffix];
|
|
115
|
-
for (const suffix of suffixes) computedRules.push({
|
|
116
|
-
condition: snapshot.condition,
|
|
117
|
-
declarations,
|
|
118
|
-
selectorSuffix: suffix
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const mergedRules = mergeByValue(computedRules);
|
|
112
|
+
const mergedRules = mergeByValue(invokeHandler(handler, computeStateCombinations(buildExclusivesForHandler(lookupStyles, styleMap, parserContext), lookupStyles), selectorSuffix));
|
|
123
113
|
for (const rule of mergedRules) {
|
|
124
114
|
const cssRules = materializeComputedRule(rule);
|
|
125
115
|
allRules.push(...cssRules);
|
|
@@ -127,6 +117,57 @@ function processStyles(styles, selectorSuffix, parserContext, allRules) {
|
|
|
127
117
|
}
|
|
128
118
|
}
|
|
129
119
|
/**
|
|
120
|
+
* Stages 0–3 for a single handler: take the handler's looked-up style names,
|
|
121
|
+
* resolve each style's value map into a list of mutually-exclusive entries.
|
|
122
|
+
* Simple non-mapping values produce a single TRUE-conditioned entry.
|
|
123
|
+
*/
|
|
124
|
+
function buildExclusivesForHandler(lookupStyles, styleMap, parserContext) {
|
|
125
|
+
const exclusiveByStyle = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const styleName of lookupStyles) {
|
|
127
|
+
const value = styleMap[styleName];
|
|
128
|
+
if (value === void 0) continue;
|
|
129
|
+
if (isValueMapping(value)) {
|
|
130
|
+
const fullyExpanded = expandExclusiveOrs(buildExclusiveConditions(expandOrConditions(mergeEntriesByValue(parseStyleEntries(styleName, extractCompoundStates(value), (stateKey) => parseStateKey(stateKey, { context: parserContext }))))));
|
|
131
|
+
exclusiveByStyle.set(styleName, fullyExpanded);
|
|
132
|
+
} else exclusiveByStyle.set(styleName, [{
|
|
133
|
+
styleKey: styleName,
|
|
134
|
+
stateKey: "",
|
|
135
|
+
value,
|
|
136
|
+
condition: trueCondition(),
|
|
137
|
+
priority: 0,
|
|
138
|
+
exclusiveCondition: trueCondition()
|
|
139
|
+
}]);
|
|
140
|
+
}
|
|
141
|
+
return exclusiveByStyle;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Stage 5: invoke the handler for each state snapshot and translate its
|
|
145
|
+
* return value into ComputedRule entries (one per declaration set, fanned
|
|
146
|
+
* out across any `$` selector suffixes the handler returns).
|
|
147
|
+
*/
|
|
148
|
+
function invokeHandler(handler, stateSnapshots, selectorSuffix) {
|
|
149
|
+
const computedRules = [];
|
|
150
|
+
for (const snapshot of stateSnapshots) {
|
|
151
|
+
const result = handler(snapshot.values);
|
|
152
|
+
if (!result) continue;
|
|
153
|
+
const results = Array.isArray(result) ? result : [result];
|
|
154
|
+
for (const r of results) {
|
|
155
|
+
if (!r || typeof r !== "object") continue;
|
|
156
|
+
const { $, ...styleProps } = r;
|
|
157
|
+
const declarations = {};
|
|
158
|
+
for (const [prop, val] of Object.entries(styleProps)) if (val != null && val !== "") declarations[prop] = String(val);
|
|
159
|
+
if (Object.keys(declarations).length === 0) continue;
|
|
160
|
+
const suffixes = $ ? (Array.isArray($) ? $ : [$]).map((s) => selectorSuffix + normalizeSelectorSuffix(String(s))) : [selectorSuffix];
|
|
161
|
+
for (const suffix of suffixes) computedRules.push({
|
|
162
|
+
condition: snapshot.condition,
|
|
163
|
+
declarations,
|
|
164
|
+
selectorSuffix: suffix
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return computedRules;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
130
171
|
* Check if a key is a CSS selector
|
|
131
172
|
*/
|
|
132
173
|
function isSelector(key) {
|