Skip to content

Commit a515b6d

Browse files
committed
fix(tasty): clean up and rules optimizations
1 parent 16dc2f5 commit a515b6d

File tree

4 files changed

+111
-707
lines changed

4 files changed

+111
-707
lines changed
Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,43 @@
1-
function filterCombinations(combinations) {
2-
return combinations.filter((combination) => {
3-
const list: string[] = [];
4-
5-
return !combination.find((mod) => {
6-
const match = mod.match(/\[(.+?)[=\]]/);
7-
8-
if (match) {
9-
if (list.includes(match[1])) {
10-
return true;
11-
}
12-
13-
list.push(match[1]);
14-
15-
return false;
16-
}
17-
18-
return false;
19-
});
20-
});
21-
}
22-
23-
export function getModCombinations(array: string[], allowEmpty?: boolean) {
24-
const result: string[][] = allowEmpty ? [[]] : [];
1+
// Enhanced conflict detection function that can accept a custom conflict checker
2+
export type ConflictChecker = (combination: string[]) => boolean;
3+
4+
/**
5+
* Efficient iterative combination generator with early pruning
6+
* Generates combinations one by one and prunes branches early when conflicts are detected
7+
* @param array - Array of mod strings to generate combinations from
8+
* @param allowEmpty - Whether to include empty combination
9+
* @param conflictChecker - Function to detect conflicting combinations (required)
10+
*/
11+
export function getModCombinationsIterative(
12+
array: string[],
13+
allowEmpty?: boolean,
14+
conflictChecker?: ConflictChecker,
15+
): string[][] {
16+
if (!conflictChecker) {
17+
throw new Error(
18+
'getModCombinationsIterative requires a conflictChecker function',
19+
);
20+
}
2521

2622
if (array.length === 0) {
27-
return [array];
23+
return allowEmpty ? [[]] : [];
2824
}
2925

30-
if (array.length < 2) {
31-
return result.concat([array]);
32-
}
26+
// Use iterative approach with early pruning for better performance
27+
const combinations: string[][] = [[]];
3328

34-
const f = function (prefix: string[] = [], array: string[]) {
35-
for (let i = 0; i < array.length; i++) {
36-
result.push([...prefix, array[i]]);
37-
f([...prefix, array[i]], array.slice(i + 1));
38-
}
39-
};
29+
for (let i = 0; i < array.length; i++) {
30+
const currentLength = combinations.length;
31+
for (let j = 0; j < currentLength; j++) {
32+
const newCombination = [...combinations[j], array[i]];
4033

41-
f([], array);
34+
// Early conflict detection - only add if no conflicts
35+
if (!conflictChecker(newCombination)) {
36+
combinations.push(newCombination);
37+
}
38+
}
39+
}
4240

43-
return filterCombinations(result);
41+
// Filter out empty combination if not allowed, but keep others
42+
return combinations.filter((combo) => allowEmpty || combo.length > 0);
4443
}

src/tasty/utils/renderStyles.ts

Lines changed: 74 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Lru } from '../parser/lru';
77
import { createStyle, STYLE_HANDLER_MAP } from '../styles';
88
import { Styles } from '../styles/types';
99

