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; 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 { export function addHoverClass(cssText: string): string {
const ast = parse(cssText, { silent: true }); const ast = parse(cssText, {
silent: true,
});
if (!ast.stylesheet) { if (!ast.stylesheet) {
return cssText; return cssText;
} }
const selectors: string[] = [];
ast.stylesheet.rules.forEach((rule) => { ast.stylesheet.rules.forEach((rule) => {
if ('selectors' in rule) { if ('selectors' in rule) {
(rule.selectors || []).forEach((selector: string) => { (rule.selectors || []).forEach((selector: string) => {
if (HOVER_SELECTOR.test(selector)) { if (HOVER_SELECTOR.test(selector)) {
const newSelector = selector.replace(HOVER_SELECTOR, '$1.\\:hover'); selectors.push(selector);
cssText = cssText.replace(selector, `${selector}, ${newSelector}`);
} }
}); });
} }
}); });
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( 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', () => { it('can add hover class when :hover is not the end of selector', () => {
const cssText = 'div:hover::after { color: white }'; const cssText = 'div:hover::after { color: white }';
expect(addHoverClass(cssText)).to.equal( expect(addHoverClass(cssText)).to.equal(