single-file-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2471 @@
1
+ /*
2
+ * Copyright 2010-2020 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ /* global globalThis */
25
+
26
+ const DEBUG = false;
27
+
28
+ const Set = globalThis.Set;
29
+ const Map = globalThis.Map;
30
+
31
+ let util, cssTree;
32
+
33
+ function getClass(...args) {
34
+ [util, cssTree] = args;
35
+ return SingleFileClass;
36
+ }
37
+
38
+ class SingleFileClass {
39
+ constructor(options) {
40
+ this.options = options;
41
+ }
42
+ async run() {
43
+ const waitForUserScript = globalThis._singleFile_waitForUserScript;
44
+ if (this.options.userScriptEnabled && waitForUserScript) {
45
+ await waitForUserScript(util.ON_BEFORE_CAPTURE_EVENT_NAME);
46
+ }
47
+ this.runner = new Runner(this.options, true);
48
+ await this.runner.loadPage();
49
+ await this.runner.initialize();
50
+ if (this.options.userScriptEnabled && waitForUserScript) {
51
+ await waitForUserScript(util.ON_AFTER_CAPTURE_EVENT_NAME);
52
+ }
53
+ await this.runner.run();
54
+ }
55
+ cancel() {
56
+ this.cancelled = true;
57
+ if (this.runner) {
58
+ this.runner.cancel();
59
+ }
60
+ }
61
+ getPageData() {
62
+ return this.runner.getPageData();
63
+ }
64
+ }
65
+
66
+ // -------------
67
+ // ProgressEvent
68
+ // -------------
69
+ const PAGE_LOADING = "page-loading";
70
+ const PAGE_LOADED = "page-loaded";
71
+ const RESOURCES_INITIALIZING = "resource-initializing";
72
+ const RESOURCES_INITIALIZED = "resources-initialized";
73
+ const RESOURCE_LOADED = "resource-loaded";
74
+ const PAGE_ENDED = "page-ended";
75
+ const STAGE_STARTED = "stage-started";
76
+ const STAGE_ENDED = "stage-ended";
77
+ const STAGE_TASK_STARTED = "stage-task-started";
78
+ const STAGE_TASK_ENDED = "stage-task-ended";
79
+
80
+ class ProgressEvent {
81
+ constructor(type, detail) {
82
+ return { type, detail, PAGE_LOADING, PAGE_LOADED, RESOURCES_INITIALIZING, RESOURCES_INITIALIZED, RESOURCE_LOADED, PAGE_ENDED, STAGE_STARTED, STAGE_ENDED, STAGE_TASK_STARTED, STAGE_TASK_ENDED };
83
+ }
84
+ }
85
+
86
+ // ------
87
+ // Runner
88
+ // ------
89
+ const RESOLVE_URLS_STAGE = 0;
90
+ const REPLACE_DATA_STAGE = 1;
91
+ const REPLACE_DOCS_STAGE = 2;
92
+ const POST_PROCESS_STAGE = 3;
93
+ const STAGES = [{
94
+ sequential: [
95
+ { action: "preProcessPage" },
96
+ { option: "loadDeferredImagesKeepZoomLevel", action: "resetZoomLevel" },
97
+ { action: "replaceStyleContents" },
98
+ { action: "resetCharsetMeta" },
99
+ { option: "saveFavicon", action: "saveFavicon" },
100
+ { action: "replaceCanvasElements" },
101
+ { action: "insertFonts" },
102
+ { action: "insertShadowRootContents" },
103
+ { action: "setInputValues" },
104
+ { option: "moveStylesInHead", action: "moveStylesInHead" },
105
+ { option: "blockScripts", action: "removeEmbedScripts" },
106
+ { option: "selected", action: "removeUnselectedElements" },
107
+ { option: "blockVideos", action: "insertVideoPosters" },
108
+ { option: "blockVideos", action: "insertVideoLinks" },
109
+ { option: "removeFrames", action: "removeFrames" },
110
+ { action: "removeDiscardedResources" },
111
+ { option: "removeHiddenElements", action: "removeHiddenElements" },
112
+ { action: "resolveHrefs" },
113
+ { action: "resolveStyleAttributeURLs" }
114
+ ],
115
+ parallel: [
116
+ { option: "blockVideos", action: "insertMissingVideoPosters" },
117
+ { action: "resolveStylesheetURLs" },
118
+ { option: "!removeFrames", action: "resolveFrameURLs" },
119
+ { action: "resolveHtmlImportURLs" }
120
+ ]
121
+ }, {
122
+ sequential: [
123
+ { option: "removeUnusedStyles", action: "removeUnusedStyles" },
124
+ { option: "removeAlternativeMedias", action: "removeAlternativeMedias" },
125
+ { option: "removeUnusedFonts", action: "removeUnusedFonts" }
126
+ ],
127
+ parallel: [
128
+ { action: "processStylesheets" },
129
+ { action: "processStyleAttributes" },
130
+ { action: "processPageResources" },
131
+ { action: "processScripts" }
132
+ ]
133
+ }, {
134
+ sequential: [
135
+ { option: "removeAlternativeImages", action: "removeAlternativeImages" }
136
+ ],
137
+ parallel: [
138
+ { option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
139
+ { option: "!removeFrames", action: "processFrames" },
140
+ { option: "!removeImports", action: "processHtmlImports" },
141
+ ]
142
+ }, {
143
+ sequential: [
144
+ { action: "replaceStylesheets" },
145
+ { action: "replaceStyleAttributes" },
146
+ { action: "insertVariables" },
147
+ { option: "compressHTML", action: "compressHTML" },
148
+ { action: "cleanupPage" }
149
+ ],
150
+ parallel: [
151
+ { option: "enableMaff", action: "insertMAFFMetaData" },
152
+ { action: "setDocInfo" }
153
+ ]
154
+ }];
155
+
156
+ class Runner {
157
+ constructor(options, root) {
158
+ const rootDocDefined = root && options.doc;
159
+ this.root = root;
160
+ this.options = options;
161
+ this.options.url = this.options.url || (rootDocDefined && this.options.doc.location.href);
162
+ const matchResourceReferrer = this.options.url.match(/^.*\//);
163
+ this.options.resourceReferrer = this.options.passReferrerOnError && matchResourceReferrer && matchResourceReferrer[0];
164
+ this.options.baseURI = rootDocDefined && this.options.doc.baseURI;
165
+ this.options.rootDocument = root;
166
+ this.options.updatedResources = this.options.updatedResources || {};
167
+ this.options.fontTests = new Map();
168
+ this.batchRequest = new BatchRequest();
169
+ this.processor = new Processor(options, this.batchRequest);
170
+ if (rootDocDefined) {
171
+ const docData = util.preProcessDoc(this.options.doc, this.options.win, this.options);
172
+ this.options.canvases = docData.canvases;
173
+ this.options.fonts = docData.fonts;
174
+ this.options.stylesheets = docData.stylesheets;
175
+ this.options.images = docData.images;
176
+ this.options.posters = docData.posters;
177
+ this.options.videos = docData.videos;
178
+ this.options.usedFonts = docData.usedFonts;
179
+ this.options.shadowRoots = docData.shadowRoots;
180
+ this.options.imports = docData.imports;
181
+ this.options.referrer = docData.referrer;
182
+ this.markedElements = docData.markedElements;
183
+ this.invalidElements = docData.invalidElements;
184
+ }
185
+ if (this.options.saveRawPage) {
186
+ this.options.removeFrames = true;
187
+ }
188
+ this.options.content = this.options.content || (rootDocDefined ? util.serialize(this.options.doc) : null);
189
+ this.onprogress = options.onprogress || (() => { });
190
+ }
191
+
192
+ async loadPage() {
193
+ this.onprogress(new ProgressEvent(PAGE_LOADING, { pageURL: this.options.url, frame: !this.root }));
194
+ await this.processor.loadPage(this.options.content);
195
+ this.onprogress(new ProgressEvent(PAGE_LOADED, { pageURL: this.options.url, frame: !this.root }));
196
+ }
197
+
198
+ async initialize() {
199
+ this.onprogress(new ProgressEvent(RESOURCES_INITIALIZING, { pageURL: this.options.url }));
200
+ await this.executeStage(RESOLVE_URLS_STAGE);
201
+ this.pendingPromises = this.executeStage(REPLACE_DATA_STAGE);
202
+ if (this.root && this.options.doc) {
203
+ util.postProcessDoc(this.options.doc, this.markedElements, this.invalidElements);
204
+ }
205
+ }
206
+
207
+ cancel() {
208
+ this.cancelled = true;
209
+ this.batchRequest.cancel();
210
+ if (this.root) {
211
+ if (this.options.frames) {
212
+ this.options.frames.forEach(cancelRunner);
213
+ }
214
+ if (this.options.imports) {
215
+ this.options.imports.forEach(cancelRunner);
216
+ }
217
+ }
218
+
219
+ function cancelRunner(resourceData) {
220
+ if (resourceData.runner) {
221
+ resourceData.runner.cancel();
222
+ }
223
+ }
224
+ }
225
+
226
+ async run() {
227
+ if (this.root) {
228
+ this.processor.initialize(this.batchRequest);
229
+ this.onprogress(new ProgressEvent(RESOURCES_INITIALIZED, { pageURL: this.options.url, max: this.processor.maxResources }));
230
+ }
231
+ await this.batchRequest.run(detail => {
232
+ detail.pageURL = this.options.url;
233
+ this.onprogress(new ProgressEvent(RESOURCE_LOADED, detail));
234
+ }, this.options);
235
+ await this.pendingPromises;
236
+ this.options.doc = null;
237
+ this.options.win = null;
238
+ await this.executeStage(REPLACE_DOCS_STAGE);
239
+ await this.executeStage(POST_PROCESS_STAGE);
240
+ this.processor.finalize();
241
+ }
242
+
243
+ getDocument() {
244
+ return this.processor.doc;
245
+ }
246
+
247
+ getStyleSheets() {
248
+ return this.processor.stylesheets;
249
+ }
250
+
251
+ getPageData() {
252
+ if (this.root) {
253
+ this.onprogress(new ProgressEvent(PAGE_ENDED, { pageURL: this.options.url }));
254
+ }
255
+ return this.processor.getPageData();
256
+ }
257
+
258
+ async executeStage(step) {
259
+ if (DEBUG) {
260
+ log("**** STARTED STAGE", step, "****");
261
+ }
262
+ const frame = !this.root;
263
+ this.onprogress(new ProgressEvent(STAGE_STARTED, { pageURL: this.options.url, step, frame }));
264
+ STAGES[step].sequential.forEach(task => {
265
+ let startTime;
266
+ if (DEBUG) {
267
+ startTime = Date.now();
268
+ log(" -- STARTED task =", task.action);
269
+ }
270
+ this.onprogress(new ProgressEvent(STAGE_TASK_STARTED, { pageURL: this.options.url, step, task: task.action, frame }));
271
+ if (!this.cancelled) {
272
+ this.executeTask(task);
273
+ }
274
+ this.onprogress(new ProgressEvent(STAGE_TASK_ENDED, { pageURL: this.options.url, step, task: task.action, frame }));
275
+ if (DEBUG) {
276
+ log(" -- ENDED task =", task.action, "delay =", Date.now() - startTime);
277
+ }
278
+ });
279
+ let parallelTasksPromise;
280
+ if (STAGES[step].parallel) {
281
+ parallelTasksPromise = await Promise.all(STAGES[step].parallel.map(async task => {
282
+ let startTime;
283
+ if (DEBUG) {
284
+ startTime = Date.now();
285
+ log(" // STARTED task =", task.action);
286
+ }
287
+ this.onprogress(new ProgressEvent(STAGE_TASK_STARTED, { pageURL: this.options.url, step, task: task.action, frame }));
288
+ if (!this.cancelled) {
289
+ await this.executeTask(task);
290
+ }
291
+ this.onprogress(new ProgressEvent(STAGE_TASK_ENDED, { pageURL: this.options.url, step, task: task.action, frame }));
292
+ if (DEBUG) {
293
+ log(" // ENDED task =", task.action, "delay =", Date.now() - startTime);
294
+ }
295
+ }));
296
+ } else {
297
+ parallelTasksPromise = Promise.resolve();
298
+ }
299
+ this.onprogress(new ProgressEvent(STAGE_ENDED, { pageURL: this.options.url, step, frame }));
300
+ if (DEBUG) {
301
+ log("**** ENDED STAGE", step, "****");
302
+ }
303
+ return parallelTasksPromise;
304
+ }
305
+
306
+ executeTask(task) {
307
+ if (!task.option || ((task.option.startsWith("!") && !this.options[task.option]) || this.options[task.option])) {
308
+ return this.processor[task.action]();
309
+ }
310
+ }
311
+ }
312
+
313
+ // ------------
314
+ // BatchRequest
315
+ // ------------
316
+ class BatchRequest {
317
+ constructor() {
318
+ this.requests = new Map();
319
+ this.duplicates = new Map();
320
+ }
321
+
322
+ addURL(resourceURL, { asBinary, expectedType, groupDuplicates, baseURI, blockMixedContent } = {}) {
323
+ return new Promise((resolve, reject) => {
324
+ const requestKey = JSON.stringify([resourceURL, asBinary, expectedType, baseURI, blockMixedContent]);
325
+ let resourceRequests = this.requests.get(requestKey);
326
+ if (!resourceRequests) {
327
+ resourceRequests = [];
328
+ this.requests.set(requestKey, resourceRequests);
329
+ }
330
+ const callbacks = { resolve, reject };
331
+ resourceRequests.push(callbacks);
332
+ if (groupDuplicates) {
333
+ let duplicateRequests = this.duplicates.get(requestKey);
334
+ if (!duplicateRequests) {
335
+ duplicateRequests = [];
336
+ this.duplicates.set(requestKey, duplicateRequests);
337
+ }
338
+ duplicateRequests.push(callbacks);
339
+ }
340
+ });
341
+ }
342
+
343
+ getMaxResources() {
344
+ return this.requests.size;
345
+ }
346
+
347
+ run(onloadListener, options) {
348
+ const resourceURLs = [...this.requests.keys()];
349
+ let indexResource = 0;
350
+ return Promise.all(resourceURLs.map(async requestKey => {
351
+ const [resourceURL, asBinary, expectedType, baseURI, blockMixedContent] = JSON.parse(requestKey);
352
+ const resourceRequests = this.requests.get(requestKey);
353
+ try {
354
+ const currentIndexResource = indexResource;
355
+ indexResource = indexResource + 1;
356
+ const content = await util.getContent(resourceURL, {
357
+ asBinary,
358
+ expectedType,
359
+ maxResourceSize: options.maxResourceSize,
360
+ maxResourceSizeEnabled: options.maxResourceSizeEnabled,
361
+ frameId: options.windowId,
362
+ resourceReferrer: options.resourceReferrer,
363
+ baseURI,
364
+ blockMixedContent,
365
+ acceptHeaders: options.acceptHeaders,
366
+ networkTimeout: options.networkTimeout
367
+ });
368
+ onloadListener({ url: resourceURL });
369
+ if (!this.cancelled) {
370
+ resourceRequests.forEach(callbacks => {
371
+ const duplicateCallbacks = this.duplicates.get(requestKey);
372
+ const duplicate = duplicateCallbacks && duplicateCallbacks.length > 1 && duplicateCallbacks.includes(callbacks);
373
+ callbacks.resolve({ content: content.data, indexResource: currentIndexResource, duplicate });
374
+ });
375
+ }
376
+ } catch (error) {
377
+ indexResource = indexResource + 1;
378
+ onloadListener({ url: resourceURL });
379
+ resourceRequests.forEach(resourceRequest => resourceRequest.reject(error));
380
+ }
381
+ this.requests.delete(requestKey);
382
+ }));
383
+ }
384
+
385
+ cancel() {
386
+ this.cancelled = true;
387
+ const resourceURLs = [...this.requests.keys()];
388
+ resourceURLs.forEach(requestKey => {
389
+ const resourceRequests = this.requests.get(requestKey);
390
+ resourceRequests.forEach(callbacks => callbacks.reject());
391
+ this.requests.delete(requestKey);
392
+ });
393
+ }
394
+ }
395
+
396
+ // ---------
397
+ // Processor
398
+ // ---------
399
+ const PREFIXES_FORBIDDEN_DATA_URI = ["data:text/"];
400
+ const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
401
+ const SCRIPT_TAG_FOUND = /<script/gi;
402
+ const NOSCRIPT_TAG_FOUND = /<noscript/gi;
403
+ const CANVAS_TAG_FOUND = /<canvas/gi;
404
+ const SHADOWROOT_ATTRIBUTE_NAME = "shadowroot";
405
+ const SCRIPT_TEMPLATE_SHADOW_ROOT = "data-template-shadow-root";
406
+ const UTF8_CHARSET = "utf-8";
407
+
408
+ class Processor {
409
+ constructor(options, batchRequest) {
410
+ this.options = options;
411
+ this.stats = new Stats(options);
412
+ this.baseURI = normalizeURL(options.baseURI || options.url);
413
+ this.batchRequest = batchRequest;
414
+ this.stylesheets = new Map();
415
+ this.styles = new Map();
416
+ this.cssVariables = new Map();
417
+ this.fontTests = options.fontTests;
418
+ }
419
+
420
+ initialize() {
421
+ this.options.saveDate = new Date();
422
+ this.options.saveUrl = this.options.url;
423
+ if (this.options.enableMaff) {
424
+ this.maffMetaDataPromise = this.batchRequest.addURL(util.resolveURL("index.rdf", this.options.baseURI || this.options.url), { expectedType: "document" });
425
+ }
426
+ this.maxResources = this.batchRequest.getMaxResources();
427
+ if (!this.options.saveRawPage && !this.options.removeFrames && this.options.frames) {
428
+ this.options.frames.forEach(frameData => this.maxResources += frameData.maxResources || 0);
429
+ }
430
+ if (!this.options.removeImports && this.options.imports) {
431
+ this.options.imports.forEach(importData => this.maxResources += importData.maxResources || 0);
432
+ }
433
+ this.stats.set("processed", "resources", this.maxResources);
434
+ }
435
+
436
+ async loadPage(pageContent, charset) {
437
+ let content;
438
+ if (!pageContent || this.options.saveRawPage) {
439
+ content = await util.getContent(this.baseURI, {
440
+ maxResourceSize: this.options.maxResourceSize,
441
+ maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
442
+ charset,
443
+ frameId: this.options.windowId,
444
+ resourceReferrer: this.options.resourceReferrer,
445
+ expectedType: "document",
446
+ acceptHeaders: this.options.acceptHeaders,
447
+ networkTimeout: this.options.networkTimeout
448
+ });
449
+ pageContent = content.data;
450
+ }
451
+ this.doc = util.parseDocContent(pageContent, this.baseURI);
452
+ if (this.options.saveRawPage) {
453
+ let charset;
454
+ this.doc.querySelectorAll("meta[charset], meta[http-equiv=\"content-type\"]").forEach(element => {
455
+ const charsetDeclaration = element.content.split(";")[1];
456
+ if (charsetDeclaration && !charset) {
457
+ charset = charsetDeclaration.split("=")[1].trim().toLowerCase();
458
+ }
459
+ });
460
+ if (charset && content.charset && charset.toLowerCase() != content.charset.toLowerCase()) {
461
+ return this.loadPage(pageContent, charset);
462
+ }
463
+ }
464
+ this.workStyleElement = this.doc.createElement("style");
465
+ this.doc.body.appendChild(this.workStyleElement);
466
+ this.onEventAttributeNames = getOnEventAttributeNames(this.doc);
467
+ }
468
+
469
+ finalize() {
470
+ if (this.workStyleElement.parentNode) {
471
+ this.workStyleElement.remove();
472
+ }
473
+ }
474
+
475
+ async getPageData() {
476
+ util.postProcessDoc(this.doc);
477
+ const url = util.parseURL(this.baseURI);
478
+ if (this.options.insertSingleFileComment) {
479
+ const firstComment = this.doc.documentElement.firstChild;
480
+ let infobarURL = this.options.saveUrl, infobarSaveDate = this.options.saveDate;
481
+ if (firstComment.nodeType == 8 && (firstComment.textContent.includes(util.COMMENT_HEADER_LEGACY) || firstComment.textContent.includes(util.COMMENT_HEADER))) {
482
+ const info = this.doc.documentElement.firstChild.textContent.split("\n");
483
+ try {
484
+ const [, , url, saveDate] = info;
485
+ infobarURL = url.split("url: ")[1];
486
+ infobarSaveDate = saveDate.split("saved date: ")[1];
487
+ firstComment.remove();
488
+ } catch (error) {
489
+ // ignored
490
+ }
491
+ }
492
+ const infobarContent = (this.options.infobarContent || "").replace(/\\n/g, "\n").replace(/\\t/g, "\t");
493
+ const commentNode = this.doc.createComment("\n " + (this.options.useLegacyCommentHeader ? util.COMMENT_HEADER_LEGACY : util.COMMENT_HEADER) +
494
+ " \n url: " + infobarURL +
495
+ " \n saved date: " + infobarSaveDate +
496
+ (infobarContent ? " \n info: " + infobarContent : "") + "\n");
497
+ this.doc.documentElement.insertBefore(commentNode, this.doc.documentElement.firstChild);
498
+ }
499
+ if (this.options.insertCanonicalLink && this.options.saveUrl.match(HTTP_URI_PREFIX)) {
500
+ let canonicalLink = this.doc.querySelector("link[rel=canonical]");
501
+ if (!canonicalLink) {
502
+ canonicalLink = this.doc.createElement("link");
503
+ canonicalLink.setAttribute("rel", "canonical");
504
+ this.doc.head.appendChild(canonicalLink);
505
+ }
506
+ if (canonicalLink && !canonicalLink.href) {
507
+ canonicalLink.href = this.options.saveUrl;
508
+ }
509
+ }
510
+ if (this.options.insertMetaCSP) {
511
+ const metaTag = this.doc.createElement("meta");
512
+ metaTag.httpEquiv = "content-security-policy";
513
+ metaTag.content = "default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:;";
514
+ this.doc.head.appendChild(metaTag);
515
+ }
516
+ if (this.options.insertMetaNoIndex) {
517
+ let metaElement = this.doc.querySelector("meta[name=robots][content*=noindex]");
518
+ if (!metaElement) {
519
+ metaElement = this.doc.createElement("meta");
520
+ metaElement.setAttribute("name", "robots");
521
+ metaElement.setAttribute("content", "noindex");
522
+ this.doc.head.appendChild(metaElement);
523
+ }
524
+ }
525
+ const styleElement = this.doc.createElement("style");
526
+ styleElement.textContent = "img[src=\"data:,\"],source[src=\"data:,\"]{display:none!important}";
527
+ this.doc.head.appendChild(styleElement);
528
+ let size;
529
+ if (this.options.displayStats) {
530
+ size = util.getContentSize(this.doc.documentElement.outerHTML);
531
+ }
532
+ const content = util.serialize(this.doc, this.options.compressHTML);
533
+ if (this.options.displayStats) {
534
+ const contentSize = util.getContentSize(content);
535
+ this.stats.set("processed", "HTML bytes", contentSize);
536
+ this.stats.add("discarded", "HTML bytes", size - contentSize);
537
+ }
538
+ let filename = await ProcessorHelper.evalTemplate(this.options.filenameTemplate, this.options, content) || "";
539
+ const replacementCharacter = this.options.filenameReplacementCharacter;
540
+ filename = util.getValidFilename(filename, this.options.filenameReplacedCharacters, replacementCharacter);
541
+ if (!this.options.backgroundSave) {
542
+ filename = filename.replace(/\//g, replacementCharacter);
543
+ }
544
+ if (!this.options.saveToGDrive && !this.options.saveToGitHub && !this.options.saveWithCompanion &&
545
+ ((this.options.filenameMaxLengthUnit == "bytes" && util.getContentSize(filename) > this.options.filenameMaxLength) || (filename.length > this.options.filenameMaxLength))) {
546
+ const extensionMatch = filename.match(/(\.[^.]{3,4})$/);
547
+ const extension = extensionMatch && extensionMatch[0] && extensionMatch[0].length > 1 ? extensionMatch[0] : "";
548
+ filename = this.options.filenameMaxLengthUnit == "bytes" ?
549
+ await util.truncateText(filename, this.options.filenameMaxLength - extension.length) :
550
+ filename.substring(0, this.options.filenameMaxLength - extension.length);
551
+ filename = filename + "…" + extension;
552
+ }
553
+ if (!filename) {
554
+ filename = "Unnamed page";
555
+ }
556
+ const matchTitle = this.baseURI.match(/([^/]*)\/?(\.html?.*)$/) || this.baseURI.match(/\/\/([^/]*)\/?$/);
557
+ const pageData = {
558
+ stats: this.stats.data,
559
+ title: this.options.title || (this.baseURI && matchTitle ? matchTitle[1] : (url.hostname ? url.hostname : "")),
560
+ filename,
561
+ content
562
+ };
563
+ if (this.options.addProof) {
564
+ pageData.hash = await util.digest("SHA-256", content);
565
+ }
566
+ if (this.options.retrieveLinks) {
567
+ pageData.links = Array.from(new Set(Array.from(this.doc.links).map(linkElement => linkElement.href)));
568
+ }
569
+ return pageData;
570
+ }
571
+
572
+ preProcessPage() {
573
+ if (this.options.win) {
574
+ this.doc.body.querySelectorAll(":not(svg) title, meta, link[href][rel*=\"icon\"]").forEach(element => element instanceof this.options.win.HTMLElement && this.doc.head.appendChild(element));
575
+ }
576
+ if (this.options.images && !this.options.saveRawPage) {
577
+ this.doc.querySelectorAll("img[" + util.IMAGE_ATTRIBUTE_NAME + "]").forEach(imgElement => {
578
+ const attributeValue = imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME);
579
+ if (attributeValue) {
580
+ const imageData = this.options.images[Number(attributeValue)];
581
+ if (imageData) {
582
+ if (this.options.removeHiddenElements && (
583
+ (imageData.size && !imageData.size.pxWidth && !imageData.size.pxHeight) ||
584
+ (imgElement.getAttribute(util.HIDDEN_CONTENT_ATTRIBUTE_NAME) == "")
585
+ )) {
586
+ imgElement.setAttribute("src", util.EMPTY_RESOURCE);
587
+ } else {
588
+ if (imageData.currentSrc) {
589
+ imgElement.dataset.singleFileOriginURL = imgElement.getAttribute("src");
590
+ imgElement.setAttribute("src", imageData.currentSrc);
591
+ }
592
+ if (this.options.loadDeferredImages) {
593
+ if ((!imgElement.getAttribute("src") || imgElement.getAttribute("src") == util.EMPTY_RESOURCE) && imgElement.getAttribute("data-src")) {
594
+ imageData.src = imgElement.dataset.src;
595
+ imgElement.setAttribute("src", imgElement.dataset.src);
596
+ imgElement.removeAttribute("data-src");
597
+ }
598
+ }
599
+ }
600
+ }
601
+ }
602
+ });
603
+ if (this.options.loadDeferredImages) {
604
+ this.doc.querySelectorAll("img[data-srcset]").forEach(imgElement => {
605
+ if (!imgElement.getAttribute("srcset") && imgElement.getAttribute("data-srcset")) {
606
+ imgElement.setAttribute("srcset", imgElement.dataset.srcset);
607
+ imgElement.removeAttribute("data-srcset");
608
+ }
609
+ });
610
+ }
611
+ }
612
+ }
613
+
614
+ replaceStyleContents() {
615
+ if (this.options.stylesheets) {
616
+ this.doc.querySelectorAll("style").forEach((styleElement, styleIndex) => {
617
+ const attributeValue = styleElement.getAttribute(util.STYLESHEET_ATTRIBUTE_NAME);
618
+ if (attributeValue) {
619
+ const stylesheetContent = this.options.stylesheets[Number(styleIndex)];
620
+ if (stylesheetContent) {
621
+ styleElement.textContent = stylesheetContent;
622
+ }
623
+ }
624
+ });
625
+ }
626
+ }
627
+
628
+ removeUnselectedElements() {
629
+ removeUnmarkedElements(this.doc.body);
630
+ this.doc.body.removeAttribute(util.SELECTED_CONTENT_ATTRIBUTE_NAME);
631
+
632
+ function removeUnmarkedElements(element) {
633
+ let selectedElementFound = false;
634
+ Array.from(element.childNodes).forEach(node => {
635
+ if (node.nodeType == 1) {
636
+ const isSelectedElement = node.getAttribute(util.SELECTED_CONTENT_ATTRIBUTE_NAME) == "";
637
+ selectedElementFound = selectedElementFound || isSelectedElement;
638
+ if (isSelectedElement) {
639
+ node.removeAttribute(util.SELECTED_CONTENT_ATTRIBUTE_NAME);
640
+ removeUnmarkedElements(node);
641
+ } else if (selectedElementFound) {
642
+ removeNode(node);
643
+ } else {
644
+ hideNode(node);
645
+ }
646
+ }
647
+ });
648
+ }
649
+
650
+ function removeNode(node) {
651
+ if ((node.nodeType != 1 || !node.querySelector("svg,style,link")) && canHideNode(node)) {
652
+ node.remove();
653
+ } else {
654
+ hideNode(node);
655
+ }
656
+ }
657
+
658
+ function hideNode(node) {
659
+ if (canHideNode(node)) {
660
+ node.style.setProperty("display", "none", "important");
661
+ Array.from(node.childNodes).forEach(removeNode);
662
+ }
663
+ }
664
+
665
+ function canHideNode(node) {
666
+ if (node.nodeType == 1) {
667
+ const tagName = node.tagName && node.tagName.toLowerCase();
668
+ return (tagName != "svg" && tagName != "style" && tagName != "link");
669
+ }
670
+ }
671
+ }
672
+
673
+ insertVideoPosters() {
674
+ if (this.options.posters) {
675
+ this.doc.querySelectorAll("video[src], video > source[src]").forEach(element => {
676
+ let videoElement;
677
+ if (element.tagName == "VIDEO") {
678
+ videoElement = element;
679
+ } else {
680
+ videoElement = element.parentElement;
681
+ }
682
+ const attributeValue = element.getAttribute(util.POSTER_ATTRIBUTE_NAME);
683
+ if (attributeValue) {
684
+ const posterURL = this.options.posters[Number(attributeValue)];
685
+ if (!videoElement.poster && posterURL) {
686
+ videoElement.setAttribute("poster", posterURL);
687
+ }
688
+ }
689
+ });
690
+ }
691
+ }
692
+
693
+ insertVideoLinks() {
694
+ const LINK_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAABhmlDQ1BJQ0MgcHJvZmlsZQAAKJF9kj1Iw0AYht+mSkUrDnYQcchQnSyIijqWKhbBQmkrtOpgcukfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfEydFJ0UVK/C4ptIjx4LiH9+59+e67A4RGhalm1wSgapaRisfEbG5VDLyiDwEAvZiVmKkn0osZeI6ve/j4ehfhWd7n/hz9St5kgE8kjjLdsIg3iGc2LZ3zPnGIlSSF+Jx43KACiR+5Lrv8xrnosMAzQ0YmNU8cIhaLHSx3MCsZKvE0cVhRNcoXsi4rnLc4q5Uaa9XJbxjMaytprtMcQRxLSCAJETJqKKMCCxFaNVJMpGg/5uEfdvxJcsnkKoORYwFVqJAcP/gb/O6tWZiadJOCMaD7xbY/RoHALtCs2/b3sW03TwD/M3Cltf3VBjD3SXq9rYWPgIFt4OK6rcl7wOUOMPSkS4bkSH6aQqEAvJ/RM+WAwVv6EGtu31r7OH0AMtSr5Rvg4BAYK1L2use9ezr79u+ZVv9+AFlNcp0UUpiqAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH5AsHAB8H+DhhoQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAJUExURQAAAICHi4qKioTuJAkAAAABdFJOUwBA5thmAAAAAWJLR0QCZgt8ZAAAAJJJREFUOI3t070NRCEMA2CnYAOyDyPwpHj/Va7hJ3FzV7zy3ET5JIwoAF6Jk4wzAJAkzxAYG9YRTgB+24wBgKmfrGAKTcEfAY4KRlRoIeBTgKOCERVaCPgU4Khge2GqKOBTgKOCERVaAEC/4PNcnyoSWHpjqkhwKxbcig0Q6AorXYF/+A6eIYD1lVbwG/jdA6/kA2THRAURVubcAAAAAElFTkSuQmCC";
695
+ const ICON_SIZE = "16px";
696
+ this.doc.querySelectorAll("video").forEach(videoElement => {
697
+ const attributeValue = videoElement.getAttribute(util.VIDEO_ATTRIBUTE_NAME);
698
+ if (attributeValue) {
699
+ const videoData = this.options.videos[Number(attributeValue)];
700
+ const src = videoData.src || videoElement.src;
701
+ if (videoElement && src) {
702
+ const linkElement = this.doc.createElement("a");
703
+ const imgElement = this.doc.createElement("img");
704
+ linkElement.href = src;
705
+ linkElement.target = "_blank";
706
+ linkElement.style.setProperty("z-index", 2147483647, "important");
707
+ linkElement.style.setProperty("position", "absolute", "important");
708
+ linkElement.style.setProperty("top", "8px", "important");
709
+ linkElement.style.setProperty("left", "8px", "important");
710
+ linkElement.style.setProperty("width", ICON_SIZE, "important");
711
+ linkElement.style.setProperty("height", ICON_SIZE, "important");
712
+ linkElement.style.setProperty("min-width", ICON_SIZE, "important");
713
+ linkElement.style.setProperty("min-height", ICON_SIZE, "important");
714
+ linkElement.style.setProperty("max-width", ICON_SIZE, "important");
715
+ linkElement.style.setProperty("max-height", ICON_SIZE, "important");
716
+ imgElement.src = LINK_ICON;
717
+ imgElement.style.setProperty("width", ICON_SIZE, "important");
718
+ imgElement.style.setProperty("height", ICON_SIZE, "important");
719
+ imgElement.style.setProperty("min-width", ICON_SIZE, "important");
720
+ imgElement.style.setProperty("min-height", ICON_SIZE, "important");
721
+ imgElement.style.setProperty("max-width", ICON_SIZE, "important");
722
+ imgElement.style.setProperty("max-height", ICON_SIZE, "important");
723
+ linkElement.appendChild(imgElement);
724
+ videoElement.insertAdjacentElement("afterend", linkElement);
725
+ const positionInlineParent = videoElement.parentNode.style.getPropertyValue("position");
726
+ if ((!videoData.positionParent && (!positionInlineParent || positionInlineParent != "static")) || videoData.positionParent == "static") {
727
+ videoElement.parentNode.style.setProperty("position", "relative", "important");
728
+ }
729
+ }
730
+ }
731
+ });
732
+ }
733
+
734
+ removeFrames() {
735
+ const frameElements = this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]");
736
+ this.stats.set("discarded", "frames", frameElements.length);
737
+ this.stats.set("processed", "frames", frameElements.length);
738
+ this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]").forEach(element => element.remove());
739
+ }
740
+
741
+ removeEmbedScripts() {
742
+ const JAVASCRIPT_URI_PREFIX = "javascript:";
743
+ this.onEventAttributeNames.forEach(attributeName => this.doc.querySelectorAll("[" + attributeName + "]").forEach(element => element.removeAttribute(attributeName)));
744
+ this.doc.querySelectorAll("[href]").forEach(element => {
745
+ if (element.href && element.href.match && element.href.trim().startsWith(JAVASCRIPT_URI_PREFIX)) {
746
+ element.removeAttribute("href");
747
+ }
748
+ });
749
+ this.doc.querySelectorAll("[src]").forEach(element => {
750
+ if (element.src && element.src.trim().startsWith(JAVASCRIPT_URI_PREFIX)) {
751
+ element.removeAttribute("src");
752
+ }
753
+ });
754
+ const scriptElements = this.doc.querySelectorAll("script:not([type=\"application/ld+json\"]):not([" + SCRIPT_TEMPLATE_SHADOW_ROOT + "])");
755
+ this.stats.set("discarded", "scripts", scriptElements.length);
756
+ this.stats.set("processed", "scripts", scriptElements.length);
757
+ scriptElements.forEach(element => element.remove());
758
+ }
759
+
760
+ removeDiscardedResources() {
761
+ this.doc.querySelectorAll("." + util.SINGLE_FILE_UI_ELEMENT_CLASS).forEach(element => element.remove());
762
+ const noscriptPlaceholders = new Map();
763
+ this.doc.querySelectorAll("noscript").forEach(noscriptElement => {
764
+ const placeholderElement = this.doc.createElement("div");
765
+ placeholderElement.innerHTML = noscriptElement.dataset.singleFileDisabledNoscript;
766
+ noscriptElement.replaceWith(placeholderElement);
767
+ noscriptPlaceholders.set(placeholderElement, noscriptElement);
768
+ });
769
+ this.doc.querySelectorAll("meta[http-equiv=refresh], meta[disabled-http-equiv]").forEach(element => element.remove());
770
+ Array.from(noscriptPlaceholders).forEach(([placeholderElement, noscriptElement]) => {
771
+ noscriptElement.dataset.singleFileDisabledNoscript = placeholderElement.innerHTML;
772
+ placeholderElement.replaceWith(noscriptElement);
773
+ });
774
+ this.doc.querySelectorAll("meta[http-equiv=\"content-security-policy\"]").forEach(element => element.remove());
775
+ const objectElements = this.doc.querySelectorAll("applet, object[data]:not([type=\"image/svg+xml\"]):not([type=\"image/svg-xml\"]):not([type=\"text/html\"]), embed[src]:not([src*=\".svg\"]):not([src*=\".pdf\"])");
776
+ this.stats.set("discarded", "objects", objectElements.length);
777
+ this.stats.set("processed", "objects", objectElements.length);
778
+ objectElements.forEach(element => element.remove());
779
+ const replacedAttributeValue = this.doc.querySelectorAll("link[rel~=preconnect], link[rel~=prerender], link[rel~=dns-prefetch], link[rel~=preload], link[rel~=manifest], link[rel~=prefetch]");
780
+ replacedAttributeValue.forEach(element => {
781
+ const relValue = element.getAttribute("rel").replace(/(preconnect|prerender|dns-prefetch|preload|prefetch|manifest)/g, "").trim();
782
+ if (relValue.length) {
783
+ element.setAttribute("rel", relValue);
784
+ } else {
785
+ element.remove();
786
+ }
787
+ });
788
+ this.doc.querySelectorAll("link[rel*=stylesheet][rel*=alternate][title],link[rel*=stylesheet]:not([href]),link[rel*=stylesheet][href=\"\"]").forEach(element => element.remove());
789
+ if (this.options.removeHiddenElements) {
790
+ this.doc.querySelectorAll("input[type=hidden]").forEach(element => element.remove());
791
+ }
792
+ if (!this.options.saveFavicon) {
793
+ this.doc.querySelectorAll("link[rel*=\"icon\"]").forEach(element => element.remove());
794
+ }
795
+ this.doc.querySelectorAll("a[ping]").forEach(element => element.removeAttribute("ping"));
796
+ }
797
+
798
+ resetCharsetMeta() {
799
+ let charset;
800
+ this.doc.querySelectorAll("meta[charset], meta[http-equiv=\"content-type\"]").forEach(element => {
801
+ const charsetDeclaration = element.content.split(";")[1];
802
+ if (charsetDeclaration && !charset) {
803
+ charset = charsetDeclaration.split("=")[1];
804
+ if (charset) {
805
+ this.charset = charset.trim().toLowerCase();
806
+ }
807
+ }
808
+ element.remove();
809
+ });
810
+ const metaElement = this.doc.createElement("meta");
811
+ metaElement.setAttribute("charset", UTF8_CHARSET);
812
+ if (this.doc.head.firstChild) {
813
+ this.doc.head.insertBefore(metaElement, this.doc.head.firstChild);
814
+ } else {
815
+ this.doc.head.appendChild(metaElement);
816
+ }
817
+ }
818
+
819
+ setInputValues() {
820
+ this.doc.querySelectorAll("input:not([type=radio]):not([type=checkbox])").forEach(input => {
821
+ const value = input.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME);
822
+ input.setAttribute("value", value || "");
823
+ });
824
+ this.doc.querySelectorAll("input[type=radio], input[type=checkbox]").forEach(input => {
825
+ const value = input.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME);
826
+ if (value == "true") {
827
+ input.setAttribute("checked", "");
828
+ }
829
+ });
830
+ this.doc.querySelectorAll("textarea").forEach(textarea => {
831
+ const value = textarea.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME);
832
+ textarea.textContent = value || "";
833
+ });
834
+ this.doc.querySelectorAll("select").forEach(select => {
835
+ select.querySelectorAll("option").forEach(option => {
836
+ const selected = option.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME) != null;
837
+ if (selected) {
838
+ option.setAttribute("selected", "");
839
+ }
840
+ });
841
+ });
842
+ }
843
+
844
+ moveStylesInHead() {
845
+ this.doc.querySelectorAll("style").forEach(stylesheet => {
846
+ if (stylesheet.getAttribute(util.STYLE_ATTRIBUTE_NAME) == "") {
847
+ this.doc.head.appendChild(stylesheet);
848
+ }
849
+ });
850
+ }
851
+
852
+ saveFavicon() {
853
+ let faviconElement = this.doc.querySelector("link[href][rel=\"icon\"]");
854
+ if (!faviconElement) {
855
+ faviconElement = this.doc.querySelector("link[href][rel=\"shortcut icon\"]");
856
+ }
857
+ if (!faviconElement) {
858
+ faviconElement = this.doc.createElement("link");
859
+ faviconElement.setAttribute("type", "image/x-icon");
860
+ faviconElement.setAttribute("rel", "shortcut icon");
861
+ faviconElement.setAttribute("href", "/favicon.ico");
862
+ }
863
+ this.doc.head.appendChild(faviconElement);
864
+ }
865
+
866
+ replaceCanvasElements() {
867
+ if (this.options.canvases) {
868
+ this.doc.querySelectorAll("canvas").forEach(canvasElement => {
869
+ const attributeValue = canvasElement.getAttribute(util.CANVAS_ATTRIBUTE_NAME);
870
+ if (attributeValue) {
871
+ const canvasData = this.options.canvases[Number(attributeValue)];
872
+ if (canvasData) {
873
+ ProcessorHelper.setBackgroundImage(canvasElement, "url(" + canvasData.dataURI + ")");
874
+ this.stats.add("processed", "canvas", 1);
875
+ }
876
+ }
877
+ });
878
+ }
879
+ }
880
+
881
+ insertFonts() {
882
+ if (this.options.fonts && this.options.fonts.length) {
883
+ let stylesheetContent = "";
884
+ this.options.fonts.forEach(fontData => {
885
+ if (fontData["font-family"] && fontData.src) {
886
+ stylesheetContent += "@font-face{";
887
+ let stylesContent = "";
888
+ Object.keys(fontData).forEach(fontStyle => {
889
+ if (stylesContent) {
890
+ stylesContent += ";";
891
+ }
892
+ stylesContent += fontStyle + ":" + fontData[fontStyle];
893
+ });
894
+ stylesheetContent += stylesContent + "}";
895
+ }
896
+ });
897
+ if (stylesheetContent) {
898
+ const styleElement = this.doc.createElement("style");
899
+ styleElement.textContent = stylesheetContent;
900
+ const existingStyleElement = this.doc.querySelector("style");
901
+ if (existingStyleElement) {
902
+ existingStyleElement.parentElement.insertBefore(styleElement, existingStyleElement);
903
+ } else {
904
+ this.doc.head.insertBefore(styleElement, this.doc.head.firstChild);
905
+ }
906
+ }
907
+ }
908
+ }
909
+
910
+ removeHiddenElements() {
911
+ const hiddenElements = this.doc.querySelectorAll("[" + util.HIDDEN_CONTENT_ATTRIBUTE_NAME + "]");
912
+ const removedElements = this.doc.querySelectorAll("[" + util.REMOVED_CONTENT_ATTRIBUTE_NAME + "]");
913
+ this.stats.set("discarded", "hidden elements", removedElements.length);
914
+ this.stats.set("processed", "hidden elements", removedElements.length);
915
+ if (hiddenElements.length) {
916
+ const styleElement = this.doc.createElement("style");
917
+ styleElement.textContent = ".sf-hidden{display:none!important;}";
918
+ this.doc.head.appendChild(styleElement);
919
+ hiddenElements.forEach(element => {
920
+ if (element.style.getPropertyValue("display") != "none") {
921
+ if (element.style.getPropertyPriority("display") == "important") {
922
+ element.style.setProperty("display", "none", "important");
923
+ } else {
924
+ element.classList.add("sf-hidden");
925
+ }
926
+ }
927
+ });
928
+ }
929
+ removedElements.forEach(element => element.remove());
930
+ }
931
+
932
+ resolveHrefs() {
933
+ this.doc.querySelectorAll("a[href], area[href], link[href]").forEach(element => {
934
+ const href = element.getAttribute("href").trim();
935
+ if (element.tagName == "LINK" && element.rel.includes("stylesheet")) {
936
+ if (this.options.saveOriginalURLs && !isDataURL(href)) {
937
+ element.setAttribute("data-sf-original-href", href);
938
+ }
939
+ }
940
+ if (!testIgnoredPath(href)) {
941
+ let resolvedURL;
942
+ try {
943
+ resolvedURL = util.resolveURL(href, this.options.baseURI || this.options.url);
944
+ } catch (error) {
945
+ // ignored
946
+ }
947
+ if (resolvedURL) {
948
+ const url = normalizeURL(this.options.url);
949
+ if (resolvedURL.startsWith(url + "#") && !resolvedURL.startsWith(url + "#!") && !this.options.resolveFragmentIdentifierURLs) {
950
+ resolvedURL = resolvedURL.substring(url.length);
951
+ }
952
+ try {
953
+ element.setAttribute("href", resolvedURL);
954
+ } catch (error) {
955
+ // ignored
956
+ }
957
+ }
958
+ }
959
+ });
960
+ }
961
+
962
+ async insertMissingVideoPosters() {
963
+ await Promise.all(Array.from(this.doc.querySelectorAll("video[src], video > source[src]")).map(async element => {
964
+ let videoElement;
965
+ if (element.tagName == "VIDEO") {
966
+ videoElement = element;
967
+ } else {
968
+ videoElement = element.parentElement;
969
+ }
970
+ if (!videoElement.poster) {
971
+ const attributeValue = videoElement.getAttribute(util.VIDEO_ATTRIBUTE_NAME);
972
+ if (attributeValue) {
973
+ const videoData = this.options.videos[Number(attributeValue)];
974
+ const src = videoData.src || videoElement.src;
975
+ if (src) {
976
+ const temporaryVideoElement = this.doc.createElement("video");
977
+ temporaryVideoElement.src = src;
978
+ temporaryVideoElement.style.setProperty("width", videoData.size.pxWidth + "px", "important");
979
+ temporaryVideoElement.style.setProperty("height", videoData.size.pxHeight + "px", "important");
980
+ temporaryVideoElement.style.setProperty("display", "none", "important");
981
+ temporaryVideoElement.crossOrigin = "anonymous";
982
+ const canvasElement = this.doc.createElement("canvas");
983
+ const context = canvasElement.getContext("2d");
984
+ this.options.doc.body.appendChild(temporaryVideoElement);
985
+ return new Promise(resolve => {
986
+ temporaryVideoElement.currentTime = videoData.currentTime;
987
+ temporaryVideoElement.oncanplay = () => {
988
+ canvasElement.width = videoData.size.pxWidth;
989
+ canvasElement.height = videoData.size.pxHeight;
990
+ context.drawImage(temporaryVideoElement, 0, 0, canvasElement.width, canvasElement.height);
991
+ try {
992
+ videoElement.poster = canvasElement.toDataURL("image/png", "");
993
+ } catch (error) {
994
+ // ignored
995
+ }
996
+ temporaryVideoElement.remove();
997
+ resolve();
998
+ };
999
+ temporaryVideoElement.onerror = () => {
1000
+ temporaryVideoElement.remove();
1001
+ resolve();
1002
+ };
1003
+ });
1004
+ }
1005
+ }
1006
+ }
1007
+ }));
1008
+ }
1009
+
1010
+ resolveStyleAttributeURLs() {
1011
+ this.doc.querySelectorAll("[style]").forEach(element => {
1012
+ let styleContent = element.getAttribute("style");
1013
+ if (this.options.compressCSS) {
1014
+ styleContent = util.compressCSS(styleContent);
1015
+ }
1016
+ styleContent = ProcessorHelper.resolveStylesheetURLs(styleContent, this.baseURI, this.workStyleElement, this.options.saveOriginalURLs);
1017
+ const declarationList = cssTree.parse(styleContent, { context: "declarationList" });
1018
+ this.styles.set(element, declarationList);
1019
+ });
1020
+ }
1021
+
1022
+ async resolveStylesheetURLs() {
1023
+ await Promise.all(Array.from(this.doc.querySelectorAll("style, link[rel*=stylesheet]")).map(async element => {
1024
+ const options = Object.assign({}, this.options, { charset: this.charset });
1025
+ let mediaText;
1026
+ if (element.media) {
1027
+ mediaText = element.media.toLowerCase();
1028
+ }
1029
+ const stylesheetInfo = { mediaText };
1030
+ if (element.closest("[" + SHADOWROOT_ATTRIBUTE_NAME + "]")) {
1031
+ stylesheetInfo.scoped = true;
1032
+ }
1033
+ if (element.tagName == "LINK" && element.charset) {
1034
+ options.charset = element.charset;
1035
+ }
1036
+ await processElement(element, stylesheetInfo, this.stylesheets, this.baseURI, options, this.workStyleElement);
1037
+ }));
1038
+ if (this.options.rootDocument) {
1039
+ const newResources = Object.keys(this.options.updatedResources).filter(url => this.options.updatedResources[url].type == "stylesheet" && !this.options.updatedResources[url].retrieved).map(url => this.options.updatedResources[url]);
1040
+ await Promise.all(newResources.map(async resource => {
1041
+ resource.retrieved = true;
1042
+ const stylesheetInfo = {};
1043
+ const element = this.doc.createElement("style");
1044
+ this.doc.body.appendChild(element);
1045
+ element.textContent = resource.content;
1046
+ await processElement(element, stylesheetInfo, this.stylesheets, this.baseURI, this.options, this.workStyleElement);
1047
+ }));
1048
+ }
1049
+
1050
+ async function processElement(element, stylesheetInfo, stylesheets, baseURI, options, workStyleElement) {
1051
+ let stylesheet;
1052
+ stylesheets.set(element, stylesheetInfo);
1053
+ if (!options.blockStylesheets) {
1054
+ let stylesheetContent = await getStylesheetContent(element, baseURI, options, workStyleElement);
1055
+ if (!matchCharsetEquals(stylesheetContent, options.charset)) {
1056
+ options = Object.assign({}, options, { charset: getCharset(stylesheetContent) });
1057
+ stylesheetContent = await getStylesheetContent(element, baseURI, options, workStyleElement);
1058
+ }
1059
+ try {
1060
+ stylesheet = cssTree.parse(removeCssComments(stylesheetContent));
1061
+ } catch (error) {
1062
+ // ignored
1063
+ }
1064
+ }
1065
+ if (stylesheet && stylesheet.children) {
1066
+ if (options.compressCSS) {
1067
+ ProcessorHelper.removeSingleLineCssComments(stylesheet);
1068
+ }
1069
+ stylesheetInfo.stylesheet = stylesheet;
1070
+ } else {
1071
+ stylesheets.delete(element);
1072
+ }
1073
+ }
1074
+
1075
+ async function getStylesheetContent(element, baseURI, options, workStyleElement) {
1076
+ let content;
1077
+ if (!options.blockStylesheets) {
1078
+ if (element.tagName == "LINK") {
1079
+ content = await ProcessorHelper.resolveLinkStylesheetURLs(element.href, baseURI, options, workStyleElement);
1080
+ } else {
1081
+ content = await ProcessorHelper.resolveImportURLs(element.textContent, baseURI, options, workStyleElement);
1082
+ }
1083
+ }
1084
+ return content || "";
1085
+ }
1086
+ }
1087
+
1088
+ async resolveFrameURLs() {
1089
+ if (!this.options.saveRawPage) {
1090
+ const frameElements = Array.from(this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]"));
1091
+ await Promise.all(frameElements.map(async frameElement => {
1092
+ if (frameElement.tagName == "OBJECT") {
1093
+ frameElement.setAttribute("data", "data:text/html,");
1094
+ } else {
1095
+ const src = frameElement.getAttribute("src");
1096
+ if (this.options.saveOriginalURLs && !isDataURL(src)) {
1097
+ frameElement.setAttribute("data-sf-original-src", src);
1098
+ }
1099
+ frameElement.removeAttribute("src");
1100
+ frameElement.removeAttribute("srcdoc");
1101
+ }
1102
+ Array.from(frameElement.childNodes).forEach(node => node.remove());
1103
+ const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
1104
+ if (this.options.frames && frameWindowId) {
1105
+ const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
1106
+ if (frameData) {
1107
+ await initializeProcessor(frameData, frameElement, frameWindowId, this.batchRequest, Object.create(this.options));
1108
+ }
1109
+ }
1110
+ }));
1111
+ }
1112
+
1113
+ async function initializeProcessor(frameData, frameElement, frameWindowId, batchRequest, options) {
1114
+ options.insertSingleFileComment = false;
1115
+ options.insertCanonicalLink = false;
1116
+ options.insertMetaNoIndex = false;
1117
+ options.saveFavicon = false;
1118
+ options.url = frameData.baseURI;
1119
+ options.windowId = frameWindowId;
1120
+ if (frameData.content) {
1121
+ options.content = frameData.content;
1122
+ options.canvases = frameData.canvases;
1123
+ options.fonts = frameData.fonts;
1124
+ options.stylesheets = frameData.stylesheets;
1125
+ options.images = frameData.images;
1126
+ options.posters = frameData.posters;
1127
+ options.videos = frameData.videos;
1128
+ options.usedFonts = frameData.usedFonts;
1129
+ options.shadowRoots = frameData.shadowRoots;
1130
+ options.imports = frameData.imports;
1131
+ frameData.runner = new Runner(options);
1132
+ frameData.frameElement = frameElement;
1133
+ await frameData.runner.loadPage();
1134
+ await frameData.runner.initialize();
1135
+ frameData.maxResources = batchRequest.getMaxResources();
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ insertShadowRootContents() {
1141
+ const doc = this.doc;
1142
+ const options = this.options;
1143
+ if (options.shadowRoots && options.shadowRoots.length) {
1144
+ processElement(this.doc);
1145
+ if (options.blockScripts) {
1146
+ this.doc.querySelectorAll("script[" + SCRIPT_TEMPLATE_SHADOW_ROOT + "]").forEach(element => element.remove());
1147
+ }
1148
+ const scriptElement = doc.createElement("script");
1149
+ scriptElement.setAttribute(SCRIPT_TEMPLATE_SHADOW_ROOT, "");
1150
+ scriptElement.textContent = `(()=>{document.currentScript.remove();processNode(document);function processNode(node){node.querySelectorAll("template[${SHADOWROOT_ATTRIBUTE_NAME}]").forEach(element=>{let shadowRoot = element.parentElement.shadowRoot;if (!shadowRoot) {try {shadowRoot=element.parentElement.attachShadow({mode:element.getAttribute("${SHADOWROOT_ATTRIBUTE_NAME}")});shadowRoot.innerHTML=element.innerHTML;element.remove()} catch (error) {} if (shadowRoot) {processNode(shadowRoot)}}})}})()`;
1151
+ doc.body.appendChild(scriptElement);
1152
+ }
1153
+
1154
+ function processElement(element) {
1155
+ const shadowRootElements = Array.from((element.querySelectorAll("[" + util.SHADOW_ROOT_ATTRIBUTE_NAME + "]")));
1156
+ shadowRootElements.forEach(element => {
1157
+ const attributeValue = element.getAttribute(util.SHADOW_ROOT_ATTRIBUTE_NAME);
1158
+ if (attributeValue) {
1159
+ const shadowRootData = options.shadowRoots[Number(attributeValue)];
1160
+ if (shadowRootData) {
1161
+ const templateElement = doc.createElement("template");
1162
+ templateElement.setAttribute(SHADOWROOT_ATTRIBUTE_NAME, shadowRootData.mode);
1163
+ if (shadowRootData.adoptedStyleSheets) {
1164
+ shadowRootData.adoptedStyleSheets.forEach(stylesheetContent => {
1165
+ const styleElement = doc.createElement("style");
1166
+ styleElement.textContent = stylesheetContent;
1167
+ templateElement.appendChild(styleElement);
1168
+ });
1169
+ }
1170
+ const shadowDoc = util.parseDocContent(shadowRootData.content);
1171
+ if (shadowDoc.head) {
1172
+ const metaCharset = shadowDoc.head.querySelector("meta[charset]");
1173
+ if (metaCharset) {
1174
+ metaCharset.remove();
1175
+ }
1176
+ shadowDoc.head.childNodes.forEach(node => templateElement.appendChild(shadowDoc.importNode(node, true)));
1177
+ }
1178
+ if (shadowDoc.body) {
1179
+ shadowDoc.body.childNodes.forEach(node => templateElement.appendChild(shadowDoc.importNode(node, true)));
1180
+ }
1181
+ processElement(templateElement);
1182
+ if (element.firstChild) {
1183
+ element.insertBefore(templateElement, element.firstChild);
1184
+ } else {
1185
+ element.appendChild(templateElement);
1186
+ }
1187
+ }
1188
+ }
1189
+ });
1190
+ }
1191
+ }
1192
+
1193
+ async resolveHtmlImportURLs() {
1194
+ const linkElements = Array.from(this.doc.querySelectorAll("link[rel=import][href]"));
1195
+ await Promise.all(linkElements.map(async linkElement => {
1196
+ const resourceURL = linkElement.href;
1197
+ if (this.options.saveOriginalURLs && !isDataURL(resourceURL)) {
1198
+ linkElement.setAttribute("data-sf-original-href", resourceURL);
1199
+ }
1200
+ linkElement.removeAttribute("href");
1201
+ const options = Object.create(this.options);
1202
+ options.insertSingleFileComment = false;
1203
+ options.insertCanonicalLink = false;
1204
+ options.insertMetaNoIndex = false;
1205
+ options.saveFavicon = false;
1206
+ options.removeUnusedStyles = false;
1207
+ options.removeAlternativeMedias = false;
1208
+ options.removeUnusedFonts = false;
1209
+ options.removeHiddenElements = false;
1210
+ options.url = resourceURL;
1211
+ const attributeValue = linkElement.getAttribute(util.HTML_IMPORT_ATTRIBUTE_NAME);
1212
+ if (attributeValue) {
1213
+ const importData = options.imports[Number(attributeValue)];
1214
+ if (importData) {
1215
+ options.content = importData.content;
1216
+ importData.runner = new Runner(options);
1217
+ await importData.runner.loadPage();
1218
+ await importData.runner.initialize();
1219
+ if (!options.removeImports) {
1220
+ importData.maxResources = importData.runner.batchRequest.getMaxResources();
1221
+ }
1222
+ importData.runner.getStyleSheets().forEach(stylesheet => {
1223
+ const importedStyleElement = this.doc.createElement("style");
1224
+ linkElement.insertAdjacentElement("afterEnd", importedStyleElement);
1225
+ this.stylesheets.set(importedStyleElement, stylesheet);
1226
+ });
1227
+ }
1228
+ }
1229
+ if (options.removeImports) {
1230
+ linkElement.remove();
1231
+ this.stats.add("discarded", "HTML imports", 1);
1232
+ }
1233
+ }));
1234
+ }
1235
+
1236
+ removeUnusedStyles() {
1237
+ if (!this.mediaAllInfo) {
1238
+ this.mediaAllInfo = util.getMediaAllInfo(this.doc, this.stylesheets, this.styles);
1239
+ }
1240
+ const stats = util.minifyCSSRules(this.stylesheets, this.styles, this.mediaAllInfo);
1241
+ this.stats.set("processed", "CSS rules", stats.processed);
1242
+ this.stats.set("discarded", "CSS rules", stats.discarded);
1243
+ }
1244
+
1245
+ removeUnusedFonts() {
1246
+ util.removeUnusedFonts(this.doc, this.stylesheets, this.styles, this.options);
1247
+ }
1248
+
1249
+ removeAlternativeMedias() {
1250
+ const stats = util.minifyMedias(this.stylesheets);
1251
+ this.stats.set("processed", "medias", stats.processed);
1252
+ this.stats.set("discarded", "medias", stats.discarded);
1253
+ }
1254
+
1255
+ async processStylesheets() {
1256
+ this.options.fontDeclarations = new Map();
1257
+ await Promise.all([...this.stylesheets].map(([, stylesheetInfo]) =>
1258
+ ProcessorHelper.processStylesheet(stylesheetInfo.stylesheet.children, this.baseURI, this.options, this.cssVariables, this.batchRequest)
1259
+ ));
1260
+ }
1261
+
1262
+ async processStyleAttributes() {
1263
+ return Promise.all([...this.styles].map(([, declarationList]) =>
1264
+ ProcessorHelper.processStyle(declarationList.children.toArray(), this.baseURI, this.options, this.cssVariables, this.batchRequest)
1265
+ ));
1266
+ }
1267
+
1268
+ async processPageResources() {
1269
+ const processAttributeArgs = [
1270
+ ["link[href][rel*=\"icon\"]", "href", false, true],
1271
+ ["object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"]", "data"],
1272
+ ["img[src], input[src][type=image]", "src", true],
1273
+ ["embed[src*=\".svg\"], embed[src*=\".pdf\"]", "src"],
1274
+ ["video[poster]", "poster"],
1275
+ ["*[background]", "background"],
1276
+ ["image", "xlink:href"],
1277
+ ["image", "href"]
1278
+ ];
1279
+ if (this.options.blockImages) {
1280
+ this.doc.querySelectorAll("svg").forEach(element => element.remove());
1281
+ }
1282
+ let resourcePromises = processAttributeArgs.map(([selector, attributeName, processDuplicates, removeElementIfMissing]) =>
1283
+ ProcessorHelper.processAttribute(this.doc.querySelectorAll(selector), attributeName, this.baseURI, this.options, "image", this.cssVariables, this.styles, this.batchRequest, processDuplicates, removeElementIfMissing)
1284
+ );
1285
+ resourcePromises = resourcePromises.concat([
1286
+ ProcessorHelper.processXLinks(this.doc.querySelectorAll("use"), this.doc, this.baseURI, this.options, this.batchRequest),
1287
+ ProcessorHelper.processSrcset(this.doc.querySelectorAll("img[srcset], source[srcset]"), this.baseURI, this.options, this.batchRequest)
1288
+ ]);
1289
+ resourcePromises.push(ProcessorHelper.processAttribute(this.doc.querySelectorAll("audio[src], audio > source[src]"), "src", this.baseURI, this.options, "audio", this.cssVariables, this.styles, this.batchRequest));
1290
+ resourcePromises.push(ProcessorHelper.processAttribute(this.doc.querySelectorAll("video[src], video > source[src]"), "src", this.baseURI, this.options, "video", this.cssVariables, this.styles, this.batchRequest));
1291
+ await Promise.all(resourcePromises);
1292
+ if (this.options.saveFavicon) {
1293
+ ProcessorHelper.processShortcutIcons(this.doc);
1294
+ }
1295
+ }
1296
+
1297
+ async processScripts() {
1298
+ await Promise.all(Array.from(this.doc.querySelectorAll("script[src]")).map(async element => {
1299
+ let resourceURL;
1300
+ let scriptSrc;
1301
+ scriptSrc = element.getAttribute("src");
1302
+ if (this.options.saveOriginalURLs && !isDataURL(scriptSrc)) {
1303
+ element.setAttribute("data-sf-original-src", scriptSrc);
1304
+ }
1305
+ element.removeAttribute("integrity");
1306
+ if (!this.options.blockScripts) {
1307
+ element.textContent = "";
1308
+ try {
1309
+ resourceURL = util.resolveURL(scriptSrc, this.baseURI);
1310
+ } catch (error) {
1311
+ // ignored
1312
+ }
1313
+ if (testValidURL(resourceURL)) {
1314
+ element.removeAttribute("src");
1315
+ const content = await util.getContent(resourceURL, {
1316
+ asBinary: true,
1317
+ charset: this.charset != UTF8_CHARSET && this.charset,
1318
+ maxResourceSize: this.options.maxResourceSize,
1319
+ maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
1320
+ frameId: this.options.windowId,
1321
+ resourceReferrer: this.options.resourceReferrer,
1322
+ baseURI: this.options.baseURI,
1323
+ blockMixedContent: this.options.blockMixedContent,
1324
+ expectedType: "script",
1325
+ acceptHeaders: this.options.acceptHeaders,
1326
+ networkTimeout: this.options.networkTimeout
1327
+ });
1328
+ content.data = getUpdatedResourceContent(resourceURL, content, this.options);
1329
+ element.setAttribute("src", content.data);
1330
+ if (element.getAttribute("async") == "async" || element.getAttribute(util.ASYNC_SCRIPT_ATTRIBUTE_NAME) == "") {
1331
+ element.setAttribute("async", "");
1332
+ }
1333
+ }
1334
+ } else {
1335
+ element.removeAttribute("src");
1336
+ }
1337
+ this.stats.add("processed", "scripts", 1);
1338
+ }));
1339
+ }
1340
+
1341
+ removeAlternativeImages() {
1342
+ util.removeAlternativeImages(this.doc);
1343
+ }
1344
+
1345
+ async removeAlternativeFonts() {
1346
+ await util.removeAlternativeFonts(this.doc, this.stylesheets, this.options.fontDeclarations, this.options.fontTests);
1347
+ }
1348
+
1349
+ async processFrames() {
1350
+ if (this.options.frames) {
1351
+ const frameElements = Array.from(this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]"));
1352
+ await Promise.all(frameElements.map(async frameElement => {
1353
+ const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
1354
+ if (frameWindowId) {
1355
+ const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
1356
+ if (frameData) {
1357
+ this.options.frames = this.options.frames.filter(frame => frame.windowId != frameWindowId);
1358
+ if (frameData.runner && frameElement.getAttribute(util.HIDDEN_FRAME_ATTRIBUTE_NAME) != "") {
1359
+ this.stats.add("processed", "frames", 1);
1360
+ await frameData.runner.run();
1361
+ const pageData = await frameData.runner.getPageData();
1362
+ frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
1363
+ let sandbox = "allow-popups allow-top-navigation allow-top-navigation-by-user-activation";
1364
+ if (pageData.content.match(NOSCRIPT_TAG_FOUND) || pageData.content.match(CANVAS_TAG_FOUND) || pageData.content.match(SCRIPT_TAG_FOUND)) {
1365
+ sandbox += " allow-scripts allow-same-origin";
1366
+ }
1367
+ frameElement.setAttribute("sandbox", sandbox);
1368
+ if (frameElement.tagName == "OBJECT") {
1369
+ frameElement.setAttribute("data", "data:text/html," + pageData.content);
1370
+ } else {
1371
+ if (frameElement.tagName == "FRAME") {
1372
+ frameElement.setAttribute("src", "data:text/html," + pageData.content.replace(/%/g, "%25").replace(/#/g, "%23"));
1373
+ } else {
1374
+ frameElement.setAttribute("srcdoc", pageData.content);
1375
+ frameElement.removeAttribute("src");
1376
+ }
1377
+ }
1378
+ this.stats.addAll(pageData);
1379
+ } else {
1380
+ frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
1381
+ this.stats.add("discarded", "frames", 1);
1382
+ }
1383
+ }
1384
+ }
1385
+ }));
1386
+ }
1387
+ }
1388
+
1389
+ async processHtmlImports() {
1390
+ const linkElements = Array.from(this.doc.querySelectorAll("link[rel=import]"));
1391
+ await Promise.all(linkElements.map(async linkElement => {
1392
+ const attributeValue = linkElement.getAttribute(util.HTML_IMPORT_ATTRIBUTE_NAME);
1393
+ if (attributeValue) {
1394
+ const importData = this.options.imports[Number(attributeValue)];
1395
+ if (importData.runner) {
1396
+ this.stats.add("processed", "HTML imports", 1);
1397
+ await importData.runner.run();
1398
+ const pageData = await importData.runner.getPageData();
1399
+ linkElement.removeAttribute(util.HTML_IMPORT_ATTRIBUTE_NAME);
1400
+ linkElement.setAttribute("href", "data:text/html," + pageData.content);
1401
+ this.stats.addAll(pageData);
1402
+ } else {
1403
+ this.stats.add("discarded", "HTML imports", 1);
1404
+ }
1405
+ }
1406
+ }));
1407
+ }
1408
+
1409
+ replaceStylesheets() {
1410
+ this.doc.querySelectorAll("style").forEach(styleElement => {
1411
+ const stylesheetInfo = this.stylesheets.get(styleElement);
1412
+ if (stylesheetInfo) {
1413
+ this.stylesheets.delete(styleElement);
1414
+ let stylesheetContent = cssTree.generate(stylesheetInfo.stylesheet);
1415
+ if (this.options.saveOriginalURLs) {
1416
+ stylesheetContent = replaceOriginalURLs(stylesheetContent);
1417
+ }
1418
+ styleElement.textContent = stylesheetContent;
1419
+ if (stylesheetInfo.mediaText) {
1420
+ styleElement.media = stylesheetInfo.mediaText;
1421
+ }
1422
+ } else {
1423
+ styleElement.remove();
1424
+ }
1425
+ });
1426
+ this.doc.querySelectorAll("link[rel*=stylesheet]").forEach(linkElement => {
1427
+ const stylesheetInfo = this.stylesheets.get(linkElement);
1428
+ if (stylesheetInfo) {
1429
+ this.stylesheets.delete(linkElement);
1430
+ const styleElement = this.doc.createElement("style");
1431
+ if (stylesheetInfo.mediaText) {
1432
+ styleElement.media = stylesheetInfo.mediaText;
1433
+ }
1434
+ let stylesheetContent = cssTree.generate(stylesheetInfo.stylesheet);
1435
+ if (this.options.saveOriginalURLs) {
1436
+ stylesheetContent = replaceOriginalURLs(stylesheetContent);
1437
+ styleElement.setAttribute("data-sf-original-href", linkElement.getAttribute("data-sf-original-href"));
1438
+ }
1439
+ styleElement.textContent = stylesheetContent;
1440
+ linkElement.parentElement.replaceChild(styleElement, linkElement);
1441
+ } else {
1442
+ linkElement.remove();
1443
+ }
1444
+ });
1445
+ }
1446
+
1447
+ replaceStyleAttributes() {
1448
+ this.doc.querySelectorAll("[style]").forEach(element => {
1449
+ const declarations = this.styles.get(element);
1450
+ if (declarations) {
1451
+ this.styles.delete(element);
1452
+ let styleContent = cssTree.generate(declarations);
1453
+ if (this.options.saveOriginalURLs) {
1454
+ styleContent = replaceOriginalURLs(styleContent);
1455
+ }
1456
+ element.setAttribute("style", styleContent);
1457
+ } else {
1458
+ element.setAttribute("style", "");
1459
+ }
1460
+ });
1461
+ }
1462
+
1463
+ insertVariables() {
1464
+ if (this.cssVariables.size) {
1465
+ const styleElement = this.doc.createElement("style");
1466
+ const firstStyleElement = this.doc.head.querySelector("style");
1467
+ if (firstStyleElement) {
1468
+ this.doc.head.insertBefore(styleElement, firstStyleElement);
1469
+ } else {
1470
+ this.doc.head.appendChild(styleElement);
1471
+ }
1472
+ let stylesheetContent = "";
1473
+ this.cssVariables.forEach(({ content, url }, indexResource) => {
1474
+ this.cssVariables.delete(indexResource);
1475
+ if (stylesheetContent) {
1476
+ stylesheetContent += ";";
1477
+ }
1478
+ stylesheetContent += `${SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource}: `;
1479
+ if (this.options.saveOriginalURLs) {
1480
+ stylesheetContent += `/* original URL: ${url} */ `;
1481
+ }
1482
+ stylesheetContent += `url("${content}")`;
1483
+ });
1484
+ styleElement.textContent = ":root{" + stylesheetContent + "}";
1485
+ }
1486
+ }
1487
+
1488
+ compressHTML() {
1489
+ let size;
1490
+ if (this.options.displayStats) {
1491
+ size = util.getContentSize(this.doc.documentElement.outerHTML);
1492
+ }
1493
+ util.minifyHTML(this.doc, { PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME: util.PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME });
1494
+ if (this.options.displayStats) {
1495
+ this.stats.add("discarded", "HTML bytes", size - util.getContentSize(this.doc.documentElement.outerHTML));
1496
+ }
1497
+ }
1498
+
1499
+ cleanupPage() {
1500
+ this.doc.querySelectorAll("base").forEach(element => element.remove());
1501
+ const metaCharset = this.doc.head.querySelector("meta[charset]");
1502
+ if (metaCharset) {
1503
+ this.doc.head.insertBefore(metaCharset, this.doc.head.firstChild);
1504
+ if (this.doc.head.querySelectorAll("*").length == 1 && this.doc.body.childNodes.length == 0) {
1505
+ this.doc.head.querySelector("meta[charset]").remove();
1506
+ }
1507
+ }
1508
+ }
1509
+
1510
+ resetZoomLevel() {
1511
+ const transform = this.doc.documentElement.style.getPropertyValue("-sf-transform");
1512
+ const transformPriority = this.doc.documentElement.style.getPropertyPriority("-sf-transform");
1513
+ const transformOrigin = this.doc.documentElement.style.getPropertyValue("-sf-transform-origin");
1514
+ const transformOriginPriority = this.doc.documentElement.style.getPropertyPriority("-sf-transform-origin");
1515
+ const minHeight = this.doc.documentElement.style.getPropertyValue("-sf-min-height");
1516
+ const minHeightPriority = this.doc.documentElement.style.getPropertyPriority("-sf-min-height");
1517
+ this.doc.documentElement.style.setProperty("transform", transform, transformPriority);
1518
+ this.doc.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
1519
+ this.doc.documentElement.style.setProperty("min-height", minHeight, minHeightPriority);
1520
+ this.doc.documentElement.style.removeProperty("-sf-transform");
1521
+ this.doc.documentElement.style.removeProperty("-sf-transform-origin");
1522
+ this.doc.documentElement.style.removeProperty("-sf-min-height");
1523
+ }
1524
+
1525
+ async insertMAFFMetaData() {
1526
+ const maffMetaData = await this.maffMetaDataPromise;
1527
+ if (maffMetaData && maffMetaData.content) {
1528
+ const NAMESPACE_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
1529
+ const maffDoc = util.parseXMLContent(maffMetaData.content);
1530
+ const originalURLElement = maffDoc.querySelector("RDF > Description > originalurl");
1531
+ const archiveTimeElement = maffDoc.querySelector("RDF > Description > archivetime");
1532
+ if (originalURLElement) {
1533
+ this.options.saveUrl = originalURLElement.getAttributeNS(NAMESPACE_RDF, "resource");
1534
+ }
1535
+ if (archiveTimeElement) {
1536
+ const value = archiveTimeElement.getAttributeNS(NAMESPACE_RDF, "resource");
1537
+ if (value) {
1538
+ const date = new Date(value);
1539
+ if (!isNaN(date.getTime())) {
1540
+ this.options.saveDate = new Date(value);
1541
+ }
1542
+ }
1543
+ }
1544
+ }
1545
+ }
1546
+
1547
+ async setDocInfo() {
1548
+ const titleElement = this.doc.querySelector("title");
1549
+ const descriptionElement = this.doc.querySelector("meta[name=description]");
1550
+ const authorElement = this.doc.querySelector("meta[name=author]");
1551
+ const creatorElement = this.doc.querySelector("meta[name=creator]");
1552
+ const publisherElement = this.doc.querySelector("meta[name=publisher]");
1553
+ const headingElement = this.doc.querySelector("h1");
1554
+ this.options.title = titleElement ? titleElement.textContent.trim() : "";
1555
+ this.options.info = {
1556
+ description: descriptionElement && descriptionElement.content ? descriptionElement.content.trim() : "",
1557
+ lang: this.doc.documentElement.lang,
1558
+ author: authorElement && authorElement.content ? authorElement.content.trim() : "",
1559
+ creator: creatorElement && creatorElement.content ? creatorElement.content.trim() : "",
1560
+ publisher: publisherElement && publisherElement.content ? publisherElement.content.trim() : "",
1561
+ heading: headingElement && headingElement.textContent ? headingElement.textContent.trim() : ""
1562
+ };
1563
+ this.options.infobarContent = await ProcessorHelper.evalTemplate(this.options.infobarTemplate, this.options, null, true);
1564
+ }
1565
+ }
1566
+
1567
+ // ---------------
1568
+ // ProcessorHelper
1569
+ // ---------------
1570
+ const DATA_URI_PREFIX = "data:";
1571
+ const ABOUT_BLANK_URI = "about:blank";
1572
+ const REGEXP_URL_HASH = /(#.+?)$/;
1573
+ const SINGLE_FILE_VARIABLE_NAME_PREFIX = "--sf-img-";
1574
+ const SINGLE_FILE_VARIABLE_MAX_SIZE = 512 * 1024;
1575
+
1576
+ class ProcessorHelper {
1577
+ static async evalTemplate(template = "", options, content, dontReplaceSlash) {
1578
+ const url = util.parseURL(options.saveUrl);
1579
+ template = await evalTemplateVariable(template, "page-title", () => options.title || "No title", dontReplaceSlash, options.filenameReplacementCharacter);
1580
+ template = await evalTemplateVariable(template, "page-heading", () => options.info.heading || "No heading", dontReplaceSlash, options.filenameReplacementCharacter);
1581
+ template = await evalTemplateVariable(template, "page-language", () => options.info.lang || "No language", dontReplaceSlash, options.filenameReplacementCharacter);
1582
+ template = await evalTemplateVariable(template, "page-description", () => options.info.description || "No description", dontReplaceSlash, options.filenameReplacementCharacter);
1583
+ template = await evalTemplateVariable(template, "page-author", () => options.info.author || "No author", dontReplaceSlash, options.filenameReplacementCharacter);
1584
+ template = await evalTemplateVariable(template, "page-creator", () => options.info.creator || "No creator", dontReplaceSlash, options.filenameReplacementCharacter);
1585
+ template = await evalTemplateVariable(template, "page-publisher", () => options.info.publisher || "No publisher", dontReplaceSlash, options.filenameReplacementCharacter);
1586
+ await evalDate(options.saveDate);
1587
+ await evalDate(options.visitDate, "visit-");
1588
+ template = await evalTemplateVariable(template, "url-hash", () => url.hash.substring(1) || "No hash", dontReplaceSlash, options.filenameReplacementCharacter);
1589
+ template = await evalTemplateVariable(template, "url-host", () => url.host.replace(/\/$/, "") || "No host", dontReplaceSlash, options.filenameReplacementCharacter);
1590
+ template = await evalTemplateVariable(template, "url-hostname", () => url.hostname.replace(/\/$/, "") || "No hostname", dontReplaceSlash, options.filenameReplacementCharacter);
1591
+ const urlHref = decode(url.href);
1592
+ template = await evalTemplateVariable(template, "url-href", () => urlHref || "No href", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
1593
+ template = await evalTemplateVariable(template, "url-href-digest-sha-1", urlHref ? async () => util.digest("SHA-1", urlHref) : "No href", dontReplaceSlash, options.filenameReplacementCharacter);
1594
+ template = await evalTemplateVariable(template, "url-href-flat", () => decode(url.href) || "No href", false, options.filenameReplacementCharacter);
1595
+ template = await evalTemplateVariable(template, "url-referrer", () => decode(options.referrer) || "No referrer", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
1596
+ template = await evalTemplateVariable(template, "url-referrer-flat", () => decode(options.referrer) || "No referrer", false, options.filenameReplacementCharacter);
1597
+ template = await evalTemplateVariable(template, "url-password", () => url.password || "No password", dontReplaceSlash, options.filenameReplacementCharacter);
1598
+ template = await evalTemplateVariable(template, "url-pathname", () => decode(url.pathname).replace(/^\//, "").replace(/\/$/, "") || "No pathname", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
1599
+ template = await evalTemplateVariable(template, "url-pathname-flat", () => decode(url.pathname) || "No pathname", false, options.filenameReplacementCharacter);
1600
+ template = await evalTemplateVariable(template, "url-port", () => url.port || "No port", dontReplaceSlash, options.filenameReplacementCharacter);
1601
+ template = await evalTemplateVariable(template, "url-protocol", () => url.protocol || "No protocol", dontReplaceSlash, options.filenameReplacementCharacter);
1602
+ template = await evalTemplateVariable(template, "url-search", () => url.search.substring(1) || "No search", dontReplaceSlash, options.filenameReplacementCharacter);
1603
+ const params = util.getSearchParams(url.search);
1604
+ for (const [name, value] of params) {
1605
+ template = await evalTemplateVariable(template, "url-search-" + name, () => value || "", dontReplaceSlash, options.filenameReplacementCharacter);
1606
+ }
1607
+ template = template.replace(/{\s*url-search-[^}\s]*\s*}/gi, "");
1608
+ template = await evalTemplateVariable(template, "url-username", () => url.username || "No username", dontReplaceSlash, options.filenameReplacementCharacter);
1609
+ template = await evalTemplateVariable(template, "tab-id", () => String(options.tabId || "No tab id"), dontReplaceSlash, options.filenameReplacementCharacter);
1610
+ template = await evalTemplateVariable(template, "tab-index", () => String(options.tabIndex || "No tab index"), dontReplaceSlash, options.filenameReplacementCharacter);
1611
+ template = await evalTemplateVariable(template, "url-last-segment", () => decode(getLastSegment(url, options.filenameReplacementCharacter)) || "No last segment", dontReplaceSlash, options.filenameReplacementCharacter);
1612
+ if (content) {
1613
+ template = await evalTemplateVariable(template, "digest-sha-256", async () => util.digest("SHA-256", content), dontReplaceSlash, options.filenameReplacementCharacter);
1614
+ template = await evalTemplateVariable(template, "digest-sha-384", async () => util.digest("SHA-384", content), dontReplaceSlash, options.filenameReplacementCharacter);
1615
+ template = await evalTemplateVariable(template, "digest-sha-512", async () => util.digest("SHA-512", content), dontReplaceSlash, options.filenameReplacementCharacter);
1616
+ }
1617
+ const bookmarkFolder = (options.bookmarkFolders && options.bookmarkFolders.join("/")) || "";
1618
+ template = await evalTemplateVariable(template, "bookmark-pathname", () => bookmarkFolder, dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
1619
+ template = await evalTemplateVariable(template, "bookmark-pathname-flat", () => bookmarkFolder, false, options.filenameReplacementCharacter);
1620
+ template = await evalTemplateVariable(template, "profile-name", () => options.profileName, dontReplaceSlash, options.filenameReplacementCharacter);
1621
+ return template.trim();
1622
+
1623
+ function decode(value) {
1624
+ try {
1625
+ return decodeURI(value);
1626
+ } catch (error) {
1627
+ return value;
1628
+ }
1629
+ }
1630
+
1631
+ async function evalDate(date, prefix = "") {
1632
+ if (date) {
1633
+ template = await evalTemplateVariable(template, prefix + "datetime-iso", () => date.toISOString(), dontReplaceSlash, options.filenameReplacementCharacter);
1634
+ template = await evalTemplateVariable(template, prefix + "date-iso", () => date.toISOString().split("T")[0], dontReplaceSlash, options.filenameReplacementCharacter);
1635
+ template = await evalTemplateVariable(template, prefix + "time-iso", () => date.toISOString().split("T")[1].split("Z")[0], dontReplaceSlash, options.filenameReplacementCharacter);
1636
+ template = await evalTemplateVariable(template, prefix + "date-locale", () => date.toLocaleDateString(), dontReplaceSlash, options.filenameReplacementCharacter);
1637
+ template = await evalTemplateVariable(template, prefix + "time-locale", () => date.toLocaleTimeString(), dontReplaceSlash, options.filenameReplacementCharacter);
1638
+ template = await evalTemplateVariable(template, prefix + "day-locale", () => String(date.getDate()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1639
+ template = await evalTemplateVariable(template, prefix + "month-locale", () => String(date.getMonth() + 1).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1640
+ template = await evalTemplateVariable(template, prefix + "year-locale", () => String(date.getFullYear()), dontReplaceSlash, options.filenameReplacementCharacter);
1641
+ template = await evalTemplateVariable(template, prefix + "datetime-locale", () => date.toLocaleString(), dontReplaceSlash, options.filenameReplacementCharacter);
1642
+ template = await evalTemplateVariable(template, prefix + "datetime-utc", () => date.toUTCString(), dontReplaceSlash, options.filenameReplacementCharacter);
1643
+ template = await evalTemplateVariable(template, prefix + "day-utc", () => String(date.getUTCDate()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1644
+ template = await evalTemplateVariable(template, prefix + "month-utc", () => String(date.getUTCMonth() + 1).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1645
+ template = await evalTemplateVariable(template, prefix + "year-utc", () => String(date.getUTCFullYear()), dontReplaceSlash, options.filenameReplacementCharacter);
1646
+ template = await evalTemplateVariable(template, prefix + "hours-locale", () => String(date.getHours()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1647
+ template = await evalTemplateVariable(template, prefix + "minutes-locale", () => String(date.getMinutes()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1648
+ template = await evalTemplateVariable(template, prefix + "seconds-locale", () => String(date.getSeconds()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1649
+ template = await evalTemplateVariable(template, prefix + "hours-utc", () => String(date.getUTCHours()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1650
+ template = await evalTemplateVariable(template, prefix + "minutes-utc", () => String(date.getUTCMinutes()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1651
+ template = await evalTemplateVariable(template, prefix + "seconds-utc", () => String(date.getUTCSeconds()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
1652
+ template = await evalTemplateVariable(template, prefix + "time-ms", () => String(date.getTime()), dontReplaceSlash, options.filenameReplacementCharacter);
1653
+ }
1654
+ }
1655
+ }
1656
+
1657
+ static setBackgroundImage(element, url, style) {
1658
+ element.style.setProperty("background-blend-mode", "normal", "important");
1659
+ element.style.setProperty("background-clip", "content-box", "important");
1660
+ element.style.setProperty("background-position", style && style["background-position"] ? style["background-position"] : "center", "important");
1661
+ element.style.setProperty("background-color", style && style["background-color"] ? style["background-color"] : "transparent", "important");
1662
+ element.style.setProperty("background-image", url, "important");
1663
+ element.style.setProperty("background-size", style && style["background-size"] ? style["background-size"] : "100% 100%", "important");
1664
+ element.style.setProperty("background-origin", "content-box", "important");
1665
+ element.style.setProperty("background-repeat", "no-repeat", "important");
1666
+ }
1667
+
1668
+ static processShortcutIcons(doc) {
1669
+ let shortcutIcon = findShortcutIcon(Array.from(doc.querySelectorAll("link[href][rel=\"icon\"], link[href][rel=\"shortcut icon\"]")));
1670
+ if (!shortcutIcon) {
1671
+ shortcutIcon = findShortcutIcon(Array.from(doc.querySelectorAll("link[href][rel*=\"icon\"]")));
1672
+ if (shortcutIcon) {
1673
+ shortcutIcon.rel = "icon";
1674
+ }
1675
+ }
1676
+ if (shortcutIcon) {
1677
+ doc.querySelectorAll("link[href][rel*=\"icon\"]").forEach(linkElement => {
1678
+ if (linkElement != shortcutIcon) {
1679
+ linkElement.remove();
1680
+ }
1681
+ });
1682
+ }
1683
+ }
1684
+
1685
+ static removeSingleLineCssComments(stylesheet) {
1686
+ const removedRules = [];
1687
+ for (let cssRule = stylesheet.children.head; cssRule; cssRule = cssRule.next) {
1688
+ const ruleData = cssRule.data;
1689
+ if (ruleData.type == "Raw" && ruleData.value && ruleData.value.trim().startsWith("//")) {
1690
+ removedRules.push(cssRule);
1691
+ }
1692
+ }
1693
+ removedRules.forEach(cssRule => stylesheet.children.remove(cssRule));
1694
+ }
1695
+
1696
+ static async resolveImportURLs(stylesheetContent, baseURI, options, workStylesheet, importedStyleSheets = new Set()) {
1697
+ stylesheetContent = ProcessorHelper.resolveStylesheetURLs(stylesheetContent, baseURI, workStylesheet, options.saveOriginalURLs);
1698
+ const imports = getImportFunctions(stylesheetContent);
1699
+ await Promise.all(imports.map(async cssImport => {
1700
+ const match = matchImport(cssImport);
1701
+ if (match) {
1702
+ const regExpCssImport = getRegExp(cssImport);
1703
+ let resourceURL = normalizeURL(match.resourceURL);
1704
+ if (!testIgnoredPath(resourceURL) && testValidPath(resourceURL)) {
1705
+ try {
1706
+ resourceURL = util.resolveURL(match.resourceURL, baseURI);
1707
+ } catch (error) {
1708
+ // ignored
1709
+ }
1710
+ if (testValidURL(resourceURL) && !importedStyleSheets.has(resourceURL)) {
1711
+ const content = await getStylesheetContent(resourceURL);
1712
+ resourceURL = content.resourceURL;
1713
+ content.data = getUpdatedResourceContent(resourceURL, content, options);
1714
+ if (content.data && content.data.match(/^<!doctype /i)) {
1715
+ content.data = "";
1716
+ }
1717
+ let importedStylesheetContent = removeCssComments(content.data);
1718
+ if (options.compressCSS) {
1719
+ importedStylesheetContent = util.compressCSS(importedStylesheetContent);
1720
+ }
1721
+ importedStylesheetContent = wrapMediaQuery(importedStylesheetContent, match.media);
1722
+ if (stylesheetContent.includes(cssImport)) {
1723
+ const ancestorStyleSheets = new Set(importedStyleSheets);
1724
+ ancestorStyleSheets.add(resourceURL);
1725
+ importedStylesheetContent = await ProcessorHelper.resolveImportURLs(importedStylesheetContent, resourceURL, options, workStylesheet, ancestorStyleSheets);
1726
+ workStylesheet.textContent = importedStylesheetContent;
1727
+ if ((workStylesheet.sheet && workStylesheet.sheet.cssRules.length) || (!workStylesheet.sheet && importedStylesheetContent)) {
1728
+ stylesheetContent = stylesheetContent.replace(regExpCssImport, importedStylesheetContent);
1729
+ } else {
1730
+ stylesheetContent = stylesheetContent.replace(regExpCssImport, "");
1731
+ }
1732
+ }
1733
+ } else {
1734
+ stylesheetContent = stylesheetContent.replace(regExpCssImport, "");
1735
+ }
1736
+ } else {
1737
+ stylesheetContent = stylesheetContent.replace(regExpCssImport, "");
1738
+ }
1739
+ }
1740
+ }));
1741
+ return stylesheetContent;
1742
+
1743
+ async function getStylesheetContent(resourceURL) {
1744
+ const content = await util.getContent(resourceURL, {
1745
+ maxResourceSize: options.maxResourceSize,
1746
+ maxResourceSizeEnabled: options.maxResourceSizeEnabled,
1747
+ validateTextContentType: true,
1748
+ frameId: options.frameId,
1749
+ charset: options.charset,
1750
+ resourceReferrer: options.resourceReferrer,
1751
+ baseURI: options.baseURI,
1752
+ blockMixedContent: options.blockMixedContent,
1753
+ expectedType: "stylesheet",
1754
+ acceptHeaders: options.acceptHeaders,
1755
+ networkTimeout: options.networkTimeout
1756
+ });
1757
+ if (!(matchCharsetEquals(content.data, content.charset) || matchCharsetEquals(content.data, options.charset))) {
1758
+ options = Object.assign({}, options, { charset: getCharset(content.data) });
1759
+ return util.getContent(resourceURL, {
1760
+ maxResourceSize: options.maxResourceSize,
1761
+ maxResourceSizeEnabled: options.maxResourceSizeEnabled,
1762
+ validateTextContentType: true,
1763
+ frameId: options.frameId,
1764
+ charset: options.charset,
1765
+ resourceReferrer: options.resourceReferrer,
1766
+ baseURI: options.baseURI,
1767
+ blockMixedContent: options.blockMixedContent,
1768
+ expectedType: "stylesheet",
1769
+ acceptHeaders: options.acceptHeaders,
1770
+ networkTimeout: options.networkTimeout
1771
+ });
1772
+ } else {
1773
+ return content;
1774
+ }
1775
+ }
1776
+ }
1777
+
1778
+ static resolveStylesheetURLs(stylesheetContent, baseURI, workStylesheet, saveOriginalURLs) {
1779
+ const urlFunctions = getUrlFunctions(stylesheetContent, true);
1780
+ if (saveOriginalURLs) {
1781
+ stylesheetContent = addOriginalURLs(stylesheetContent);
1782
+ }
1783
+ urlFunctions.map(urlFunction => {
1784
+ const originalResourceURL = matchURL(urlFunction);
1785
+ let resourceURL = normalizeURL(originalResourceURL);
1786
+ workStylesheet.textContent = "tmp { content:\"" + resourceURL + "\"}";
1787
+ if (workStylesheet.sheet && workStylesheet.sheet.cssRules) {
1788
+ resourceURL = util.removeQuotes(workStylesheet.sheet.cssRules[0].style.getPropertyValue("content"));
1789
+ }
1790
+ if (!testIgnoredPath(resourceURL)) {
1791
+ if (!resourceURL || testValidPath(resourceURL)) {
1792
+ let resolvedURL;
1793
+ if (!originalResourceURL.startsWith("#")) {
1794
+ try {
1795
+ resolvedURL = util.resolveURL(resourceURL, baseURI);
1796
+ } catch (error) {
1797
+ // ignored
1798
+ }
1799
+ }
1800
+ if (testValidURL(resolvedURL) && originalResourceURL != resolvedURL && stylesheetContent.includes(urlFunction)) {
1801
+ try {
1802
+ stylesheetContent = stylesheetContent.replace(getRegExp(urlFunction), originalResourceURL ? urlFunction.replace(originalResourceURL, resolvedURL) : "url(" + resolvedURL + ")");
1803
+ } catch (error) {
1804
+ // ignored
1805
+ }
1806
+ }
1807
+ } else {
1808
+ let newUrlFunction;
1809
+ if (originalResourceURL) {
1810
+ newUrlFunction = urlFunction.replace(originalResourceURL, util.EMPTY_RESOURCE);
1811
+ } else {
1812
+ newUrlFunction = "url(" + util.EMPTY_RESOURCE + ")";
1813
+ }
1814
+ stylesheetContent = stylesheetContent.replace(getRegExp(urlFunction), newUrlFunction);
1815
+ }
1816
+ }
1817
+ });
1818
+ return stylesheetContent;
1819
+ }
1820
+
1821
+ static async resolveLinkStylesheetURLs(resourceURL, baseURI, options, workStylesheet) {
1822
+ resourceURL = normalizeURL(resourceURL);
1823
+ if (resourceURL && resourceURL != baseURI && resourceURL != ABOUT_BLANK_URI) {
1824
+ const content = await util.getContent(resourceURL, {
1825
+ maxResourceSize: options.maxResourceSize,
1826
+ maxResourceSizeEnabled: options.maxResourceSizeEnabled,
1827
+ charset: options.charset,
1828
+ frameId: options.frameId,
1829
+ resourceReferrer: options.resourceReferrer,
1830
+ validateTextContentType: true,
1831
+ baseURI: baseURI,
1832
+ blockMixedContent: options.blockMixedContent,
1833
+ expectedType: "stylesheet",
1834
+ acceptHeaders: options.acceptHeaders,
1835
+ networkTimeout: options.networkTimeout
1836
+ });
1837
+ if (!(matchCharsetEquals(content.data, content.charset) || matchCharsetEquals(content.data, options.charset))) {
1838
+ options = Object.assign({}, options, { charset: getCharset(content.data) });
1839
+ return ProcessorHelper.resolveLinkStylesheetURLs(resourceURL, baseURI, options, workStylesheet);
1840
+ }
1841
+ resourceURL = content.resourceURL;
1842
+ content.data = getUpdatedResourceContent(content.resourceURL, content, options);
1843
+ if (content.data && content.data.match(/^<!doctype /i)) {
1844
+ content.data = "";
1845
+ }
1846
+ let stylesheetContent = removeCssComments(content.data);
1847
+ if (options.compressCSS) {
1848
+ stylesheetContent = util.compressCSS(stylesheetContent);
1849
+ }
1850
+ stylesheetContent = await ProcessorHelper.resolveImportURLs(stylesheetContent, resourceURL, options, workStylesheet);
1851
+ return stylesheetContent;
1852
+ }
1853
+ }
1854
+
1855
+ static async processStylesheet(cssRules, baseURI, options, cssVariables, batchRequest) {
1856
+ const promises = [];
1857
+ const removedRules = [];
1858
+ for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
1859
+ const ruleData = cssRule.data;
1860
+ if (ruleData.type == "Atrule" && ruleData.name == "charset") {
1861
+ removedRules.push(cssRule);
1862
+ } else if (ruleData.block && ruleData.block.children) {
1863
+ if (ruleData.type == "Rule") {
1864
+ promises.push(this.processStyle(ruleData.block.children.toArray(), baseURI, options, cssVariables, batchRequest));
1865
+ } else if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports")) {
1866
+ promises.push(this.processStylesheet(ruleData.block.children, baseURI, options, cssVariables, batchRequest));
1867
+ } else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
1868
+ promises.push(processFontFaceRule(ruleData));
1869
+ }
1870
+ }
1871
+ }
1872
+ removedRules.forEach(cssRule => cssRules.remove(cssRule));
1873
+ await Promise.all(promises);
1874
+
1875
+ async function processFontFaceRule(ruleData) {
1876
+ await Promise.all(ruleData.block.children.toArray().map(async declaration => {
1877
+ if (declaration.type == "Declaration" && declaration.value.children) {
1878
+ const urlFunctions = getUrlFunctions(getCSSValue(declaration.value), true);
1879
+ await Promise.all(urlFunctions.map(async urlFunction => {
1880
+ const originalResourceURL = matchURL(urlFunction);
1881
+ if (!options.blockFonts) {
1882
+ const resourceURL = normalizeURL(originalResourceURL);
1883
+ if (!testIgnoredPath(resourceURL) && testValidURL(resourceURL)) {
1884
+ let { content } = await batchRequest.addURL(resourceURL,
1885
+ { asBinary: true, expectedType: "font", baseURI, blockMixedContent: options.blockMixedContent });
1886
+ let resourceURLs = options.fontDeclarations.get(declaration);
1887
+ if (!resourceURLs) {
1888
+ resourceURLs = [];
1889
+ options.fontDeclarations.set(declaration, resourceURLs);
1890
+ }
1891
+ resourceURLs.push(resourceURL);
1892
+ replaceURLs(declaration, originalResourceURL, content);
1893
+ }
1894
+ } else {
1895
+ replaceURLs(declaration, originalResourceURL, util.EMPTY_RESOURCE);
1896
+ }
1897
+ }));
1898
+ }
1899
+ }));
1900
+
1901
+ function replaceURLs(declaration, oldURL, newURL) {
1902
+ declaration.value.children.forEach(token => {
1903
+ if (token.type == "Url" && util.removeQuotes(getCSSValue(token.value)) == oldURL) {
1904
+ token.value = newURL;
1905
+ }
1906
+ });
1907
+ }
1908
+ }
1909
+ }
1910
+
1911
+ static async processStyle(declarations, baseURI, options, cssVariables, batchRequest) {
1912
+ await Promise.all(declarations.map(async declaration => {
1913
+ if (declaration.value && !declaration.value.children && declaration.value.type == "Raw") {
1914
+ try {
1915
+ declaration.value = cssTree.parse(declaration.value.value, { context: "value" });
1916
+ } catch (error) {
1917
+ // ignored
1918
+ }
1919
+ }
1920
+ if (declaration.type == "Declaration" && declaration.value.children) {
1921
+ const urlFunctions = getUrlFunctions(getCSSValue(declaration.value));
1922
+ await Promise.all(urlFunctions.map(async urlFunction => {
1923
+ const originalResourceURL = matchURL(urlFunction);
1924
+ if (!options.blockImages) {
1925
+ const resourceURL = normalizeURL(originalResourceURL);
1926
+ if (!testIgnoredPath(resourceURL) && testValidURL(resourceURL)) {
1927
+ let { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL,
1928
+ { asBinary: true, expectedType: "image", groupDuplicates: options.groupDuplicateImages });
1929
+ let variableDefined;
1930
+ const tokens = [];
1931
+ findURLToken(originalResourceURL, declaration.value.children, (token, parent, rootFunction) => {
1932
+ if (!originalResourceURL.startsWith("#")) {
1933
+ if (duplicate && options.groupDuplicateImages && rootFunction && util.getContentSize(content) < SINGLE_FILE_VARIABLE_MAX_SIZE) {
1934
+ const value = cssTree.parse("var(" + SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource + ")", { context: "value" }).children.head;
1935
+ tokens.push({ parent, token, value });
1936
+ variableDefined = true;
1937
+ } else {
1938
+ token.data.value = content;
1939
+ }
1940
+ }
1941
+ });
1942
+ if (variableDefined) {
1943
+ cssVariables.set(indexResource, { content, url: originalResourceURL });
1944
+ tokens.forEach(({ parent, token, value }) => parent.replace(token, value));
1945
+ }
1946
+ }
1947
+ } else {
1948
+ findURLToken(originalResourceURL, declaration.value.children, token => token.data.value = util.EMPTY_RESOURCE);
1949
+ }
1950
+ }));
1951
+ }
1952
+ }));
1953
+
1954
+ function findURLToken(url, children, callback, depth = 0) {
1955
+ for (let token = children.head; token; token = token.next) {
1956
+ if (token.data.children) {
1957
+ findURLToken(url, token.data.children, callback, depth + 1);
1958
+ }
1959
+ if (token.data.type == "Url" && util.removeQuotes(getCSSValue(token.data.value)) == url) {
1960
+ callback(token, children, depth == 0);
1961
+ }
1962
+ }
1963
+ }
1964
+ }
1965
+
1966
+ static async processAttribute(resourceElements, attributeName, baseURI, options, expectedType, cssVariables, styles, batchRequest, processDuplicates, removeElementIfMissing) {
1967
+ await Promise.all(Array.from(resourceElements).map(async resourceElement => {
1968
+ let resourceURL = resourceElement.getAttribute(attributeName);
1969
+ if (resourceURL != null) {
1970
+ resourceURL = normalizeURL(resourceURL);
1971
+ let originURL = resourceElement.dataset.singleFileOriginURL;
1972
+ if (options.saveOriginalURLs && !isDataURL(resourceURL)) {
1973
+ resourceElement.setAttribute("data-sf-original-" + attributeName, resourceURL);
1974
+ }
1975
+ delete resourceElement.dataset.singleFileOriginURL;
1976
+ if (!options["block" + expectedType.charAt(0).toUpperCase() + expectedType.substring(1) + "s"]) {
1977
+ if (!testIgnoredPath(resourceURL)) {
1978
+ setAttributeEmpty(resourceElement, attributeName, expectedType);
1979
+ if (testValidPath(resourceURL)) {
1980
+ try {
1981
+ resourceURL = util.resolveURL(resourceURL, baseURI);
1982
+ } catch (error) {
1983
+ // ignored
1984
+ }
1985
+ if (testValidURL(resourceURL)) {
1986
+ let { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL,
1987
+ { asBinary: true, expectedType, groupDuplicates: options.groupDuplicateImages && resourceElement.tagName == "IMG" && attributeName == "src" });
1988
+ if (originURL) {
1989
+ if (content == util.EMPTY_RESOURCE) {
1990
+ try {
1991
+ originURL = util.resolveURL(originURL, baseURI);
1992
+ } catch (error) {
1993
+ // ignored
1994
+ }
1995
+ try {
1996
+ resourceURL = originURL;
1997
+ content = (await util.getContent(resourceURL, {
1998
+ asBinary: true,
1999
+ expectedType,
2000
+ maxResourceSize: options.maxResourceSize,
2001
+ maxResourceSizeEnabled: options.maxResourceSizeEnabled,
2002
+ frameId: options.windowId,
2003
+ resourceReferrer: options.resourceReferrer,
2004
+ acceptHeaders: options.acceptHeaders,
2005
+ networkTimeout: options.networkTimeout
2006
+ })).data;
2007
+ } catch (error) {
2008
+ // ignored
2009
+ }
2010
+ }
2011
+ }
2012
+ if (removeElementIfMissing && content == util.EMPTY_RESOURCE) {
2013
+ resourceElement.remove();
2014
+ } else if (content !== util.EMPTY_RESOURCE) {
2015
+ const forbiddenPrefixFound = PREFIXES_FORBIDDEN_DATA_URI.filter(prefixDataURI => content.startsWith(prefixDataURI)).length;
2016
+ if (!forbiddenPrefixFound) {
2017
+ const isSVG = content.startsWith(PREFIX_DATA_URI_IMAGE_SVG);
2018
+ if (expectedType == "image" && processDuplicates && duplicate && !isSVG && util.getContentSize(content) < SINGLE_FILE_VARIABLE_MAX_SIZE) {
2019
+ if (ProcessorHelper.replaceImageSource(resourceElement, SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource, options)) {
2020
+ cssVariables.set(indexResource, { content, url: originURL });
2021
+ const declarationList = cssTree.parse(resourceElement.getAttribute("style"), { context: "declarationList" });
2022
+ styles.set(resourceElement, declarationList);
2023
+ } else {
2024
+ resourceElement.setAttribute(attributeName, content);
2025
+ }
2026
+ } else {
2027
+ resourceElement.setAttribute(attributeName, content);
2028
+ }
2029
+ }
2030
+ }
2031
+ }
2032
+ }
2033
+ }
2034
+ } else {
2035
+ setAttributeEmpty(resourceElement, attributeName, expectedType);
2036
+ }
2037
+ }
2038
+ }));
2039
+
2040
+ function setAttributeEmpty(resourceElement, attributeName, expectedType) {
2041
+ if (expectedType == "video" || expectedType == "audio") {
2042
+ resourceElement.removeAttribute(attributeName);
2043
+ } else {
2044
+ resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
2045
+ }
2046
+ }
2047
+ }
2048
+
2049
+ static async processXLinks(resourceElements, doc, baseURI, options, batchRequest) {
2050
+ let attributeName = "xlink:href";
2051
+ await Promise.all(Array.from(resourceElements).map(async resourceElement => {
2052
+ let originalResourceURL = resourceElement.getAttribute(attributeName);
2053
+ if (originalResourceURL == null) {
2054
+ attributeName = "href";
2055
+ originalResourceURL = resourceElement.getAttribute(attributeName);
2056
+ }
2057
+ if (options.saveOriginalURLs && !isDataURL(originalResourceURL)) {
2058
+ resourceElement.setAttribute("data-sf-original-href", originalResourceURL);
2059
+ }
2060
+ let resourceURL = normalizeURL(originalResourceURL);
2061
+ if (!options.blockImages) {
2062
+ if (testValidPath(resourceURL) && !testIgnoredPath(resourceURL)) {
2063
+ resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
2064
+ try {
2065
+ resourceURL = util.resolveURL(resourceURL, baseURI);
2066
+ } catch (error) {
2067
+ // ignored
2068
+ }
2069
+ if (testValidURL(resourceURL)) {
2070
+ const hashMatch = originalResourceURL.match(REGEXP_URL_HASH);
2071
+ if (originalResourceURL.startsWith(baseURI + "#")) {
2072
+ resourceElement.setAttribute(attributeName, hashMatch[0]);
2073
+ } else {
2074
+ const response = await batchRequest.addURL(resourceURL, { expectedType: "image" });
2075
+ const svgDoc = util.parseSVGContent(response.content);
2076
+ if (hashMatch && hashMatch[0]) {
2077
+ let symbolElement;
2078
+ try {
2079
+ symbolElement = svgDoc.querySelector(hashMatch[0]);
2080
+ } catch (error) {
2081
+ // ignored
2082
+ }
2083
+ if (symbolElement) {
2084
+ resourceElement.setAttribute(attributeName, hashMatch[0]);
2085
+ resourceElement.parentElement.insertBefore(symbolElement, resourceElement.parentElement.firstChild);
2086
+ }
2087
+ } else {
2088
+ const content = await batchRequest.addURL(resourceURL, { expectedType: "image" });
2089
+ resourceElement.setAttribute(attributeName, PREFIX_DATA_URI_IMAGE_SVG + "," + content);
2090
+ }
2091
+ }
2092
+ }
2093
+ } else if (resourceURL == options.url) {
2094
+ resourceElement.setAttribute(attributeName, originalResourceURL.substring(resourceURL.length));
2095
+ }
2096
+ } else {
2097
+ resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
2098
+ }
2099
+ }));
2100
+ }
2101
+
2102
+ static async processSrcset(resourceElements, baseURI, options, batchRequest) {
2103
+ await Promise.all(Array.from(resourceElements).map(async resourceElement => {
2104
+ const originSrcset = resourceElement.getAttribute("srcset");
2105
+ const srcset = util.parseSrcset(originSrcset);
2106
+ if (options.saveOriginalURLs && !isDataURL(originSrcset)) {
2107
+ resourceElement.setAttribute("data-sf-original-srcset", originSrcset);
2108
+ }
2109
+ if (!options.blockImages) {
2110
+ const srcsetValues = await Promise.all(srcset.map(async srcsetValue => {
2111
+ let resourceURL = normalizeURL(srcsetValue.url);
2112
+ if (!testIgnoredPath(resourceURL)) {
2113
+ if (testValidPath(resourceURL)) {
2114
+ try {
2115
+ resourceURL = util.resolveURL(resourceURL, baseURI);
2116
+ } catch (error) {
2117
+ // ignored
2118
+ }
2119
+ if (testValidURL(resourceURL)) {
2120
+ const { content } = await batchRequest.addURL(resourceURL, { asBinary: true, expectedType: "image" });
2121
+ const forbiddenPrefixFound = PREFIXES_FORBIDDEN_DATA_URI.filter(prefixDataURI => content.startsWith(prefixDataURI)).length;
2122
+ if (forbiddenPrefixFound) {
2123
+ return "";
2124
+ }
2125
+ return content + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
2126
+ } else {
2127
+ return "";
2128
+ }
2129
+ } else {
2130
+ return "";
2131
+ }
2132
+ } else {
2133
+ return resourceURL + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
2134
+ }
2135
+ }));
2136
+ resourceElement.setAttribute("srcset", srcsetValues.join(", "));
2137
+ } else {
2138
+ resourceElement.setAttribute("srcset", "");
2139
+ }
2140
+ }));
2141
+ }
2142
+
2143
+ static replaceImageSource(imgElement, variableName, options) {
2144
+ const attributeValue = imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME);
2145
+ if (attributeValue) {
2146
+ const imageData = options.images[Number(imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME))];
2147
+ if (imageData && imageData.replaceable) {
2148
+ imgElement.setAttribute("src", `${PREFIX_DATA_URI_IMAGE_SVG},<svg xmlns="http://www.w3.org/2000/svg" width="${imageData.size.pxWidth}" height="${imageData.size.pxHeight}"><rect fill-opacity="0"/></svg>`);
2149
+ const backgroundStyle = {};
2150
+ const backgroundSize = (imageData.objectFit == "content" || imageData.objectFit == "cover") && imageData.objectFit;
2151
+ if (backgroundSize) {
2152
+ backgroundStyle["background-size"] = imageData.objectFit;
2153
+ }
2154
+ if (imageData.objectPosition) {
2155
+ backgroundStyle["background-position"] = imageData.objectPosition;
2156
+ }
2157
+ if (imageData.backgroundColor) {
2158
+ backgroundStyle["background-color"] = imageData.backgroundColor;
2159
+ }
2160
+ ProcessorHelper.setBackgroundImage(imgElement, "var(" + variableName + ")", backgroundStyle);
2161
+ imgElement.removeAttribute(util.IMAGE_ATTRIBUTE_NAME);
2162
+ return true;
2163
+ }
2164
+ }
2165
+ }
2166
+ }
2167
+
2168
+ // ----
2169
+ // Util
2170
+ // ----
2171
+ const BLOB_URI_PREFIX = "blob:";
2172
+ const HTTP_URI_PREFIX = /^https?:\/\//;
2173
+ const FILE_URI_PREFIX = /^file:\/\//;
2174
+ const EMPTY_URL = /^https?:\/\/+\s*$/;
2175
+ const NOT_EMPTY_URL = /^(https?:\/\/|file:\/\/|blob:).+/;
2176
+ const REGEXP_URL_FN = /(url\s*\(\s*'(.*?)'\s*\))|(url\s*\(\s*"(.*?)"\s*\))|(url\s*\(\s*(.*?)\s*\))/gi;
2177
+ const REGEXP_URL_SIMPLE_QUOTES_FN = /^url\s*\(\s*'(.*?)'\s*\)$/i;
2178
+ const REGEXP_URL_DOUBLE_QUOTES_FN = /^url\s*\(\s*"(.*?)"\s*\)$/i;
2179
+ const REGEXP_URL_NO_QUOTES_FN = /^url\s*\(\s*(.*?)\s*\)$/i;
2180
+ const REGEXP_IMPORT_FN = /(@import\s*url\s*\(\s*'(.*?)'\s*\)\s*(.*?)(;|$|}))|(@import\s*url\s*\(\s*"(.*?)"\s*\)\s*(.*?)(;|$|}))|(@import\s*url\s*\(\s*(.*?)\s*\)\s*(.*?)(;|$|}))|(@import\s*'(.*?)'\s*(.*?)(;|$|}))|(@import\s*"(.*?)"\s*(.*?)(;|$|}))|(@import\s*(.*?)\s*(.*?)(;|$|}))/gi;
2181
+ const REGEXP_IMPORT_URL_SIMPLE_QUOTES_FN = /@import\s*url\s*\(\s*'(.*?)'\s*\)\s*(.*?)(;|$|})/i;
2182
+ const REGEXP_IMPORT_URL_DOUBLE_QUOTES_FN = /@import\s*url\s*\(\s*"(.*?)"\s*\)\s*(.*?)(;|$|})/i;
2183
+ const REGEXP_IMPORT_URL_NO_QUOTES_FN = /@import\s*url\s*\(\s*(.*?)\s*\)\s*(.*?)(;|$|})/i;
2184
+ const REGEXP_IMPORT_SIMPLE_QUOTES_FN = /@import\s*'(.*?)'\s*(.*?)(;|$|})/i;
2185
+ const REGEXP_IMPORT_DOUBLE_QUOTES_FN = /@import\s*"(.*?)"\s*(.*?)(;|$|})/i;
2186
+ const REGEXP_IMPORT_NO_QUOTES_FN = /@import\s*(.*?)\s*(.*?)(;|$|})/i;
2187
+ const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
2188
+
2189
+ function getUpdatedResourceContent(resourceURL, content, options) {
2190
+ if (options.rootDocument && options.updatedResources[resourceURL]) {
2191
+ options.updatedResources[resourceURL].retrieved = true;
2192
+ return options.updatedResources[resourceURL].content;
2193
+ } else {
2194
+ return content.data || "";
2195
+ }
2196
+ }
2197
+
2198
+ function normalizeURL(url) {
2199
+ if (!url || url.startsWith(DATA_URI_PREFIX)) {
2200
+ return url;
2201
+ } else {
2202
+ return url.split("#")[0];
2203
+ }
2204
+ }
2205
+
2206
+ function getCSSValue(value) {
2207
+ if (typeof value == "string") {
2208
+ return value;
2209
+ } else {
2210
+ let result = "";
2211
+ try {
2212
+ result = cssTree.generate(value);
2213
+ } catch (error) {
2214
+ // ignored
2215
+ }
2216
+ return result;
2217
+ }
2218
+ }
2219
+
2220
+ function matchCharsetEquals(stylesheetContent, charset = UTF8_CHARSET) {
2221
+ const stylesheetCharset = getCharset(stylesheetContent);
2222
+ if (stylesheetCharset) {
2223
+ return stylesheetCharset == charset.toLowerCase();
2224
+ } else {
2225
+ return true;
2226
+ }
2227
+ }
2228
+
2229
+ function getCharset(stylesheetContent) {
2230
+ const match = stylesheetContent.match(/^@charset\s+"([^"]*)";/i);
2231
+ if (match && match[1]) {
2232
+ return match[1].toLowerCase().trim();
2233
+ }
2234
+ }
2235
+
2236
+ function getOnEventAttributeNames(doc) {
2237
+ const element = doc.createElement("div");
2238
+ const attributeNames = [];
2239
+ for (const propertyName in element) {
2240
+ if (propertyName.startsWith("on")) {
2241
+ attributeNames.push(propertyName);
2242
+ }
2243
+ }
2244
+ attributeNames.push("onunload");
2245
+ return attributeNames;
2246
+ }
2247
+
2248
+ async function evalTemplateVariable(template, variableName, valueGetter, dontReplaceSlash, replacementCharacter) {
2249
+ let maxLength, maxCharLength;
2250
+ if (template) {
2251
+ const regExpVariable = "{\\s*" + variableName.replace(/\W|_/g, "[$&]") + "\\s*}";
2252
+ let replaceRegExp = new RegExp(regExpVariable + "\\[\\d+(ch)?\\]", "g");
2253
+ if (template.match(replaceRegExp)) {
2254
+ const matchedLength = template.match(replaceRegExp)[0];
2255
+ if (matchedLength.match(/\[(\d+)\]$/)) {
2256
+ maxLength = Number(matchedLength.match(/\[(\d+)\]$/)[1]);
2257
+ if (isNaN(maxLength) || maxLength <= 0) {
2258
+ maxLength = null;
2259
+ }
2260
+ } else {
2261
+ maxCharLength = Number(matchedLength.match(/\[(\d+)ch\]$/)[1]);
2262
+ if (isNaN(maxCharLength) || maxCharLength <= 0) {
2263
+ maxCharLength = null;
2264
+ }
2265
+ }
2266
+ } else {
2267
+ replaceRegExp = new RegExp(regExpVariable, "g");
2268
+ }
2269
+ if (template.match(replaceRegExp)) {
2270
+ let value = await valueGetter();
2271
+ if (!dontReplaceSlash) {
2272
+ value = value.replace(/\/+/g, replacementCharacter);
2273
+ }
2274
+ if (maxLength) {
2275
+ value = await util.truncateText(value, maxLength);
2276
+ } else if (maxCharLength) {
2277
+ value = value.substring(0, maxCharLength);
2278
+ }
2279
+ return template.replace(replaceRegExp, value);
2280
+ }
2281
+ }
2282
+ return template;
2283
+ }
2284
+
2285
+ function getLastSegment(url, replacementCharacter) {
2286
+ let lastSegmentMatch = url.pathname.match(/\/([^/]+)$/), lastSegment = lastSegmentMatch && lastSegmentMatch[0];
2287
+ if (!lastSegment) {
2288
+ lastSegmentMatch = url.href.match(/([^/]+)\/?$/);
2289
+ lastSegment = lastSegmentMatch && lastSegmentMatch[0];
2290
+ }
2291
+ if (!lastSegment) {
2292
+ lastSegmentMatch = lastSegment.match(/(.*)\.[^.]+$/);
2293
+ lastSegment = lastSegmentMatch && lastSegmentMatch[0];
2294
+ }
2295
+ if (!lastSegment) {
2296
+ lastSegment = url.hostname.replace(/\/+/g, replacementCharacter).replace(/\/$/, "");
2297
+ }
2298
+ lastSegmentMatch = lastSegment.match(/(.*)\.[^.]+$/);
2299
+ if (lastSegmentMatch && lastSegmentMatch[1]) {
2300
+ lastSegment = lastSegmentMatch[1];
2301
+ }
2302
+ lastSegment = lastSegment.replace(/\/$/, "").replace(/^\//, "");
2303
+ return lastSegment;
2304
+ }
2305
+
2306
+ function getRegExp(string) {
2307
+ return new RegExp(string.replace(REGEXP_ESCAPE, "\\$1"), "gi");
2308
+ }
2309
+
2310
+ function getUrlFunctions(stylesheetContent, unique) {
2311
+ const result = stylesheetContent.match(REGEXP_URL_FN) || [];
2312
+ if (unique) {
2313
+ return [...new Set(result)];
2314
+ } else {
2315
+ return result;
2316
+ }
2317
+ }
2318
+
2319
+ function getImportFunctions(stylesheetContent) {
2320
+ return stylesheetContent.match(REGEXP_IMPORT_FN) || [];
2321
+ }
2322
+
2323
+ function findShortcutIcon(shortcutIcons) {
2324
+ shortcutIcons = shortcutIcons.filter(linkElement => linkElement.href != util.EMPTY_RESOURCE);
2325
+ shortcutIcons.sort((linkElement1, linkElement2) => (parseInt(linkElement2.sizes, 10) || 16) - (parseInt(linkElement1.sizes, 10) || 16));
2326
+ return shortcutIcons[0];
2327
+ }
2328
+
2329
+ function matchURL(stylesheetContent) {
2330
+ const match = stylesheetContent.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
2331
+ stylesheetContent.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
2332
+ stylesheetContent.match(REGEXP_URL_NO_QUOTES_FN);
2333
+ return match && match[1];
2334
+ }
2335
+
2336
+ function addOriginalURLs(stylesheetContent) {
2337
+ return stylesheetContent.replace(REGEXP_URL_FN, function (match, _0, url, _1, url2, _2, url3) {
2338
+ url = url || url2 || url3;
2339
+ if (isDataURL(url)) {
2340
+ return match;
2341
+ } else {
2342
+ return "-sf-url-original(" + JSON.stringify(url) + ") " + match;
2343
+ }
2344
+ });
2345
+ }
2346
+
2347
+ function isDataURL(url) {
2348
+ return url && (url.startsWith(DATA_URI_PREFIX) || url.startsWith(BLOB_URI_PREFIX));
2349
+ }
2350
+
2351
+ function replaceOriginalURLs(stylesheetContent) {
2352
+ return stylesheetContent.replace(/-sf-url-original\("(.*?)"\)/g, "/* original URL: $1 */");
2353
+ }
2354
+
2355
+ function testIgnoredPath(resourceURL) {
2356
+ return resourceURL && (resourceURL.startsWith(DATA_URI_PREFIX) || resourceURL == ABOUT_BLANK_URI);
2357
+ }
2358
+
2359
+ function testValidPath(resourceURL) {
2360
+ return resourceURL && !resourceURL.match(EMPTY_URL);
2361
+ }
2362
+
2363
+ function testValidURL(resourceURL) {
2364
+ return testValidPath(resourceURL) && (resourceURL.match(HTTP_URI_PREFIX) || resourceURL.match(FILE_URI_PREFIX) || resourceURL.startsWith(BLOB_URI_PREFIX)) && resourceURL.match(NOT_EMPTY_URL);
2365
+ }
2366
+
2367
+ function matchImport(stylesheetContent) {
2368
+ const match = stylesheetContent.match(REGEXP_IMPORT_URL_SIMPLE_QUOTES_FN) ||
2369
+ stylesheetContent.match(REGEXP_IMPORT_URL_DOUBLE_QUOTES_FN) ||
2370
+ stylesheetContent.match(REGEXP_IMPORT_URL_NO_QUOTES_FN) ||
2371
+ stylesheetContent.match(REGEXP_IMPORT_SIMPLE_QUOTES_FN) ||
2372
+ stylesheetContent.match(REGEXP_IMPORT_DOUBLE_QUOTES_FN) ||
2373
+ stylesheetContent.match(REGEXP_IMPORT_NO_QUOTES_FN);
2374
+ if (match) {
2375
+ const [, resourceURL, media] = match;
2376
+ return { resourceURL, media };
2377
+ }
2378
+ }
2379
+
2380
+ function removeCssComments(stylesheetContent) {
2381
+ try {
2382
+ return stylesheetContent.replace(/\/\*(.|[\r\n])*?\*\//g, "");
2383
+ } catch (error) {
2384
+ let start, end;
2385
+ do {
2386
+ start = stylesheetContent.indexOf("/*");
2387
+ end = stylesheetContent.indexOf("*/", start + 2);
2388
+ if (start != -1 && end != -1) {
2389
+ stylesheetContent = stylesheetContent.substring(0, start) + stylesheetContent.substr(end + 2);
2390
+ }
2391
+ } while (start != -1 && end != -1);
2392
+ return stylesheetContent;
2393
+ }
2394
+ }
2395
+
2396
+ function wrapMediaQuery(stylesheetContent, mediaQuery) {
2397
+ if (mediaQuery) {
2398
+ return "@media " + mediaQuery + "{ " + stylesheetContent + " }";
2399
+ } else {
2400
+ return stylesheetContent;
2401
+ }
2402
+ }
2403
+
2404
+ function log(...args) {
2405
+ console.log("S-File <core> ", ...args); // eslint-disable-line no-console
2406
+ }
2407
+
2408
+ // -----
2409
+ // Stats
2410
+ // -----
2411
+ const STATS_DEFAULT_VALUES = {
2412
+ discarded: {
2413
+ "HTML bytes": 0,
2414
+ "hidden elements": 0,
2415
+ "HTML imports": 0,
2416
+ scripts: 0,
2417
+ objects: 0,
2418
+ "audio sources": 0,
2419
+ "video sources": 0,
2420
+ frames: 0,
2421
+ "CSS rules": 0,
2422
+ canvas: 0,
2423
+ stylesheets: 0,
2424
+ resources: 0,
2425
+ medias: 0
2426
+ },
2427
+ processed: {
2428
+ "HTML bytes": 0,
2429
+ "hidden elements": 0,
2430
+ "HTML imports": 0,
2431
+ scripts: 0,
2432
+ objects: 0,
2433
+ "audio sources": 0,
2434
+ "video sources": 0,
2435
+ frames: 0,
2436
+ "CSS rules": 0,
2437
+ canvas: 0,
2438
+ stylesheets: 0,
2439
+ resources: 0,
2440
+ medias: 0
2441
+ }
2442
+ };
2443
+
2444
+ class Stats {
2445
+ constructor(options) {
2446
+ this.options = options;
2447
+ if (options.displayStats) {
2448
+ this.data = JSON.parse(JSON.stringify(STATS_DEFAULT_VALUES));
2449
+ }
2450
+ }
2451
+ set(type, subType, value) {
2452
+ if (this.options.displayStats) {
2453
+ this.data[type][subType] = value;
2454
+ }
2455
+ }
2456
+ add(type, subType, value) {
2457
+ if (this.options.displayStats) {
2458
+ this.data[type][subType] += value;
2459
+ }
2460
+ }
2461
+ addAll(pageData) {
2462
+ if (this.options.displayStats) {
2463
+ Object.keys(this.data.discarded).forEach(key => this.add("discarded", key, pageData.stats.discarded[key] || 0));
2464
+ Object.keys(this.data.processed).forEach(key => this.add("processed", key, pageData.stats.processed[key] || 0));
2465
+ }
2466
+ }
2467
+ }
2468
+
2469
+ export {
2470
+ getClass
2471
+ };