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
This commit is contained in:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 19038fe593
commit 74706eeac6
2 changed files with 39 additions and 5 deletions

View File

@@ -57,23 +57,50 @@ function getTagName(n: elementNode): string {
return tagName;
}
const HOVER_SELECTOR = /([^\\]):hover/g;
// based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
const HOVER_SELECTOR = /([^\\]):hover/;
const HOVER_SELECTOR_GLOBAL = new RegExp(HOVER_SELECTOR, 'g');
export function addHoverClass(cssText: string): string {
const ast = parse(cssText, { silent: true });
const ast = parse(cssText, {
silent: true,
});
if (!ast.stylesheet) {
return cssText;
}
const selectors: string[] = [];
ast.stylesheet.rules.forEach((rule) => {
if ('selectors' in rule) {
(rule.selectors || []).forEach((selector: string) => {
if (HOVER_SELECTOR.test(selector)) {
const newSelector = selector.replace(HOVER_SELECTOR, '$1.\\:hover');
cssText = cssText.replace(selector, `${selector}, ${newSelector}`);
selectors.push(selector);
}
});
}
});
return cssText;
if (selectors.length === 0) return cssText;
const selectorMatcher = new RegExp(
selectors
.filter((selector, index) => selectors.indexOf(selector) === index)
.sort((a, b) => b.length - a.length)
.map((selector) => {
return escapeRegExp(selector);
})
.join('|'),
'g',
);
return cssText.replace(selectorMatcher, (selector) => {
const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover');
return `${selector}, ${newSelector}`;
});
}
function buildNode(

View File

@@ -24,6 +24,13 @@ describe('add hover class to hover selector related rules', () => {
);
});
it('can add hover class when there is a multi selector with the same prefix', () => {
const cssText = '.a:hover, .a:hover::after { color: white }';
expect(addHoverClass(cssText)).to.equal(
'.a:hover, .a.\\:hover, .a:hover::after, .a.\\:hover::after { color: white }',
);
});
it('can add hover class when :hover is not the end of selector', () => {
const cssText = 'div:hover::after { color: white }';
expect(addHoverClass(cssText)).to.equal(