puppeteer/packages/puppeteer-core/src/injected/PSelectorParser.ts
2023-02-15 10:42:32 -08:00

159 lines
4.1 KiB
TypeScript

/**
* Copyright 2023 Google Inc. All rights reserved.
*
* 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.
*/
type CSSSelector = string;
export type PSelector =
| {
name: string;
value: string;
}
| CSSSelector;
const PUPPETEER_PSEUDO_ELEMENT = /^::-p-([-a-zA-Z_]+)\(/;
class PSelectorParser {
#input: string;
#escaped = false;
#quoted = false;
// The first level are deep roots. The second level are shallow roots.
#selectors: PSelector[][][] = [[[]]];
constructor(input: string) {
this.#input = input;
}
get selectors(): PSelector[][][] {
return this.#selectors;
}
parse(): void {
for (let i = 0; i < this.#input.length; ++i) {
if (this.#escaped) {
this.#escaped = false;
continue;
}
switch (this.#input[i]) {
case '\\': {
this.#escaped = true;
break;
}
case '"': {
this.#quoted = !this.#quoted;
break;
}
default: {
if (this.#quoted) {
break;
}
const remainder = this.#input.slice(i);
if (remainder.startsWith('>>>>')) {
this.#push(this.#input.slice(0, i));
this.#input = remainder.slice('>>>>'.length);
this.#parseDeepChild();
} else if (remainder.startsWith('>>>')) {
this.#push(this.#input.slice(0, i));
this.#input = remainder.slice('>>>'.length);
this.#parseDeepDescendent();
} else {
const result = PUPPETEER_PSEUDO_ELEMENT.exec(remainder);
if (!result) {
continue;
}
const [match, name] = result;
this.#push(this.#input.slice(0, i));
this.#input = remainder.slice(match.length);
this.#push({
name: name as string,
value: this.#scanParameter(),
});
}
}
}
}
this.#push(this.#input);
}
#push(selector: PSelector) {
if (typeof selector === 'string') {
// We only trim the end only since `.foo` and ` .foo` are different.
selector = selector.trimEnd();
if (selector.length === 0) {
return;
}
}
const roots = this.#selectors[this.#selectors.length - 1]!;
roots[roots.length - 1]!.push(selector);
}
#parseDeepChild() {
this.#selectors[this.#selectors.length - 1]!.push([]);
}
#parseDeepDescendent() {
this.#selectors.push([[]]);
}
#scanParameter(): string {
const char = this.#input[0];
switch (char) {
case "'":
case '"':
this.#input = this.#input.slice(1);
const parameter = this.#scanEscapedValueTill(char);
if (!this.#input.startsWith(')')) {
throw new Error("Expected ')'");
}
this.#input = this.#input.slice(1);
return parameter;
default:
return this.#scanEscapedValueTill(')');
}
}
#scanEscapedValueTill(end: string): string {
let string = '';
for (let i = 0; i < this.#input.length; ++i) {
if (this.#escaped) {
this.#escaped = false;
string += this.#input[i];
continue;
}
switch (this.#input[i]) {
case '\\': {
this.#escaped = true;
break;
}
case end: {
this.#input = this.#input.slice(i + 1);
return string;
}
default: {
string += this.#input[i];
}
}
}
throw new Error(`Expected \`${end}\``);
}
}
export function parsePSelectors(selector: string): PSelector[][][] {
const parser = new PSelectorParser(selector);
parser.parse();
return parser.selectors;
}