single-file-core 1.6.4 → 1.6.6
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.
- package/core/helper.js +174 -37
- package/core/index.js +56 -32
- package/core/infobar.js +55 -47
- package/core/lib/processor-helper-common.js +26 -5
- package/core/lib/processor-helper-inline.js +115 -0
- package/core/lib/processor-helper.js +86 -60
- package/modules/css-fonts-minifier.js +139 -7
- package/modules/css-rules-minifier.js +32 -6
- package/modules/css-selector-sanitizer.js +12 -0
- package/package.json +1 -1
- package/processors/compression/compression-packager.js +8 -0
- package/processors/compression/compression.js +33 -11
- package/processors/frame-tree/content/content-frame-tree.js +2 -0
- package/processors/hooks/content/content-hooks-frames-web.js +3 -1
- package/single-file-infobar.js +2 -1
package/core/helper.js
CHANGED
|
@@ -67,7 +67,12 @@ const INVALID_ELEMENT_ATTRIBUTE_NAME = "data-" + SINGLE_FILE_PREFIX + "invalid-e
|
|
|
67
67
|
const ASYNC_SCRIPT_ATTRIBUTE_NAME = "data-" + SINGLE_FILE_PREFIX + "async-script";
|
|
68
68
|
const FLOW_ELEMENTS_SELECTOR = "*:not(base):not(link):not(meta):not(noscript):not(script):not(style):not(template):not(title)";
|
|
69
69
|
const KEPT_TAG_NAMES = ["NOSCRIPT", "DISABLED-NOSCRIPT", "META", "LINK", "STYLE", "TITLE", "TEMPLATE", "SOURCE", "OBJECT", "SCRIPT", "HEAD", "BODY"];
|
|
70
|
+
const INVOKABLE_ELEMENTS_SELECTOR = "[popover][id], dialog[id]";
|
|
71
|
+
const INVOKER_ATTRIBUTE_NAMES = ["popovertarget", "commandfor"];
|
|
72
|
+
const INVOKERS_SELECTOR = INVOKER_ATTRIBUTE_NAMES.map(attributeName => "[" + attributeName + "]").join(",");
|
|
70
73
|
const IGNORED_TAG_NAMES = ["SCRIPT", "NOSCRIPT", "META", "LINK", "TEMPLATE"];
|
|
74
|
+
const ROOT_PSEUDO_ELEMENT_NAMES = [":before", ":after"];
|
|
75
|
+
const BLOCK_CONTAINER_DISPLAY_VALUES = ["block", "flow-root", "list-item", "inline-block", "table-cell", "table-caption"];
|
|
71
76
|
const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
|
|
72
77
|
const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
|
|
73
78
|
const FONT_WEIGHTS = {
|
|
@@ -78,6 +83,11 @@ const FONT_WEIGHTS = {
|
|
|
78
83
|
lighter: "100"
|
|
79
84
|
};
|
|
80
85
|
const COMMENT_HEADER_LEGACY = "Archive processed by SingleFile";
|
|
86
|
+
const ELEMENT_NODE_TYPE = 1;
|
|
87
|
+
const TEXT_NODE_TYPE = 3;
|
|
88
|
+
const REGEXP_QUOTED_STRING = /"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'/g;
|
|
89
|
+
const REGEXP_GENERATED_CONTENT_IMAGE = /(url|image-set|-webkit-image-set|linear-gradient|radial-gradient|conic-gradient)\([^)]*\)/g;
|
|
90
|
+
const REGEXP_BACKSLASH = /\\/;
|
|
81
91
|
const SINGLE_FILE_UI_ELEMENT_CLASS = "single-file-ui-element";
|
|
82
92
|
const INFOBAR_TAGNAME = infobar.INFOBAR_TAGNAME;
|
|
83
93
|
const EMPTY_RESOURCE = "data:,";
|
|
@@ -164,10 +174,6 @@ export {
|
|
|
164
174
|
getPosterDataURI
|
|
165
175
|
};
|
|
166
176
|
|
|
167
|
-
// a poster is a still of a video the reader cannot play, so it is stored in a
|
|
168
|
-
// lossy format: the same frame costs an order of magnitude less than as a PNG.
|
|
169
|
-
// toDataURL falls back to PNG without reporting it when it does not support the
|
|
170
|
-
// format, so the type it returned is tested instead of assumed
|
|
171
177
|
function normalizeOptions(options) {
|
|
172
178
|
let normalizedOptions = options;
|
|
173
179
|
if (options) {
|
|
@@ -185,13 +191,18 @@ function normalizeOptions(options) {
|
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
function getPosterDataURI(canvasElement) {
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
194
|
+
if (canvasElement.width && canvasElement.height) {
|
|
195
|
+
for (const contentType of POSTER_CONTENT_TYPES) {
|
|
196
|
+
const dataURI = canvasElement.toDataURL(contentType, POSTER_QUALITY);
|
|
197
|
+
if (dataURI.startsWith("data:" + contentType)) {
|
|
198
|
+
return dataURI;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const dataURI = canvasElement.toDataURL("image/png");
|
|
202
|
+
if (dataURI != EMPTY_RESOURCE) {
|
|
191
203
|
return dataURI;
|
|
192
204
|
}
|
|
193
205
|
}
|
|
194
|
-
return canvasElement.toDataURL("image/png");
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
let userScriptHandlerObserver;
|
|
@@ -276,6 +287,9 @@ function preProcessDoc(doc, win, options) {
|
|
|
276
287
|
if (win && doc.documentElement) {
|
|
277
288
|
markInvalidNesting(doc);
|
|
278
289
|
elementsInfo = getElementsInfo(win, doc, doc.documentElement, options);
|
|
290
|
+
if (options.removeUnusedFonts && doc.defaultView) {
|
|
291
|
+
getRootElementUsedFonts(win, doc.documentElement, elementsInfo);
|
|
292
|
+
}
|
|
279
293
|
if (options.moveStylesInHead) {
|
|
280
294
|
doc.querySelectorAll("body style, body ~ style").forEach(element => {
|
|
281
295
|
const computedStyle = getComputedStyle(win, element);
|
|
@@ -292,6 +306,7 @@ function preProcessDoc(doc, win, options) {
|
|
|
292
306
|
posters: [],
|
|
293
307
|
videos: [],
|
|
294
308
|
usedFonts: [],
|
|
309
|
+
usedFontsCharacters: new Map(),
|
|
295
310
|
shadowRoots: [],
|
|
296
311
|
markedElements: []
|
|
297
312
|
};
|
|
@@ -314,6 +329,7 @@ function preProcessDoc(doc, win, options) {
|
|
|
314
329
|
posters: elementsInfo.posters,
|
|
315
330
|
videos: elementsInfo.videos,
|
|
316
331
|
usedFonts: Array.from(elementsInfo.usedFonts.values()),
|
|
332
|
+
usedFontsCharacters: serializeUsedFontsCharacters(elementsInfo.usedFontsCharacters),
|
|
317
333
|
shadowRoots: elementsInfo.shadowRoots,
|
|
318
334
|
referrer,
|
|
319
335
|
markedElements: elementsInfo.markedElements,
|
|
@@ -414,16 +430,19 @@ function fixInvalidNesting(document, NESTING_TRACK_ID_ATTRIBUTE_NAME, preventCle
|
|
|
414
430
|
}
|
|
415
431
|
}
|
|
416
432
|
|
|
417
|
-
function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map(), canvases: [], images: [], posters: [], videos: [], shadowRoots: [], markedElements: [] }, adoptedStyleSheetsCache = new Map(), ascendantHidden) {
|
|
433
|
+
function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map(), usedFontsCharacters: new Map(), canvases: [], images: [], posters: [], videos: [], shadowRoots: [], markedElements: [] }, adoptedStyleSheetsCache = new Map(), ascendantHidden) {
|
|
418
434
|
if (element.childNodes) {
|
|
419
435
|
const elements = Array.from(element.childNodes).filter(node => (node instanceof win.HTMLElement) || (node instanceof win.SVGElement) || (node instanceof globalThis.HTMLElement) || (node instanceof globalThis.SVGElement));
|
|
420
436
|
elements.forEach(element => {
|
|
421
|
-
let elementHidden, elementKept, computedStyle;
|
|
437
|
+
let elementHidden, elementKept, computedStyle, headChild;
|
|
422
438
|
if (!options.autoSaveExternalSave && (options.removeHiddenElements || options.removeUnusedFonts || options.compressHTML)) {
|
|
423
439
|
computedStyle = getComputedStyle(win, element);
|
|
440
|
+
if (options.removeHiddenElements || options.removeUnusedFonts) {
|
|
441
|
+
headChild = Boolean(element.closest("html > head"));
|
|
442
|
+
}
|
|
424
443
|
if ((element instanceof win.HTMLElement) || (element instanceof globalThis.HTMLElement)) {
|
|
425
444
|
if (options.removeHiddenElements) {
|
|
426
|
-
elementKept = ((ascendantHidden ||
|
|
445
|
+
elementKept = ((ascendantHidden || headChild) && KEPT_TAG_NAMES.includes(element.tagName.toUpperCase())) || element.closest("details") || testInvokedElement(element, doc, data);
|
|
427
446
|
if (!elementKept) {
|
|
428
447
|
elementHidden = ascendantHidden || testHiddenElement(element, computedStyle);
|
|
429
448
|
if (elementHidden && !IGNORED_TAG_NAMES.includes(element.tagName.toUpperCase())) {
|
|
@@ -441,11 +460,28 @@ function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map
|
|
|
441
460
|
data.markedElements.push(element);
|
|
442
461
|
}
|
|
443
462
|
}
|
|
444
|
-
if (options.removeUnusedFonts && doc.defaultView) {
|
|
445
|
-
|
|
446
|
-
getUsedFont(
|
|
447
|
-
getUsedFont(getComputedStyle(win, element, ":
|
|
448
|
-
|
|
463
|
+
if (options.removeUnusedFonts && doc.defaultView && !headChild) {
|
|
464
|
+
const elementCharacters = getElementCharacters(win, element);
|
|
465
|
+
getUsedFont(computedStyle, data.usedFonts, data.usedFontsCharacters, elementCharacters);
|
|
466
|
+
getUsedFont(getComputedStyle(win, element, ":first-letter"), data.usedFonts, data.usedFontsCharacters, elementCharacters);
|
|
467
|
+
const beforeStyle = getComputedStyle(win, element, ":before");
|
|
468
|
+
getUsedFont(beforeStyle, data.usedFonts, data.usedFontsCharacters, getPseudoElementCharacters(beforeStyle));
|
|
469
|
+
const afterStyle = getComputedStyle(win, element, ":after");
|
|
470
|
+
getUsedFont(afterStyle, data.usedFonts, data.usedFontsCharacters, getPseudoElementCharacters(afterStyle));
|
|
471
|
+
const display = computedStyle ? computedStyle.getPropertyValue("display") : "";
|
|
472
|
+
const tagName = element.tagName.toUpperCase();
|
|
473
|
+
if (display == "list-item") {
|
|
474
|
+
getUsedFont(getComputedStyle(win, element, "::marker"), data.usedFonts);
|
|
475
|
+
}
|
|
476
|
+
if (BLOCK_CONTAINER_DISPLAY_VALUES.includes(display)) {
|
|
477
|
+
getUsedFont(getComputedStyle(win, element, "::first-line"), data.usedFonts, data.usedFontsCharacters, elementCharacters);
|
|
478
|
+
}
|
|
479
|
+
if ((tagName == "INPUT" || tagName == "TEXTAREA") && element.getAttribute("placeholder")) {
|
|
480
|
+
getUsedFont(getComputedStyle(win, element, "::placeholder"), data.usedFonts, data.usedFontsCharacters, { characters: element.getAttribute("placeholder") });
|
|
481
|
+
}
|
|
482
|
+
if (tagName == "INPUT" && element.type == "file") {
|
|
483
|
+
getUsedFont(getComputedStyle(win, element, "::file-selector-button"), data.usedFonts);
|
|
484
|
+
}
|
|
449
485
|
}
|
|
450
486
|
}
|
|
451
487
|
}
|
|
@@ -458,15 +494,15 @@ function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map
|
|
|
458
494
|
data.shadowRoots.push(shadowRootInfo);
|
|
459
495
|
try {
|
|
460
496
|
if (shadowRoot.adoptedStyleSheets) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
497
|
+
|
|
498
|
+
const listener = event => shadowRootInfo.adoptedStyleSheets = event.detail.adoptedStyleSheets;
|
|
499
|
+
shadowRoot.addEventListener(GET_ADOPTED_STYLESHEETS_RESPONSE_EVENT, listener);
|
|
500
|
+
shadowRoot.dispatchEvent(new CustomEvent(GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, { bubbles: true }));
|
|
501
|
+
if (!shadowRootInfo.adoptedStyleSheets) {
|
|
502
|
+
element.dispatchEvent(new CustomEvent(GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, { bubbles: true }));
|
|
503
|
+
}
|
|
504
|
+
shadowRoot.removeEventListener(GET_ADOPTED_STYLESHEETS_RESPONSE_EVENT, listener);
|
|
505
|
+
|
|
470
506
|
}
|
|
471
507
|
// eslint-disable-next-line no-unused-vars
|
|
472
508
|
} catch (error) {
|
|
@@ -527,8 +563,6 @@ function getStylesheetsContent(styleSheets, adoptedStyleSheetsCache = new Map())
|
|
|
527
563
|
}
|
|
528
564
|
}
|
|
529
565
|
|
|
530
|
-
// an untouched canvas is fully transparent, so it encodes exactly like a blank one of the same
|
|
531
|
-
// size: comparing the two is cheaper than reading the pixels back and needs no drawing context
|
|
532
566
|
function isBlankCanvas(doc, element, dataURI) {
|
|
533
567
|
const blankElement = doc.createElement("canvas");
|
|
534
568
|
blankElement.width = element.width;
|
|
@@ -545,9 +579,6 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
|
|
|
545
579
|
};
|
|
546
580
|
try {
|
|
547
581
|
const dataURI = element.toDataURL("image/png");
|
|
548
|
-
// a canvas in a page SingleFile has already saved is empty, no script ran to draw into
|
|
549
|
-
// it, and the picture it displays is the background image the previous save left
|
|
550
|
-
// behind. An empty bitmap must not overwrite it
|
|
551
582
|
const backgroundImage = canvasComputedStyle ? canvasComputedStyle.getPropertyValue("background-image") : element.style.getPropertyValue("background-image");
|
|
552
583
|
if (backgroundImage && backgroundImage != "none" && isBlankCanvas(doc, element, dataURI)) {
|
|
553
584
|
canvasData.blank = true;
|
|
@@ -605,8 +636,6 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
|
|
|
605
636
|
});
|
|
606
637
|
element.setAttribute(VIDEO_ATTRIBUTE_NAME, data.videos.length - 1);
|
|
607
638
|
}
|
|
608
|
-
// the posters are only inserted when the videos are blocked, capturing them
|
|
609
|
-
// otherwise encodes a frame per video for nothing
|
|
610
639
|
if (options.blockVideos && !element.getAttribute("poster")) {
|
|
611
640
|
const canvasElement = doc.createElement("canvas");
|
|
612
641
|
const context = canvasElement.getContext("2d");
|
|
@@ -614,9 +643,12 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
|
|
|
614
643
|
canvasElement.height = element.videoHeight;
|
|
615
644
|
try {
|
|
616
645
|
context.drawImage(element, 0, 0, canvasElement.width, canvasElement.height);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
646
|
+
const posterDataURI = getPosterDataURI(canvasElement);
|
|
647
|
+
if (posterDataURI) {
|
|
648
|
+
data.posters.push(posterDataURI);
|
|
649
|
+
element.setAttribute(POSTER_ATTRIBUTE_NAME, data.posters.length - 1);
|
|
650
|
+
data.markedElements.push(element);
|
|
651
|
+
}
|
|
620
652
|
// eslint-disable-next-line no-unused-vars
|
|
621
653
|
} catch (error) {
|
|
622
654
|
// ignored
|
|
@@ -660,7 +692,7 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
|
|
|
660
692
|
}
|
|
661
693
|
}
|
|
662
694
|
|
|
663
|
-
function getUsedFont(computedStyle, usedFonts) {
|
|
695
|
+
function getUsedFont(computedStyle, usedFonts, usedFontsCharacters, drawnCharacters) {
|
|
664
696
|
if (computedStyle) {
|
|
665
697
|
const fontStyle = computedStyle.getPropertyValue("font-style") || "normal";
|
|
666
698
|
computedStyle.getPropertyValue("font-family").split(",").forEach(fontFamilyName => {
|
|
@@ -670,9 +702,97 @@ function getUsedFont(computedStyle, usedFonts) {
|
|
|
670
702
|
const fontVariant = computedStyle.getPropertyValue("font-variant") || "normal";
|
|
671
703
|
const value = [fontFamilyName, fontWeight, fontStyle, fontVariant];
|
|
672
704
|
usedFonts.set(JSON.stringify(value), [fontFamilyName, fontWeight, fontStyle, fontVariant]);
|
|
705
|
+
if (usedFontsCharacters && drawnCharacters) {
|
|
706
|
+
addUsedFontCharacters(usedFontsCharacters, fontFamilyName, fontStyle, drawnCharacters);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function addUsedFontCharacters(usedFontsCharacters, fontFamilyName, fontStyle, drawnCharacters) {
|
|
714
|
+
const key = fontFamilyName + "|" + fontStyle;
|
|
715
|
+
let bucket = usedFontsCharacters.get(key);
|
|
716
|
+
if (!bucket) {
|
|
717
|
+
bucket = { fontFamily: fontFamilyName, fontStyle, charCodes: new Set(), unknown: false };
|
|
718
|
+
usedFontsCharacters.set(key, bucket);
|
|
719
|
+
}
|
|
720
|
+
if (drawnCharacters.unknown) {
|
|
721
|
+
bucket.unknown = true;
|
|
722
|
+
}
|
|
723
|
+
for (const character of drawnCharacters.characters) {
|
|
724
|
+
bucket.charCodes.add(character.codePointAt(0));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function getElementCharacters(win, element) {
|
|
729
|
+
let characters = "";
|
|
730
|
+
const tagName = element.tagName && element.tagName.toUpperCase();
|
|
731
|
+
if (tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "BUTTON") {
|
|
732
|
+
characters += (element.value || "") + (element.getAttribute("placeholder") || "");
|
|
733
|
+
}
|
|
734
|
+
if (element.childNodes) {
|
|
735
|
+
Array.from(element.childNodes).forEach(node => {
|
|
736
|
+
if (node.nodeType == TEXT_NODE_TYPE) {
|
|
737
|
+
characters += node.data;
|
|
738
|
+
} else if (node.nodeType == ELEMENT_NODE_TYPE &&
|
|
739
|
+
!((node instanceof win.HTMLElement) || (node instanceof win.SVGElement) ||
|
|
740
|
+
(node instanceof globalThis.HTMLElement) || (node instanceof globalThis.SVGElement))) {
|
|
741
|
+
characters += node.textContent;
|
|
673
742
|
}
|
|
674
743
|
});
|
|
675
744
|
}
|
|
745
|
+
return { characters };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// getElementsInfo iterates the children of the element it is given, so the root is never visited.
|
|
749
|
+
// An inherited property costs nothing there, since every descendant carries the root's value, but a
|
|
750
|
+
// pseudo-element on the root has no descendant to speak for it: html::before renders a box with a
|
|
751
|
+
// font of its own and nothing else records it. Only a pseudo that generates content is recorded,
|
|
752
|
+
// because the computed style of a pseudo that draws nothing still reports the inherited family and
|
|
753
|
+
// registering that would mark the whole root font stack as used.
|
|
754
|
+
function getRootElementUsedFonts(win, element, data) {
|
|
755
|
+
ROOT_PSEUDO_ELEMENT_NAMES.forEach(pseudoElementName => {
|
|
756
|
+
const computedStyle = getComputedStyle(win, element, pseudoElementName);
|
|
757
|
+
const drawnCharacters = getPseudoElementCharacters(computedStyle);
|
|
758
|
+
if (drawnCharacters.characters || drawnCharacters.unknown) {
|
|
759
|
+
getUsedFont(computedStyle, data.usedFonts, data.usedFontsCharacters, drawnCharacters);
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function getPseudoElementCharacters(computedStyle) {
|
|
765
|
+
const content = computedStyle && computedStyle.getPropertyValue("content");
|
|
766
|
+
if (!content || content == "none" || content == "normal") {
|
|
767
|
+
return { characters: "" };
|
|
768
|
+
}
|
|
769
|
+
let characters = "";
|
|
770
|
+
REGEXP_QUOTED_STRING.lastIndex = 0;
|
|
771
|
+
let match = REGEXP_QUOTED_STRING.exec(content);
|
|
772
|
+
while (match) {
|
|
773
|
+
characters += match[1] === undefined ? match[2] : match[1];
|
|
774
|
+
match = REGEXP_QUOTED_STRING.exec(content);
|
|
775
|
+
}
|
|
776
|
+
const remainder = content
|
|
777
|
+
.replace(REGEXP_QUOTED_STRING, "")
|
|
778
|
+
.replace(REGEXP_GENERATED_CONTENT_IMAGE, "")
|
|
779
|
+
.trim();
|
|
780
|
+
return { characters, unknown: Boolean(remainder) || REGEXP_BACKSLASH.test(characters) };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function serializeUsedFontsCharacters(usedFontsCharacters) {
|
|
784
|
+
return Array.from(usedFontsCharacters.values()).map(bucket => {
|
|
785
|
+
const ranges = [];
|
|
786
|
+
Array.from(bucket.charCodes).sort((charCode1, charCode2) => charCode1 - charCode2).forEach(charCode => {
|
|
787
|
+
const lastRange = ranges[ranges.length - 1];
|
|
788
|
+
if (lastRange && charCode == lastRange[1] + 1) {
|
|
789
|
+
lastRange[1] = charCode;
|
|
790
|
+
} else {
|
|
791
|
+
ranges.push([charCode, charCode]);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
return [bucket.fontFamily, bucket.fontStyle, ranges, bucket.unknown ? 1 : 0];
|
|
795
|
+
});
|
|
676
796
|
}
|
|
677
797
|
|
|
678
798
|
function getShadowRoot(element) {
|
|
@@ -699,13 +819,30 @@ function normalizeFontFamily(fontFamilyName = "") {
|
|
|
699
819
|
return removeQuotes(cssUnescape.process(fontFamilyName.trim())).toLowerCase();
|
|
700
820
|
}
|
|
701
821
|
|
|
822
|
+
function testInvokedElement(element, doc, data) {
|
|
823
|
+
const invokableElement = element.closest(INVOKABLE_ELEMENTS_SELECTOR);
|
|
824
|
+
if (invokableElement) {
|
|
825
|
+
if (!data.invokedIds) {
|
|
826
|
+
data.invokedIds = new Set();
|
|
827
|
+
doc.querySelectorAll(INVOKERS_SELECTOR).forEach(invoker => INVOKER_ATTRIBUTE_NAMES.forEach(attributeName => {
|
|
828
|
+
if (invoker.hasAttribute(attributeName)) {
|
|
829
|
+
data.invokedIds.add(invoker.getAttribute(attributeName));
|
|
830
|
+
}
|
|
831
|
+
}));
|
|
832
|
+
}
|
|
833
|
+
return data.invokedIds.has(invokableElement.id);
|
|
834
|
+
}
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
|
|
702
838
|
function testHiddenElement(element, computedStyle) {
|
|
703
839
|
let hidden = false;
|
|
704
840
|
if (computedStyle) {
|
|
705
841
|
const display = computedStyle.getPropertyValue("display");
|
|
706
842
|
const opacity = computedStyle.getPropertyValue("opacity");
|
|
707
843
|
const visibility = computedStyle.getPropertyValue("visibility");
|
|
708
|
-
|
|
844
|
+
const tagName = element.tagName && element.tagName.toUpperCase();
|
|
845
|
+
hidden = display == "none" || (visibility == "hidden" && tagName == "IFRAME");
|
|
709
846
|
if (!hidden && (opacity == "0" || visibility == "hidden") && element.getBoundingClientRect) {
|
|
710
847
|
const boundingRect = element.getBoundingClientRect();
|
|
711
848
|
hidden = !boundingRect.width && !boundingRect.height;
|
package/core/index.js
CHANGED
|
@@ -145,7 +145,8 @@ const STAGES = [{
|
|
|
145
145
|
]
|
|
146
146
|
}, {
|
|
147
147
|
sequential: [
|
|
148
|
-
{ option: "removeAlternativeImages", action: "removeAlternativeImages" }
|
|
148
|
+
{ option: "removeAlternativeImages", action: "removeAlternativeImages" },
|
|
149
|
+
{ action: "groupDuplicateFonts" }
|
|
149
150
|
],
|
|
150
151
|
parallel: [
|
|
151
152
|
{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
|
|
@@ -199,6 +200,7 @@ class Runner {
|
|
|
199
200
|
this.options.posters = docData.posters;
|
|
200
201
|
this.options.videos = docData.videos;
|
|
201
202
|
this.options.usedFonts = docData.usedFonts;
|
|
203
|
+
this.options.usedFontsCharacters = docData.usedFontsCharacters;
|
|
202
204
|
this.options.shadowRoots = docData.shadowRoots;
|
|
203
205
|
this.options.referrer = docData.referrer;
|
|
204
206
|
this.options.adoptedStyleSheets = docData.adoptedStyleSheets;
|
|
@@ -222,6 +224,7 @@ class Runner {
|
|
|
222
224
|
await this.onprogress(new ProgressEvent(RESOURCES_INITIALIZING, { pageURL: this.options.url, options: this.options }));
|
|
223
225
|
await this.executeStage(RESOLVE_URLS_STAGE);
|
|
224
226
|
this.pendingPromises = this.executeStage(REPLACE_DATA_STAGE);
|
|
227
|
+
this.pendingPromises.catch(() => { });
|
|
225
228
|
if (this.root && this.options.doc) {
|
|
226
229
|
util.postProcessDoc(this.options.doc, this.markedElements, this.invalidElements);
|
|
227
230
|
}
|
|
@@ -282,7 +285,7 @@ class Runner {
|
|
|
282
285
|
log("**** STARTED STAGE", step, "****");
|
|
283
286
|
}
|
|
284
287
|
const frame = !this.root;
|
|
285
|
-
|
|
288
|
+
const stageStartedPromise = this.onprogress(new ProgressEvent(STAGE_STARTED, { pageURL: this.options.url, step, frame, options: this.options }));
|
|
286
289
|
for (const task of STAGES[step].sequential) {
|
|
287
290
|
let startTime;
|
|
288
291
|
if (DEBUG) {
|
|
@@ -314,6 +317,7 @@ class Runner {
|
|
|
314
317
|
} else {
|
|
315
318
|
parallelTasksPromise = Promise.resolve();
|
|
316
319
|
}
|
|
320
|
+
await stageStartedPromise;
|
|
317
321
|
await this.onprogress(new ProgressEvent(STAGE_ENDED, { pageURL: this.options.url, step, frame, options: this.options }));
|
|
318
322
|
if (DEBUG) {
|
|
319
323
|
log("**** ENDED STAGE", step, "****");
|
|
@@ -341,19 +345,25 @@ class BatchRequest {
|
|
|
341
345
|
return new Promise((resolve, reject) => {
|
|
342
346
|
const requestKey = JSON.stringify([resourceURL, asBinary, expectedType, baseURI, blockMixedContent, contentType]);
|
|
343
347
|
let resourceRequests = this.requests.get(requestKey);
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
348
|
+
if (this.cancelled) {
|
|
349
|
+
reject();
|
|
350
|
+
} else if (this.running && !resourceRequests) {
|
|
351
|
+
reject(new Error("Resource requested after the batch started: " + resourceURL));
|
|
352
|
+
} else {
|
|
353
|
+
if (!resourceRequests) {
|
|
354
|
+
resourceRequests = [];
|
|
355
|
+
this.requests.set(requestKey, resourceRequests);
|
|
356
|
+
}
|
|
357
|
+
const callbacks = { resolve, reject };
|
|
358
|
+
resourceRequests.push(callbacks);
|
|
359
|
+
if (groupDuplicates) {
|
|
360
|
+
let duplicateRequests = this.duplicates.get(requestKey);
|
|
361
|
+
if (!duplicateRequests) {
|
|
362
|
+
duplicateRequests = [];
|
|
363
|
+
this.duplicates.set(requestKey, duplicateRequests);
|
|
364
|
+
}
|
|
365
|
+
duplicateRequests.push(callbacks);
|
|
355
366
|
}
|
|
356
|
-
duplicateRequests.push(callbacks);
|
|
357
367
|
}
|
|
358
368
|
});
|
|
359
369
|
}
|
|
@@ -363,6 +373,7 @@ class BatchRequest {
|
|
|
363
373
|
}
|
|
364
374
|
|
|
365
375
|
run(onloadListener, options) {
|
|
376
|
+
this.running = true;
|
|
366
377
|
const resourceURLs = [...this.requests.keys()];
|
|
367
378
|
let indexResource = 0;
|
|
368
379
|
return Promise.all(resourceURLs.map(async requestKey => {
|
|
@@ -385,7 +396,6 @@ class BatchRequest {
|
|
|
385
396
|
acceptHeaders: options.acceptHeaders,
|
|
386
397
|
networkTimeout: options.networkTimeout
|
|
387
398
|
});
|
|
388
|
-
await onloadListener({ url: resourceURL });
|
|
389
399
|
if (!this.cancelled) {
|
|
390
400
|
const extension = util.getContentTypeExtension(content.contentType) || util.getFilenameExtension(resourceURL, options.filenameReplacedCharacters, options.filenameReplacementCharacter, options.filenameReplacementCharacters);
|
|
391
401
|
resourceRequests.forEach(callbacks => {
|
|
@@ -395,11 +405,10 @@ class BatchRequest {
|
|
|
395
405
|
});
|
|
396
406
|
}
|
|
397
407
|
} catch (error) {
|
|
398
|
-
indexResource = indexResource + 1;
|
|
399
|
-
await onloadListener({ url: resourceURL });
|
|
400
408
|
resourceRequests.forEach(resourceRequest => resourceRequest.reject(error));
|
|
401
409
|
}
|
|
402
410
|
this.requests.delete(requestKey);
|
|
411
|
+
await onloadListener({ url: resourceURL });
|
|
403
412
|
}));
|
|
404
413
|
}
|
|
405
414
|
|
|
@@ -427,6 +436,7 @@ const DISABLED_SCRIPT_URI = "javascript:void(0)";
|
|
|
427
436
|
const SCRIPT_URI_ATTRIBUTE_NAMES = ["href", "src", "action", "formaction", "data"];
|
|
428
437
|
const UTF8_CHARSET = "utf-8";
|
|
429
438
|
const TAINTED_CANVAS_WARNING_MESSAGE = "SingleFile: canvas elements tainted by a cross-origin resource, dropped from the page:";
|
|
439
|
+
const EMPTY_RESOURCE = "data:,";
|
|
430
440
|
|
|
431
441
|
class Processor {
|
|
432
442
|
constructor(options, processorHelper, batchRequest) {
|
|
@@ -457,7 +467,7 @@ class Processor {
|
|
|
457
467
|
}
|
|
458
468
|
this.maxResources = this.batchRequest.getMaxResources();
|
|
459
469
|
if (!this.options.removeFrames && this.options.frames) {
|
|
460
|
-
this.options.frames.forEach(frameData => this.maxResources += frameData.
|
|
470
|
+
this.options.frames.forEach(frameData => this.maxResources += frameData.runner ? frameData.runner.batchRequest.getMaxResources() : 0);
|
|
461
471
|
}
|
|
462
472
|
this.stats.set("processed", "resources", this.maxResources);
|
|
463
473
|
}
|
|
@@ -785,7 +795,7 @@ class Processor {
|
|
|
785
795
|
const attributeValue = element.getAttribute(util.POSTER_ATTRIBUTE_NAME);
|
|
786
796
|
if (attributeValue) {
|
|
787
797
|
const posterURL = this.options.posters[Number(attributeValue)];
|
|
788
|
-
if (!videoElement.getAttribute("poster") && posterURL) {
|
|
798
|
+
if (!videoElement.getAttribute("poster") && posterURL && posterURL != EMPTY_RESOURCE) {
|
|
789
799
|
videoElement.setAttribute("poster", posterURL);
|
|
790
800
|
}
|
|
791
801
|
}
|
|
@@ -1204,12 +1214,15 @@ class Processor {
|
|
|
1204
1214
|
} else {
|
|
1205
1215
|
videoElement = element.parentElement;
|
|
1206
1216
|
}
|
|
1207
|
-
if (!videoElement.poster) {
|
|
1217
|
+
if (!videoElement.poster || videoElement.poster == EMPTY_RESOURCE) {
|
|
1208
1218
|
const attributeValue = videoElement.getAttribute(util.VIDEO_ATTRIBUTE_NAME);
|
|
1209
1219
|
if (attributeValue) {
|
|
1210
1220
|
const videoData = this.options.videos[Number(attributeValue)];
|
|
1211
1221
|
const src = videoData.src || videoElement.src;
|
|
1212
1222
|
if (src) {
|
|
1223
|
+
const blankPosterURI = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='" +
|
|
1224
|
+
(videoData.size.videoWidth || videoData.size.pxWidth) + "' height='" +
|
|
1225
|
+
(videoData.size.videoHeight || videoData.size.pxHeight) + "'%3E%3C/svg%3E";
|
|
1213
1226
|
const temporaryVideoElement = this.doc.createElement("video");
|
|
1214
1227
|
temporaryVideoElement.src = src;
|
|
1215
1228
|
temporaryVideoElement.style.setProperty("width", videoData.size.pxWidth + "px", "important");
|
|
@@ -1222,21 +1235,21 @@ class Processor {
|
|
|
1222
1235
|
return new Promise(resolve => {
|
|
1223
1236
|
temporaryVideoElement.currentTime = videoData.currentTime;
|
|
1224
1237
|
temporaryVideoElement.oncanplay = () => {
|
|
1225
|
-
canvasElement.width = videoData.size.videoWidth;
|
|
1226
|
-
canvasElement.height = videoData.size.videoHeight;
|
|
1238
|
+
canvasElement.width = temporaryVideoElement.videoWidth || videoData.size.videoWidth;
|
|
1239
|
+
canvasElement.height = temporaryVideoElement.videoHeight || videoData.size.videoHeight;
|
|
1227
1240
|
context.drawImage(temporaryVideoElement, 0, 0, canvasElement.width, canvasElement.height);
|
|
1228
1241
|
try {
|
|
1229
|
-
videoElement.poster = util.getPosterDataURI(canvasElement);
|
|
1242
|
+
videoElement.poster = util.getPosterDataURI(canvasElement) || blankPosterURI;
|
|
1230
1243
|
// eslint-disable-next-line no-unused-vars
|
|
1231
1244
|
} catch (error) {
|
|
1232
|
-
videoElement.poster =
|
|
1245
|
+
videoElement.poster = blankPosterURI;
|
|
1233
1246
|
// ignored
|
|
1234
1247
|
}
|
|
1235
1248
|
temporaryVideoElement.remove();
|
|
1236
1249
|
resolve();
|
|
1237
1250
|
};
|
|
1238
1251
|
temporaryVideoElement.onerror = () => {
|
|
1239
|
-
videoElement.poster =
|
|
1252
|
+
videoElement.poster = blankPosterURI;
|
|
1240
1253
|
temporaryVideoElement.remove();
|
|
1241
1254
|
resolve();
|
|
1242
1255
|
};
|
|
@@ -1346,19 +1359,20 @@ class Processor {
|
|
|
1346
1359
|
stylesheets: [],
|
|
1347
1360
|
url,
|
|
1348
1361
|
usedFonts: [],
|
|
1362
|
+
usedFontsCharacters: [],
|
|
1349
1363
|
videos: [],
|
|
1350
1364
|
worklets: []
|
|
1351
1365
|
};
|
|
1352
1366
|
this.options.frames.push(frameData);
|
|
1353
1367
|
frameData.windowId = (this.options.windowId || "0") + "." + this.options.frames.length;
|
|
1354
1368
|
frameElement.setAttribute(util.WIN_ID_ATTRIBUTE_NAME, frameData.windowId);
|
|
1355
|
-
await initializeProcessor(frameData, frameElement, null,
|
|
1369
|
+
await initializeProcessor(frameData, frameElement, null, Object.assign({}, this.options));
|
|
1356
1370
|
} else {
|
|
1357
1371
|
const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
|
|
1358
1372
|
if (this.options.frames && frameWindowId) {
|
|
1359
1373
|
const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
|
|
1360
1374
|
if (frameData && frameData.content) {
|
|
1361
|
-
await initializeProcessor(frameData, frameElement, frameWindowId,
|
|
1375
|
+
await initializeProcessor(frameData, frameElement, frameWindowId, Object.assign({}, this.options));
|
|
1362
1376
|
}
|
|
1363
1377
|
}
|
|
1364
1378
|
}
|
|
@@ -1369,7 +1383,7 @@ class Processor {
|
|
|
1369
1383
|
}
|
|
1370
1384
|
});
|
|
1371
1385
|
|
|
1372
|
-
async function initializeProcessor(frameData, frameElement, frameWindowId,
|
|
1386
|
+
async function initializeProcessor(frameData, frameElement, frameWindowId, options) {
|
|
1373
1387
|
options.insertSingleFileComment = false;
|
|
1374
1388
|
options.insertCanonicalLink = false;
|
|
1375
1389
|
options.insertMetaNoIndex = false;
|
|
@@ -1390,6 +1404,7 @@ class Processor {
|
|
|
1390
1404
|
options.posters = frameData.posters;
|
|
1391
1405
|
options.videos = frameData.videos;
|
|
1392
1406
|
options.usedFonts = frameData.usedFonts;
|
|
1407
|
+
options.usedFontsCharacters = frameData.usedFontsCharacters;
|
|
1393
1408
|
options.shadowRoots = frameData.shadowRoots;
|
|
1394
1409
|
options.scrollPosition = frameData.scrollPosition;
|
|
1395
1410
|
options.scrolling = frameData.scrolling;
|
|
@@ -1398,7 +1413,6 @@ class Processor {
|
|
|
1398
1413
|
frameData.frameElement = frameElement;
|
|
1399
1414
|
await frameData.runner.loadPage();
|
|
1400
1415
|
await frameData.runner.initialize();
|
|
1401
|
-
frameData.maxResources = batchRequest.getMaxResources();
|
|
1402
1416
|
}
|
|
1403
1417
|
}
|
|
1404
1418
|
|
|
@@ -1538,6 +1552,10 @@ class Processor {
|
|
|
1538
1552
|
util.removeAlternativeImages(this.doc);
|
|
1539
1553
|
}
|
|
1540
1554
|
|
|
1555
|
+
groupDuplicateFonts() {
|
|
1556
|
+
this.processorHelper.groupDuplicateFonts(this.stylesheets, this.resources.fonts, this.options);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1541
1559
|
async removeAlternativeFonts() {
|
|
1542
1560
|
await this.processorHelper.removeAlternativeFonts(this.doc, this.stylesheets, this.resources.fonts, this.options.fontTests);
|
|
1543
1561
|
}
|
|
@@ -1545,7 +1563,7 @@ class Processor {
|
|
|
1545
1563
|
async processFrames() {
|
|
1546
1564
|
if (this.options.frames) {
|
|
1547
1565
|
const frameElements = Array.from(this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]"));
|
|
1548
|
-
await Promise.all(frameElements.map(async frameElement => {
|
|
1566
|
+
const capturedFrames = await Promise.all(frameElements.map(async frameElement => {
|
|
1549
1567
|
const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
|
|
1550
1568
|
if (frameWindowId) {
|
|
1551
1569
|
const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
|
|
@@ -1556,8 +1574,7 @@ class Processor {
|
|
|
1556
1574
|
await frameData.runner.run();
|
|
1557
1575
|
const pageData = await frameData.runner.getPageData();
|
|
1558
1576
|
frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
|
|
1559
|
-
|
|
1560
|
-
this.stats.addAll(pageData);
|
|
1577
|
+
return { frameElement, pageData, frameWindowId, frameData };
|
|
1561
1578
|
} else {
|
|
1562
1579
|
frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
|
|
1563
1580
|
this.stats.add("discarded", "frames", 1);
|
|
@@ -1565,6 +1582,13 @@ class Processor {
|
|
|
1565
1582
|
}
|
|
1566
1583
|
}
|
|
1567
1584
|
}));
|
|
1585
|
+
capturedFrames.forEach(capturedFrame => {
|
|
1586
|
+
if (capturedFrame) {
|
|
1587
|
+
const { frameElement, pageData, frameWindowId, frameData } = capturedFrame;
|
|
1588
|
+
this.processorHelper.processFrame(frameElement, pageData, this.options, this.resources, frameWindowId, frameData);
|
|
1589
|
+
this.stats.addAll(pageData);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1568
1592
|
}
|
|
1569
1593
|
}
|
|
1570
1594
|
|