single-file-core 1.6.24 → 1.6.26

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
@@ -69,6 +69,7 @@ const DISABLED_NOSCRIPT_ATTRIBUTE_NAME = "data-" + SINGLE_FILE_PREFIX + "disable
69
69
  const SELECTED_CONTENT_ATTRIBUTE_NAME = "data-" + SINGLE_FILE_PREFIX + "selected-content";
70
70
  const INVALID_ELEMENT_ATTRIBUTE_NAME = "data-" + SINGLE_FILE_PREFIX + "invalid-element";
71
71
  const ASYNC_SCRIPT_ATTRIBUTE_NAME = "data-" + SINGLE_FILE_PREFIX + "async-script";
72
+ const CUSTOM_STYLESHEET_ATTRIBUTE_NAME = "data-" + SINGLE_FILE_PREFIX + "custom-stylesheet";
72
73
  const FLOW_ELEMENTS_SELECTOR = "*:not(base):not(link):not(meta):not(noscript):not(script):not(style):not(template):not(title)";
73
74
  const KEPT_TAG_NAMES = ["NOSCRIPT", "DISABLED-NOSCRIPT", "META", "LINK", "STYLE", "TITLE", "TEMPLATE", "SOURCE", "OBJECT", "SCRIPT", "HEAD", "BODY"];
74
75
  const INVOKABLE_ELEMENTS_SELECTOR = "[popover][id], dialog[id]";
@@ -173,6 +174,7 @@ export {
173
174
  WIN_ID_ATTRIBUTE_NAME,
174
175
  PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME,
175
176
  REMOVED_CONTENT_ATTRIBUTE_NAME,
177
+ CUSTOM_STYLESHEET_ATTRIBUTE_NAME,
176
178
  HIDDEN_CONTENT_ATTRIBUTE_NAME,
177
179
  HIDDEN_FRAME_ATTRIBUTE_NAME,
178
180
  IMAGE_ATTRIBUTE_NAME,
@@ -305,6 +307,9 @@ function preProcessDoc(doc, win, options) {
305
307
  element.textContent = "";
306
308
  });
307
309
  initDoc(doc);
310
+ if (options.customStylesheet) {
311
+ insertCustomStylesheet(doc, options.customStylesheet);
312
+ }
308
313
  if (doc.head) {
309
314
  doc.head.querySelectorAll(FLOW_ELEMENTS_SELECTOR).forEach(element => element.hidden = true);
310
315
  }
@@ -371,10 +376,75 @@ function preProcessDoc(doc, win, options) {
371
376
  markedElements: elementsInfo.markedElements,
372
377
  invalidElements,
373
378
  scrollPosition: { x: win.scrollX, y: win.scrollY },
374
- adoptedStyleSheets: getStylesheetsContent(doc.adoptedStyleSheets)
379
+ adoptedStyleSheets: getStylesheetsContent(doc.adoptedStyleSheets),
380
+ manifestData: options.manifest ? getManifestData(doc, win) : undefined
375
381
  };
376
382
  }
377
383
 
