Skip to content

Commit 3c2d7c1

Browse files
authored
Speed up addHoverClass on large stylesheets (#72)
* speed up addHoverClass on large style sheets * longer strings first to prevent accidental partial matches * can add hover class when there is a multi selector with the same prefix * tweak performance
1 parent 9dfbd52 commit 3c2d7c1

File tree

2 files changed

+39
-5
lines changed

2 files changed

+39
-5
lines changed

src/rebuild.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,50 @@ function getTagName(n: elementNode): string {
5757
return tagName;
5858
}
5959

60-
const HOVER_SELECTOR = /([^\\]):hover/g;
60+
// based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
61+
function escapeRegExp(string: string) {
62+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
63+
}
64+
65+
const HOVER_SELECTOR = /([^\\]):hover/;
66+
const HOVER_SELECTOR_GLOBAL = new RegExp(HOVER_SELECTOR, 'g');
6167
export function addHoverClass(cssText: string): string {
62-
const ast = parse(cssText, { silent: true });
68+
const ast = parse(cssText, {
69+
silent: true,
70+
});
71+
6372
if (!ast.stylesheet) {
6473
return cssText;
6574
}
75+
76+
const selectors: string[] = [];
6677
ast.stylesheet.rules.forEach((rule) => {
6778
if ('selectors' in rule) {
6879
(rule.selectors || []).forEach((selector: string) => {
6980
if (HOVER_SELECTOR.test(selector)) {
70-
const newSelector = selector.replace(HOVER_SELECTOR, '$1.\\:hover');
71-
cssText = cssText.replace(selector, `${selector}, ${newSelector}`);
81+
selectors.push(selector);
7282
}
7383
});
7484
}
7585
});
76-
return cssText;
86+
87+
if (selectors.length === 0) return cssText;
88+
89+
const selectorMatcher = new RegExp(
90+
selectors
91+
.filter((selector, index) => selectors.indexOf(selector) === index)
92+
.sort((a, b) => b.length - a.length)
93+
.map((selector) => {
94+
return escapeRegExp(selector);
95+
})
96+
.join('|'),
97+
'g',
98+
);
99+
100+
return cssText.replace(selectorMatcher, (selector) => {
101+
const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover');
102+
return `${selector}, ${newSelector}`;
103+
});
77104
}
78105

79106
function buildNode(

test/rebuild.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ describe('add hover class to hover selector related rules', () => {
2424
);
2525
});
2626

27+
it('can add hover class when there is a multi selector with the same prefix', () => {
28+
const cssText = '.a:hover, .a:hover::after { color: white }';
29+
expect(addHoverClass(cssText)).to.equal(
30+
'.a:hover, .a.\\:hover, .a:hover::after, .a.\\:hover::after { color: white }',
31+
);
32+
});
33+
2734
it('can add hover class when :hover is not the end of selector', () => {
2835
const cssText = 'div:hover::after { color: white }';
2936
expect(addHoverClass(cssText)).to.equal(

0 commit comments

Comments
 (0)