chore: remove unused selector engines (#4950)
css and id are superseeded by the new css engine.
This commit is contained in:
parent
3f904056ee
commit
f0a8729137
|
|
@ -1,66 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SelectorEngine, SelectorRoot } from './selectorEngine';
|
|
||||||
|
|
||||||
export function createAttributeEngine(attribute: string, shadow: boolean): SelectorEngine {
|
|
||||||
const engine: SelectorEngine = {
|
|
||||||
query(root: SelectorRoot, selector: string): Element | undefined {
|
|
||||||
if (!shadow)
|
|
||||||
return root.querySelector(`[${attribute}=${JSON.stringify(selector)}]`) || undefined;
|
|
||||||
return queryShadowInternal(root, attribute, selector);
|
|
||||||
},
|
|
||||||
|
|
||||||
queryAll(root: SelectorRoot, selector: string): Element[] {
|
|
||||||
if (!shadow)
|
|
||||||
return Array.from(root.querySelectorAll(`[${attribute}=${JSON.stringify(selector)}]`));
|
|
||||||
const result: Element[] = [];
|
|
||||||
queryShadowAllInternal(root, attribute, selector, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return engine;
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryShadowInternal(root: SelectorRoot, attribute: string, value: string): Element | undefined {
|
|
||||||
const single = root.querySelector(`[${attribute}=${JSON.stringify(value)}]`);
|
|
||||||
if (single)
|
|
||||||
return single;
|
|
||||||
const all = root.querySelectorAll('*');
|
|
||||||
for (let i = 0; i < all.length; i++) {
|
|
||||||
const shadowRoot = all[i].shadowRoot;
|
|
||||||
if (shadowRoot) {
|
|
||||||
const single = queryShadowInternal(shadowRoot, attribute, value);
|
|
||||||
if (single)
|
|
||||||
return single;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryShadowAllInternal(root: SelectorRoot, attribute: string, value: string, result: Element[]) {
|
|
||||||
const document = root instanceof Document ? root : root.ownerDocument;
|
|
||||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
||||||
const shadowRoots = [];
|
|
||||||
while (walker.nextNode()) {
|
|
||||||
const element = walker.currentNode as Element;
|
|
||||||
if (element.getAttribute(attribute) === value)
|
|
||||||
result.push(element);
|
|
||||||
if (element.shadowRoot)
|
|
||||||
shadowRoots.push(element.shadowRoot);
|
|
||||||
}
|
|
||||||
for (const shadowRoot of shadowRoots)
|
|
||||||
queryShadowAllInternal(shadowRoot, attribute, value, result);
|
|
||||||
}
|
|
||||||
|
|
@ -1,264 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SelectorEngine, SelectorRoot } from './selectorEngine';
|
|
||||||
|
|
||||||
export function createCSSEngine(shadow: boolean): SelectorEngine {
|
|
||||||
const engine: SelectorEngine = {
|
|
||||||
query(root: SelectorRoot, selector: string): Element | undefined {
|
|
||||||
// TODO: uncomment for performance.
|
|
||||||
// const simple = root.querySelector(selector);
|
|
||||||
// if (simple)
|
|
||||||
// return simple;
|
|
||||||
// if (!shadow)
|
|
||||||
// return;
|
|
||||||
const selectors = split(selector);
|
|
||||||
// Note: we do not just merge results produced by each selector, as that
|
|
||||||
// will not return them in the tree traversal order, but rather in the selectors
|
|
||||||
// matching order.
|
|
||||||
if (!selectors.length)
|
|
||||||
return;
|
|
||||||
return queryShadowInternal(root, root, selectors, shadow);
|
|
||||||
},
|
|
||||||
|
|
||||||
queryAll(root: SelectorRoot, selector: string): Element[] {
|
|
||||||
// TODO: uncomment for performance.
|
|
||||||
// if (!shadow)
|
|
||||||
// return Array.from(root.querySelectorAll(selector));
|
|
||||||
const result: Element[] = [];
|
|
||||||
const selectors = split(selector);
|
|
||||||
// Note: we do not just merge results produced by each selector, as that
|
|
||||||
// will not return them in the tree traversal order, but rather in the selectors
|
|
||||||
// matching order.
|
|
||||||
if (selectors.length)
|
|
||||||
queryShadowAllInternal(root, root, selectors, shadow, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(engine as any)._test = () => test(engine);
|
|
||||||
return engine;
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryShadowInternal(boundary: SelectorRoot, root: SelectorRoot, selectors: string[][], shadow: boolean): Element | undefined {
|
|
||||||
let elements: NodeListOf<Element> | undefined;
|
|
||||||
if (selectors.length === 1) {
|
|
||||||
// Fast path for a single selector - query only matching elements, not all.
|
|
||||||
const parts = selectors[0];
|
|
||||||
const matching = root.querySelectorAll(parts[0]);
|
|
||||||
for (const element of matching) {
|
|
||||||
// If there is a single part, there are no ancestors to match.
|
|
||||||
if (parts.length === 1 || ancestorsMatch(element, parts, boundary))
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Multiple selectors: visit each element in tree-traversal order and check whether it matches.
|
|
||||||
elements = root.querySelectorAll('*');
|
|
||||||
for (const element of elements) {
|
|
||||||
for (const parts of selectors) {
|
|
||||||
if (!element.matches(parts[0]))
|
|
||||||
continue;
|
|
||||||
// If there is a single part, there are no ancestors to match.
|
|
||||||
if (parts.length === 1 || ancestorsMatch(element, parts, boundary))
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visit shadow dom after the light dom to preserve the tree-traversal order.
|
|
||||||
if (!shadow)
|
|
||||||
return;
|
|
||||||
if ((root as Element).shadowRoot) {
|
|
||||||
const child = queryShadowInternal(boundary, (root as Element).shadowRoot!, selectors, shadow);
|
|
||||||
if (child)
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
if (!elements)
|
|
||||||
elements = root.querySelectorAll('*');
|
|
||||||
for (const element of elements) {
|
|
||||||
if (element.shadowRoot) {
|
|
||||||
const child = queryShadowInternal(boundary, element.shadowRoot, selectors, shadow);
|
|
||||||
if (child)
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryShadowAllInternal(boundary: SelectorRoot, root: SelectorRoot, selectors: string[][], shadow: boolean, result: Element[]) {
|
|
||||||
let elements: NodeListOf<Element> | undefined;
|
|
||||||
if (selectors.length === 1) {
|
|
||||||
// Fast path for a single selector - query only matching elements, not all.
|
|
||||||
const parts = selectors[0];
|
|
||||||
const matching = root.querySelectorAll(parts[0]);
|
|
||||||
for (const element of matching) {
|
|
||||||
// If there is a single part, there are no ancestors to match.
|
|
||||||
if (parts.length === 1 || ancestorsMatch(element, parts, boundary))
|
|
||||||
result.push(element);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Multiple selectors: visit each element in tree-traversal order and check whether it matches.
|
|
||||||
elements = root.querySelectorAll('*');
|
|
||||||
for (const element of elements) {
|
|
||||||
for (const parts of selectors) {
|
|
||||||
if (!element.matches(parts[0]))
|
|
||||||
continue;
|
|
||||||
// If there is a single part, there are no ancestors to match.
|
|
||||||
if (parts.length === 1 || ancestorsMatch(element, parts, boundary))
|
|
||||||
result.push(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visit shadow dom after the light dom to preserve the tree-traversal order.
|
|
||||||
if (!shadow)
|
|
||||||
return;
|
|
||||||
if ((root as Element).shadowRoot)
|
|
||||||
queryShadowAllInternal(boundary, (root as Element).shadowRoot!, selectors, shadow, result);
|
|
||||||
if (!elements)
|
|
||||||
elements = root.querySelectorAll('*');
|
|
||||||
for (const element of elements) {
|
|
||||||
if (element.shadowRoot)
|
|
||||||
queryShadowAllInternal(boundary, element.shadowRoot, selectors, shadow, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ancestorsMatch(element: Element | undefined, parts: string[], boundary: SelectorRoot): boolean {
|
|
||||||
let i = 1;
|
|
||||||
while (i < parts.length && (element = parentElementOrShadowHost(element!)) && element !== boundary) {
|
|
||||||
if (element.matches(parts[i]))
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return i === parts.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parentElementOrShadowHost(element: Element): Element | undefined {
|
|
||||||
if (element.parentElement)
|
|
||||||
return element.parentElement;
|
|
||||||
if (!element.parentNode)
|
|
||||||
return;
|
|
||||||
if (element.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE && (element.parentNode as ShadowRoot).host)
|
|
||||||
return (element.parentNode as ShadowRoot).host;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Splits the string into separate selectors by comma, and then each selector by the descendant combinator (space).
|
|
||||||
// Parts of each selector are reversed, so that the first one matches the target element.
|
|
||||||
function split(selector: string): string[][] {
|
|
||||||
let index = 0;
|
|
||||||
let quote: string | undefined;
|
|
||||||
let insideAttr = false;
|
|
||||||
let start = 0;
|
|
||||||
const result: string[][] = [];
|
|
||||||
let current: string[] = [];
|
|
||||||
const appendToCurrent = () => {
|
|
||||||
const part = selector.substring(start, index).trim();
|
|
||||||
if (part.length)
|
|
||||||
current.push(part);
|
|
||||||
};
|
|
||||||
const appendToResult = () => {
|
|
||||||
appendToCurrent();
|
|
||||||
result.push(current);
|
|
||||||
current = [];
|
|
||||||
};
|
|
||||||
const isCombinator = (char: string) => {
|
|
||||||
return char === '>' || char === '+' || char === '~';
|
|
||||||
};
|
|
||||||
const peekForward = () => {
|
|
||||||
return selector.substring(index).trim()[0];
|
|
||||||
};
|
|
||||||
const peekBackward = () => {
|
|
||||||
const s = selector.substring(0, index).trim();
|
|
||||||
return s[s.length - 1];
|
|
||||||
};
|
|
||||||
while (index < selector.length) {
|
|
||||||
const c = selector[index];
|
|
||||||
if (!quote && !insideAttr && c === ' ' && !isCombinator(peekForward()) && !isCombinator(peekBackward())) {
|
|
||||||
appendToCurrent();
|
|
||||||
start = index;
|
|
||||||
index++;
|
|
||||||
} else {
|
|
||||||
if (c === '\\' && index + 1 < selector.length) {
|
|
||||||
index += 2;
|
|
||||||
} else if (c === quote) {
|
|
||||||
quote = undefined;
|
|
||||||
index++;
|
|
||||||
} else if (!quote && (c === '\'' || c === '"')) {
|
|
||||||
quote = c;
|
|
||||||
index++;
|
|
||||||
} else if (!quote && c === '[') {
|
|
||||||
insideAttr = true;
|
|
||||||
index++;
|
|
||||||
} else if (!quote && insideAttr && c === ']') {
|
|
||||||
insideAttr = false;
|
|
||||||
index++;
|
|
||||||
} else if (!quote && !insideAttr && c === ',') {
|
|
||||||
appendToResult();
|
|
||||||
index++;
|
|
||||||
start = index;
|
|
||||||
} else {
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
appendToResult();
|
|
||||||
return result.filter(parts => !!parts.length).map(parts => parts.reverse());
|
|
||||||
}
|
|
||||||
|
|
||||||
function test(engine: SelectorEngine) {
|
|
||||||
let id = 0;
|
|
||||||
|
|
||||||
function createShadow(level: number): Element {
|
|
||||||
const root = document.createElement('div');
|
|
||||||
root.id = 'id' + id;
|
|
||||||
root.textContent = 'root #id' + id;
|
|
||||||
id++;
|
|
||||||
const shadow = root.attachShadow({ mode: 'open' });
|
|
||||||
for (let i = 0; i < 9; i++) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.id = 'id' + id;
|
|
||||||
div.textContent = '#id' + id;
|
|
||||||
id++;
|
|
||||||
shadow.appendChild(div);
|
|
||||||
}
|
|
||||||
if (level) {
|
|
||||||
shadow.appendChild(createShadow(level - 1));
|
|
||||||
shadow.appendChild(createShadow(level - 1));
|
|
||||||
}
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {query, queryAll} = engine;
|
|
||||||
|
|
||||||
document.body.textContent = '';
|
|
||||||
document.body.appendChild(createShadow(10));
|
|
||||||
console.time('found');
|
|
||||||
for (let i = 0; i < id; i += 17) {
|
|
||||||
const e = query(document, `div #id${i}`);
|
|
||||||
if (!e || e.id !== 'id' + i)
|
|
||||||
console.log(`div #id${i}`); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
console.timeEnd('found');
|
|
||||||
console.time('not found');
|
|
||||||
for (let i = 0; i < id; i += 17) {
|
|
||||||
const e = query(document, `div div div div div #d${i}`);
|
|
||||||
if (e)
|
|
||||||
console.log(`div div div div div #d${i}`); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
console.timeEnd('not found');
|
|
||||||
console.log(query(document, '#id543 + #id544')); // eslint-disable-line no-console
|
|
||||||
console.log(query(document, '#id542 ~ #id545')); // eslint-disable-line no-console
|
|
||||||
console.time('all');
|
|
||||||
queryAll(document, 'div div div + div');
|
|
||||||
console.timeEnd('all');
|
|
||||||
}
|
|
||||||
|
|
@ -14,14 +14,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createAttributeEngine } from './attributeSelectorEngine';
|
|
||||||
import { SelectorEngine, SelectorRoot } from './selectorEngine';
|
import { SelectorEngine, SelectorRoot } from './selectorEngine';
|
||||||
import { createTextSelector } from './textSelectorEngine';
|
import { createTextSelector } from './textSelectorEngine';
|
||||||
import { XPathEngine } from './xpathSelectorEngine';
|
import { XPathEngine } from './xpathSelectorEngine';
|
||||||
import { ParsedSelector, ParsedSelectorPart, parseSelector } from '../common/selectorParser';
|
import { ParsedSelector, ParsedSelectorPart, parseSelector } from '../common/selectorParser';
|
||||||
import { FatalDOMError } from '../common/domErrors';
|
import { FatalDOMError } from '../common/domErrors';
|
||||||
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost } from './selectorEvaluator';
|
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost } from './selectorEvaluator';
|
||||||
import { createCSSEngine } from './cssSelectorEngine';
|
import { CSSComplexSelectorList } from '../common/cssParser';
|
||||||
|
|
||||||
type Predicate<T> = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol;
|
type Predicate<T> = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol;
|
||||||
|
|
||||||
|
|
@ -46,20 +45,18 @@ export class InjectedScript {
|
||||||
|
|
||||||
constructor(customEngines: { name: string, engine: SelectorEngine}[]) {
|
constructor(customEngines: { name: string, engine: SelectorEngine}[]) {
|
||||||
this._enginesV1 = new Map();
|
this._enginesV1 = new Map();
|
||||||
this._enginesV1.set('css', createCSSEngine(true));
|
|
||||||
this._enginesV1.set('css:light', createCSSEngine(false));
|
|
||||||
this._enginesV1.set('xpath', XPathEngine);
|
this._enginesV1.set('xpath', XPathEngine);
|
||||||
this._enginesV1.set('xpath:light', XPathEngine);
|
this._enginesV1.set('xpath:light', XPathEngine);
|
||||||
this._enginesV1.set('text', createTextSelector(true));
|
this._enginesV1.set('text', createTextSelector(true));
|
||||||
this._enginesV1.set('text:light', createTextSelector(false));
|
this._enginesV1.set('text:light', createTextSelector(false));
|
||||||
this._enginesV1.set('id', createAttributeEngine('id', true));
|
this._enginesV1.set('id', this._createAttributeEngine('id', true));
|
||||||
this._enginesV1.set('id:light', createAttributeEngine('id', false));
|
this._enginesV1.set('id:light', this._createAttributeEngine('id', false));
|
||||||
this._enginesV1.set('data-testid', createAttributeEngine('data-testid', true));
|
this._enginesV1.set('data-testid', this._createAttributeEngine('data-testid', true));
|
||||||
this._enginesV1.set('data-testid:light', createAttributeEngine('data-testid', false));
|
this._enginesV1.set('data-testid:light', this._createAttributeEngine('data-testid', false));
|
||||||
this._enginesV1.set('data-test-id', createAttributeEngine('data-test-id', true));
|
this._enginesV1.set('data-test-id', this._createAttributeEngine('data-test-id', true));
|
||||||
this._enginesV1.set('data-test-id:light', createAttributeEngine('data-test-id', false));
|
this._enginesV1.set('data-test-id:light', this._createAttributeEngine('data-test-id', false));
|
||||||
this._enginesV1.set('data-test', createAttributeEngine('data-test', true));
|
this._enginesV1.set('data-test', this._createAttributeEngine('data-test', true));
|
||||||
this._enginesV1.set('data-test:light', createAttributeEngine('data-test', false));
|
this._enginesV1.set('data-test:light', this._createAttributeEngine('data-test', false));
|
||||||
for (const { name, engine } of customEngines)
|
for (const { name, engine } of customEngines)
|
||||||
this._enginesV1.set(name, engine);
|
this._enginesV1.set(name, engine);
|
||||||
|
|
||||||
|
|
@ -133,6 +130,21 @@ export class InjectedScript {
|
||||||
return this._enginesV1.get(part.name)!.queryAll(root, part.body);
|
return this._enginesV1.get(part.name)!.queryAll(root, part.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _createAttributeEngine(attribute: string, shadow: boolean): SelectorEngine {
|
||||||
|
const toCSS = (selector: string): CSSComplexSelectorList => {
|
||||||
|
const css = `[${attribute}=${JSON.stringify(selector)}]`;
|
||||||
|
return [{ simples: [{ selector: { css, functions: [] }, combinator: '' }] }];
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
query: (root: SelectorRoot, selector: string): Element | undefined => {
|
||||||
|
return this._evaluator.evaluate({ scope: root as Document | Element, pierceShadow: shadow }, toCSS(selector))[0];
|
||||||
|
},
|
||||||
|
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
||||||
|
return this._evaluator.evaluate({ scope: root as Document | Element, pierceShadow: shadow }, toCSS(selector));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
extend(source: string, params: any): any {
|
extend(source: string, params: any): any {
|
||||||
const constrFunction = global.eval(source);
|
const constrFunction = global.eval(source);
|
||||||
return new constrFunction(this, params);
|
return new constrFunction(this, params);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export class Selectors {
|
||||||
readonly _engines: Map<string, { source: string, contentScript: boolean }>;
|
readonly _engines: Map<string, { source: string, contentScript: boolean }>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Note: keep in sync with SelectorEvaluator class.
|
// Note: keep in sync with InjectedScript class.
|
||||||
this._builtinEngines = new Set([
|
this._builtinEngines = new Set([
|
||||||
'css', 'css:light',
|
'css', 'css:light',
|
||||||
'xpath', 'xpath:light',
|
'xpath', 'xpath:light',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue