single-file-core 1.5.118 → 1.5.120

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 CHANGED
@@ -493,6 +493,15 @@ function getStylesheetsContent(styleSheets, adoptedStyleSheetsCache = new Map())
493
493
  }
494
494
  }
495
495
 
496
+ // an untouched canvas is fully transparent, so it encodes exactly like a blank one of the same
497
+ // size: comparing the two is cheaper than reading the pixels back and needs no drawing context
498
+ function isBlankCanvas(doc, element, dataURI) {
499
+ const blankElement = doc.createElement("canvas");
500
+ blankElement.width = element.width;
501
+ blankElement.height = element.height;
502
+ return blankElement.toDataURL("image/png") == dataURI;
503
+ }
504
+
496
505
  function getResourcesInfo(win, doc, element, options, data, elementHidden, computedStyle) {
497
506
  const tagName = element.tagName && element.tagName.toUpperCase();
498
507
  if (tagName == "CANVAS") {
@@ -501,13 +510,19 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
501
510
  backgroundColor: canvasComputedStyle && canvasComputedStyle.getPropertyValue("background-color")
502
511
  };
503
512
  try {
504
- canvasData.dataURI = element.toDataURL("image/png");
513
+ const dataURI = element.toDataURL("image/png");
514
+ // a canvas in a page SingleFile has already saved is empty, no script ran to draw into
515
+ // it, and the picture it displays is the background image the previous save left
516
+ // behind. An empty bitmap must not overwrite it
517
+ const backgroundImage = canvasComputedStyle ? canvasComputedStyle.getPropertyValue("background-image") : element.style.getPropertyValue("background-image");
518
+ if (backgroundImage && backgroundImage != "none" && isBlankCanvas(doc, element, dataURI)) {
519
+ canvasData.blank = true;
520
+ } else {
521
+ canvasData.dataURI = dataURI;
522
+ }
505
523
  // eslint-disable-next-line no-unused-vars
506
524
  } catch (error) {
507
- // a canvas painted with a cross-origin resource is tainted and toDataURL throws for the
508
- // whole element, its own drawing included. The entry is pushed and the element marked
509
- // anyway: the loss is then counted and reported instead of being silent, and an
510
- // out-of-page fallback re-rendering the element has something to find it by
525
+ // ignored
511
526
  }
512
527
  data.canvases.push(canvasData);
513
528
  element.setAttribute(CANVAS_ATTRIBUTE_NAME, data.canvases.length - 1);
@@ -611,27 +626,11 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
611
626
  }
612
627
  }
613
628
 
