single-file-core 1.5.57 → 1.5.59

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,91 @@ 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, preventCleanup = false) {
318
+ const trackIds = {};
319
+ if (document.currentScript) {
320
+ document.currentScript.remove();
321
+ }
322
+ buildTrackIdMap(document.body);
323
+ Object.keys(trackIds).forEach(id => {
324
+ const element = trackIds[id];
325
+ const idParts = id.split(".");
326
+ if (idParts.length > 1) {
327
+ const parentId = idParts.slice(0, -1).join(".");
328
+ const expectedParent = trackIds[parentId];
329
+ if (expectedParent && element.parentElement !== expectedParent) {
330
+ expectedParent.appendChild(element);
331
+ }
332
+ }
333
+ });
334
+ if (!preventCleanup) {
335
+ Object.keys(trackIds).forEach(id => trackIds[id].removeAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME));
336
+ }
337
+
338
+ function buildTrackIdMap(element) {
339
+ const id = element.getAttribute(NESTING_TRACK_ID_ATTRIBUTE_NAME);
340
+ if (id) {
341
+ trackIds[id] = element;
342
+ }
343
+ Array.from(element.children).forEach(buildTrackIdMap);
344
+ }
345
+ }
346
+
263
347
  function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map(), canvases: [], images: [], posters: [], videos: [], shadowRoots: [], markedElements: [] }, adoptedStyleSheetsCache = new Map(), ascendantHidden) {
264
348
  if (element.childNodes) {
265
349
  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 +830,21 @@ function getValidFilename(filename, replacedCharacters = DEFAULT_REPLACED_CHARAC
746
830
  .replace(/\.\//g, "." + replacementCharacter)
747
831
  .replace(/\/\./g, "/" + replacementCharacter);
748
832
  return filename;
833
+ }
834
+
835
+ function parseDocContent(content, baseURI) {
836
+ const doc = (new DOMParser()).parseFromString(content, "text/html");
837
+ if (!doc.head) {
838
+ doc.documentElement.insertBefore(doc.createElement("HEAD"), doc.body);
839
+ }
840
+ let baseElement = doc.querySelector("base");
841
+ if (!baseElement || !baseElement.getAttribute("href")) {
842
+ if (baseElement) {
843
+ baseElement.remove();
844
+ }
845
+ baseElement = doc.createElement("base");
846
+ baseElement.setAttribute("href", baseURI);
847
+ doc.head.insertBefore(baseElement, doc.head.firstChild);
848
+ }
849
+ return doc;
749
850
  }
package/core/index.js CHANGED
@@ -470,6 +470,7 @@ class Processor {
470
470
  pageContent = content.data || "";
471
471
  }
472
472
  this.doc = util.parseDocContent(pageContent, this.baseURI);
473
+ util.fixInvalidNesting(this.doc);
473
474
  if (this.options.saveRawPage) {
474
475
  let charset;
475
476
  this.doc.querySelectorAll("meta[charset]").forEach(element => {
@@ -582,6 +583,11 @@ class Processor {
582
583
  if (this.options.displayStats) {
583
584
  size = util.getContentSize(this.doc.documentElement.outerHTML);
584
585
  }
586
+ if (this.doc.querySelector(`[${util.NESTING_TRACK_ID_ATTRIBUTE_NAME}]`)) {
587
+ const scriptElement = this.doc.createElement("script");
588
+ scriptElement.textContent = `(${util.getFixInvalidNestingSource()})(document, "${util.NESTING_TRACK_ID_ATTRIBUTE_NAME}");`;
589
+ this.doc.body.appendChild(scriptElement);
590
+ }
585
591
  const content = util.serialize(this.doc, this.options.compressHTML);
586
592
  if (this.options.displayStats) {
587
593
  const contentSize = util.getContentSize(content);
@@ -896,6 +902,7 @@ class Processor {
896
902
  this.doc.querySelectorAll("a[ping], area[ping]").forEach(element => element.removeAttribute("ping"));
897
903
  this.doc.querySelectorAll("a[attributionsrc], img[attributionsrc], script[attributionsrc]").forEach(element => element.removeAttribute("attributionsrc"));
898
904
  this.doc.querySelectorAll("link[rel=import][href]").forEach(element => element.remove());
905
+ this.doc.querySelectorAll("link[rel=compression-dictionary]").forEach(element => element.remove());
899
906
  }
900
907
 
901
908
  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
  });
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,12 @@ function getInstance(utilOptions) {
142
129
  return doc;
143
130
  }
144
131
  },
132
+ fixInvalidNesting(doc) {
133
+ helper.fixInvalidNesting(doc, helper.NESTING_TRACK_ID_ATTRIBUTE_NAME, true);
134
+ },
135
+ getFixInvalidNestingSource() {
136
+ return helper.fixInvalidNesting.toString().replace(/\s+/g, " ");
137
+ },
145
138
  async digest(algo, text) {
146
139
  return helper.digest(algo, text);
147
140
  },
@@ -229,7 +222,8 @@ function getInstance(utilOptions) {
229
222
  EMPTY_RESOURCE: helper.EMPTY_RESOURCE,
230
223
  INFOBAR_TAGNAME: helper.INFOBAR_TAGNAME,
231
224
  WAIT_FOR_USERSCRIPT_PROPERTY_NAME: helper.WAIT_FOR_USERSCRIPT_PROPERTY_NAME,
232
- NO_SCRIPT_PROPERTY_NAME: helper.NO_SCRIPT_PROPERTY_NAME
225
+ NO_SCRIPT_PROPERTY_NAME: helper.NO_SCRIPT_PROPERTY_NAME,
226
+ NESTING_TRACK_ID_ATTRIBUTE_NAME: helper.NESTING_TRACK_ID_ATTRIBUTE_NAME
233
227
  };
234
228
 
235
229
  async function getContent(resourceURL, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.57",
3
+ "version": "1.5.59",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -16,6 +16,6 @@
16
16
  },
17
17
  "homepage": "https://github.com/gildas-lormeau/single-file-core#readme",
18
18
  "devDependencies": {
19
- "eslint": "^9.38.0"
19
+ "eslint": "^9.39.1"
20
20
  }
21
21
  }