UNPKG

rebrowser-playwright-core

Version:

A drop-in replacement for playwright-core patched with rebrowser-patches. It allows to pass modern automation detection tests.

250 lines (245 loc) 9.43 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.InvalidSelectorError = void 0; exports.isInvalidSelectorError = isInvalidSelectorError; exports.parseCSS = parseCSS; exports.serializeSelector = serializeSelector; var css = _interopRequireWildcard(require("./cssTokenizer")); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /** * 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. */ class InvalidSelectorError extends Error {} exports.InvalidSelectorError = InvalidSelectorError; function isInvalidSelectorError(error) { return error instanceof InvalidSelectorError; } // Note: '>=' is used internally for text engine to preserve backwards compatibility. // TODO: consider // - key=value // - operators like `=`, `|=`, `~=`, `*=`, `/` // - <empty>~=value // - argument modes: "parse all", "parse commas", "just a string" function parseCSS(selector, customNames) { let tokens; try { tokens = css.tokenize(selector); if (!(tokens[tokens.length - 1] instanceof css.EOFToken)) tokens.push(new css.EOFToken()); } catch (e) { const newMessage = e.message + ` while parsing selector "${selector}"`; const index = (e.stack || '').indexOf(e.message); if (index !== -1) e.stack = e.stack.substring(0, index) + newMessage + e.stack.substring(index + e.message.length); e.message = newMessage; throw e; } const unsupportedToken = tokens.find(token => { return token instanceof css.AtKeywordToken || token instanceof css.BadStringToken || token instanceof css.BadURLToken || token instanceof css.ColumnToken || token instanceof css.CDOToken || token instanceof css.CDCToken || token instanceof css.SemicolonToken || // TODO: Consider using these for something, e.g. to escape complex strings. // For example :xpath{ (//div/bar[@attr="foo"])[2]/baz } // Or this way :xpath( {complex-xpath-goes-here("hello")} ) token instanceof css.OpenCurlyToken || token instanceof css.CloseCurlyToken || // TODO: Consider treating these as strings? token instanceof css.URLToken || token instanceof css.PercentageToken; }); if (unsupportedToken) throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing selector "${selector}"`); let pos = 0; const names = new Set(); function unexpected() { return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing selector "${selector}"`); } function skipWhitespace() { while (tokens[pos] instanceof css.WhitespaceToken) pos++; } function isIdent(p = pos) { return tokens[p] instanceof css.IdentToken; } function isString(p = pos) { return tokens[p] instanceof css.StringToken; } function isNumber(p = pos) { return tokens[p] instanceof css.NumberToken; } function isComma(p = pos) { return tokens[p] instanceof css.CommaToken; } function isOpenParen(p = pos) { return tokens[p] instanceof css.OpenParenToken; } function isCloseParen(p = pos) { return tokens[p] instanceof css.CloseParenToken; } function isFunction(p = pos) { return tokens[p] instanceof css.FunctionToken; } function isStar(p = pos) { return tokens[p] instanceof css.DelimToken && tokens[p].value === '*'; } function isEOF(p = pos) { return tokens[p] instanceof css.EOFToken; } function isClauseCombinator(p = pos) { return tokens[p] instanceof css.DelimToken && ['>', '+', '~'].includes(tokens[p].value); } function isSelectorClauseEnd(p = pos) { return isComma(p) || isCloseParen(p) || isEOF(p) || isClauseCombinator(p) || tokens[p] instanceof css.WhitespaceToken; } function consumeFunctionArguments() { const result = [consumeArgument()]; while (true) { skipWhitespace(); if (!isComma()) break; pos++; result.push(consumeArgument()); } return result; } function consumeArgument() { skipWhitespace(); if (isNumber()) return tokens[pos++].value; if (isString()) return tokens[pos++].value; return consumeComplexSelector(); } function consumeComplexSelector() { const result = { simples: [] }; skipWhitespace(); if (isClauseCombinator()) { // Put implicit ":scope" at the start. https://drafts.csswg.org/selectors-4/#relative result.simples.push({ selector: { functions: [{ name: 'scope', args: [] }] }, combinator: '' }); } else { result.simples.push({ selector: consumeSimpleSelector(), combinator: '' }); } while (true) { skipWhitespace(); if (isClauseCombinator()) { result.simples[result.simples.length - 1].combinator = tokens[pos++].value; skipWhitespace(); } else if (isSelectorClauseEnd()) { break; } result.simples.push({ combinator: '', selector: consumeSimpleSelector() }); } return result; } function consumeSimpleSelector() { let rawCSSString = ''; const functions = []; while (!isSelectorClauseEnd()) { if (isIdent() || isStar()) { rawCSSString += tokens[pos++].toSource(); } else if (tokens[pos] instanceof css.HashToken) { rawCSSString += tokens[pos++].toSource(); } else if (tokens[pos] instanceof css.DelimToken && tokens[pos].value === '.') { pos++; if (isIdent()) rawCSSString += '.' + tokens[pos++].toSource();else throw unexpected(); } else if (tokens[pos] instanceof css.ColonToken) { pos++; if (isIdent()) { if (!customNames.has(tokens[pos].value.toLowerCase())) { rawCSSString += ':' + tokens[pos++].toSource(); } else { const name = tokens[pos++].value.toLowerCase(); functions.push({ name, args: [] }); names.add(name); } } else if (isFunction()) { const name = tokens[pos++].value.toLowerCase(); if (!customNames.has(name)) { rawCSSString += `:${name}(${consumeBuiltinFunctionArguments()})`; } else { functions.push({ name, args: consumeFunctionArguments() }); names.add(name); } skipWhitespace(); if (!isCloseParen()) throw unexpected(); pos++; } else { throw unexpected(); } } else if (tokens[pos] instanceof css.OpenSquareToken) { rawCSSString += '['; pos++; while (!(tokens[pos] instanceof css.CloseSquareToken) && !isEOF()) rawCSSString += tokens[pos++].toSource(); if (!(tokens[pos] instanceof css.CloseSquareToken)) throw unexpected(); rawCSSString += ']'; pos++; } else { throw unexpected(); } } if (!rawCSSString && !functions.length) throw unexpected(); return { css: rawCSSString || undefined, functions }; } function consumeBuiltinFunctionArguments() { let s = ''; let balance = 1; // First open paren is a part of a function token. while (!isEOF()) { if (isOpenParen() || isFunction()) balance++; if (isCloseParen()) balance--; if (!balance) break; s += tokens[pos++].toSource(); } return s; } const result = consumeFunctionArguments(); if (!isEOF()) throw unexpected(); if (result.some(arg => typeof arg !== 'object' || !('simples' in arg))) throw new InvalidSelectorError(`Error while parsing selector "${selector}"`); return { selector: result, names: Array.from(names) }; } function serializeSelector(args) { return args.map(arg => { if (typeof arg === 'string') return `"${arg}"`; if (typeof arg === 'number') return String(arg); return arg.simples.map(({ selector, combinator }) => { let s = selector.css || ''; s = s + selector.functions.map(func => `:${func.name}(${serializeSelector(func.args)})`).join(''); if (combinator) s += ' ' + combinator; return s; }).join(' '); }).join(', '); }