single-file-core 1.6.28 → 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",
@@ -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.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 => {