diff --git a/cjs/shared/matches.js b/cjs/shared/matches.js index e4deed74..86785ccd 100644 --- a/cjs/shared/matches.js +++ b/cjs/shared/matches.js @@ -108,14 +108,29 @@ const prepareMatch = (element, selectors) => CSSselect.compile( ); exports.prepareMatch = prepareMatch; -const matches = (element, selectors) => CSSselect.is( - element, - selectors, - { - strict: true, - context: selectors.includes(':scope') ? element : void 0, - xmlMode: !ignoreCase(element), - adapter +// matches() is called once per element and selector pair, so the selector +// is compiled once and kept, instead of parsed again on every call. +// :scope depends on the element, so those are compiled each time. +const compiled = new Map; +const MAX_COMPILED = 1024; + +/** @returns {boolean} */ +const matches = (element, selectors) => { + const xmlMode = !ignoreCase(element); + if (selectors.includes(':scope')) + return CSSselect.is( + element, + selectors, + {strict: true, context: element, xmlMode, adapter} + ); + const key = (xmlMode ? 'x' : 'h') + selectors; + let test = compiled.get(key); + if (!test) { + if (compiled.size === MAX_COMPILED) + compiled.delete(compiled.keys().next().value); + test = CSSselect.compile(selectors, {strict: true, xmlMode, adapter}); + compiled.set(key, test); } -); + return test(element); +}; exports.matches = matches; diff --git a/esm/shared/matches.js b/esm/shared/matches.js index dbc3339a..3012e470 100644 --- a/esm/shared/matches.js +++ b/esm/shared/matches.js @@ -106,13 +106,28 @@ export const prepareMatch = (element, selectors) => CSSselect.compile( } ); -export const matches = (element, selectors) => CSSselect.is( - element, - selectors, - { - strict: true, - context: selectors.includes(':scope') ? element : void 0, - xmlMode: !ignoreCase(element), - adapter +// matches() is called once per element and selector pair, so the selector +// is compiled once and kept, instead of parsed again on every call. +// :scope depends on the element, so those are compiled each time. +const compiled = new Map; +const MAX_COMPILED = 1024; + +/** @returns {boolean} */ +export const matches = (element, selectors) => { + const xmlMode = !ignoreCase(element); + if (selectors.includes(':scope')) + return CSSselect.is( + element, + selectors, + {strict: true, context: element, xmlMode, adapter} + ); + const key = (xmlMode ? 'x' : 'h') + selectors; + let test = compiled.get(key); + if (!test) { + if (compiled.size === MAX_COMPILED) + compiled.delete(compiled.keys().next().value); + test = CSSselect.compile(selectors, {strict: true, xmlMode, adapter}); + compiled.set(key, test); } -); + return test(element); +}; diff --git a/test/html/document.js b/test/html/document.js index b5e15062..eedd4f17 100644 --- a/test/html/document.js +++ b/test/html/document.js @@ -92,6 +92,18 @@ emptyClass.getElementById('b').className = 'x'; emptyClass.getElementById('b').classList.remove('x'); assert(emptyClass.querySelectorAll('[class=""]').map(({id}) => id).join(','), 'b,c', '[class=""] follows class mutations'); +const {document: compiled} = parseHTML('

'); +const compiledP = compiled.querySelector('p'); +assert(compiledP.matches('div .a'), true, 'matches() compiles a selector'); +compiledP.className = 'b'; +assert(compiledP.matches('div .a'), false, 'a compiled selector follows mutations'); +const compiledXML = (new DOMParser).parseFromString('

', 'text/xml'); +assert(compiledXML.querySelector('p').matches('DIV P'), false, 'compiled selectors are kept per document kind'); +assert(compiledP.matches('DIV P'), true, 'compiled selectors are kept per document kind'); +for (let i = 0; i < 1100; i++) + compiledP.matches(`.b, .c${i}`); +assert(compiledP.matches('div .b'), true, 'the oldest compiled selectors are dropped'); + assert((new DOMParser).parseFromString('...', 'text/html').firstElementChild.localName, 'html'); const issue187 = (new DOMParser).parseFromString(``, 'text/html').defaultView.window.document;