614
- // Every family in the computed font-family is recorded, and the set of faces the document actually
615
- // resolved is deliberately NOT consulted. Narrowing the list to the loaded faces is the obvious
616
- // idea — a fallback stack names every family the page might need and only some are used — and it
617
- // was written in 2019, wired to an option nothing ever set, and dead until it was reconnected here.
618
- // Reconnected, it saved nothing measurable on nine real pages and cost fidelity twice: a face
619
- // matched on its declared style is not found when the browser SYNTHESIZED that style, so a family
620
- // drawn in an italic it declares no face for was dropped from the page that draws it; and a webfont
621
- // that merely failed to load on the capturing machine would be dropped from the archive for good.
622
- // It belongs with the unicode-range work, which needs the same information and can weigh a measured
623
- // benefit against those, not here.
624
629
  function getUsedFont(computedStyle, usedFonts) {
625
630
  if (computedStyle) {
626
631
  const fontStyle = computedStyle.getPropertyValue("font-style") || "normal";
627
632
  computedStyle.getPropertyValue("font-family").split(",").forEach(fontFamilyName => {
628
633
  fontFamilyName = normalizeFontFamily(fontFamilyName);
629
- // an element with no computed font-family at all is one whose styles could not be read,
630
- // not one drawn in a family with no name. It happens to every element of a frame that
631
- // was re-parsed from its srcdoc, because the computed style is asked of the parent
632
- // window and the document it belongs to is not the one being rendered. Recorded, it
633
- // turns "nothing is known here" into a list of length one, which reads downstream as a
634
- // complete answer and prunes every face the frame declares
635
634
  if (fontFamilyName) {
636
635
  const fontWeight = getFontWeight(computedStyle.getPropertyValue("font-weight"));
637
636
  const fontVariant = computedStyle.getPropertyValue("font-variant") || "normal";
@@ -899,7 +898,9 @@ function getComputedStyle(win, element, pseudoElement) {
899
898
  function getValidFilename(filename, replacedCharacters = DEFAULT_REPLACED_CHARACTERS, replacementCharacter = DEFAULT_REPLACEMENT_CHARACTER, replacementCharacters = DEFAULT_REPLACEMENT_CHARACTERS) {
900
899
  replacementCharacters.forEach((indexReplacementCharacter, index) => {
901
900
  if (replacedCharacters[index] !== undefined && indexReplacementCharacter != replacedCharacters[index]) {
902
- filename = filename.replace(new RegExp("[" + getCharacterClassContent(replacedCharacters[index]) + "]+", "g"), indexReplacementCharacter);
901
+ // no "+" here, unlike the fallback below: a lookalike replaces its character one for
902
+ // one, so collapsing a run would drop characters the name needs ("C++" -> "C+")
903
+ filename = filename.replace(new RegExp("[" + getCharacterClassContent(replacedCharacters[index]) + "]", "g"), indexReplacementCharacter);
903
904
  }
904
905
  });
905
906
  replacedCharacters.forEach((replacedCharacter, index) => {
package/core/index.js CHANGED
@@ -530,6 +530,10 @@ class Processor {
530
530
  " \n url: " + infobarURL +
531
531
  (this.options.removeSavedDate ? " " : " \n saved date: " + infobarSaveDate) +
532
532
  (infobarContent ? " \n info: " + infobarContent : "") + "\n";
533
+ // the infobar template resolves {page-title} and its kind against the captured page, so
534
+ // this text can carry "-->" and close the comment early, turning the rest into markup.
535
+ // A comment cannot escape its own delimiters, so the run is broken with a space
536
+ commentText = commentText.replace(/--(!?)>/g, "--$1 >");
533
537
  const commentNode = this.doc.createComment(commentText);
534
538
  this.doc.documentElement.insertBefore(commentNode, this.doc.documentElement.firstChild);
535
539
  }
@@ -826,11 +830,6 @@ class Processor {
826
830
  });
827
831
  }
828
832
 
829
- // a media element left without any source can never start, and the attribute
830
- // would keep it announcing a playback that never happens. The sources are
831
- // dropped whenever they could not be stored, so this is not specific to
832
- // blocked videos, and it runs on the final state to leave alone an element
833
- // that kept one of several sources
834
833
  removeEmptyMediaAutoplay() {
835
834
  this.doc.querySelectorAll("video[autoplay], audio[autoplay]").forEach(element => {
836
835
  const sourceElements = Array.from(element.querySelectorAll("source"));
@@ -1069,7 +1068,7 @@ class Processor {
1069
1068
  }
1070
1069
  this.processorHelper.setBackgroundImage(canvasElement, "url(" + canvasData.dataURI + ")", backgroundStyle);
1071
1070
  this.stats.add("processed", "canvas", 1);
1072
- } else {
1071
+ } else if (!canvasData.blank) {
1073
1072
  discardedCount++;
1074
1073
  this.stats.add("discarded", "canvas", 1);
1075
1074
  }
@@ -21,9 +21,6 @@
21
21
  * Source.
22
22
  */
23
23
 
24
- // this module is imported by the script embedded in self-extracting archives, it
25
- // must stay free of dependencies
26
-
27
24
  export {
28
25
  getDoctypeString
29
26
  };
@@ -102,10 +102,6 @@ class ProcessorHelperCommon {
102
102
  ["image, feImage", "xlink:href"],
103
103
  ["image, feImage", "href"]
104
104
  ];
105
- // an inline svg is document content, not a fetched resource: blocking images empties the
106
- // references it makes outwards (image, feImage and use are all processed below) and leaves
107
- // the markup itself alone. Removing it deleted JS-drawn charts, inline icons, and the
108
- // <defs> holding gradients and filters that CSS applies to ordinary HTML elements
109
105
  let resourcePromises = processAttributeArgs.map(([selector, attributeName, removeElementIfMissing, processDuplicates]) =>
110
106
  this.processAttribute(doc, doc.querySelectorAll(selector), attributeName, baseURI, options, "image", resources, removeElementIfMissing, batchRequest, styles, processDuplicates)
111
107
  );
@@ -137,9 +133,6 @@ class ProcessorHelperCommon {
137
133
  resourceElement.setAttribute("data-sf-original-href", originalResourceURL);
138
134
  }
139
135
  let resourceURL = normalizeURL(originalResourceURL);
140
- // a reference into the page itself costs no request, so blocking images must not empty
141
- // it: normalizeURL drops the fragment, which leaves a bare "#symbol" as the empty path
142
- // below and a same-document URL matching options.url
143
136
  if (testValidPath(resourceURL) && !testIgnoredPath(resourceURL)) {
144
137
  resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
145
138
  if (!options.blockImages) {
@@ -257,9 +250,6 @@ class ProcessorHelperCommon {
257
250
  }));
258
251
  }
259
252
 
260
- // a video or an audio element gets the attribute REMOVED rather than emptied: util.EMPTY_RESOURCE
261
- // is inert on an image but not on a media element, which would attempt the load, fail, and sit
262
- // in an error state where removing the attribute leaves the poster showing cleanly
263
253
  setAttributeEmpty(resourceElement, attributeName, expectedType) {
264
254
  if (expectedType == "video" || expectedType == "audio") {
265
255
  resourceElement.removeAttribute(attributeName);
@@ -454,11 +444,6 @@ class ProcessorHelperCommon {
454
444
  const key = this.getFontKey(ruleData);
455
445
  const fontInfo = fontsDetails.fonts.get(key);
456
446
  if (fontInfo) {
457
- // a face declaring the same descriptors and the same sources as one already kept
458
- // cannot change what that one did, and its sources are embedded a second time. A
459
- // stylesheet reached from two <link> elements, or imported by two sheets, repeats
460
- // every face it carries. The test is made before the await: the sources are read
461
- // as they stand for both rules, and processing them is what would interleave
462
447
  const ruleKey = key + " " + this.getPropertyValue(ruleData, "src");
463
448
  if (fontsDetails.emittedFonts.has(ruleKey)) {
464
449
  removedRules.push(cssRule);
@@ -524,10 +509,6 @@ class ProcessorHelperCommon {
524
509
  medias: new Map(),
525
510
  supports: new Map(),
526
511
  layers: new Map(),
527
- // the faces already emitted in this cascade context, so a second declaration of one of
528
- // them can be dropped instead of embedding the same bytes again. Each media, supports
529
- // and layer block gets its own info, so only rules that apply under the same conditions
530
- // are ever compared
531
512
  emittedFonts: new Set()
532
513
  };
533
514
  }
@@ -44,8 +44,6 @@ const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
44
44
  const LOCAL_SOURCE = "local(";
45
45
  const FONT_MAX_LOAD_DELAY = 5000;
46
46
  const DUPLICATE_STYLESHEET_ATTRIBUTE_NAME = "data-sf-duplicate-stylesheet-ref";
47
- // the attributes that say how the link fetched its stylesheet, which the style element now holding
48
- // that stylesheet has no use for. Everything else identified the element in the page
49
47
  const LINK_FETCH_ATTRIBUTE_NAMES = ["rel", "href", "type", "media", "as", "crossorigin", "integrity",
50
48
  "referrerpolicy", "hreflang", "sizes", "imagesrcset", "imagesizes", "fetchpriority"];
51
49
 
@@ -166,10 +164,6 @@ function getProcessorHelperClass(utilInstance) {
166
164
  if (stylesheetInfo) {
167
165
  stylesheets.delete(linkElement);
168
166
  const styleElement = doc.createElement("style");
169
- // the element is replaced rather than rewritten, so whatever identified it in
170
- // the page has to be carried over: an id a script still looks up, a class a
171
- // selector still matches. The attributes left out are the ones that describe
172
- // the fetch the style element no longer performs
173
167
  Array.from(linkElement.attributes).forEach(({ name, value }) => {
174
168
  if (!LINK_FETCH_ATTRIBUTE_NAMES.includes(name.toLowerCase())) {
175
169
  styleElement.setAttribute(name, value);
@@ -36,6 +36,7 @@ const CANVAS_TAG_FOUND = /<canvas/gi;
36
36
  const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
37
37
  const LOCAL_SOURCE = "local(";
38
38
  const FONT_MAX_LOAD_DELAY = 5000;
39
+ const SCRIPT_EXTENSION = ".js";
39
40
  const LINK_OWN_ATTRIBUTE_NAMES = ["rel", "type", "href", "media"];
40
41
 
41
42
  let util;
@@ -108,10 +109,6 @@ function getProcessorHelperClass(utilInstance) {
108
109
  linkElement.setAttribute("type", "text/css");
109
110
  const name = "stylesheet_" + resources.stylesheets.size + ".css";
110
111
  linkElement.setAttribute("href", name);
111
- // the shared copy is generated from the stylesheet of the element the duplicates were
112
- // folded into, not from the text that was captured: that text still names the resources
113
- // by their addresses in the original page, which resolve to nothing once the page is
114
- // inside the archive
115
112
  const { styleElement, content } = options.inlineStylesheets.get(stylesheetRefIndex);
116
113
  const sharedEntry = entries.find(([key]) => key.element == styleElement);
117
114
  const stylesheet = sharedEntry
@@ -119,10 +116,6 @@ function getProcessorHelperClass(utilInstance) {
119
116
  : cssTree.parse(content, { context: "stylesheet", parseCustomProperty: true });
120
117
  resources.stylesheets.set(resources.stylesheets.size, { name, content: this.generateStylesheetContent(stylesheet, options) });
121
118
  linkElements.set(stylesheetRefIndex, linkElement);
122
- // the element the duplicates were folded into is not itself a duplicate, so it is
123
- // absent from inlineStylesheetsRefs and would keep its content inline: the archive
124
- // would then hold the same stylesheet twice, once as the entry the links point at
125
- // and once in the page
126
119
  sharedStyleElements.set(styleElement, stylesheetRefIndex);
127
120
  });
128
121
  for (const [key, stylesheetInfo] of entries) {
@@ -149,10 +142,6 @@ function getProcessorHelperClass(utilInstance) {
149
142
  styleElement.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
150
143
  } else {
151
144
  const linkElement = linkElements.get(stylesheetRefIndex).cloneNode(true);
152
- // the element is replaced rather than rewritten, so whatever identified it in
153
- // the page has to be carried over: an id a script still looks up, a class a
154
- // selector still matches. The attributes left out are the ones that describe
155
- // the link itself, which the code around here sets
156
145
  Array.from(styleElement.attributes).forEach(({ name, value }) => {
157
146
  if (!LINK_OWN_ATTRIBUTE_NAMES.includes(name.toLowerCase())) {
158
147
  linkElement.setAttribute(name, value);
@@ -191,9 +180,6 @@ function getProcessorHelperClass(utilInstance) {
191
180
  } catch (error) {
192
181
  // ignored
193
182
  }
194
- // a sheet already open higher in this import chain must not be entered again: the
195
- // ancestors are carried down the branch, not accumulated across the whole document,
196
- // so two sibling imports of one sheet are still both resolved
197
183
  if (testValidURL(resourceURL) && !importedStyleSheets.has(resourceURL)) {
198
184
  const mediaQueryListNode = cssTree.find(node, node => node.type == "MediaQueryList");
199
185
  let mediaText, layerName, supportsCondition;
@@ -231,9 +217,6 @@ function getProcessorHelperClass(utilInstance) {
231
217
  stylesheetInfo.stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
232
218
  stylesheet = stylesheetInfo.stylesheet;
233
219
  const ancestorStyleSheets = new Set(importedStyleSheets);
234
- // both identities of the sheet are remembered: a redirect makes the URL that was
235
- // requested and the URL that answered differ, and an import of either one is the
236
- // same cycle
237
220
  ancestorStyleSheets.add(requestedURL);
238
221
  ancestorStyleSheets.add(resourceURL);
239
222
  await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets, ancestorStyleSheets);
@@ -472,7 +455,11 @@ function getProcessorHelperClass(utilInstance) {
472
455
  networkTimeout: options.networkTimeout
473
456
  });
474
457
  content = getUpdatedResourceContent(resourceURL, options) || content;
475
- const name = "scripts/" + indexResource + extension;
458
+ // the extension is taken from the URL when the content type is not in the map, and a
459
+ // script URL says nothing about the content: a module served as text/javascript from a
460
+ // ".ts" URL was named ".ts", stored uncompressed and served back as video/mp2t, which
461
+ // the browser refuses to execute. Stylesheets have always been named this way
462
+ const name = "scripts/" + indexResource + SCRIPT_EXTENSION;
476
463
  element.setAttribute("src", name);
477
464
  resources.scripts.set(indexResource, { name, content, extension, contentType, url: resourceURL });
478
465
  }
@@ -491,7 +478,7 @@ function getProcessorHelperClass(utilInstance) {
491
478
  acceptHeaders: options.acceptHeaders,
492
479
  networkTimeout: options.networkTimeout
493
480
  });
494
- const name = "scripts/" + indexResource + extension;
481
+ const name = "scripts/" + indexResource + SCRIPT_EXTENSION;
495
482
  if (workletOptions) {
496
483
  scriptElement.textContent += ` CSS.paintWorklet.addModule("${name}", ${JSON.stringify(workletOptions)});\n`;
497
484
  } else {