single-file-core 1.2.35 → 1.2.37

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
@@ -21,7 +21,7 @@
21
21
  * Source.
22
22
  */
23
23
 
24
- /* global globalThis, CustomEvent */
24
+ /* global globalThis */
25
25
 
26
26
  import * as cssUnescape from "./../vendor/css-unescape.js";
27
27
  import * as hooksFrames from "./../processors/hooks/content/content-hooks-frames.js";
@@ -77,6 +77,10 @@ const EMPTY_RESOURCE = "data:,";
77
77
  const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
78
78
  const dispatchEvent = event => { try { globalThis.dispatchEvent(event); } catch (error) { /* ignored */ } };
79
79
  const JSON = globalThis.JSON;
80
+ const crypto = globalThis.crypto;
81
+ const TextEncoder = globalThis.TextEncoder;
82
+ const Blob = globalThis.Blob;
83
+ const CustomEvent = globalThis.CustomEvent;
80
84
 
81
85
  export {
82
86
  initUserScriptHandler,
@@ -90,6 +94,8 @@ export {
90
94
  normalizeFontFamily,
91
95
  getShadowRoot,
92
96
  appendInfobar,
97
+ getContentSize,
98
+ digest,
93
99
  ON_BEFORE_CAPTURE_EVENT_NAME,
94
100
  ON_AFTER_CAPTURE_EVENT_NAME,
95
101
  WIN_ID_ATTRIBUTE_NAME,
@@ -596,6 +602,33 @@ function getFontWeight(weight) {
596
602
  return FONT_WEIGHTS[weight.toLowerCase().trim()] || weight;
597
603
  }
598
604
 
605
+ function getContentSize(content) {
606
+ return new Blob([content]).size;
607
+ }
608
+
609
+ async function digest(algo, text) {
610
+ try {
611
+ const hash = await crypto.subtle.digest(algo, new TextEncoder("utf-8").encode(text));
612
+ return hex(hash);
613
+ } catch (error) {
614
+ return "";
615
+ }
616
+ }
617
+
618
+ // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
619
+ function hex(buffer) {
620
+ const hexCodes = [];
621
+ const view = new DataView(buffer);
622
+ for (let i = 0; i < view.byteLength; i += 4) {
623
+ const value = view.getUint32(i);
624
+ const stringValue = value.toString(16);
625
+ const padding = "00000000";
626
+ const paddedValue = (padding + stringValue).slice(-padding.length);
627
+ hexCodes.push(paddedValue);
628
+ }
629
+ return hexCodes.join("");
630
+ }
631
+
599
632
  function flatten(array) {
600
633
  return array.flat ? array.flat() : array.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
601
634
  }
package/core/index.js CHANGED
@@ -102,6 +102,7 @@ const RESOLVE_URLS_STAGE = 0;
102
102
  const REPLACE_DATA_STAGE = 1;
103
103
  const REPLACE_DOCS_STAGE = 2;
104
104
  const POST_PROCESS_STAGE = 3;
105
+ const FINALIZE_STAGE = 4;
105
106
  const STAGES = [{
106
107
  sequential: [
107
108
  { action: "preProcessPage" },
@@ -164,6 +165,11 @@ const STAGES = [{
164
165
  { option: "enableMaff", action: "insertMAFFMetaData" },
165
166
  { action: "setDocInfo" }
166
167
  ]
168
+ }, {
169
+ sequential: [
170
+ { action: "loadOptionsFromPage" },
171
+ { option: "saveFilenameTemplateData", action: "saveFilenameTemplateData" },
172
+ ]
167
173
  }];
168
174
 
169
175
  class Runner {
@@ -247,6 +253,7 @@ class Runner {
247
253
  this.options.win = null;
248
254
  await this.executeStage(REPLACE_DOCS_STAGE);
249
255
  await this.executeStage(POST_PROCESS_STAGE);
256
+ await this.executeStage(FINALIZE_STAGE);
250
257
  this.processor.finalize();
251
258
  }
252
259
 
@@ -410,6 +417,7 @@ class BatchRequest {
410
417
  // ---------
411
418
  const SHADOWROOT_ATTRIBUTE_NAME = "shadowrootmode";
412
419
  const SCRIPT_TEMPLATE_SHADOW_ROOT = "data-template-shadow-root";
420
+ const SCRIPT_OPTIONS = "data-single-file-options";
413
421
  const UTF8_CHARSET = "utf-8";
414
422
 
415
423
  class Processor {
@@ -632,6 +640,44 @@ class Processor {
632
640
  }
633
641
  }
634
642
 
643
+ loadOptionsFromPage() {
644
+ const optionsElement = this.doc.body.querySelector("script[type=\"application/json\"][" + SCRIPT_OPTIONS + "]");
645
+ if (optionsElement) {
646
+ const options = JSON.parse(optionsElement.textContent);
647
+ Object.keys(options).forEach(option => this.options[option] = options[option]);
648
+ this.options.saveDate = new Date(this.options.saveDate);
649
+ this.options.visitDate = new Date(this.options.visitDate);
650
+ }
651
+ }
652
+
653
+ saveFilenameTemplateData() {
654
+ const optionsElement = this.doc.createElement("script");
655
+ optionsElement.type = "application/json";
656
+ optionsElement.setAttribute(SCRIPT_OPTIONS, "");
657
+ optionsElement.textContent = JSON.stringify({
658
+ saveUrl: this.options.url,
659
+ saveDate: this.options.saveDate.getTime(),
660
+ visitDate: this.options.visitDate.getTime(),
661
+ filenameTemplate: this.options.filenameTemplate,
662
+ filenameReplacedCharacters: this.options.filenameReplacedCharacters,
663
+ filenameReplacementCharacter: this.options.filenameReplacementCharacter,
664
+ filenameMaxLengthUnit: this.options.filenameMaxLengthUnit,
665
+ filenameMaxLength: this.options.filenameMaxLength,
666
+ replaceEmojisInFilename: this.options.replaceEmojisInFilename,
667
+ compressContent: this.options.compressContent,
668
+ selfExtractingArchive: this.options.selfExtractingArchive,
669
+ extractDataFromPage: this.options.extractDataFromPage,
670
+ referrer: this.options.referrer,
671
+ title: this.options.title,
672
+ info: this.options.info
673
+ });
674
+ if (this.doc.body.firstChild) {
675
+ this.doc.body.insertBefore(optionsElement, this.doc.body.firstChild);
676
+ } else {
677
+ this.doc.body.appendChild(optionsElement);
678
+ }
679
+ }
680
+
635
681
  replaceStyleContents() {
636
682
  if (this.options.stylesheets) {
637
683
  this.doc.querySelectorAll("style").forEach((styleElement, styleIndex) => {
@@ -781,7 +827,7 @@ class Processor {
781
827
  element.setAttribute("src", DISABLED_SCRIPT);
782
828
  }
783
829
  });
784
- const scriptElements = this.doc.querySelectorAll("script:not([type=\"application/ld+json\"]):not([" + SCRIPT_TEMPLATE_SHADOW_ROOT + "])");
830
+ const scriptElements = this.doc.querySelectorAll("script:not([type=\"application/ld+json\"]):not([" + SCRIPT_TEMPLATE_SHADOW_ROOT + "]):not([" + SCRIPT_OPTIONS + "])");
785
831
  this.stats.set("discarded", "scripts", scriptElements.length);
786
832
  this.stats.set("processed", "scripts", scriptElements.length);
787
833
  scriptElements.forEach(element => element.remove());
package/core/util.js CHANGED
@@ -72,9 +72,7 @@ const DOMParser = globalThis.DOMParser;
72
72
  const Blob = globalThis.Blob;
73
73
  const FileReader = globalThis.FileReader;
74
74
  const fetch = (url, options) => globalThis.fetch(url, options);
75
- const crypto = globalThis.crypto;
76
75
  const TextDecoder = globalThis.TextDecoder;
77
- const TextEncoder = globalThis.TextEncoder;
78
76
 
79
77
  export {
80
78
  getInstance
@@ -147,21 +145,16 @@ function getInstance(utilOptions) {
147
145
  }
148
146
  },
149
147
  async digest(algo, text) {
150
- try {
151
- const hash = await crypto.subtle.digest(algo, new TextEncoder("utf-8").encode(text));
152
- return hex(hash);
153
- } catch (error) {
154
- return "";
155
- }
148
+ return helper.digest(algo, text);
156
149
  },
157
150
  getContentSize(content) {
158
- return new Blob([content]).size;
151
+ return helper.getContentSize(content);
159
152
  },
160
153
  formatFilename(content, doc, options) {
161
- return modules.templateFormatter.formatFilename(content, doc, options, this);
154
+ return modules.templateFormatter.formatFilename(content, doc, options);
162
155
  },
163
156
  async evalTemplate(template, options, content, doc, dontReplaceSlash) {
164
- return modules.templateFormatter.evalTemplate(template, options, this, content, doc, dontReplaceSlash);
157
+ return modules.templateFormatter.evalTemplate(template, options, content, doc, dontReplaceSlash);
165
158
  },
166
159
  minifyHTML(doc, options) {
167
160
  return modules.htmlMinifier.process(doc, options);
@@ -484,20 +477,6 @@ function guessMIMEType(expectedType, buffer, asBinary) {
484
477
  }
485
478
  }
486
479
 
487
- // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
488
- function hex(buffer) {
489
- const hexCodes = [];
490
- const view = new DataView(buffer);
491
- for (let i = 0; i < view.byteLength; i += 4) {
492
- const value = view.getUint32(i);
493
- const stringValue = value.toString(16);
494
- const padding = "00000000";
495
- const paddedValue = (padding + stringValue).slice(-padding.length);
496
- hexCodes.push(paddedValue);
497
- }
498
- return hexCodes.join("");
499
- }
500
-
501
480
  function getDoctypeString(doc) {
502
481
  const docType = doc.doctype;
503
482
  let docTypeString = "";
@@ -24,6 +24,7 @@
24
24
  /* global Blob, FileReader, URL, URLSearchParams */
25
25
 
26
26
  import { parse } from "./template-parser.js";
27
+ import { getContentSize, digest } from "./../core/helper.js";
27
28
 
28
29
  // eslint-disable-next-line quotes
29
30
  const DEFAULT_REPLACED_CHARACTERS = ["~", "+", "\\\\", "?", "%", "*", ":", "|", '"', "<", ">", "\x00-\x1f", "\x7F"];
@@ -16865,8 +16866,8 @@ enterprisecloud.nu
16865
16866
 
16866
16867
  export { formatFilename, evalTemplate };
16867
16868
 
16868
- async function formatFilename(content, doc, options, util) {
16869
- let filename = (await evalTemplate(options.filenameTemplate, options, util, content, doc)) || "";
16869
+ async function formatFilename(content, doc, options) {
16870
+ let filename = (await evalTemplate(options.filenameTemplate, options, content, doc)) || "";
16870
16871
  filename = filename.trim();
16871
16872
  if (options.replaceEmojisInFilename) {
16872
16873
  EMOJIS.forEach(emoji => (filename = replaceAll(filename, emoji, " _" + EMOJI_NAMES[emoji] + "_ ")));
@@ -16876,7 +16877,7 @@ async function formatFilename(content, doc, options, util) {
16876
16877
  if (!options.backgroundSave) {
16877
16878
  filename = filename.replace(/\//g, replacementCharacter);
16878
16879
  }
16879
- if (!options.keepFilename && ((options.filenameMaxLengthUnit == "bytes" && util.getContentSize(filename) > options.filenameMaxLength) || filename.length > options.filenameMaxLength)) {
16880
+ if (!options.keepFilename && ((options.filenameMaxLengthUnit == "bytes" && getContentSize(filename) > options.filenameMaxLength) || filename.length > options.filenameMaxLength)) {
16880
16881
  const extensionMatch = filename.match(/(\.[^.]{3,4})$/);
16881
16882
  const extension = extensionMatch && extensionMatch[0] && extensionMatch[0].length > 1 ? extensionMatch[0] : "";
16882
16883
  filename = options.filenameMaxLengthUnit == "bytes" ? await truncateText(filename, options.filenameMaxLength - extension.length) : filename.substring(0, options.filenameMaxLength - extension.length);
@@ -16891,7 +16892,7 @@ async function formatFilename(content, doc, options, util) {
16891
16892
  return filename.trim();
16892
16893
  }
16893
16894
 
16894
- async function evalTemplate(template = "", options, util, content, doc, dontReplaceSlash) {
16895
+ async function evalTemplate(template = "", options, content, doc, dontReplaceSlash) {
16895
16896
  const url = new URL(options.saveUrl);
16896
16897
  const urlHref = decode(url.href);
16897
16898
  const params = Array.from(new URLSearchParams(url.search));
@@ -16923,7 +16924,7 @@ async function evalTemplate(template = "", options, util, content, doc, dontRepl
16923
16924
  "url-hostname-root": { getter: () => urlRoot },
16924
16925
  "url-hostname-subdomains": { getter: () => urlSubDomains },
16925
16926
  "url-href": { getter: () => urlHref, dontReplaceSlash: dontReplaceSlashIfUndefined },
16926
- "url-href-digest-sha-1": { getter: urlHref ? async () => util.digest("SHA-1", urlHref) : "" },
16927
+ "url-href-digest-sha-1": { getter: urlHref ? async () => digest("SHA-1", urlHref) : "" },
16927
16928
  "url-href-flat": { getter: () => decode(url.href), dontReplaceSlash: false },
16928
16929
  "url-referrer": { getter: () => decode(options.referrer), dontReplaceSlash: dontReplaceSlashIfUndefined },
16929
16930
  "url-referrer-flat": { getter: () => decode(options.referrer), dontReplaceSlash: false },
@@ -16943,9 +16944,9 @@ async function evalTemplate(template = "", options, util, content, doc, dontRepl
16943
16944
  "filename-extension": { getter: () => getFilenameExtension(options) }
16944
16945
  };
16945
16946
  if (content) {
16946
- variables["digest-sha-256"] = { getter: async () => util.digest("SHA-256", content) };
16947
- variables["digest-sha-384"] = { getter: async () => util.digest("SHA-384", content) };
16948
- variables["digest-sha-512"] = { getter: async () => util.digest("SHA-512", content) };
16947
+ variables["digest-sha-256"] = { getter: async () => digest("SHA-256", content) };
16948
+ variables["digest-sha-384"] = { getter: async () => digest("SHA-384", content) };
16949
+ variables["digest-sha-512"] = { getter: async () => digest("SHA-512", content) };
16949
16950
  }
16950
16951
  if (options.saveDate) {
16951
16952
  addDateVariables(options.saveDate);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.2.35",
3
+ "version": "1.2.37",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import * as serializer from "./modules/html-serializer.js";
25
+ import { formatFilename } from "./modules/template-formatter.js";
25
26
  import * as infobar from "./core/infobar.js";
26
27
  import { getInstance } from "./core/util.js";
27
28
  import * as zip from "./vendor/zip/zip.js";
@@ -48,6 +49,7 @@ const helper = {
48
49
  zip,
49
50
  extract,
50
51
  display,
52
+ formatFilename,
51
53
  INFOBAR_TAGNAME: infobar.INFOBAR_TAGNAME
52
54
  };
53
55