CustomQueryHandler.ts (4824B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type PuppeteerUtil from '../injected/injected.js'; 8 import {assert} from '../util/assert.js'; 9 import {interpolateFunction, stringifyFunction} from '../util/Function.js'; 10 11 import { 12 QueryHandler, 13 type QuerySelector, 14 type QuerySelectorAll, 15 } from './QueryHandler.js'; 16 import {scriptInjector} from './ScriptInjector.js'; 17 18 /** 19 * @public 20 */ 21 export interface CustomQueryHandler { 22 /** 23 * Searches for a {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Node} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}. 24 */ 25 queryOne?: (node: Node, selector: string) => Node | null; 26 /** 27 * Searches for some {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Nodes} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}. 28 */ 29 queryAll?: (node: Node, selector: string) => Iterable<Node>; 30 } 31 32 /** 33 * The registry of {@link CustomQueryHandler | custom query handlers}. 34 * 35 * @example 36 * 37 * ```ts 38 * Puppeteer.customQueryHandlers.register('lit', { … }); 39 * const aHandle = await page.$('lit/…'); 40 * ``` 41 * 42 * @internal 43 */ 44 export class CustomQueryHandlerRegistry { 45 #handlers = new Map< 46 string, 47 [registerScript: string, Handler: typeof QueryHandler] 48 >(); 49 50 get(name: string): typeof QueryHandler | undefined { 51 const handler = this.#handlers.get(name); 52 return handler ? handler[1] : undefined; 53 } 54 55 /** 56 * Registers a {@link CustomQueryHandler | custom query handler}. 57 * 58 * @remarks 59 * After registration, the handler can be used everywhere where a selector is 60 * expected by prepending the selection string with `<name>/`. The name is 61 * only allowed to consist of lower- and upper case latin letters. 62 * 63 * @example 64 * 65 * ```ts 66 * Puppeteer.customQueryHandlers.register('lit', { … }); 67 * const aHandle = await page.$('lit/…'); 68 * ``` 69 * 70 * @param name - Name to register under. 71 * @param queryHandler - {@link CustomQueryHandler | Custom query handler} to 72 * register. 73 */ 74 register(name: string, handler: CustomQueryHandler): void { 75 assert( 76 !this.#handlers.has(name), 77 `Cannot register over existing handler: ${name}`, 78 ); 79 assert( 80 /^[a-zA-Z]+$/.test(name), 81 `Custom query handler names may only contain [a-zA-Z]`, 82 ); 83 assert( 84 handler.queryAll || handler.queryOne, 85 `At least one query method must be implemented.`, 86 ); 87 88 const Handler = class extends QueryHandler { 89 static override querySelectorAll: QuerySelectorAll = interpolateFunction( 90 (node, selector, PuppeteerUtil) => { 91 return PuppeteerUtil.customQuerySelectors 92 .get(PLACEHOLDER('name'))! 93 .querySelectorAll(node, selector); 94 }, 95 {name: JSON.stringify(name)}, 96 ); 97 static override querySelector: QuerySelector = interpolateFunction( 98 (node, selector, PuppeteerUtil) => { 99 return PuppeteerUtil.customQuerySelectors 100 .get(PLACEHOLDER('name'))! 101 .querySelector(node, selector); 102 }, 103 {name: JSON.stringify(name)}, 104 ); 105 }; 106 const registerScript = interpolateFunction( 107 (PuppeteerUtil: PuppeteerUtil) => { 108 PuppeteerUtil.customQuerySelectors.register(PLACEHOLDER('name'), { 109 queryAll: PLACEHOLDER('queryAll'), 110 queryOne: PLACEHOLDER('queryOne'), 111 }); 112 }, 113 { 114 name: JSON.stringify(name), 115 queryAll: handler.queryAll 116 ? stringifyFunction(handler.queryAll) 117 : String(undefined), 118 queryOne: handler.queryOne 119 ? stringifyFunction(handler.queryOne) 120 : String(undefined), 121 }, 122 ).toString(); 123 124 this.#handlers.set(name, [registerScript, Handler]); 125 scriptInjector.append(registerScript); 126 } 127 128 /** 129 * Unregisters the {@link CustomQueryHandler | custom query handler} for the 130 * given name. 131 * 132 * @throws `Error` if there is no handler under the given name. 133 */ 134 unregister(name: string): void { 135 const handler = this.#handlers.get(name); 136 if (!handler) { 137 throw new Error(`Cannot unregister unknown handler: ${name}`); 138 } 139 scriptInjector.pop(handler[0]); 140 this.#handlers.delete(name); 141 } 142 143 /** 144 * Gets the names of all {@link CustomQueryHandler | custom query handlers}. 145 */ 146 names(): string[] { 147 return [...this.#handlers.keys()]; 148 } 149 150 /** 151 * Unregisters all custom query handlers. 152 */ 153 clear(): void { 154 for (const [registerScript] of this.#handlers) { 155 scriptInjector.pop(registerScript); 156 } 157 this.#handlers.clear(); 158 } 159 } 160 161 /** 162 * @internal 163 */ 164 export const customQueryHandlers = new CustomQueryHandlerRegistry();