1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | import type {OperatorFunction} from '../../third_party/rxjs/rxjs.js';
|
8 | import {
|
9 | filter,
|
10 | from,
|
11 | fromEvent,
|
12 | map,
|
13 | mergeMap,
|
14 | NEVER,
|
15 | Observable,
|
16 | timer,
|
17 | } from '../../third_party/rxjs/rxjs.js';
|
18 | import type {CDPSession} from '../api/CDPSession.js';
|
19 | import {environment} from '../environment.js';
|
20 | import {packageVersion} from '../generated/version.js';
|
21 | import {assert} from '../util/assert.js';
|
22 | import {mergeUint8Arrays} from '../util/encoding.js';
|
23 |
|
24 | import {debug} from './Debug.js';
|
25 | import {TimeoutError} from './Errors.js';
|
26 | import type {EventEmitter, EventType} from './EventEmitter.js';
|
27 | import type {
|
28 | LowerCasePaperFormat,
|
29 | ParsedPDFOptions,
|
30 | PDFOptions,
|
31 | } from './PDFOptions.js';
|
32 | import {paperFormats} from './PDFOptions.js';
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | export const debugError = debug('puppeteer:error');
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | export const DEFAULT_VIEWPORT = Object.freeze({width: 800, height: 600});
|
43 |
|
44 |
|
45 |
|
46 |
|
47 | const SOURCE_URL = Symbol('Source URL for Puppeteer evaluation scripts');
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | export class PuppeteerURL {
|
53 | static INTERNAL_URL = 'pptr:internal';
|
54 |
|
55 | static fromCallSite(
|
56 | functionName: string,
|
57 | site: NodeJS.CallSite,
|
58 | ): PuppeteerURL {
|
59 | const url = new PuppeteerURL();
|
60 | url.#functionName = functionName;
|
61 | url.#siteString = site.toString();
|
62 | return url;
|
63 | }
|
64 |
|
65 | static parse = (url: string): PuppeteerURL => {
|
66 | url = url.slice('pptr:'.length);
|
67 | const [functionName = '', siteString = ''] = url.split(';');
|
68 | const puppeteerUrl = new PuppeteerURL();
|
69 | puppeteerUrl.#functionName = functionName;
|
70 | puppeteerUrl.#siteString = decodeURIComponent(siteString);
|
71 | return puppeteerUrl;
|
72 | };
|
73 |
|
74 | static isPuppeteerURL = (url: string): boolean => {
|
75 | return url.startsWith('pptr:');
|
76 | };
|
77 |
|
78 | #functionName!: string;
|
79 | #siteString!: string;
|
80 |
|
81 | get functionName(): string {
|
82 | return this.#functionName;
|
83 | }
|
84 |
|
85 | get siteString(): string {
|
86 | return this.#siteString;
|
87 | }
|
88 |
|
89 | toString(): string {
|
90 | return `pptr:${[
|
91 | this.#functionName,
|
92 | encodeURIComponent(this.#siteString),
|
93 | ].join(';')}`;
|
94 | }
|
95 | }
|
96 |
|
97 |
|
98 |
|
99 |
|
100 | export const withSourcePuppeteerURLIfNone = <T extends NonNullable<unknown>>(
|
101 | functionName: string,
|
102 | object: T,
|
103 | ): T => {
|
104 | if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
|
105 | return object;
|
106 | }
|
107 | const original = Error.prepareStackTrace;
|
108 | Error.prepareStackTrace = (_, stack) => {
|
109 |
|
110 |
|
111 |
|
112 |
|
113 | return stack[2];
|
114 | };
|
115 | const site = new Error().stack as unknown as NodeJS.CallSite;
|
116 | Error.prepareStackTrace = original;
|
117 | return Object.assign(object, {
|
118 | [SOURCE_URL]: PuppeteerURL.fromCallSite(functionName, site),
|
119 | });
|
120 | };
|
121 |
|
122 |
|
123 |
|
124 |
|
125 | export const getSourcePuppeteerURLIfAvailable = <
|
126 | T extends NonNullable<unknown>,
|
127 | >(
|
128 | object: T,
|
129 | ): PuppeteerURL | undefined => {
|
130 | if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
|
131 | return object[SOURCE_URL as keyof T] as PuppeteerURL;
|
132 | }
|
133 | return undefined;
|
134 | };
|
135 |
|
136 |
|
137 |
|
138 |
|
139 | export const isString = (obj: unknown): obj is string => {
|
140 | return typeof obj === 'string' || obj instanceof String;
|
141 | };
|
142 |
|
143 |
|
144 |
|
145 |
|
146 | export const isNumber = (obj: unknown): obj is number => {
|
147 | return typeof obj === 'number' || obj instanceof Number;
|
148 | };
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | export const isPlainObject = (obj: unknown): obj is Record<any, unknown> => {
|
154 | return typeof obj === 'object' && obj?.constructor === Object;
|
155 | };
|
156 |
|
157 |
|
158 |
|
159 |
|
160 | export const isRegExp = (obj: unknown): obj is RegExp => {
|
161 | return typeof obj === 'object' && obj?.constructor === RegExp;
|
162 | };
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | export const isDate = (obj: unknown): obj is Date => {
|
168 | return typeof obj === 'object' && obj?.constructor === Date;
|
169 | };
|
170 |
|
171 |
|
172 |
|
173 |
|
174 | export function evaluationString(
|
175 |
|
176 | fun: Function | string,
|
177 | ...args: unknown[]
|
178 | ): string {
|
179 | if (isString(fun)) {
|
180 | assert(args.length === 0, 'Cannot evaluate a string with arguments');
|
181 | return fun;
|
182 | }
|
183 |
|
184 | function serializeArgument(arg: unknown): string {
|
185 | if (Object.is(arg, undefined)) {
|
186 | return 'undefined';
|
187 | }
|
188 | return JSON.stringify(arg);
|
189 | }
|
190 |
|
191 | return `(${fun})(${args.map(serializeArgument).join(',')})`;
|
192 | }
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | export async function getReadableAsTypedArray(
|
198 | readable: ReadableStream<Uint8Array>,
|
199 | path?: string,
|
200 | ): Promise<Uint8Array | null> {
|
201 | const buffers: Uint8Array[] = [];
|
202 | const reader = readable.getReader();
|
203 | if (path) {
|
204 | const fileHandle = await environment.value.fs.promises.open(path, 'w+');
|
205 | try {
|
206 | while (true) {
|
207 | const {done, value} = await reader.read();
|
208 | if (done) {
|
209 | break;
|
210 | }
|
211 | buffers.push(value);
|
212 | await fileHandle.writeFile(value);
|
213 | }
|
214 | } finally {
|
215 | await fileHandle.close();
|
216 | }
|
217 | } else {
|
218 | while (true) {
|
219 | const {done, value} = await reader.read();
|
220 | if (done) {
|
221 | break;
|
222 | }
|
223 | buffers.push(value);
|
224 | }
|
225 | }
|
226 | try {
|
227 | const concat = mergeUint8Arrays(buffers);
|
228 | if (concat.length === 0) {
|
229 | return null;
|
230 | }
|
231 | return concat;
|
232 | } catch (error) {
|
233 | debugError(error);
|
234 | return null;
|
235 | }
|
236 | }
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 | export async function getReadableFromProtocolStream(
|
246 | client: CDPSession,
|
247 | handle: string,
|
248 | ): Promise<ReadableStream<Uint8Array>> {
|
249 | return new ReadableStream({
|
250 | async pull(controller) {
|
251 | function getUnit8Array(data: string, isBase64: boolean): Uint8Array {
|
252 | if (isBase64) {
|
253 | return Uint8Array.from(atob(data), m => {
|
254 | return m.codePointAt(0)!;
|
255 | });
|
256 | }
|
257 | const encoder = new TextEncoder();
|
258 | return encoder.encode(data);
|
259 | }
|
260 |
|
261 | const {data, base64Encoded, eof} = await client.send('IO.read', {
|
262 | handle,
|
263 | });
|
264 |
|
265 | controller.enqueue(getUnit8Array(data, base64Encoded ?? false));
|
266 | if (eof) {
|
267 | await client.send('IO.close', {handle});
|
268 | controller.close();
|
269 | }
|
270 | },
|
271 | });
|
272 | }
|
273 |
|
274 |
|
275 |
|
276 |
|
277 | export function validateDialogType(
|
278 | type: string,
|
279 | ): 'alert' | 'confirm' | 'prompt' | 'beforeunload' {
|
280 | let dialogType = null;
|
281 | const validDialogTypes = new Set([
|
282 | 'alert',
|
283 | 'confirm',
|
284 | 'prompt',
|
285 | 'beforeunload',
|
286 | ]);
|
287 |
|
288 | if (validDialogTypes.has(type)) {
|
289 | dialogType = type;
|
290 | }
|
291 | assert(dialogType, `Unknown javascript dialog type: ${type}`);
|
292 | return dialogType as 'alert' | 'confirm' | 'prompt' | 'beforeunload';
|
293 | }
|
294 |
|
295 |
|
296 |
|
297 |
|
298 | export function timeout(ms: number, cause?: Error): Observable<never> {
|
299 | return ms === 0
|
300 | ? NEVER
|
301 | : timer(ms).pipe(
|
302 | map(() => {
|
303 | throw new TimeoutError(`Timed out after waiting ${ms}ms`, {cause});
|
304 | }),
|
305 | );
|
306 | }
|
307 |
|
308 |
|
309 |
|
310 |
|
311 | export const UTILITY_WORLD_NAME =
|
312 | '__puppeteer_utility_world__' + packageVersion;
|
313 |
|
314 |
|
315 |
|
316 |
|
317 | export const SOURCE_URL_REGEX =
|
318 | /^[\x20\t]*\/\/[@#] sourceURL=\s{0,10}(\S*?)\s{0,10}$/m;
|
319 |
|
320 |
|
321 |
|
322 | export function getSourceUrlComment(url: string): string {
|
323 | return `//# sourceURL=${url}`;
|
324 | }
|
325 |
|
326 |
|
327 |
|
328 |
|
329 | export const NETWORK_IDLE_TIME = 500;
|
330 |
|
331 |
|
332 |
|
333 |
|
334 | export function parsePDFOptions(
|
335 | options: PDFOptions = {},
|
336 | lengthUnit: 'in' | 'cm' = 'in',
|
337 | ): ParsedPDFOptions {
|
338 | const defaults: Omit<ParsedPDFOptions, 'width' | 'height' | 'margin'> = {
|
339 | scale: 1,
|
340 | displayHeaderFooter: false,
|
341 | headerTemplate: '',
|
342 | footerTemplate: '',
|
343 | printBackground: false,
|
344 | landscape: false,
|
345 | pageRanges: '',
|
346 | preferCSSPageSize: false,
|
347 | omitBackground: false,
|
348 | outline: false,
|
349 | tagged: true,
|
350 | waitForFonts: true,
|
351 | };
|
352 |
|
353 | let width = 8.5;
|
354 | let height = 11;
|
355 | if (options.format) {
|
356 | const format =
|
357 | paperFormats[options.format.toLowerCase() as LowerCasePaperFormat];
|
358 | assert(format, 'Unknown paper format: ' + options.format);
|
359 | width = format.width;
|
360 | height = format.height;
|
361 | } else {
|
362 | width = convertPrintParameterToInches(options.width, lengthUnit) ?? width;
|
363 | height =
|
364 | convertPrintParameterToInches(options.height, lengthUnit) ?? height;
|
365 | }
|
366 |
|
367 | const margin = {
|
368 | top: convertPrintParameterToInches(options.margin?.top, lengthUnit) || 0,
|
369 | left: convertPrintParameterToInches(options.margin?.left, lengthUnit) || 0,
|
370 | bottom:
|
371 | convertPrintParameterToInches(options.margin?.bottom, lengthUnit) || 0,
|
372 | right:
|
373 | convertPrintParameterToInches(options.margin?.right, lengthUnit) || 0,
|
374 | };
|
375 |
|
376 |
|
377 | if (options.outline) {
|
378 | options.tagged = true;
|
379 | }
|
380 |
|
381 | return {
|
382 | ...defaults,
|
383 | ...options,
|
384 | width,
|
385 | height,
|
386 | margin,
|
387 | };
|
388 | }
|
389 |
|
390 |
|
391 |
|
392 |
|
393 | export const unitToPixels = {
|
394 | px: 1,
|
395 | in: 96,
|
396 | cm: 37.8,
|
397 | mm: 3.78,
|
398 | };
|
399 |
|
400 | function convertPrintParameterToInches(
|
401 | parameter?: string | number,
|
402 | lengthUnit: 'in' | 'cm' = 'in',
|
403 | ): number | undefined {
|
404 | if (typeof parameter === 'undefined') {
|
405 | return undefined;
|
406 | }
|
407 | let pixels;
|
408 | if (isNumber(parameter)) {
|
409 |
|
410 | pixels = parameter;
|
411 | } else if (isString(parameter)) {
|
412 | const text = parameter;
|
413 | let unit = text.substring(text.length - 2).toLowerCase();
|
414 | let valueText = '';
|
415 | if (unit in unitToPixels) {
|
416 | valueText = text.substring(0, text.length - 2);
|
417 | } else {
|
418 |
|
419 |
|
420 | unit = 'px';
|
421 | valueText = text;
|
422 | }
|
423 | const value = Number(valueText);
|
424 | assert(!isNaN(value), 'Failed to parse parameter value: ' + text);
|
425 | pixels = value * unitToPixels[unit as keyof typeof unitToPixels];
|
426 | } else {
|
427 | throw new Error(
|
428 | 'page.pdf() Cannot handle parameter type: ' + typeof parameter,
|
429 | );
|
430 | }
|
431 | return pixels / unitToPixels[lengthUnit];
|
432 | }
|
433 |
|
434 |
|
435 |
|
436 |
|
437 | export function fromEmitterEvent<
|
438 | Events extends Record<EventType, unknown>,
|
439 | Event extends keyof Events,
|
440 | >(emitter: EventEmitter<Events>, eventName: Event): Observable<Events[Event]> {
|
441 | return new Observable(subscriber => {
|
442 | const listener = (event: Events[Event]) => {
|
443 | subscriber.next(event);
|
444 | };
|
445 | emitter.on(eventName, listener);
|
446 | return () => {
|
447 | emitter.off(eventName, listener);
|
448 | };
|
449 | });
|
450 | }
|
451 |
|
452 |
|
453 |
|
454 |
|
455 | export function fromAbortSignal(
|
456 | signal?: AbortSignal,
|
457 | cause?: Error,
|
458 | ): Observable<never> {
|
459 | return signal
|
460 | ? fromEvent(signal, 'abort').pipe(
|
461 | map(() => {
|
462 | if (signal.reason instanceof Error) {
|
463 | signal.reason.cause = cause;
|
464 | throw signal.reason;
|
465 | }
|
466 |
|
467 | throw new Error(signal.reason, {cause});
|
468 | }),
|
469 | )
|
470 | : NEVER;
|
471 | }
|
472 |
|
473 |
|
474 |
|
475 |
|
476 | export function filterAsync<T>(
|
477 | predicate: (value: T) => boolean | PromiseLike<boolean>,
|
478 | ): OperatorFunction<T, T> {
|
479 | return mergeMap<T, Observable<T>>((value): Observable<T> => {
|
480 | return from(Promise.resolve(predicate(value))).pipe(
|
481 | filter(isMatch => {
|
482 | return isMatch;
|
483 | }),
|
484 | map(() => {
|
485 | return value;
|
486 | }),
|
487 | );
|
488 | });
|
489 | }
|