single-file-core 1.5.56 → 1.5.58

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
@@ -77,6 +77,7 @@ const EMPTY_RESOURCE = "data:,";
77
77
  const DEFAULT_REPLACED_CHARACTERS = ["~", "+", "?", "%", "*", ":", "|", "\"", "<", ">", "\\\\", "\x00-\x1f", "\x7F"];
78
78
  const DEFAULT_REPLACEMENT_CHARACTER = "_";
79
79
  const DEFAULT_REPLACEMENT_CHARACTERS = ["~", "+", "?", "%", "*", ":", "|", """, "<", ">", "\"];
80
+ const NESTING_TRACK_ID_ATTRIBUTE_NAME = "data-sf-nesting-track-id";
80
81
  const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
81
82
  // eslint-disable-next-line no-unused-vars
82
83
  const dispatchEvent = event => { try { globalThis.dispatchEvent(event); } catch (error) { /* ignored */ } };
@@ -87,6 +88,7 @@ const Blob = globalThis.Blob;
87
88
  const CustomEvent = globalThis.CustomEvent;
88
89
  const MutationObserver = globalThis.MutationObserver;
89
90
  const URL = globalThis.URL;
91
+ const DOMParser = globalThis.DOMParser;
90
92
 
91
93
  export {
92
94
  initUserScriptHandler,
@@ -103,6 +105,8 @@ export {
103
105
  getContentSize,
104
106
  digest,
105
107
  getValidFilename,
108
+ parseDocContent,
109
+ fixInvalidNesting,
106
110
  ON_BEFORE_CAPTURE_EVENT_NAME,
107
111
  ON_AFTER_CAPTURE_EVENT_NAME,
108
112
  WIN_ID_ATTRIBUTE_NAME,
@@ -130,7 +134,8 @@ export {
130
134
  INFOBAR_TAGNAME,
131
135
  WAIT_FOR_USERSCRIPT_PROPERTY_NAME,
132
136
  MESSAGE_PREFIX,
133
- NO_SCRIPT_PROPERTY_NAME
137
+ NO_SCRIPT_PROPERTY_NAME,
138
+ NESTING_TRACK_ID_ATTRIBUTE_NAME
134
139
  };
135
140
 
136
141
  function initUserScriptHandler() {
@@ -205,13 +210,7 @@ function preProcessDoc(doc, win, options) {
205
210
  const invalidElements = new Map();
206
211
  let elementsInfo;
207
212
  if (win && doc.documentElement) {
208
- doc.querySelectorAll("button button, a a").forEach(element => {
209
- const placeHolderElement = doc.createElement("template");
210
- placeHolderElement.setAttribute(INVALID_ELEMENT_ATTRIBUTE_NAME, "");
211
- placeHolderElement.content.appendChild(element.cloneNode(true));
212
- invalidElements.set(element, placeHolderElement);
213
- element.replaceWith(placeHolderElement);
214
- });
213
+ markInvalidNesting(doc);
215
214
  elementsInfo = getElementsInfo(win, doc, doc.documentElement, options);
216
215
  if (options.moveStylesInHead) {
217
216
  doc.querySelectorAll("body style, body ~ style").forEach(element => {
@@ -260,6 +259,87 @@ function preProcessDoc(doc, win, options) {
260
259
  };
261
260
  }
262
261
 
262
+ function markInvalidNesting(doc) {
263
+ addTrackIds(doc.body);
264
+ const verificationDoc = parseDocContent(serialize(doc));
265
+ const markedMap = buildTrackIdMap(doc.body);
266
+ const normalizedMap = buildTrackIdMap(verificationDoc.body);
267
+ const trackIds = new Set();
268
+ Object.keys(markedMap).forEach(id => {
269
+ if (id in normalizedMap) {
270
+ const markedParent = markedMap[id].parentElement?.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME) || null;
271
+ const normalizedParent = normalizedMap[id]?.parentElement?.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME) || null;
272
+ if (markedParent !== normalizedParent) {
273
+ let current = markedMap[id];
274
+ while (current && current !== doc.body) {
275
+ const currentId = current.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
276
+ if (currentId) {
277
+ trackIds.add(currentId);
278
+ }
279
+ current = current.parentElement;
280
+ }
281
+ }
282
+ }
283
+ });
284
+ cleanupTrackIds(doc.body, trackIds);
285
+
286
+ function addTrackIds(element, index = 0, parentTrackId = "") {
287
+ const trackId = parentTrackId ? `${parentTrackId}.${index + 1}` : `${index + 1}`;
288
+ element.setAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME, trackId);
289
+ Array.from(element.children).forEach((child, indexChild) => addTrackIds(child, indexChild, trackId));
290
+ }
291
+
292
+ function buildTrackIdMap(element) {
293
+ const trackIds = {};
294
+ traverse(element);
295
+ return trackIds;
296
+
297
+ function traverse(element) {
298
+ if (element.getAttribute) {
299
+ const id = element.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
300
+ if (id) {
301
+ trackIds[id] = element;
302
+ }
303
+ Array.from(element.children).forEach(traverse);
304
+ }
305
+ }
306
+ }
307
+
308
+ function cleanupTrackIds(element, toKeep) {
309
+ const id = element.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
310
+ if (id && !toKeep.has(id)) {
311
+ element.removeAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
312
+ }
313
+ Array.from(element.children).forEach(child => cleanupTrackIds(child, toKeep));
314
+ }
315
+ }
316
+
317
+ function fixInvalidNesting(document, NESTING_TRACK_ID_ATTRIBUTE_NAME) {
318
+ const trackIds = {};
319
+ document.currentScript.remove();
320
+ buildTrackIdMap(document.body);
321
+ Object.keys(trackIds).forEach(id => {
322
+ const element = trackIds[id];
323
+ const idParts = id.split(".");
324
+ if (idParts.length > 1) {
325
+ const parentId = idParts.slice(0, -1).join(".");
326
+ const expectedParent = trackIds[parentId];
327
+ if (expectedParent && element.parentElement !== expectedParent) {
328
+ expectedParent.appendChild(element);
329
+ }
330
+ }
331
+ });
332
+ Object.keys(trackIds).forEach(id => trackIds[id].removeAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME));
333
+
334
+ function buildTrackIdMap(element) {
335
+ const id = element.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
336
+ if (id) {
337
+ trackIds[id] = element;
338
+ }
339
+ Array.from(element.children).forEach(buildTrackIdMap);
340
+ }
341
+ }
342
+
263
343
  function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map(), canvases: [], images: [], posters: [], videos: [], shadowRoots: [], markedElements: [] }, adoptedStyleSheetsCache = new Map(), ascendantHidden) {
264
344
  if (element.childNodes) {
265
345
  const elements = Array.from(element.childNodes).filter(node => (node instanceof win.HTMLElement) || (node instanceof win.SVGElement) || (node instanceof globalThis.HTMLElement) || (node instanceof globalThis.SVGElement));
@@ -746,4 +826,21 @@ function getValidFilename(filename, replacedCharacters = DEFAULT_REPLACED_CHARAC
746
826
  .replace(/\.\//g, "." + replacementCharacter)
747
827
  .replace(/\/\./g, "/" + replacementCharacter);
748
828
  return filename;
829
+ }
830
+
831
+ function parseDocContent(content, baseURI) {
832
+ const doc = (new DOMParser()).parseFromString(content, "text/html");
833
+ if (!doc.head) {
834
+ doc.documentElement.insertBefore(doc.createElement("HEAD"), doc.body);
835
+ }
836
+ let baseElement = doc.querySelector("base");
837
+ if (!baseElement || !baseElement.getAttribute("href")) {
838
+ if (baseElement) {
839
+ baseElement.remove();
840
+ }
841
+ baseElement = doc.createElement("base");
842
+ baseElement.setAttribute("href", baseURI);
843
+ doc.head.insertBefore(baseElement, doc.head.firstChild);
844
+ }
845
+ return doc;
749
846
  }
package/core/index.js CHANGED
@@ -582,6 +582,11 @@ class Processor {
582
582
  if (this.options.displayStats) {
583
583
  size = util.getContentSize(this.doc.documentElement.outerHTML);
584
584
  }
585
+ if (this.doc.querySelector(`[${util.NESTING_TRACK_ID_ATTRIBUTE_NAME}]`)) {
586
+ const scriptElement = this.doc.createElement("script");
587
+ scriptElement.textContent = `(${util.getFixInvalidNestingSource()})(document, "${util.NESTING_TRACK_ID_ATTRIBUTE_NAME}");`;
588
+ this.doc.body.appendChild(scriptElement);
589
+ }
585
590
  const content = util.serialize(this.doc, this.options.compressHTML);
586
591
  if (this.options.displayStats) {
587
592
  const contentSize = util.getContentSize(content);
@@ -893,8 +898,10 @@ class Processor {
893
898
  if (!this.options.saveFavicon) {
894
899
  this.doc.querySelectorAll("link[rel*=\"icon\"]").forEach(element => element.remove());
895
900
  }
896
- this.doc.querySelectorAll("a[ping]").forEach(element => element.removeAttribute("ping"));
901
+ this.doc.querySelectorAll("a[ping], area[ping]").forEach(element => element.removeAttribute("ping"));
902
+ this.doc.querySelectorAll("a[attributionsrc], img[attributionsrc], script[attributionsrc]").forEach(element => element.removeAttribute("attributionsrc"));
897
903
  this.doc.querySelectorAll("link[rel=import][href]").forEach(element => element.remove());
904
+ this.doc.querySelectorAll("link[rel=compression-dictionary]").forEach(element => element.remove());
898
905
  }
899
906
 
900
907
  replaceInvalidElements() {
@@ -329,37 +329,17 @@ class ProcessorHelperCommon {
329
329
  }
330
330
 
331
331
  replacePseudoClassDefined(stylesheet) {
332
- const removedSelectors = [];
333
- if (stylesheet.children) {
334
- for (let cssRule = stylesheet.children.head; cssRule; cssRule = cssRule.next) {
335
- const ruleData = cssRule.data;
336
- if (ruleData.type == "Rule" && ruleData.prelude && ruleData.prelude.children) {
337
- for (let selector = ruleData.prelude.children.head; selector; selector = selector.next) {
338
- replacePseudoDefinedSelector(selector, ruleData.prelude);
332
+ cssTree.walk(stylesheet, {
333
+ enter: function(node, item, list) {
334
+ if (node.type == "PseudoClassSelector" && node.name == "defined") {
335
+ if (item.prev == null || item.prev.data.type == "Combinator" || item.prev.data.type == "WhiteSpace") {
336
+ list.replace(item, cssTree.parse("*", { context: "selector" }).children.head);
337
+ } else {
338
+ list.remove(item);
339
339
  }
340
340
  }
341
341
  }
342
- }
343
- if (removedSelectors.length) {
344
- removedSelectors.forEach(({ parentSelector, selector }) => {
345
- if (parentSelector.data.children.size == 0 || !selector.prev || selector.prev.data.type == "Combinator" || selector.prev.data.type == "WhiteSpace") {
346
- parentSelector.data.children.replace(selector, cssTree.parse("*", { context: "selector" }).children.head);
347
- } else {
348
- parentSelector.data.children.remove(selector);
349
- }
350
- });
351
- }
352
-
353
- function replacePseudoDefinedSelector(selector, parentSelector) {
354
- if (selector.data.children) {
355
- for (let childSelector = selector.data.children.head; childSelector; childSelector = childSelector.next) {
356
- replacePseudoDefinedSelector(childSelector, selector);
357
- }
358
- }
359
- if (selector.data.type == "PseudoClassSelector" && selector.data.name == "defined") {
360
- removedSelectors.push({ parentSelector, selector });
361
- }
362
- }
342
+ });
363
343
  }
364
344
 
365
345
  resolveStylesheetURLs(stylesheet, baseURI, workStylesheet) {
@@ -99,6 +99,11 @@ function getProcessorHelperClass(utilInstance) {
99
99
  this.removeSingleLineCssComments(stylesheet);
100
100
  }
101
101
  this.replacePseudoClassDefined(stylesheet);
102
+ options.inlineStylesheets.forEach((content, index) => {
103
+ if (content === element.textContent) {
104
+ options.inlineStylesheets.set(index, this.generateStylesheetContent(stylesheet, options));
105
+ }
106
+ });
102
107
  stylesheetInfo.stylesheet = stylesheet;
103
108
  } else {
104
109
  stylesheets.delete(element);
@@ -102,7 +102,10 @@ function getProcessorHelperClass(utilInstance) {
102
102
  linkElement.setAttribute("type", "text/css");
103
103
  const name = "stylesheet_" + resources.stylesheets.size + ".css";
104
104
  linkElement.setAttribute("href", name);
105
- const content = options.inlineStylesheets.get(stylesheetRefIndex);
105
+ let content = options.inlineStylesheets.get(stylesheetRefIndex);
106
+ const stylesheet = cssTree.parse(content, { context: "stylesheet", parseCustomProperty: true });
107
+ this.replacePseudoClassDefined(stylesheet);
108
+ content = this.generateStylesheetContent(stylesheet, options);
106
109
  resources.stylesheets.set(resources.stylesheets.size, { name, content });
107
110
  linkElements.set(stylesheetRefIndex, linkElement);
108
111
  });
@@ -247,9 +250,9 @@ function getProcessorHelperClass(utilInstance) {
247
250
 
248
251
  async processFrame(frameElement, pageData, options, resources, frameWindowId, frameData) {
249
252
  const name = "frames/" + resources.frames.size + "/";
250
- let sandbox = "allow-popups allow-top-navigation-by-user-activation";
253
+ let sandbox = "allow-popups allow-top-navigation-by-user-activation allow-scripts";
251
254
  if (pageData.content.match(NOSCRIPT_TAG_FOUND) || pageData.content.match(CANVAS_TAG_FOUND) || pageData.content.match(SCRIPT_TAG_FOUND) || options.saveRawPage) {
252
- sandbox += " allow-scripts allow-modals allow-popups allow-downloads allow-pointer-lock allow-presentation";
255
+ sandbox += " allow-modals allow-popups allow-downloads allow-pointer-lock allow-presentation";
253
256
  }
254
257
  frameElement.setAttribute("sandbox", sandbox);
255
258
  if (frameElement.tagName.toUpperCase() == "OBJECT") {
package/core/util.js CHANGED
@@ -116,20 +116,7 @@ function getInstance(utilOptions) {
116
116
  return helper.getValidFilename(filename, replacedCharacters, replacementCharacter, replacementCharacters);
117
117
  },
118
118
  parseDocContent(content, baseURI) {
119
- const doc = (new DOMParser()).parseFromString(content, "text/html");
120
- if (!doc.head) {
121
- doc.documentElement.insertBefore(doc.createElement("HEAD"), doc.body);
122
- }
123
- let baseElement = doc.querySelector("base");
124
- if (!baseElement || !baseElement.getAttribute("href")) {
125
- if (baseElement) {
126
- baseElement.remove();
127
- }
128
- baseElement = doc.createElement("base");
129
- baseElement.setAttribute("href", baseURI);
130
- doc.head.insertBefore(baseElement, doc.head.firstChild);
131
- }
132
- return doc;
119
+ return helper.parseDocContent(content, baseURI);
133
120
  },
134
121
  parseXMLContent(content) {
135
122
  return (new DOMParser()).parseFromString(content, "text/xml");
@@ -142,6 +129,9 @@ function getInstance(utilOptions) {
142
129
  return doc;
143
130
  }
144
131
  },
132
+ getFixInvalidNestingSource() {
133
+ return helper.fixInvalidNesting.toString().replace(/\s+/g, " ");
134
+ },
145
135
  async digest(algo, text) {
146
136
  return helper.digest(algo, text);
147
137
  },
@@ -229,7 +219,8 @@ function getInstance(utilOptions) {
229
219
  EMPTY_RESOURCE: helper.EMPTY_RESOURCE,
230
220
  INFOBAR_TAGNAME: helper.INFOBAR_TAGNAME,
231
221
  WAIT_FOR_USERSCRIPT_PROPERTY_NAME: helper.WAIT_FOR_USERSCRIPT_PROPERTY_NAME,
232
- NO_SCRIPT_PROPERTY_NAME: helper.NO_SCRIPT_PROPERTY_NAME
222
+ NO_SCRIPT_PROPERTY_NAME: helper.NO_SCRIPT_PROPERTY_NAME,
223
+ NESTING_TRACK_ID_ATTRIBUTE_NAME: helper.NESTING_TRACK_ID_ATTRIBUTE_NAME
233
224
  };
234
225
 
235
226
  async function getContent(resourceURL, options) {
@@ -217,7 +217,7 @@ function minifyRule(ruleData, cssRule, stylesheets, processingContext, removedRu
217
217
  }
218
218
  }
219
219
 
220
- function minifyImportRule(ruleData, cssRule, stylesheets, processingContext, removedRules, docContext) {
220
+ function minifyImportRule(ruleData, _cssRule, stylesheets, processingContext, _removedRules, docContext) {
221
221
  const urlNode = ruleData.prelude.children.head.data;
222
222
  const topConditionalStack = urlNode.importedMediaText ? [{ name: "media", prelude: urlNode.importedMediaText }] : [];
223
223
  if (urlNode.importedLayerName !== undefined) {
@@ -594,8 +594,8 @@ function compareDeclarations(declarationA, declarationB, docContext) {
594
594
  }
595
595
  if (declarationA.isInline && !declarationB.isInline) return 1;
596
596
  if (!declarationA.isInline && declarationB.isInline) return -1;
597
- let selectorDataA = declarationA.selector ? docContext.selectorData.get(declarationA.selector) : null;
598
- let selectorDataB = declarationB.selector ? docContext.selectorData.get(declarationB.selector) : null;
597
+ const selectorDataA = declarationA.selector ? docContext.selectorData.get(declarationA.selector) : null;
598
+ const selectorDataB = declarationB.selector ? docContext.selectorData.get(declarationB.selector) : null;
599
599
  if (selectorDataA && selectorDataB) {
600
600
  const layerComparison = compareLayers(selectorDataA.layerStack, selectorDataB.layerStack, docContext);
601
601
  if (layerComparison !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.56",
3
+ "version": "1.5.58",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -439,7 +439,7 @@ function getFrames(document) {
439
439
  document.querySelectorAll(ALL_ELEMENTS_CSS_SELECTOR).forEach(element => {
440
440
  const shadowRoot = helper.getShadowRoot(element);
441
441
  if (shadowRoot) {
442
- frames = frames.concat(...shadowRoot.querySelectorAll(FRAMES_CSS_SELECTOR));
442
+ frames = frames.concat(...getFrames(shadowRoot));
443
443
  }
444
444
  });
445
445
  return frames;
@@ -52,12 +52,16 @@
52
52
  const BOOTSTRAP_EVENT = "single-file-bootstrap";
53
53
  const FONT_STYLE_PROPERTIES = {
54
54
  family: "font-family",
55
- style: "font-style",
56
- weight: "font-weight",
55
+ ascentOverride: "ascent-override",
56
+ descentOverride: "descent-override",
57
+ display: "font-display",
58
+ featureSettings: "font-feature-settings",
59
+ lineGapOverride: "line-gap-override",
57
60
  stretch: "font-stretch",
61
+ style: "font-style",
58
62
  unicodeRange: "unicode-range",
59
- variant: "font-variant",
60
- featureSettings: "font-feature-settings"
63
+ variationSettings: "font-variation-settings",
64
+ weight: "font-weight"
61
65
  };
62
66
 
63
67
  const fetch = globalThis.fetch.bind(globalThis);