UNPKG

11.4 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright 2017 Google Inc.
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7import type {OperatorFunction} from '../../third_party/rxjs/rxjs.js';
8import {
9 filter,
10 from,
11 fromEvent,
12 map,
13 mergeMap,
14 NEVER,
15 Observable,
16 timer,
17} from '../../third_party/rxjs/rxjs.js';
18import type {CDPSession} from '../api/CDPSession.js';
19import {environment} from '../environment.js';
20import {packageVersion} from '../generated/version.js';
21import {assert} from '../util/assert.js';
22import {mergeUint8Arrays} from '../util/encoding.js';
23
24import {debug} from './Debug.js';
25import {TimeoutError} from './Errors.js';
26import type {EventEmitter, EventType} from './EventEmitter.js';
27import type {
28 LowerCasePaperFormat,
29 ParsedPDFOptions,
30 PDFOptions,
31} from './PDFOptions.js';
32import {paperFormats} from './PDFOptions.js';
33
34/**
35 * @internal
36 */
37export const debugError = debug('puppeteer:error');
38
39/**
40 * @internal
41 */
42export const DEFAULT_VIEWPORT = Object.freeze({width: 800, height: 600});
43
44/**
45 * @internal
46 */
47const SOURCE_URL = Symbol('Source URL for Puppeteer evaluation scripts');
48
49/**
50 * @internal
51 */
52export 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 * @internal
99 */
100export 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 // First element is the function.
110 // Second element is the caller of this function.
111 // Third element is the caller of the caller of this function
112 // which is precisely what we want.
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 * @internal
124 */
125export 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 * @internal
138 */
139export const isString = (obj: unknown): obj is string => {
140 return typeof obj === 'string' || obj instanceof String;
141};
142
143/**
144 * @internal
145 */
146export const isNumber = (obj: unknown): obj is number => {
147 return typeof obj === 'number' || obj instanceof Number;
148};
149
150/**
151 * @internal
152 */
153export const isPlainObject = (obj: unknown): obj is Record<any, unknown> => {
154 return typeof obj === 'object' && obj?.constructor === Object;
155};
156
157/**
158 * @internal
159 */
160export const isRegExp = (obj: unknown): obj is RegExp => {
161 return typeof obj === 'object' && obj?.constructor === RegExp;
162};
163
164/**
165 * @internal
166 */
167export const isDate = (obj: unknown): obj is Date => {
168 return typeof obj === 'object' && obj?.constructor === Date;
169};
170
171/**
172 * @internal
173 */
174export function evaluationString(
175 // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
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 * @internal
196 */
197export 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 * @internal
240 */
241
242/**
243 * @internal
244 */
245export 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 * @internal
276 */
277export 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 * @internal
297 */
298export 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 * @internal
310 */
311export const UTILITY_WORLD_NAME =
312 '__puppeteer_utility_world__' + packageVersion;
313
314/**
315 * @internal
316 */
317export const SOURCE_URL_REGEX =
318 /^[\x20\t]*\/\/[@#] sourceURL=\s{0,10}(\S*?)\s{0,10}$/m;
319/**
320 * @internal
321 */
322export function getSourceUrlComment(url: string): string {
323 return `//# sourceURL=${url}`;
324}
325
326/**
327 * @internal
328 */
329export const NETWORK_IDLE_TIME = 500;
330
331/**
332 * @internal
333 */
334export 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 // Quirk https://bugs.chromium.org/p/chromium/issues/detail?id=840455#c44
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 * @internal
392 */
393export const unitToPixels = {
394 px: 1,
395 in: 96,
396 cm: 37.8,
397 mm: 3.78,
398};
399
400function 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 // Treat numbers as pixel values to be aligned with phantom's paperSize.
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 // In case of unknown unit try to parse the whole parameter as number of pixels.
419 // This is consistent with phantom's paperSize behavior.
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 * @internal
436 */
437export 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 * @internal
454 */
455export 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 * @internal
475 */
476export 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}