single-file-core 1.6.28 → 1.6.30

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
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import * as cssUnescape from "./../vendor/css-unescape.js";
25
+ import * as srcsetParser from "./../vendor/html-srcset-parser.js";
25
26
  import * as sha from "./lib/sha.js";
26
27
  import * as hooksFrames from "./../processors/hooks/content/content-hooks-frames.js";
27
28
  import * as infobar from "./infobar.js";
@@ -44,6 +45,7 @@ const ON_AFTER_CAPTURE_EVENT_NAME = SINGLE_FILE_PREFIX + "on-after-capture";
44
45
  const GET_ADOPTED_STYLESHEETS_REQUEST_EVENT = SINGLE_FILE_PREFIX + "request-get-adopted-stylesheets";
45
46
  const GET_ADOPTED_STYLESHEETS_RESPONSE_EVENT = SINGLE_FILE_PREFIX + "response-get-adopted-stylesheets";
46
47
  const UNREGISTER_GET_ADOPTED_STYLESHEETS_REQUEST_EVENT = SINGLE_FILE_PREFIX + "unregister-request-get-adopted-stylesheets";
48
+ const CLEAR_ADOPTED_STYLESHEETS_CACHE_EVENT = SINGLE_FILE_PREFIX + "clear-adopted-stylesheets-cache";
47
49
  const ON_INIT_USERSCRIPT_EVENT = SINGLE_FILE_PREFIX + "user-script-init";
48
50
  const REMOVED_CONTENT_ATTRIBUTE_NAME = "data-" + SINGLE_FILE_PREFIX + "removed-content";
49
51
  const HIDDEN_CONTENT_ATTRIBUTE_NAME = "data-" + SINGLE_FILE_PREFIX + "hidden-content";
@@ -117,6 +119,37 @@ const NESTING_SHADOW_ROOT_TRACK_ID_PREFIX = "s";
117
119
  const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
118
120
  const RAW_TEXT_TAG_NAMES = ["SCRIPT", "STYLE", "TEXTAREA", "TITLE", "XMP", "IFRAME", "NOEMBED", "NOFRAMES", "PLAINTEXT", "NOSCRIPT"];
119
121
  const COMMENT_NODE_FILTER = 128;