10+
import { getModCombinationsIterative } from './getModCombinations';
1011
import {
1112
mediaWrapper,
1213
normalizeStyleZones,
@@ -200,6 +201,17 @@ function hasConflictingAttributeSelectors(
200201
return false;
201202
}
202203

204+
/**
205+
* Create a conflict checker function that uses precomputed attribute maps
206+
* for efficient conflict detection during combination generation
207+
*/
208+
function createAttributeConflictChecker(
209+
parsedMods: Map<string, ParsedAttributeSelector | null>,
210+
): (combination: string[]) => boolean {
211+
return (combination: string[]) =>
212+
hasConflictingAttributeSelectors(combination, parsedMods);
213+
}
214+
203215
// Interface for precomputed attribute maps to optimize not selector generation
204216
interface AttributeMaps {
205217
allAttributes: Map<string, Set<string>>;
@@ -372,10 +384,34 @@ function explodeHandlerResult(
372384
)
373385
: [selectorSuffix];
374386

375-
// Create logical rules for each breakpoint × selector combination
376-
for (const [breakpointIdx, declarations] of breakpointGroups) {
387+
// Early identical-breakpoint coalescing: skip duplicate declarations
388+
const seenDeclarations = new Map<string, number>();
389+
390+
// Process breakpoints in order to prefer lower breakpoint indices
391+
const sortedBreakpoints = Array.from(breakpointGroups.entries()).sort(
392+
([a], [b]) => a - b,
393+
);
394+
395+
for (const [breakpointIdx, declarations] of sortedBreakpoints) {
377396
if (Object.keys(declarations).length === 0) continue;
378397

398+
// Create a stable hash key for identical declarations
399+
const declarationKeys = Object.keys(declarations).sort();
400+
const declarationHash = declarationKeys
401+
.map((key) => `${key}:${declarations[key]}`)
402+
.join(';');
403+
404+
const existingBreakpointIdx = seenDeclarations.get(declarationHash);
405+
if (existingBreakpointIdx !== undefined) {
406+
// Skip this breakpoint as it has identical declarations to a previous one
407+
// The CSS cascade will handle the responsive behavior correctly
408+
continue;
409+
}
410+
411+
// Mark this declaration set as seen
412+
seenDeclarations.set(declarationHash, breakpointIdx);
413+
414+
// Create logical rules for this unique declaration set
379415
for (const suffix of suffixes) {
380416
logicalRules.push({
381417
selectorSuffix: suffix,
@@ -626,21 +662,15 @@ export function renderStyles(
626662
// Precompute attribute maps once for all combinations
627663
const attributeMaps = buildAttributeMaps([], allModsArray);
628664

629-
const combinations: string[][] = [[]];
630-
for (let i = 0; i < allModsArray.length; i++) {
631-
const currentLength = combinations.length;
632-
for (let j = 0; j < currentLength; j++) {
633-
const newCombination = [...combinations[j], allModsArray[i]];
634-
if (
635-
!hasConflictingAttributeSelectors(
636-
newCombination,
637-
attributeMaps.parsedMods,
638-
)
639-
) {
640-
combinations.push(newCombination);
641-
}
642-
}
643-
}
665+
// Generate combinations with conflict-aware pruning
666+
const conflictChecker = createAttributeConflictChecker(
667+
attributeMaps.parsedMods,
668+
);
669+
const combinations = getModCombinationsIterative(
670+
allModsArray,
671+
true,
672+
conflictChecker,
673+
);
644674

645675
combinations.forEach((modCombination) => {
646676
const stateProps: Record<string, any> = {};
@@ -734,24 +764,15 @@ export function renderStyles(
734764
// Precompute attribute maps once for all combinations
735765
const attributeMaps = buildAttributeMaps([], allModsArray);
736766

737-
const combinations: string[][] = [[]]; // Start with empty combination
738-
739-
// Generate all combinations (including empty)
740-
for (let i = 0; i < allModsArray.length; i++) {
741-
const currentLength = combinations.length;
742-
for (let j = 0; j < currentLength; j++) {
743-
const newCombination = [...combinations[j], allModsArray[i]];
744-
// Skip combinations with conflicting attribute selectors
745-
if (
746-
!hasConflictingAttributeSelectors(
747-
newCombination,
748-
attributeMaps.parsedMods,
749-
)
750-
) {
751-
combinations.push(newCombination);
752-
}
753-
}
754-
}
767+
// Generate combinations with conflict-aware pruning
768+
const conflictChecker = createAttributeConflictChecker(
769+
attributeMaps.parsedMods,
770+
);
771+
const combinations = getModCombinationsIterative(
772+
allModsArray,
773+
true,
774+
conflictChecker,
775+
);
755776

756777
combinations.forEach((modCombination) => {
757778
const stateProps: Record<string, any> = {};
@@ -1016,21 +1037,15 @@ export function renderStylesForGlobal(
10161037
// Precompute attribute maps once for all combinations
10171038
const attributeMaps = buildAttributeMaps([], allModsArray);
10181039

1019-
const combinations: string[][] = [[]];
1020-
for (let a = 0; a < allModsArray.length; a++) {
1021-
const currentLength = combinations.length;
1022-
for (let b = 0; b < currentLength; b++) {
1023-
const newCombination = [...combinations[b], allModsArray[a]];
1024-
if (
1025-
!hasConflictingAttributeSelectors(
1026-
newCombination,
1027-
attributeMaps.parsedMods,
1028-
)
1029-
) {
1030-
combinations.push(newCombination);
1031-
}
1032-
}
1033-
}
1040+
// Generate combinations with conflict-aware pruning
1041+
const conflictChecker = createAttributeConflictChecker(
1042+
attributeMaps.parsedMods,
1043+
);
1044+
const combinations = getModCombinationsIterative(
1045+
allModsArray,
1046+
true,
1047+
conflictChecker,
1048+
);
10341049

10351050
combinations.forEach((modCombination) => {
10361051
const stateProps: Record<string, any> = {};
@@ -1115,24 +1130,15 @@ export function renderStylesForGlobal(
11151130
// Precompute attribute maps once for all combinations
11161131
const attributeMaps = buildAttributeMaps([], allModsArray);
11171132

1118-
const combinations: string[][] = [[]]; // Start with empty combination
1119-
1120-
// Generate all combinations (including empty)
1121-
for (let i = 0; i < allModsArray.length; i++) {
1122-
const currentLength = combinations.length;
1123-
for (let j = 0; j < currentLength; j++) {
1124-
const newCombination = [...combinations[j], allModsArray[i]];
1125-
// Skip combinations with conflicting attribute selectors
1126-
if (
1127-
!hasConflictingAttributeSelectors(
1128-
newCombination,
1129-
attributeMaps.parsedMods,
1130-
)
1131-
) {
1132-
combinations.push(newCombination);
1133-
}
1134-
}
1135-
}
1133+
// Generate combinations with conflict-aware pruning
1134+
const conflictChecker = createAttributeConflictChecker(
1135+
attributeMaps.parsedMods,
1136+
);
1137+
const combinations = getModCombinationsIterative(
1138+
allModsArray,
1139+
true,
1140+
conflictChecker,
1141+
);
11361142

11371143
combinations.forEach((modCombination) => {
11381144
const stateProps: Record<string, any> = {};

0 commit comments

Comments
 (0)