384
+ function getManifestData(doc, win) {
385
+ const manifestData = { url: doc.documentURI, timings: {} };
386
+ try {
387
+ const lastModified = new Date(doc.lastModified);
388
+ if (!isNaN(lastModified)) {
389
+ manifestData.lastModified = lastModified.toISOString();
390
+ }
391
+ // eslint-disable-next-line no-unused-vars
392
+ } catch (error) {
393
+ // ignored
394
+ }
395
+ try {
396
+ const performance = win.performance;
397
+ manifestData.timeOrigin = new Date(performance.timeOrigin).toISOString();
398
+ const [navigation] = performance.getEntriesByType("navigation");
399
+ if (navigation) {
400
+ manifestData.navigation = getTimingData(navigation);
401
+ manifestData.navigation.type = navigation.type;
402
+ manifestData.navigation.redirectCount = navigation.redirectCount;
403
+ }
404
+ performance.getEntriesByType("resource").forEach(entry => manifestData.timings[entry.name] = getTimingData(entry));
405
+ // eslint-disable-next-line no-unused-vars
406
+ } catch (error) {
407
+ // ignored
408
+ }
409
+ try {
410
+ manifestData.environment = {
411
+ userAgent: win.navigator.userAgent,
412
+ viewport: { width: win.innerWidth, height: win.innerHeight, devicePixelRatio: win.devicePixelRatio },
413
+ media: {
414
+ prefersColorScheme: win.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
415
+ prefersReducedMotion: win.matchMedia("(prefers-reduced-motion: reduce)").matches ? "reduce" : "no-preference"
416
+ }
417
+ };
418
+ // eslint-disable-next-line no-unused-vars
419
+ } catch (error) {
420
+ // ignored
421
+ }
422
+ return manifestData;
423
+ }
424
+
425
+ function getTimingData(entry) {
426
+ const timingData = {
427
+ transferSize: entry.transferSize,
428
+ encodedBodySize: entry.encodedBodySize,
429
+ decodedBodySize: entry.decodedBodySize,
430
+ nextHopProtocol: entry.nextHopProtocol
431
+ };
432
+ if (entry.responseStatus) {
433
+ timingData.status = entry.responseStatus;
434
+ }
435
+ return timingData;
436
+ }
437
+
438
+ function insertCustomStylesheet(doc, content) {
439
+ const parentElement = doc.body || doc.documentElement;
440
+ if (parentElement) {
441
+ const styleElement = doc.createElement("style");
442
+ styleElement.setAttribute(CUSTOM_STYLESHEET_ATTRIBUTE_NAME, "");
443
+ styleElement.textContent = content;
444
+ parentElement.appendChild(styleElement);
445
+ }
446
+ }
447
+
378
448
  function markInvalidNesting(doc) {
379
449
  if (!doc.body) {
380
450
  return;
@@ -1093,6 +1163,7 @@ function testHiddenElement(element, computedStyle) {
1093
1163
 
1094
1164
  function postProcessDoc(doc, markedElements, invalidElements) {
1095
1165
  removeNestingMarkers(doc);
1166
+ doc.querySelectorAll("style[" + CUSTOM_STYLESHEET_ATTRIBUTE_NAME + "]").forEach(element => element.remove());
1096
1167
  doc.querySelectorAll("[" + DISABLED_NOSCRIPT_ATTRIBUTE_NAME + "]").forEach(element => {
1097
1168
  element.textContent = element.getAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME);
1098
1169
  element.removeAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME);
@@ -1335,7 +1406,7 @@ async function getDataURI(blob) {
1335
1406
 
1336
1407
  async function digest(algo, text) {
1337
1408
  try {
1338
- const data = new TextEncoder("utf-8").encode(text);
1409
+ const data = typeof text == "string" ? new TextEncoder("utf-8").encode(text) : text instanceof Uint8Array ? text : new Uint8Array(text);
1339
1410
  const hash = globalThis.crypto && crypto.subtle ? await crypto.subtle.digest(algo, data) : sha.digest(algo, data);
1340
1411
  return hex(hash);
1341
1412
  // eslint-disable-next-line no-unused-vars
package/core/index.js CHANGED
@@ -207,6 +207,7 @@ class Runner {
207
207
  this.options.shadowRoots = docData.shadowRoots;
208
208
  this.options.referrer = docData.referrer;
209
209
  this.options.adoptedStyleSheets = docData.adoptedStyleSheets;
210
+ this.options.manifestData = docData.manifestData;
210
211
  this.markedElements = docData.markedElements;
211
212
  this.invalidElements = docData.invalidElements;
212
213
  }
@@ -403,8 +404,12 @@ class BatchRequest {
403
404
  baseURI,
404
405
  blockMixedContent,
405
406
  acceptHeaders: options.acceptHeaders,
406
- networkTimeout: options.networkTimeout
407
+ networkTimeout: options.networkTimeout,
408
+ manifestEntries: options.manifestEntries
407
409
  });
410
+ if (content.manifestEntry) {
411
+ content.manifestEntry.references = resourceRequests.length;
412
+ }
408
413
  if (!this.cancelled) {
409
414
  const extension = util.getContentTypeExtension(content.contentType) || util.getFilenameExtension(resourceURL, options.filenameReplacedCharacters, options.filenameReplacementCharacter, options.filenameReplacementCharacters);
410
415
  resourceRequests.forEach(callbacks => {
@@ -451,6 +456,10 @@ const UTF8_CHARSET = "utf-8";
451
456
  const TAINTED_CANVAS_WARNING_MESSAGE = "SingleFile: canvas elements tainted by a cross-origin resource, dropped from the page:";
452
457
  const EMPTY_RESOURCE = "data:,";
453
458
  const ORDER_DEPENDENT_RULE = /@(layer|import)\b/i;
459
+ const MANIFEST_TOP_WINDOW_ID = "0";
460
+ const WINDOW_ID_SEPARATOR = ".";
461
+ const MANIFEST_EXCLUDED_OPTION_NAMES = ["rootDocument", "saveFilenameTemplateData", "omitReferrerInTemplateData", "manifest"];
462
+ const MANIFEST_STRING_OPTION_NAMES = ["customStylesheet", "filenameTemplate"];
454
463
 
455
464
  class Processor {
456
465
  constructor(options, processorHelper, batchRequest) {
@@ -471,7 +480,10 @@ class Processor {
471
480
  frames: new Map()
472
481
  };
473
482
  this.fontTests = options.fontTests;
483
+ this.frameManifests = [];
484
+ this.startDate = new Date();
474
485
  options.generatedDataURIs = new Set();
486
+ options.manifestEntries = options.manifest ? [] : undefined;
475
487
  }
476
488
 
477
489
  registerGeneratedDataURI(dataURI) {
@@ -504,7 +516,8 @@ class Processor {
504
516
  resourceReferrer: this.options.resourceReferrer,
505
517
  expectedType: "document",
506
518
  acceptHeaders: this.options.acceptHeaders,
507
- networkTimeout: this.options.networkTimeout
519
+ networkTimeout: this.options.networkTimeout,
520
+ manifestEntries: this.options.manifestEntries
508
521
  });
509
522
  pageContent = content.data || "";
510
523
  }
@@ -654,13 +667,54 @@ class Processor {
654
667
  if (this.options.addProof) {
655
668
  pageData.hash = await util.digest("SHA-256", content);
656
669
  }
670
+ if (this.options.manifest) {
671
+ pageData.manifest = this.getManifest();
672
+ }
657
673
  if (this.options.retrieveLinks) {
658
674
  pageData.links = Array.from(new Set(Array.from(this.doc.links).map(linkElement => linkElement.href)));
659
675
  }
660
676
  return pageData;
661
677
  }
662
678
 
679
+ getManifest() {
680
+ const windowId = this.options.windowId || MANIFEST_TOP_WINDOW_ID;
681
+ const manifestData = this.options.manifestData || {};
682
+ const frame = {
683
+ id: windowId,
684
+ parent: windowId.includes(WINDOW_ID_SEPARATOR) ? windowId.substring(0, windowId.lastIndexOf(WINDOW_ID_SEPARATOR)) : null,
685
+ url: this.baseURI
686
+ };
687
+ if (manifestData.lastModified) {
688
+ frame.lastModified = manifestData.lastModified;
689
+ }
690
+ if (manifestData.navigation) {
691
+ frame.navigation = manifestData.navigation;
692
+ }
693
+ frame.resources = this.options.manifestEntries.map(entry => {
694
+ const timing = manifestData.timings && (manifestData.timings[entry.finalUrl] || manifestData.timings[entry.url]);
695
+ return timing ? Object.assign({}, entry, { timing }) : entry;
696
+ });
697
+ const frames = [frame, ...this.frameManifests];
698
+ if (!this.options.rootDocument) {
699
+ return { frames };
700
+ }
701
+ const manifest = {
702
+ subject: { url: this.options.saveUrl, finalUrl: manifestData.url || this.baseURI, title: this.doc.title },
703
+ time: { captureStarted: this.startDate.toISOString(), captureEnded: new Date().toISOString() }
704
+ };
705
+ if (manifestData.timeOrigin) {
706
+ manifest.time.loaded = manifestData.timeOrigin;
707
+ }
708
+ if (manifestData.environment) {
709
+ manifest.environment = manifestData.environment;
710
+ }
711
+ manifest.options = getManifestOptions(this.options);
712
+ manifest.frames = frames;
713
+ return manifest;
714
+ }
715
+
663
716
  preProcessPage() {
717
+ this.doc.querySelectorAll("style[" + util.CUSTOM_STYLESHEET_ATTRIBUTE_NAME + "]").forEach(element => element.removeAttribute(util.CUSTOM_STYLESHEET_ATTRIBUTE_NAME));
664
718
  this.doc.body.querySelectorAll(":not(svg) title, meta, link[href][rel*=\"icon\"]").forEach(element => {
665
719
  if ((this.options.win && element instanceof this.options.win.HTMLElement) || element instanceof globalThis.HTMLElement) {
666
720
  this.doc.head.appendChild(element);
@@ -1462,6 +1516,7 @@ class Processor {
1462
1516
  options.scrollPosition = frameData.scrollPosition;
1463
1517
  options.scrolling = frameData.scrolling;
1464
1518
  options.adoptedStyleSheets = frameData.adoptedStyleSheets;
1519
+ options.manifestData = frameData.manifestData;
1465
1520
  frameData.runner = new Runner(options, processorHelper);
1466
1521
  frameData.frameElement = frameElement;
1467
1522
  await frameData.runner.loadPage();
@@ -1667,6 +1722,9 @@ class Processor {
1667
1722
  const { frameElement, pageData, frameWindowId, frameData } = capturedFrame;
1668
1723
  this.processorHelper.processFrame(frameElement, pageData, this.options, this.resources, frameWindowId, frameData);
1669
1724
  this.stats.addAll(pageData);
1725
+ if (pageData.manifest) {
1726
+ this.frameManifests.push(...pageData.manifest.frames);
1727
+ }
1670
1728
  }
1671
1729
  });
1672
1730
  }
@@ -1910,6 +1968,18 @@ function nameAssignedElements(hostElement) {
1910
1968
  return splitSlots;
1911
1969
  }
1912
1970
 
1971
+ function getManifestOptions(options) {
1972
+ const manifestOptions = {};
1973
+ Object.keys(options).sort().forEach(name => {
1974
+ const value = options[name];
1975
+ const primitive = typeof value == "boolean" || typeof value == "number";
1976
+ if ((primitive && !MANIFEST_EXCLUDED_OPTION_NAMES.includes(name)) || (typeof value == "string" && MANIFEST_STRING_OPTION_NAMES.includes(name))) {
1977
+ manifestOptions[name] = value;
1978
+ }
1979
+ });
1980
+ return manifestOptions;
1981
+ }
1982
+
1913
1983
  function removeInsertedParagraphs(doc) {
1914
1984
  getInsertedParagraphs(doc).forEach((_, paragraph) => paragraph.remove());
1915
1985
  }
@@ -312,7 +312,8 @@ class ProcessorHelperCommon {
312
312
  blockMixedContent: options.blockMixedContent,
313
313
  expectedType: "stylesheet",
314
314
  acceptHeaders: options.acceptHeaders,
315
- networkTimeout: options.networkTimeout
315
+ networkTimeout: options.networkTimeout,
316
+ manifestEntries: options.manifestEntries
316
317
  });
317
318
  if (!(matchCharsetEquals(content.data, content.charset) || matchCharsetEquals(content.data, options.charset))) {
318
319
  options = Object.assign({}, options, { charset: getCharset(content.data) });
@@ -263,7 +263,8 @@ function getProcessorHelperClass(utilInstance) {
263
263
  blockMixedContent: options.blockMixedContent,
264
264
  expectedType: "stylesheet",
265
265
  acceptHeaders: options.acceptHeaders,
266
- networkTimeout: options.networkTimeout
266
+ networkTimeout: options.networkTimeout,
267
+ manifestEntries: options.manifestEntries
267
268
  });
268
269
  if (content.failed) {
269
270
  const liveContent = this.getLiveStylesheetContent(element, options);
@@ -405,7 +406,8 @@ function getProcessorHelperClass(utilInstance) {
405
406
  resourceReferrer: options.resourceReferrer,
406
407
  referrerPolicy: getReferrerPolicy(resourceElement),
407
408
  acceptHeaders: options.acceptHeaders,
408
- networkTimeout: options.networkTimeout
409
+ networkTimeout: options.networkTimeout,
410
+ manifestEntries: options.manifestEntries
409
411
  })).data;
410
412
  // eslint-disable-next-line no-unused-vars
411
413
  } catch (error) {
@@ -288,7 +288,8 @@ function getProcessorHelperClass(utilInstance) {
288
288
  blockMixedContent: options.blockMixedContent,
289
289
  expectedType: "stylesheet",
290
290
  acceptHeaders: options.acceptHeaders,
291
- networkTimeout: options.networkTimeout
291
+ networkTimeout: options.networkTimeout,
292
+ manifestEntries: options.manifestEntries
292
293
  });
293
294
  if (content.failed) {
294
295
  const liveContent = this.getLiveStylesheetContent(element, options);
@@ -331,13 +332,15 @@ function getProcessorHelperClass(utilInstance) {
331
332
  blockMixedContent: options.blockMixedContent,
332
333
  ...getStylesheetReferrer(resourceURL, options)
333
334
  });
334
- const name = "fonts/" + indexResource + extension;
335
+ const name = this.testEmptyResource(content) ? util.EMPTY_RESOURCE : "fonts/" + indexResource + extension;
335
336
  if (!isDataURL(resourceURL) && options.saveOriginalURLs) {
336
337
  urlNode.value = "-sf-url-original(" + JSON.stringify(originalResourceURL) + ") " + name;
337
338
  } else {
338
339
  urlNode.value = name;
339
340
  }
340
- resources.fonts.set(indexResource, { name, content, extension, contentType, url: resourceURL });
341
+ if (!this.testEmptyResource(content)) {
342
+ resources.fonts.set(indexResource, { name, content, extension, contentType, url: resourceURL });
343
+ }
341
344
  }
342
345
 
343
346
  groupDuplicateFonts(stylesheets, fonts) {
@@ -412,13 +415,15 @@ function getProcessorHelperClass(utilInstance) {
412
415
  if ((!testIgnoredPath(resourceURL) && testValidURL(resourceURL)) || testGeneratedDataURI(resourceURL, options)) {
413
416
  let { content, indexResource, contentType, extension } = await batchRequest.addURL(resourceURL,
414
417
  { asBinary: true, expectedType: "image", ...getStylesheetReferrer(resourceURL, options) });
415
- const name = "images/" + indexResource + extension;
418
+ const name = this.testEmptyResource(content) ? util.EMPTY_RESOURCE : "images/" + indexResource + extension;
416
419
  if (!isDataURL(resourceURL) && options.saveOriginalURLs) {
417
420
  urlNode.value = "-sf-url-original(" + JSON.stringify(originalResourceURL) + ") " + name;
418
421
  } else {
419
422
  urlNode.value = name;
420
423
  }
421
- resources.images.set(indexResource, { name, content, extension, contentType, url: resourceURL });
424
+ if (!this.testEmptyResource(content)) {
425
+ resources.images.set(indexResource, { name, content, extension, contentType, url: resourceURL });
426
+ }
422
427
  }
423
428
  } else {
424
429
  urlNode.value = util.EMPTY_RESOURCE;
@@ -473,7 +478,8 @@ function getProcessorHelperClass(utilInstance) {
473
478
  resourceReferrer: options.resourceReferrer,
474
479
  referrerPolicy: getReferrerPolicy(resourceElement),
475
480
  acceptHeaders: options.acceptHeaders,
476
- networkTimeout: options.networkTimeout
481
+ networkTimeout: options.networkTimeout,
482
+ manifestEntries: options.manifestEntries
477
483
  })).data;
478
484
  // eslint-disable-next-line no-unused-vars
479
485
  } catch (error) {
@@ -505,8 +511,10 @@ function getProcessorHelperClass(utilInstance) {
505
511
 
506
512
  async processImageSrcset(resourceURL, srcsetValue, resources, batchRequest, resourceElement) {
507
513
  const { content, indexResource, extension, contentType } = await batchRequest.addURL(resourceURL, { asBinary: true, expectedType: "image", referrerPolicy: getReferrerPolicy(resourceElement) });
508
- const name = "images/" + indexResource + extension;
509
- resources.images.set(indexResource, { name, content, extension, contentType, url: resourceURL });
514
+ const name = this.testEmptyResource(content) ? util.EMPTY_RESOURCE : "images/" + indexResource + extension;
515
+ if (!this.testEmptyResource(content)) {
516
+ resources.images.set(indexResource, { name, content, extension, contentType, url: resourceURL });
517
+ }
510
518
  resourceElement.removeAttribute(CROSS_ORIGIN_ATTRIBUTE_NAME);
511
519
  return serializeSrcset([Object.assign({}, srcsetValue, { url: name })]);
512
520
  }
package/core/util.js CHANGED
@@ -70,6 +70,17 @@ const ZIP_FILENAME_OFFSET = 30;
70
70
  const USD_FILENAME_REGEXP = /\.usd[ac]?$/i;
71
71
  const CONTENT_TYPES_HTML = ["text/html", "application/xhtml+xml"];
72
72
  const EXPECTED_TYPES_MEDIA = ["font", "image", "video", "audio", "model"];
73
+ const EXPECTED_TYPE_IMAGE = "image";
74
+ const EXPECTED_TYPE_DOCUMENT = "document";
75
+ const DATA_URI_PREFIX = "data:";
76
+ const MANIFEST_OUTCOME_EMBEDDED = "embedded";
77
+ const MANIFEST_OUTCOME_FAILED = "failed";
78
+ const MANIFEST_OUTCOME_BLOCKED = "blocked";
79
+ const MANIFEST_OUTCOME_REJECTED = "rejected";
80
+ const MANIFEST_OUTCOME_OVERSIZED = "oversized";
81
+ const MANIFEST_FETCHED_BY_PAGE = "page";
82
+ const MANIFEST_FETCHED_BY_FRAME = "frame";
83
+ const MANIFEST_HEADER_NAMES = ["date", "last-modified", "etag", "content-length", "content-encoding", "cache-control", "age", "expires", "server"];
73
84
 
74
85
  const URL = globalThis.URL;
75
86
  const DOMParser = globalThis.DOMParser;
@@ -226,6 +237,7 @@ function getInstance(utilOptions) {
226
237
  PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME: helper.PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME,
227
238
  STYLESHEET_ATTRIBUTE_NAME: helper.STYLESHEET_ATTRIBUTE_NAME,
228
239
  LINK_STYLESHEET_ATTRIBUTE_NAME: helper.LINK_STYLESHEET_ATTRIBUTE_NAME,
240
+ CUSTOM_STYLESHEET_ATTRIBUTE_NAME: helper.CUSTOM_STYLESHEET_ATTRIBUTE_NAME,
229
241
  SELECTED_CONTENT_ATTRIBUTE_NAME: helper.SELECTED_CONTENT_ATTRIBUTE_NAME,
230
242
  INVALID_ELEMENT_ATTRIBUTE_NAME: helper.INVALID_ELEMENT_ATTRIBUTE_NAME,
231
243
  COMMENT_HEADER: helper.COMMENT_HEADER,
@@ -243,6 +255,16 @@ function getInstance(utilOptions) {
243
255
  };
244
256
 
245
257
  async function getContent(resourceURL, options) {
258
+ const entry = options.manifestEntries && !resourceURL.startsWith(DATA_URI_PREFIX) ? { url: resourceURL, role: options.expectedType || EXPECTED_TYPE_DOCUMENT, outcome: MANIFEST_OUTCOME_FAILED } : undefined;
259
+ const content = await fetchContent(resourceURL, options, entry);
260
+ if (entry) {
261
+ options.manifestEntries.push(entry);
262
+ content.manifestEntry = entry;
263
+ }
264
+ return content;
265
+ }
266
+
267
+ async function fetchContent(resourceURL, options, entry) {
246
268
  let response, startTime, networkTimeoutId, networkTimeoutPromise, resolveNetworkTimeoutPromise;
247
269
  const fetchResource = utilOptions.fetch;
248
270
  const fetchFrameResource = utilOptions.frameFetch;
@@ -251,6 +273,7 @@ function getInstance(utilOptions) {
251
273
  log(" // STARTED download url =", resourceURL, "asBinary =", options.asBinary);
252
274
  }
253
275
  if (options.blockMixedContent && /^https:/i.test(options.baseURI) && !/^https:/i.test(resourceURL) && !/^blob:https:/i.test(resourceURL)) {
276
+ setManifestOutcome(entry, MANIFEST_OUTCOME_BLOCKED);
254
277
  return getFetchResponse(resourceURL, options);
255
278
  }
256
279
  if (options.networkTimeout) {
@@ -271,21 +294,24 @@ function getInstance(utilOptions) {
271
294
  fetchFrameResource(resourceURL, { frameId: options.frameId, referrer: options.resourceReferrer, referrerPolicy: options.referrerPolicy || undefined, stylesheetURL: options.stylesheetURL || undefined, stylesheetReferrerPolicy: options.stylesheetReferrerPolicy || undefined, headers: { accept } }),
272
295
  networkTimeoutPromise
273
296
  ]);
297
+ setManifestResponse(entry, response, MANIFEST_FETCHED_BY_FRAME);
274
298
  // eslint-disable-next-line no-unused-vars
275
299
  } catch (error) {
276
300
  response = await Promise.race([
277
301
  fetchResource(resourceURL, { referrer: options.resourceReferrer, referrerPolicy: options.referrerPolicy || undefined, stylesheetURL: options.stylesheetURL || undefined, stylesheetReferrerPolicy: options.stylesheetReferrerPolicy || undefined, headers: { accept } }),
278
302
  networkTimeoutPromise
279
303
  ]);
304
+ setManifestResponse(entry, response, MANIFEST_FETCHED_BY_PAGE);
280
305
  }
281
306
  } else {
282
307
  response = await Promise.race([
283
308
  fetchResource(resourceURL, { referrer: options.resourceReferrer, referrerPolicy: options.referrerPolicy || undefined, stylesheetURL: options.stylesheetURL || undefined, stylesheetReferrerPolicy: options.stylesheetReferrerPolicy || undefined, headers: { accept } }),
284
309
  networkTimeoutPromise
285
310
  ]);
311
+ setManifestResponse(entry, response, MANIFEST_FETCHED_BY_PAGE);
286
312
  }
287
- // eslint-disable-next-line no-unused-vars
288
313
  } catch (error) {
314
+ setManifestError(entry, error);
289
315
  return getFailedFetchResponse(resourceURL, options);
290
316
  } finally {
291
317
  resolveNetworkTimeoutPromise();
@@ -296,10 +322,11 @@ function getInstance(utilOptions) {
296
322
  let buffer;
297
323
  try {
298
324
  buffer = await response.arrayBuffer();
299
- // eslint-disable-next-line no-unused-vars
300
325
  } catch (error) {
326
+ setManifestError(entry, error);
301
327
  return options.inline ? { data: options.asBinary ? helper.EMPTY_RESOURCE : "", resourceURL } : { resourceURL };
302
328
  }
329
+ await setManifestContent(entry, buffer);
303
330
  resourceURL = response.url || resourceURL;
304
331
  let contentType = "", charset;
305
332
  try {
@@ -319,8 +346,15 @@ function getInstance(utilOptions) {
319
346
  if (!charset && options.charset) {
320
347
  charset = options.charset;
321
348
  }
349
+ if (entry) {
350
+ entry.contentType = contentType;
351
+ if (charset) {
352
+ entry.charset = charset;
353
+ }
354
+ }
322
355
  if (options.asBinary) {
323
- if (response.status >= 400 || (EXPECTED_TYPES_MEDIA.includes(options.expectedType) && CONTENT_TYPES_HTML.includes(contentType))) {
356
+ if ((response.status >= 400 && options.expectedType != EXPECTED_TYPE_IMAGE) || (EXPECTED_TYPES_MEDIA.includes(options.expectedType) && CONTENT_TYPES_HTML.includes(contentType))) {
357
+ setManifestOutcome(entry, MANIFEST_OUTCOME_REJECTED);
324
358
  return getFetchResponse(resourceURL, options);
325
359
  }
326
360
  try {
@@ -328,19 +362,23 @@ function getInstance(utilOptions) {
328
362
  log(" // ENDED download url =", resourceURL, "delay =", Date.now() - startTime);
329
363
  }
330
364
  if (options.maxResourceSizeEnabled && buffer.byteLength > options.maxResourceSize * ONE_MB) {
365
+ setManifestOutcome(entry, MANIFEST_OUTCOME_OVERSIZED);
331
366
  return getFetchResponse(resourceURL, options);
332
367
  } else {
368
+ setManifestOutcome(entry, MANIFEST_OUTCOME_EMBEDDED);
333
369
  return getFetchResponse(resourceURL, options, buffer, null, contentType);
334
370
  }
335
- // eslint-disable-next-line no-unused-vars
336
371
  } catch (error) {
372
+ setManifestError(entry, error);
337
373
  return getFetchResponse(resourceURL, options);
338
374
  }
339
375
  } else {
340
- if (response.status >= 400) {
376
+ if (response.status >= 400 && options.expectedType != EXPECTED_TYPE_DOCUMENT) {
377
+ setManifestOutcome(entry, MANIFEST_OUTCOME_REJECTED);
341
378
  return getFailedFetchResponse(resourceURL, options);
342
379
  }
343
380
  if (options.validateTextContentType && contentType && !contentType.startsWith(PREFIX_CONTENT_TYPE_TEXT)) {
381
+ setManifestOutcome(entry, MANIFEST_OUTCOME_REJECTED);
344
382
  return getFetchResponse(resourceURL, options);
345
383
  }
346
384
  if (!charset) {
@@ -350,14 +388,16 @@ function getInstance(utilOptions) {
350
388
  log(" // ENDED download url =", resourceURL, "delay =", Date.now() - startTime);
351
389
  }
352
390
  if (options.maxResourceSizeEnabled && buffer.byteLength > options.maxResourceSize * ONE_MB) {
391
+ setManifestOutcome(entry, MANIFEST_OUTCOME_OVERSIZED);
353
392
  return getFetchResponse(resourceURL, options, null, charset);
354
393
  } else {
355
394
  try {
356
395
  const fetchResponse = await getFetchResponse(resourceURL, options, buffer, charset, contentType);
357
396
  fetchResponse.referrerPolicy = (response.headers && response.headers.get("referrer-policy")) || undefined;
397
+ setManifestOutcome(entry, MANIFEST_OUTCOME_EMBEDDED);
358
398
  return fetchResponse;
359
- // eslint-disable-next-line no-unused-vars
360
399
  } catch (error) {
400
+ setManifestError(entry, error);
361
401
  return getFetchResponse(resourceURL, options, null, charset);
362
402
  }
363
403
  }
@@ -365,6 +405,60 @@ function getInstance(utilOptions) {
365
405
  }
366
406
  }
367
407
 
408
+ function setManifestResponse(entry, response, fetchedBy) {
409
+ if (entry) {
410
+ entry.fetchedBy = fetchedBy;
411
+ if (response.url) {
412
+ entry.finalUrl = response.url;
413
+ }
414
+ if (response.status !== undefined) {
415
+ entry.status = response.status;
416
+ }
417
+ const headers = {};
418
+ MANIFEST_HEADER_NAMES.forEach(name => {
419
+ const value = getHeaderValue(response.headers, name);
420
+ if (value) {
421
+ headers[name] = value;
422
+ }
423
+ });
424
+ if (Object.keys(headers).length) {
425
+ entry.headers = headers;
426
+ }
427
+ }
428
+ }
429
+
430
+ function getHeaderValue(headers, name) {
431
+ if (headers) {
432
+ if (typeof headers.get == "function") {
433
+ return headers.get(name);
434
+ }
435
+ const key = Object.keys(headers).find(key => key.toLowerCase() == name);
436
+ return key === undefined ? undefined : headers[key];
437
+ }
438
+ }
439
+
440
+ async function setManifestContent(entry, buffer) {
441
+ if (entry) {
442
+ entry.size = buffer.byteLength;
443
+ entry.sha256 = await helper.digest("SHA-256", buffer);
444
+ }
445
+ }
446
+
447
+ function setManifestOutcome(entry, outcome) {
448
+ if (entry) {
449
+ entry.outcome = outcome;
450
+ }
451
+ }
452
+
453
+ function setManifestError(entry, error) {
454
+ if (entry) {
455
+ entry.outcome = MANIFEST_OUTCOME_FAILED;
456
+ if (error && error.message) {
457
+ entry.error = error.message;
458
+ }
459
+ }
460
+ }
461
+
368
462
  async function getFailedFetchResponse(resourceURL, options) {
369
463
  const response = await getFetchResponse(resourceURL, options);
370
464
  response.failed = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.6.24",
3
+ "version": "1.6.26",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -237,6 +237,7 @@ function initResponse(message) {
237
237
  frameData.scrollPosition = messageFrameData.scrollPosition;
238
238
  frameData.scrolling = messageFrameData.scrolling;
239
239
  frameData.adoptedStyleSheets = messageFrameData.adoptedStyleSheets;
240
+ frameData.manifestData = messageFrameData.manifestData;
240
241
  }
241
242
  });
242
243
  const remainingFrames = windowData.frames.filter(frameData => !frameData.processed).length;
@@ -459,6 +460,7 @@ function getFrameData(document, win, windowId, options, scrolling) {
459
460
  scrollPosition: docData.scrollPosition,
460
461
  scrolling,
461
462
  adoptedStyleSheets: docData.adoptedStyleSheets,
463
+ manifestData: docData.manifestData,
462
464
  processed: true
463
465
  };
464
466
  }
package/single-file.js CHANGED
@@ -131,5 +131,14 @@ async function getPageData(options = {}, initOptions, doc, win) {
131
131
  }
132
132
  pageData.content = Array.from(new Uint8Array(arrayBuffer));
133
133
  }
134
+ if (pageData.manifest) {
135
+ const content = options.compressContent ? new Uint8Array(pageData.content) : pageData.content;
136
+ pageData.manifest.output = {
137
+ filename: pageData.filename,
138
+ mimeType: pageData.mimeType,
139
+ size: options.compressContent ? content.length : helper.getContentSize(content),
140
+ sha256: await helper.digest("SHA-256", content)
141
+ };
142
+ }
134
143
  return pageData;
135
144
  }