single-file-core 1.6.5 → 1.6.7
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 +82 -43
- package/core/lib/processor-helper-common.js +29 -5
- package/core/lib/processor-helper-inline.js +143 -21
- package/core/lib/processor-helper.js +183 -76
- 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/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,9 @@ const STAGES = [{
|
|
|
145
145
|
]
|
|
146
146
|
}, {
|
|
147
147
|
sequential: [
|
|
148
|
-
{ option: "removeAlternativeImages", action: "removeAlternativeImages" }
|
|
148
|
+
{ option: "removeAlternativeImages", action: "removeAlternativeImages" },
|
|
149
|
+
{ action: "groupDuplicateFonts" },
|
|
150
|
+
{ action: "groupDuplicateImages" }
|
|
149
151
|
],
|
|
150
152
|
parallel: [
|
|
151
153
|
{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
|
|
@@ -199,6 +201,7 @@ class Runner {
|
|
|
199
201
|
this.options.posters = docData.posters;
|
|
200
202
|
this.options.videos = docData.videos;
|
|
201
203
|
this.options.usedFonts = docData.usedFonts;
|
|
204
|
+
this.options.usedFontsCharacters = docData.usedFontsCharacters;
|
|
202
205
|
this.options.shadowRoots = docData.shadowRoots;
|
|
203
206
|
this.options.referrer = docData.referrer;
|
|
204
207
|
this.options.adoptedStyleSheets = docData.adoptedStyleSheets;
|
|
@@ -222,6 +225,7 @@ class Runner {
|
|
|
222
225
|
await this.onprogress(new ProgressEvent(RESOURCES_INITIALIZING, { pageURL: this.options.url, options: this.options }));
|
|
223
226
|
await this.executeStage(RESOLVE_URLS_STAGE);
|
|
224
227
|
this.pendingPromises = this.executeStage(REPLACE_DATA_STAGE);
|
|
228
|
+
this.pendingPromises.catch(() => { });
|
|
225
229
|
if (this.root && this.options.doc) {
|
|
226
230
|
util.postProcessDoc(this.options.doc, this.markedElements, this.invalidElements);
|
|
227
231
|
}
|
|
@@ -282,7 +286,7 @@ class Runner {
|
|
|
282
286
|
log("**** STARTED STAGE", step, "****");
|
|
283
287
|
}
|
|
284
288
|
const frame = !this.root;
|
|
285
|
-
|
|
289
|
+
const stageStartedPromise = this.onprogress(new ProgressEvent(STAGE_STARTED, { pageURL: this.options.url, step, frame, options: this.options }));
|
|
286
290
|
for (const task of STAGES[step].sequential) {
|
|
287
291
|
let startTime;
|
|
288
292
|
if (DEBUG) {
|
|
@@ -314,6 +318,7 @@ class Runner {
|
|
|
314
318
|
} else {
|
|
315
319
|
parallelTasksPromise = Promise.resolve();
|
|
316
320
|
}
|
|
321
|
+
await stageStartedPromise;
|
|
317
322
|
await this.onprogress(new ProgressEvent(STAGE_ENDED, { pageURL: this.options.url, step, frame, options: this.options }));
|
|
318
323
|
if (DEBUG) {
|
|
319
324
|
log("**** ENDED STAGE", step, "****");
|
|
@@ -341,19 +346,25 @@ class BatchRequest {
|
|
|
341
346
|
return new Promise((resolve, reject) => {
|
|
342
347
|
const requestKey = JSON.stringify([resourceURL, asBinary, expectedType, baseURI, blockMixedContent, contentType]);
|
|
343
348
|
let resourceRequests = this.requests.get(requestKey);
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
349
|
+
if (this.cancelled) {
|
|
350
|
+
reject();
|
|
351
|
+
} else if (this.running && !resourceRequests) {
|
|
352
|
+
reject(new Error("Resource requested after the batch started: " + resourceURL));
|
|
353
|
+
} else {
|
|
354
|
+
if (!resourceRequests) {
|
|
355
|
+
resourceRequests = [];
|
|
356
|
+
this.requests.set(requestKey, resourceRequests);
|
|
357
|
+
}
|
|
358
|
+
const callbacks = { resolve, reject };
|
|
359
|
+
resourceRequests.push(callbacks);
|
|
360
|
+
if (groupDuplicates) {
|
|
361
|
+
let duplicateRequests = this.duplicates.get(requestKey);
|
|
362
|
+
if (!duplicateRequests) {
|
|
363
|
+
duplicateRequests = [];
|
|
364
|
+
this.duplicates.set(requestKey, duplicateRequests);
|
|
365
|
+
}
|
|
366
|
+
duplicateRequests.push(callbacks);
|
|
355
367
|
}
|
|
356
|
-
duplicateRequests.push(callbacks);
|
|
357
368
|
}
|
|
358
369
|
});
|
|
359
370
|
}
|
|
@@ -363,6 +374,7 @@ class BatchRequest {
|
|
|
363
374
|
}
|
|
364
375
|
|
|
365
376
|
run(onloadListener, options) {
|
|
377
|
+
this.running = true;
|
|
366
378
|
const resourceURLs = [...this.requests.keys()];
|
|
367
379
|
let indexResource = 0;
|
|
368
380
|
return Promise.all(resourceURLs.map(async requestKey => {
|
|
@@ -385,7 +397,6 @@ class BatchRequest {
|
|
|
385
397
|
acceptHeaders: options.acceptHeaders,
|
|
386
398
|
networkTimeout: options.networkTimeout
|
|
387
399
|
});
|
|
388
|
-
await onloadListener({ url: resourceURL });
|
|
389
400
|
if (!this.cancelled) {
|
|
390
401
|
const extension = util.getContentTypeExtension(content.contentType) || util.getFilenameExtension(resourceURL, options.filenameReplacedCharacters, options.filenameReplacementCharacter, options.filenameReplacementCharacters);
|
|
391
402
|
resourceRequests.forEach(callbacks => {
|
|
@@ -395,11 +406,10 @@ class BatchRequest {
|
|
|
395
406
|
});
|
|
396
407
|
}
|
|
397
408
|
} catch (error) {
|
|
398
|
-
indexResource = indexResource + 1;
|
|
399
|
-
await onloadListener({ url: resourceURL });
|
|
400
409
|
resourceRequests.forEach(resourceRequest => resourceRequest.reject(error));
|
|
401
410
|
}
|
|
402
411
|
this.requests.delete(requestKey);
|
|
412
|
+
await onloadListener({ url: resourceURL });
|
|
403
413
|
}));
|
|
404
414
|
}
|
|
405
415
|
|
|
@@ -427,6 +437,8 @@ const DISABLED_SCRIPT_URI = "javascript:void(0)";
|
|
|
427
437
|
const SCRIPT_URI_ATTRIBUTE_NAMES = ["href", "src", "action", "formaction", "data"];
|
|
428
438
|
const UTF8_CHARSET = "utf-8";
|
|
429
439
|
const TAINTED_CANVAS_WARNING_MESSAGE = "SingleFile: canvas elements tainted by a cross-origin resource, dropped from the page:";
|
|
440
|
+
const EMPTY_RESOURCE = "data:,";
|
|
441
|
+
const ORDER_DEPENDENT_RULE = /@(layer|import)\b/i;
|
|
430
442
|
|
|
431
443
|
class Processor {
|
|
432
444
|
constructor(options, processorHelper, batchRequest) {
|
|
@@ -457,7 +469,7 @@ class Processor {
|
|
|
457
469
|
}
|
|
458
470
|
this.maxResources = this.batchRequest.getMaxResources();
|
|
459
471
|
if (!this.options.removeFrames && this.options.frames) {
|
|
460
|
-
this.options.frames.forEach(frameData => this.maxResources += frameData.
|
|
472
|
+
this.options.frames.forEach(frameData => this.maxResources += frameData.runner ? frameData.runner.batchRequest.getMaxResources() : 0);
|
|
461
473
|
}
|
|
462
474
|
this.stats.set("processed", "resources", this.maxResources);
|
|
463
475
|
}
|
|
@@ -785,7 +797,7 @@ class Processor {
|
|
|
785
797
|
const attributeValue = element.getAttribute(util.POSTER_ATTRIBUTE_NAME);
|
|
786
798
|
if (attributeValue) {
|
|
787
799
|
const posterURL = this.options.posters[Number(attributeValue)];
|
|
788
|
-
if (!videoElement.getAttribute("poster") && posterURL) {
|
|
800
|
+
if (!videoElement.getAttribute("poster") && posterURL && posterURL != EMPTY_RESOURCE) {
|
|
789
801
|
videoElement.setAttribute("poster", posterURL);
|
|
790
802
|
}
|
|
791
803
|
}
|
|
@@ -1204,12 +1216,15 @@ class Processor {
|
|
|
1204
1216
|
} else {
|
|
1205
1217
|
videoElement = element.parentElement;
|
|
1206
1218
|
}
|
|
1207
|
-
if (!videoElement.poster) {
|
|
1219
|
+
if (!videoElement.poster || videoElement.poster == EMPTY_RESOURCE) {
|
|
1208
1220
|
const attributeValue = videoElement.getAttribute(util.VIDEO_ATTRIBUTE_NAME);
|
|
1209
1221
|
if (attributeValue) {
|
|
1210
1222
|
const videoData = this.options.videos[Number(attributeValue)];
|
|
1211
1223
|
const src = videoData.src || videoElement.src;
|
|
1212
1224
|
if (src) {
|
|
1225
|
+
const blankPosterURI = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='" +
|
|
1226
|
+
(videoData.size.videoWidth || videoData.size.pxWidth) + "' height='" +
|
|
1227
|
+
(videoData.size.videoHeight || videoData.size.pxHeight) + "'%3E%3C/svg%3E";
|
|
1213
1228
|
const temporaryVideoElement = this.doc.createElement("video");
|
|
1214
1229
|
temporaryVideoElement.src = src;
|
|
1215
1230
|
temporaryVideoElement.style.setProperty("width", videoData.size.pxWidth + "px", "important");
|
|
@@ -1222,21 +1237,21 @@ class Processor {
|
|
|
1222
1237
|
return new Promise(resolve => {
|
|
1223
1238
|
temporaryVideoElement.currentTime = videoData.currentTime;
|
|
1224
1239
|
temporaryVideoElement.oncanplay = () => {
|
|
1225
|
-
canvasElement.width = videoData.size.videoWidth;
|
|
1226
|
-
canvasElement.height = videoData.size.videoHeight;
|
|
1240
|
+
canvasElement.width = temporaryVideoElement.videoWidth || videoData.size.videoWidth;
|
|
1241
|
+
canvasElement.height = temporaryVideoElement.videoHeight || videoData.size.videoHeight;
|
|
1227
1242
|
context.drawImage(temporaryVideoElement, 0, 0, canvasElement.width, canvasElement.height);
|
|
1228
1243
|
try {
|
|
1229
|
-
videoElement.poster = util.getPosterDataURI(canvasElement);
|
|
1244
|
+
videoElement.poster = util.getPosterDataURI(canvasElement) || blankPosterURI;
|
|
1230
1245
|
// eslint-disable-next-line no-unused-vars
|
|
1231
1246
|
} catch (error) {
|
|
1232
|
-
videoElement.poster =
|
|
1247
|
+
videoElement.poster = blankPosterURI;
|
|
1233
1248
|
// ignored
|
|
1234
1249
|
}
|
|
1235
1250
|
temporaryVideoElement.remove();
|
|
1236
1251
|
resolve();
|
|
1237
1252
|
};
|
|
1238
1253
|
temporaryVideoElement.onerror = () => {
|
|
1239
|
-
videoElement.poster =
|
|
1254
|
+
videoElement.poster = blankPosterURI;
|
|
1240
1255
|
temporaryVideoElement.remove();
|
|
1241
1256
|
resolve();
|
|
1242
1257
|
};
|
|
@@ -1261,23 +1276,32 @@ class Processor {
|
|
|
1261
1276
|
}
|
|
1262
1277
|
|
|
1263
1278
|
async resolveStylesheetsURLs() {
|
|
1264
|
-
const
|
|
1279
|
+
const styleElementGroups = new Map();
|
|
1265
1280
|
this.options.inlineStylesheets = new Map();
|
|
1266
1281
|
this.options.inlineStylesheetsRefs = new Map();
|
|
1267
1282
|
this.doc.querySelectorAll("style").forEach(styleElement => {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
if (
|
|
1271
|
-
|
|
1272
|
-
styleElement,
|
|
1273
|
-
content: styleElement.textContent
|
|
1274
|
-
});
|
|
1275
|
-
stylesContents.push(styleElement.textContent);
|
|
1276
|
-
} else {
|
|
1277
|
-
this.options.inlineStylesheetsRefs.set(styleElement, indexContent);
|
|
1283
|
+
const content = styleElement.textContent;
|
|
1284
|
+
if (content) {
|
|
1285
|
+
if (!styleElementGroups.has(content)) {
|
|
1286
|
+
styleElementGroups.set(content, []);
|
|
1278
1287
|
}
|
|
1288
|
+
styleElementGroups.get(content).push(styleElement);
|
|
1279
1289
|
}
|
|
1280
1290
|
});
|
|
1291
|
+
let indexContent = 0;
|
|
1292
|
+
styleElementGroups.forEach((styleElements, content) => {
|
|
1293
|
+
const masterIndex = ORDER_DEPENDENT_RULE.test(content) ? 0 : styleElements.length - 1;
|
|
1294
|
+
this.options.inlineStylesheets.set(indexContent, {
|
|
1295
|
+
styleElement: styleElements[masterIndex],
|
|
1296
|
+
content
|
|
1297
|
+
});
|
|
1298
|
+
styleElements.forEach((styleElement, index) => {
|
|
1299
|
+
if (index != masterIndex) {
|
|
1300
|
+
this.options.inlineStylesheetsRefs.set(styleElement, indexContent);
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
indexContent++;
|
|
1304
|
+
});
|
|
1281
1305
|
await Promise.all(Array.from(this.doc.querySelectorAll("style, link[rel*=stylesheet]:not([disabled])")).map(async element => {
|
|
1282
1306
|
const options = Object.assign({}, this.options, { charset: this.charset });
|
|
1283
1307
|
let mediaText;
|
|
@@ -1346,19 +1370,20 @@ class Processor {
|
|
|
1346
1370
|
stylesheets: [],
|
|
1347
1371
|
url,
|
|
1348
1372
|
usedFonts: [],
|
|
1373
|
+
usedFontsCharacters: [],
|
|
1349
1374
|
videos: [],
|
|
1350
1375
|
worklets: []
|
|
1351
1376
|
};
|
|
1352
1377
|
this.options.frames.push(frameData);
|
|
1353
1378
|
frameData.windowId = (this.options.windowId || "0") + "." + this.options.frames.length;
|
|
1354
1379
|
frameElement.setAttribute(util.WIN_ID_ATTRIBUTE_NAME, frameData.windowId);
|
|
1355
|
-
await initializeProcessor(frameData, frameElement, null,
|
|
1380
|
+
await initializeProcessor(frameData, frameElement, null, Object.assign({}, this.options));
|
|
1356
1381
|
} else {
|
|
1357
1382
|
const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
|
|
1358
1383
|
if (this.options.frames && frameWindowId) {
|
|
1359
1384
|
const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
|
|
1360
1385
|
if (frameData && frameData.content) {
|
|
1361
|
-
await initializeProcessor(frameData, frameElement, frameWindowId,
|
|
1386
|
+
await initializeProcessor(frameData, frameElement, frameWindowId, Object.assign({}, this.options));
|
|
1362
1387
|
}
|
|
1363
1388
|
}
|
|
1364
1389
|
}
|
|
@@ -1369,7 +1394,7 @@ class Processor {
|
|
|
1369
1394
|
}
|
|
1370
1395
|
});
|
|
1371
1396
|
|
|
1372
|
-
async function initializeProcessor(frameData, frameElement, frameWindowId,
|
|
1397
|
+
async function initializeProcessor(frameData, frameElement, frameWindowId, options) {
|
|
1373
1398
|
options.insertSingleFileComment = false;
|
|
1374
1399
|
options.insertCanonicalLink = false;
|
|
1375
1400
|
options.insertMetaNoIndex = false;
|
|
@@ -1390,6 +1415,7 @@ class Processor {
|
|
|
1390
1415
|
options.posters = frameData.posters;
|
|
1391
1416
|
options.videos = frameData.videos;
|
|
1392
1417
|
options.usedFonts = frameData.usedFonts;
|
|
1418
|
+
options.usedFontsCharacters = frameData.usedFontsCharacters;
|
|
1393
1419
|
options.shadowRoots = frameData.shadowRoots;
|
|
1394
1420
|
options.scrollPosition = frameData.scrollPosition;
|
|
1395
1421
|
options.scrolling = frameData.scrolling;
|
|
@@ -1398,7 +1424,6 @@ class Processor {
|
|
|
1398
1424
|
frameData.frameElement = frameElement;
|
|
1399
1425
|
await frameData.runner.loadPage();
|
|
1400
1426
|
await frameData.runner.initialize();
|
|
1401
|
-
frameData.maxResources = batchRequest.getMaxResources();
|
|
1402
1427
|
}
|
|
1403
1428
|
}
|
|
1404
1429
|
|
|
@@ -1538,6 +1563,14 @@ class Processor {
|
|
|
1538
1563
|
util.removeAlternativeImages(this.doc);
|
|
1539
1564
|
}
|
|
1540
1565
|
|
|
1566
|
+
groupDuplicateFonts() {
|
|
1567
|
+
this.processorHelper.groupDuplicateFonts(this.stylesheets, this.resources.fonts, this.options);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
groupDuplicateImages() {
|
|
1571
|
+
this.processorHelper.groupDuplicateImages(this.doc, this.stylesheets, this.styles, this.resources.images);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1541
1574
|
async removeAlternativeFonts() {
|
|
1542
1575
|
await this.processorHelper.removeAlternativeFonts(this.doc, this.stylesheets, this.resources.fonts, this.options.fontTests);
|
|
1543
1576
|
}
|
|
@@ -1545,7 +1578,7 @@ class Processor {
|
|
|
1545
1578
|
async processFrames() {
|
|
1546
1579
|
if (this.options.frames) {
|
|
1547
1580
|
const frameElements = Array.from(this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]"));
|
|
1548
|
-
await Promise.all(frameElements.map(async frameElement => {
|
|
1581
|
+
const capturedFrames = await Promise.all(frameElements.map(async frameElement => {
|
|
1549
1582
|
const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
|
|
1550
1583
|
if (frameWindowId) {
|
|
1551
1584
|
const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
|
|
@@ -1556,8 +1589,7 @@ class Processor {
|
|
|
1556
1589
|
await frameData.runner.run();
|
|
1557
1590
|
const pageData = await frameData.runner.getPageData();
|
|
1558
1591
|
frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
|
|
1559
|
-
|
|
1560
|
-
this.stats.addAll(pageData);
|
|
1592
|
+
return { frameElement, pageData, frameWindowId, frameData };
|
|
1561
1593
|
} else {
|
|
1562
1594
|
frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
|
|
1563
1595
|
this.stats.add("discarded", "frames", 1);
|
|
@@ -1565,6 +1597,13 @@ class Processor {
|
|
|
1565
1597
|
}
|
|
1566
1598
|
}
|
|
1567
1599
|
}));
|
|
1600
|
+
capturedFrames.forEach(capturedFrame => {
|
|
1601
|
+
if (capturedFrame) {
|
|
1602
|
+
const { frameElement, pageData, frameWindowId, frameData } = capturedFrame;
|
|
1603
|
+
this.processorHelper.processFrame(frameElement, pageData, this.options, this.resources, frameWindowId, frameData);
|
|
1604
|
+
this.stats.addAll(pageData);
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1568
1607
|
}
|
|
1569
1608
|
}
|
|
1570
1609
|
|