single-file-core 1.6.27 → 1.6.29

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",
@@ -124,7 +157,8 @@ const DEPRECATED_OPTION_NAMES = {
124
157
  loadDeferredImagesBlockStorage: "loadDeferredContentBlockStorage",
125
158
  loadDeferredImagesKeepZoomLevel: "loadDeferredContentKeepZoomLevel",
126
159
  loadDeferredImagesDispatchScrollEvent: "loadDeferredContentDispatchScrollEvent",
127
- loadDeferredImagesBeforeFrames: "loadDeferredContentBeforeFrames"
160
+ loadDeferredImagesBeforeFrames: "loadDeferredContentBeforeFrames",
161
+ blockAlternativeImages: "removeAlternativeImages"
128
162
  };
129
163
  const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
130
164
  const removeEventListener = (type, listener, options) => globalThis.removeEventListener(type, listener, options);
@@ -259,7 +293,7 @@ function onInitUserScript({ detail }) {
259
293
  delete userScriptOptions.frames;
260
294
  delete userScriptOptions.taskId;
261
295
  delete userScriptOptions._migratedTemplateFormat;
262
- delete userScriptOptions.woleetKey;
296
+ PROTECTED_OPTION_NAMES.forEach(name => delete userScriptOptions[name]);
263
297
  let detailUserScript;
264
298
  try {
265
299
  detailUserScript = detail == "jsonDetail" ? JSON.stringify({ options: userScriptOptions }) : { options: userScriptOptions };
@@ -276,6 +310,7 @@ function onInitUserScript({ detail }) {
276
310
  try {
277
311
  const detail = typeof event.detail == "string" ? JSON.parse(event.detail) : event.detail;
278
312
  if (detail.options) {
313
+ PROTECTED_OPTION_NAMES.forEach(name => delete detail.options[name]);
279
314
  Object.assign(options, detail.options);
280
315
  }
281
316
  // eslint-disable-next-line no-unused-vars
@@ -324,6 +359,12 @@ function preProcessDoc(doc, win, options) {
324
359
  let elementsInfo;
325
360
  if (win && doc.documentElement) {
326
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
+ }
327
368
  elementsInfo = getElementsInfo(win, doc, doc.documentElement, options);
328
369
  if (options.removeUnusedFonts && doc.defaultView) {
329
370
  getRootElementUsedFonts(win, doc.documentElement, elementsInfo);
@@ -824,7 +865,7 @@ function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map
824
865
  shadowRootInfo.clonable = shadowRoot.clonable;
825
866
  shadowRootInfo.serializable = shadowRoot.serializable;
826
867
  try {
827
- if (shadowRoot.adoptedStyleSheets && shadowRoot.adoptedStyleSheets.length === undefined) {
868
+ if (shadowRoot.adoptedStyleSheets) {
828
869
  shadowRoot.dispatchEvent(new CustomEvent(UNREGISTER_GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, { bubbles: true }));
829
870
  }
830
871
  // eslint-disable-next-line no-unused-vars
@@ -908,6 +949,9 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
908
949
  EMPTY_RESOURCE :
909
950
  (options.loadDeferredContent && element.getAttribute(LAZY_SRC_ATTRIBUTE_NAME)) || element.currentSrc
910
951
  };
952
+ if (!elementHidden && element.parentElement && element.parentElement.tagName.toUpperCase() == "PICTURE") {
953
+ imageData.sourceSize = getSourceSize(win, element.parentElement, imageData.currentSrc);
954
+ }
911
955
  data.images.push(imageData);
912
956
  element.setAttribute(IMAGE_ATTRIBUTE_NAME, data.images.length - 1);
913
957
  data.markedElements.push(element);
@@ -1241,6 +1285,42 @@ function getStylesheetsData(doc, markedElements) {
1241
1285
  }
1242
1286
  }
1243
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
+
1244
1324
  function getSize(win, imageElement, computedStyle) {
1245
1325
  let pxWidth = imageElement.naturalWidth;
1246
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")) {
@@ -230,7 +230,7 @@ class ProcessorHelperCommon {
230
230
  if (options.saveOriginalURLs && !isDataURL(originSrcset)) {
231
231
  resourceElement.setAttribute("data-sf-original-srcset", originSrcset);
232
232
  }
233
- if (!options.blockImages && !options.blockAlternativeImages) {
233
+ if (!options.blockImages && !options.removeAlternativeImages) {
234
234
  const srcsetValues = await Promise.all(srcset.map(async srcsetValue => {
235
235
  let resourceURL = normalizeURL(srcsetValue.url);
236
236
  if (!testIgnoredPath(resourceURL)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.6.27",
3
+ "version": "1.6.29",
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,9 @@ 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
+ return Promise.resolve({ options: session && session.options });
86
108
  }
87
109
  });
88
110
  }
@@ -114,10 +136,15 @@ async function onMessage(event) {
114
136
  }
115
137
  if (!TOP_WINDOW) {
116
138
  globalThis.stop();
117
- if (message.options.loadDeferredContent) {
118
- lazy.process(message.options);
139
+ if (!message.options && RUNTIME_MESSAGING) {
140
+ message.options = await getOptions(message.sessionId);
141
+ }
142
+ if (message.options) {
143
+ if (message.options.loadDeferredContent) {
144
+ lazy.process(message.options);
145
+ }
146
+ await initRequestAsync(message);
119
147
  }
120
- await initRequestAsync(message);
121
148
  }
122
149
  } else if (message.method == ACK_INIT_REQUEST_MESSAGE) {
123
150
  clearFrameTimeout("requestTimeouts", message.sessionId, message.windowId);
@@ -133,12 +160,13 @@ async function onMessage(event) {
133
160
 
134
161
  function getAsync(options) {
135
162
  const sessionId = getNewSessionId();
136
- options = JSON.parse(JSON.stringify(options));
163
+ options = getFrameOptions(options);
137
164
  return new Promise(resolve => {
138
165
  sessions.set(sessionId, {
139
166
  frames: [],
140
167
  requestTimeouts: {},
141
168
  responseTimeouts: {},
169
+ options,
142
170
  resolve: frames => {
143
171
  frames.sessionId = sessionId;
144
172
  resolve(frames);
@@ -150,11 +178,12 @@ function getAsync(options) {
150
178
 
151
179
  function getSync(options) {
152
180
  const sessionId = getNewSessionId();
153
- options = JSON.parse(JSON.stringify(options));
181
+ options = getFrameOptions(options);
154
182
  sessions.set(sessionId, {
155
183
  frames: [],
156
184
  requestTimeouts: {},
157
- responseTimeouts: {}
185
+ responseTimeouts: {},
186
+ options
158
187
  });
159
188
  initRequestSync({ windowId, sessionId, options });
160
189
  const frames = sessions.get(sessionId).frames;
@@ -171,6 +200,20 @@ function getNewSessionId() {
171
200
  return globalThis.crypto.getRandomValues(new Uint32Array(32)).join("");
172
201
  }
173
202
 
203
+ function getFrameOptions(options) {
204
+ return JSON.parse(JSON.stringify(options, FRAME_OPTION_NAMES));
205
+ }
206
+
207
+ async function getOptions(sessionId) {
208
+ try {
209
+ const response = await browser.runtime.sendMessage({ method: GET_OPTIONS_MESSAGE, sessionId });
210
+ return response && response.options;
211
+ // eslint-disable-next-line no-unused-vars
212
+ } catch (error) {
213
+ // ignored
214
+ }
215
+ }
216
+
174
217
  function initRequestSync(message) {
175
218
  const sessionId = message.sessionId;
176
219
  delete globalThis._singleFile_cleaningUp;
@@ -282,7 +325,7 @@ function processFramesAsync(doc, frameElements, options, parentWindowId, session
282
325
  frameElements.forEach((frameElement, frameIndex) => {
283
326
  const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
284
327
  try {
285
- sendMessage(frameElement.contentWindow, { method: INIT_REQUEST_MESSAGE, windowId, sessionId, options, scrolling: frameElement.scrolling, waitForFrames });
328
+ sendMessage(frameElement.contentWindow, { method: INIT_REQUEST_MESSAGE, windowId, sessionId, options: RUNTIME_MESSAGING ? undefined : options, scrolling: frameElement.scrolling, waitForFrames });
286
329
  // eslint-disable-next-line no-unused-vars
287
330
  } catch (error) {
288
331
  // 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 => {