122
+ const PROTECTED_OPTION_NAMES = [
123
+ "password",
124
+ "woleetKey",
125
+ "saveToGDrive",
126
+ "saveToDropbox",
127
+ "saveWithCompanion",
128
+ "forceWebAuthFlow",
129
+ "saveWithWebDAV",
130
+ "webDAVURL",
131
+ "webDAVUser",
132
+ "webDAVPassword",
133
+ "saveWithMCP",
134
+ "mcpServerUrl",
135
+ "mcpAuthToken",
136
+ "saveToGitHub",
137
+ "githubToken",
138
+ "githubUser",
139
+ "githubRepository",
140
+ "githubBranch",
141
+ "saveToRestFormApi",
142
+ "saveToRestFormApiUrl",
143
+ "saveToRestFormApiToken",
144
+ "saveToRestFormApiFileFieldName",
145
+ "saveToRestFormApiUrlFieldName",
146
+ "saveToS3",
147
+ "S3Domain",
148
+ "S3Region",
149
+ "S3Bucket",
150
+ "S3AccessKey",
151
+ "S3SecretKey"
152
+ ];
120
153
  const DEPRECATED_OPTION_NAMES = {
121
154
  loadDeferredImages: "loadDeferredContent",
122
155
  loadDeferredImagesMaxIdleTime: "loadDeferredContentMaxIdleTime",
@@ -260,7 +293,7 @@ function onInitUserScript({ detail }) {
260
293
  delete userScriptOptions.frames;
261
294
  delete userScriptOptions.taskId;
262
295
  delete userScriptOptions._migratedTemplateFormat;
263
- delete userScriptOptions.woleetKey;
296
+ PROTECTED_OPTION_NAMES.forEach(name => delete userScriptOptions[name]);
264
297
  let detailUserScript;
265
298
  try {
266
299
  detailUserScript = detail == "jsonDetail" ? JSON.stringify({ options: userScriptOptions }) : { options: userScriptOptions };
@@ -277,6 +310,7 @@ function onInitUserScript({ detail }) {
277
310
  try {
278
311
  const detail = typeof event.detail == "string" ? JSON.parse(event.detail) : event.detail;
279
312
  if (detail.options) {
313
+ PROTECTED_OPTION_NAMES.forEach(name => delete detail.options[name]);
280
314
  Object.assign(options, detail.options);
281
315
  }
282
316
  // eslint-disable-next-line no-unused-vars
@@ -325,6 +359,12 @@ function preProcessDoc(doc, win, options) {
325
359
  let elementsInfo;
326
360
  if (win && doc.documentElement) {
327
361
  markInvalidNesting(doc);
362
+ try {
363
+ doc.dispatchEvent(new CustomEvent(CLEAR_ADOPTED_STYLESHEETS_CACHE_EVENT));
364
+ // eslint-disable-next-line no-unused-vars
365
+ } catch (error) {
366
+ // ignored
367
+ }
328
368
  elementsInfo = getElementsInfo(win, doc, doc.documentElement, options);
329
369
  if (options.removeUnusedFonts && doc.defaultView) {
330
370
  getRootElementUsedFonts(win, doc.documentElement, elementsInfo);
@@ -825,7 +865,7 @@ function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map
825
865
  shadowRootInfo.clonable = shadowRoot.clonable;
826
866
  shadowRootInfo.serializable = shadowRoot.serializable;
827
867
  try {
828
- if (shadowRoot.adoptedStyleSheets && shadowRoot.adoptedStyleSheets.length === undefined) {
868
+ if (shadowRoot.adoptedStyleSheets) {
829
869
  shadowRoot.dispatchEvent(new CustomEvent(UNREGISTER_GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, { bubbles: true }));
830
870
  }
831
871
  // eslint-disable-next-line no-unused-vars
@@ -909,6 +949,9 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
909
949
  EMPTY_RESOURCE :
910
950
  (options.loadDeferredContent && element.getAttribute(LAZY_SRC_ATTRIBUTE_NAME)) || element.currentSrc
911
951
  };
952
+ if (!elementHidden && element.parentElement && element.parentElement.tagName.toUpperCase() == "PICTURE") {
953
+ imageData.sourceSize = getSourceSize(win, element.parentElement, imageData.currentSrc);
954
+ }
912
955
  data.images.push(imageData);
913
956
  element.setAttribute(IMAGE_ATTRIBUTE_NAME, data.images.length - 1);
914
957
  data.markedElements.push(element);
@@ -1242,6 +1285,42 @@ function getStylesheetsData(doc, markedElements) {
1242
1285
  }
1243
1286
  }
1244
1287
 
1288
+ function getSourceSize(win, pictureElement, currentSrc) {
1289
+ if (currentSrc) {
1290
+ const baseURI = pictureElement.ownerDocument.baseURI;
1291
+ const sourceElement = Array.from(pictureElement.querySelectorAll(":scope > source")).find(sourceElement => {
1292
+ const media = sourceElement.getAttribute("media");
1293
+ return (!media || win.matchMedia(media).matches) && getSrcsetURLs(sourceElement.getAttribute("srcset"), baseURI).includes(currentSrc);
1294
+ });
1295
+ if (sourceElement && (sourceElement.hasAttribute("width") || sourceElement.hasAttribute("height"))) {
1296
+ return {
1297
+ width: sourceElement.getAttribute("width"),
1298
+ height: sourceElement.getAttribute("height")
1299
+ };
1300
+ }
1301
+ }
1302
+ }
1303
+
1304
+ function getSrcsetURLs(srcset, baseURI) {
1305
+ const urls = [];
1306
+ if (srcset) {
1307
+ try {
1308
+ srcsetParser.process(srcset).forEach(candidate => {
1309
+ try {
1310
+ urls.push(new URL(candidate.url, baseURI).href);
1311
+ // eslint-disable-next-line no-unused-vars
1312
+ } catch (error) {
1313
+ // ignored
1314
+ }
1315
+ });
1316
+ // eslint-disable-next-line no-unused-vars
1317
+ } catch (error) {
1318
+ // ignored
1319
+ }
1320
+ }
1321
+ return urls;
1322
+ }
1323
+
1245
1324
  function getSize(win, imageElement, computedStyle) {
1246
1325
  let pxWidth = imageElement.naturalWidth;
1247
1326
  let pxHeight = imageElement.naturalHeight;
package/core/index.js CHANGED
@@ -735,6 +735,13 @@ class Processor {
735
735
  if (imageData.currentSrc) {
736
736
  imgElement.dataset.singleFileOriginURL = imgElement.getAttribute("src");
737
737
  imgElement.setAttribute("src", imageData.currentSrc);
738
+ if (imageData.sourceSize && this.options.removeAlternativeImages) {
739
+ ["width", "height"].forEach(attributeName => {
740
+ if (imageData.sourceSize[attributeName] != null) {
741
+ imgElement.setAttribute(attributeName, imageData.sourceSize[attributeName]);
742
+ }
743
+ });
744
+ }
738
745
  }
739
746
  if (this.options.loadDeferredContent) {
740
747
  if ((!imgElement.getAttribute("src") || imgElement.getAttribute("src") == util.EMPTY_RESOURCE) && imgElement.getAttribute("data-src")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.6.28",
3
+ "version": "1.6.30",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -52,6 +52,24 @@ const INIT_REQUEST_MESSAGE = "singlefile.frameTree.initRequest";
52
52
  const ACK_INIT_REQUEST_MESSAGE = "singlefile.frameTree.ackInitRequest";
53
53
  const CLEANUP_REQUEST_MESSAGE = "singlefile.frameTree.cleanupRequest";
54
54
  const INIT_RESPONSE_MESSAGE = "singlefile.frameTree.initResponse";
55
+ const GET_OPTIONS_MESSAGE = "singlefile.frameTree.getOptions";
56
+ const FRAME_OPTION_NAMES = [
57
+ "removeHiddenElements",
58
+ "removeUnusedFonts",
59
+ "compressHTML",
60
+ "autoSaveExternalSave",
61
+ "moveStylesInHead",
62
+ "blockVideos",
63
+ "customStylesheet",
64
+ "manifest",
65
+ "loadDeferredContent",
66
+ "loadDeferredContentMaxIdleTime",
67
+ "loadDeferredContentKeepZoomLevel",
68
+ "loadDeferredContentMinZoomFactor",
69
+ "loadDeferredContentDispatchScrollEvent",
70
+ "loadDeferredContentBlockCookies",
71
+ "loadDeferredContentBlockStorage"
72
+ ];
55
73
  const TARGET_ORIGIN = "*";
56
74
  const TIMEOUT_INIT_REQUEST_MESSAGE = 5000;
57
75
  const TIMEOUT_INIT_RESPONSE_MESSAGE = 10000;
@@ -60,6 +78,7 @@ const WINDOW_ID_SEPARATOR = ".";
60
78
  const TOP_WINDOW = globalThis.window == globalThis.top;
61
79
 
62
80
  const browser = globalThis.browser;
81
+ const RUNTIME_MESSAGING = Boolean(browser && browser.runtime && browser.runtime.sendMessage);
63
82
  const top = globalThis.top;
64
83
  const MessageChannel = globalThis.MessageChannel;
65
84
  const document = globalThis.document;
@@ -83,6 +102,11 @@ if (TOP_WINDOW) {
83
102
  clearFrameTimeout("requestTimeouts", message.sessionId, message.windowId);
84
103
  createFrameResponseTimeout(message.sessionId, message.windowId);
85
104
  return Promise.resolve({});
105
+ } else if (message.method == GET_OPTIONS_MESSAGE) {
106
+ const session = sessions.get(message.sessionId);
107
+ if (session) {
108
+ return Promise.resolve({ options: session.options });
109
+ }
86
110
  }
87
111
  });
88
112
  }
@@ -114,10 +138,15 @@ async function onMessage(event) {
114
138
  }
115
139
  if (!TOP_WINDOW) {
116
140
  globalThis.stop();
117
- if (message.options.loadDeferredContent) {
118
- lazy.process(message.options);
141
+ if (!message.options && RUNTIME_MESSAGING) {
142
+ message.options = await getOptions(message.sessionId);
143
+ }
144
+ if (message.options) {
145
+ if (message.options.loadDeferredContent) {
146
+ lazy.process(message.options);
147
+ }
148
+ await initRequestAsync(message);
119
149
  }
120
- await initRequestAsync(message);
121
150
  }
122
151
  } else if (message.method == ACK_INIT_REQUEST_MESSAGE) {
123
152
  clearFrameTimeout("requestTimeouts", message.sessionId, message.windowId);
@@ -133,12 +162,13 @@ async function onMessage(event) {
133
162
 
134
163
  function getAsync(options) {
135
164
  const sessionId = getNewSessionId();
136
- options = JSON.parse(JSON.stringify(options));
165
+ options = getFrameOptions(options);
137
166
  return new Promise(resolve => {
138
167
  sessions.set(sessionId, {
139
168
  frames: [],
140
169
  requestTimeouts: {},
141
170
  responseTimeouts: {},
171
+ options,
142
172
  resolve: frames => {
143
173
  frames.sessionId = sessionId;
144
174
  resolve(frames);
@@ -150,11 +180,12 @@ function getAsync(options) {
150
180
 
151
181
  function getSync(options) {
152
182
  const sessionId = getNewSessionId();
153
- options = JSON.parse(JSON.stringify(options));
183
+ options = getFrameOptions(options);
154
184
  sessions.set(sessionId, {
155
185
  frames: [],
156
186
  requestTimeouts: {},
157
- responseTimeouts: {}
187
+ responseTimeouts: {},
188
+ options
158
189
  });
159
190
  initRequestSync({ windowId, sessionId, options });
160
191
  const frames = sessions.get(sessionId).frames;
@@ -171,6 +202,20 @@ function getNewSessionId() {
171
202
  return globalThis.crypto.getRandomValues(new Uint32Array(32)).join("");
172
203
  }
173
204
 
205
+ function getFrameOptions(options) {
206
+ return JSON.parse(JSON.stringify(options, FRAME_OPTION_NAMES));
207
+ }
208
+
209
+ async function getOptions(sessionId) {
210
+ try {
211
+ const response = await browser.runtime.sendMessage({ method: GET_OPTIONS_MESSAGE, sessionId });
212
+ return response && response.options;
213
+ // eslint-disable-next-line no-unused-vars
214
+ } catch (error) {
215
+ // ignored
216
+ }
217
+ }
218
+
174
219
  function initRequestSync(message) {
175
220
  const sessionId = message.sessionId;
176
221
  delete globalThis._singleFile_cleaningUp;
@@ -282,7 +327,7 @@ function processFramesAsync(doc, frameElements, options, parentWindowId, session
282
327
  frameElements.forEach((frameElement, frameIndex) => {
283
328
  const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
284
329
  try {
285
- sendMessage(frameElement.contentWindow, { method: INIT_REQUEST_MESSAGE, windowId, sessionId, options, scrolling: frameElement.scrolling, waitForFrames });
330
+ sendMessage(frameElement.contentWindow, { method: INIT_REQUEST_MESSAGE, windowId, sessionId, options: RUNTIME_MESSAGING ? undefined : options, scrolling: frameElement.scrolling, waitForFrames });
286
331
  // eslint-disable-next-line no-unused-vars
287
332
  } catch (error) {
288
333
  // ignored
@@ -45,6 +45,7 @@
45
45
  const GET_ADOPTED_STYLESHEETS_REQUEST_EVENT = "single-file-request-get-adopted-stylesheets";
46
46
  const UNREGISTER_GET_ADOPTED_STYLESHEETS_REQUEST_EVENT = "single-file-unregister-request-get-adopted-stylesheets";
47
47
  const GET_ADOPTED_STYLESHEETS_RESPONSE_EVENT = "single-file-response-get-adopted-stylesheets";
48
+ const CLEAR_ADOPTED_STYLESHEETS_CACHE_EVENT = "single-file-clear-adopted-stylesheets-cache";
48
49
  const NEW_FONT_FACE_EVENT = "single-file-new-font-face";
49
50
  const DELETE_FONT_EVENT = "single-file-delete-font";
50
51
  const CLEAR_FONTS_EVENT = "single-file-clear-fonts";
@@ -97,6 +98,7 @@
97
98
 
98
99
  let dispatchScrollEvent;
99
100
  let adoptedStylesheetsData = new WeakMap();
101
+ let adoptedStylesheetsTexts = new WeakMap();
100
102
  const shadowRootsData = new WeakMap();
101
103
 
102
104
  init();
@@ -118,6 +120,7 @@
118
120
  document.addEventListener(FETCH_SUPPORTED_REQUEST_EVENT, onFetchSupportedRequest);
119
121
  document.addEventListener(FETCH_REQUEST_EVENT, onFetchRequest);
120
122
  document.addEventListener(GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, getAdoptedStylesheetsListener);
123
+ document.addEventListener(CLEAR_ADOPTED_STYLESHEETS_CACHE_EVENT, clearAdoptedStylesheetsCache);
121
124
  document.addEventListener(BOOTSTRAP_EVENT, onBootstrap);
122
125
  }
123
126
 
@@ -553,6 +556,7 @@
553
556
  try {
554
557
  const result = originalInsertRule.apply(this, [rule, arguments[1]]);
555
558
  adoptedStylesheetsData.delete(this);
559
+ adoptedStylesheetsTexts.delete(this);
556
560
  return result;
557
561
  } catch (error) {
558
562
  error.stack = error.message + "\n" + " \n" + error.stack.trim().split("\n").slice(-1).join("\n");
@@ -566,6 +570,7 @@
566
570
  try {
567
571
  const result = originalDeleteRule.apply(this, [index]);
568
572
  adoptedStylesheetsData.delete(this);
573
+ adoptedStylesheetsTexts.delete(this);
569
574
  return result;
570
575
  } catch (error) {
571
576
  error.stack = error.message + "\n" + " \n" + error.stack.trim().split("\n").slice(-1).join("\n");
@@ -600,9 +605,11 @@
600
605
  const adoptedStyleSheets = Array.from(shadowRoot.adoptedStyleSheets).map(stylesheet => {
601
606
  if (adoptedStylesheetsData.has(stylesheet)) {
602
607
  return adoptedStylesheetsData.get(stylesheet);
608
+ } else if (adoptedStylesheetsTexts.has(stylesheet)) {
609
+ return adoptedStylesheetsTexts.get(stylesheet);
603
610
  } else {
604
611
  const text = Array.from(stylesheet.cssRules).map(cssRule => cssRule.cssText).join("\n");
605
- adoptedStylesheetsData.set(stylesheet, text);
612
+ adoptedStylesheetsTexts.set(stylesheet, text);
606
613
  return text;
607
614
  }
608
615
  });
@@ -620,6 +627,10 @@
620
627
  event.currentTarget.removeEventListener(GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, getAdoptedStylesheetsListener, { capture: true });
621
628
  }
622
629
 
630
+ function clearAdoptedStylesheetsCache() {
631
+ adoptedStylesheetsTexts = new WeakMap();
632
+ }
633
+
623
634
  async function getDetailObject(fontFace, src) {
624
635
  const detail = {};
625
636
  Object.keys(FONT_STYLE_PROPERTIES).forEach(descriptor => {