mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 23:26:39 +03:00
StaticElement.closest() handed the raw selector string to css-select's is() on every ancestor step, recompiling the same selector N times for an element N levels deep. StaticDocument now caches one compiled matcher per selector (failed compiles cached as rethrowers so bad selectors still return null). Findings are byte-identical across the fixture corpus; scan time drops to ~62% on the fixtures and ~7x faster on deep-DOM pages. Prepared with AI assistance (Cursor agent), directed by @abdulwahabone. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -835,10 +835,11 @@ class StaticElement {
|
||||
}
|
||||
}
|
||||
closest(selector) {
|
||||
const matcher = this._doc.matcherFor(selector);
|
||||
let cur = this.node;
|
||||
while (cur && cur.type === 'tag') {
|
||||
try {
|
||||
if (this._doc.is(cur, selector)) return this._doc.wrap(cur);
|
||||
if (matcher(cur)) return this._doc.wrap(cur);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -862,9 +863,10 @@ class StaticDocument {
|
||||
this.root = root;
|
||||
this.selectAll = modules.selectAll;
|
||||
this.selectOne = modules.selectOne;
|
||||
this.is = modules.is;
|
||||
this.compile = modules.compile;
|
||||
this.domutils = modules.domutils;
|
||||
this._wrappers = new WeakMap();
|
||||
this._compiledSelectors = new Map();
|
||||
this._styleMap = new WeakMap();
|
||||
this._hoverStyleMap = new WeakMap();
|
||||
this._accentDashPseudo = new WeakSet();
|
||||
@@ -882,6 +884,20 @@ class StaticDocument {
|
||||
}
|
||||
return wrapped;
|
||||
}
|
||||
matcherFor(selector) {
|
||||
let matcher = this._compiledSelectors.get(selector);
|
||||
if (!matcher) {
|
||||
try {
|
||||
matcher = this.compile(selector);
|
||||
} catch (err) {
|
||||
// Cache the failure as a rethrower so a bad selector still reaches
|
||||
// closest()'s catch on every call, first and repeat alike.
|
||||
matcher = () => { throw err; };
|
||||
}
|
||||
this._compiledSelectors.set(selector, matcher);
|
||||
}
|
||||
return matcher;
|
||||
}
|
||||
querySelectorAll(selector) {
|
||||
try {
|
||||
return this.selectAll(selector, this.root.children || []).map(node => this.wrap(node));
|
||||
|
||||
@@ -134,7 +134,7 @@ async function detectHtml(filePath, options = {}) {
|
||||
parseDocument: htmlparser2.parseDocument,
|
||||
selectAll: cssSelect.selectAll,
|
||||
selectOne: cssSelect.selectOne,
|
||||
is: cssSelect.is,
|
||||
compile: cssSelect.compile,
|
||||
csstree,
|
||||
domutils,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
buildImportGraph, resolveImport,
|
||||
detectFrameworkConfig, isPortListening, FRAMEWORK_CONFIGS,
|
||||
} from '../cli/engine/detect-antipatterns.mjs';
|
||||
import * as htmlparser2 from 'htmlparser2';
|
||||
import * as cssSelect from 'css-select';
|
||||
import * as domutils from 'domutils';
|
||||
import { StaticDocument } from '../cli/engine/engines/static-html/css-cascade.mjs';
|
||||
import { filterByScopes } from '../cli/engine/registry/antipatterns.mjs';
|
||||
import {
|
||||
checkColors,
|
||||
@@ -1236,6 +1240,46 @@ describe('detectHtml — static HTML/CSS engine', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('StaticDocument.closest — compiled selector cache', () => {
|
||||
test('compiles each selector once per document', () => {
|
||||
let compileCount = 0;
|
||||
const compile = (sel) => {
|
||||
compileCount++;
|
||||
return cssSelect.compile(sel);
|
||||
};
|
||||
const root = htmlparser2.parseDocument(
|
||||
'<html><body><div class="target-ancestor">' +
|
||||
'<div><div><div><div><div><div><div><div><div><span>deep</span></div></div></div></div></div></div></div></div></div>' +
|
||||
'</div><div><div><div><div><div><div><div><div><div><span>deep2</span></div></div></div></div></div></div></div></div></div></div></body></html>',
|
||||
);
|
||||
const doc = new StaticDocument(root, {
|
||||
selectAll: cssSelect.selectAll,
|
||||
selectOne: cssSelect.selectOne,
|
||||
compile,
|
||||
domutils,
|
||||
});
|
||||
const deep = doc.querySelectorAll('span')[0];
|
||||
const deep2 = doc.querySelectorAll('span')[1];
|
||||
expect(deep.closest('.target-ancestor').node.attribs.class).toBe('target-ancestor');
|
||||
deep.closest('.target-ancestor');
|
||||
deep2.closest('.target-ancestor');
|
||||
expect(compileCount).toBe(1);
|
||||
});
|
||||
|
||||
test('invalid selector returns null on repeat calls', () => {
|
||||
const root = htmlparser2.parseDocument('<html><body><p>x</p></body></html>');
|
||||
const doc = new StaticDocument(root, {
|
||||
selectAll: cssSelect.selectAll,
|
||||
selectOne: cssSelect.selectOne,
|
||||
compile: cssSelect.compile,
|
||||
domutils,
|
||||
});
|
||||
const p = doc.querySelector('p');
|
||||
expect(p.closest('p:has-invalid(')).toBeNull();
|
||||
expect(p.closest('p:has-invalid(')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Side-tab as absolutely-positioned pseudo-element stripe
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user