single-file-core 1.6.6 → 1.6.7

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/index.js CHANGED
@@ -146,7 +146,8 @@ const STAGES = [{
146
146
  }, {
147
147
  sequential: [
148
148
  { option: "removeAlternativeImages", action: "removeAlternativeImages" },
149
- { action: "groupDuplicateFonts" }
149
+ { action: "groupDuplicateFonts" },
150
+ { action: "groupDuplicateImages" }
150
151
  ],
151
152
  parallel: [
152
153
  { option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
@@ -437,6 +438,7 @@ const SCRIPT_URI_ATTRIBUTE_NAMES = ["href", "src", "action", "formaction", "data
437
438
  const UTF8_CHARSET = "utf-8";
438
439
  const TAINTED_CANVAS_WARNING_MESSAGE = "SingleFile: canvas elements tainted by a cross-origin resource, dropped from the page:";
439
440
  const EMPTY_RESOURCE = "data:,";
441
+ const ORDER_DEPENDENT_RULE = /@(layer|import)\b/i;
440
442
 
441
443
  class Processor {
442
444
  constructor(options, processorHelper, batchRequest) {
@@ -1274,23 +1276,32 @@ class Processor {
1274
1276
  }
1275
1277
 
1276
1278
  async resolveStylesheetsURLs() {
1277
- const stylesContents = [];
1279
+ const styleElementGroups = new Map();
1278
1280
  this.options.inlineStylesheets = new Map();
1279
1281
  this.options.inlineStylesheetsRefs = new Map();
1280
1282
  this.doc.querySelectorAll("style").forEach(styleElement => {
1281
- if (styleElement.textContent) {
1282
- const indexContent = stylesContents.indexOf(styleElement.textContent);
1283
- if (indexContent == -1) {
1284
- this.options.inlineStylesheets.set(stylesContents.length, {
1285
- styleElement,
1286
- content: styleElement.textContent
1287
- });
1288
- stylesContents.push(styleElement.textContent);
1289
- } else {
1290
- this.options.inlineStylesheetsRefs.set(styleElement, indexContent);
1283
+ const content = styleElement.textContent;
1284
+ if (content) {
1285
+ if (!styleElementGroups.has(content)) {
1286
+ styleElementGroups.set(content, []);
1291
1287
  }
1288
+ styleElementGroups.get(content).push(styleElement);
1292
1289
  }
1293
1290
  });
1291
+ let indexContent = 0;
1292
+ styleElementGroups.forEach((styleElements, content) => {
1293
+ const masterIndex = ORDER_DEPENDENT_RULE.test(content) ? 0 : styleElements.length - 1;
1294
+ this.options.inlineStylesheets.set(indexContent, {
1295
+ styleElement: styleElements[masterIndex],
1296
+ content
1297
+ });
1298
+ styleElements.forEach((styleElement, index) => {
1299
+ if (index != masterIndex) {
1300
+ this.options.inlineStylesheetsRefs.set(styleElement, indexContent);
1301
+ }
1302
+ });
1303
+ indexContent++;
1304
+ });
1294
1305
  await Promise.all(Array.from(this.doc.querySelectorAll("style, link[rel*=stylesheet]:not([disabled])")).map(async element => {
1295
1306
  const options = Object.assign({}, this.options, { charset: this.charset });
1296
1307
  let mediaText;
@@ -1556,6 +1567,10 @@ class Processor {
1556
1567
  this.processorHelper.groupDuplicateFonts(this.stylesheets, this.resources.fonts, this.options);
1557
1568
  }
1558
1569
 
1570
+ groupDuplicateImages() {
1571
+ this.processorHelper.groupDuplicateImages(this.doc, this.stylesheets, this.styles, this.resources.images);
1572
+ }
1573
+
1559
1574
  async removeAlternativeFonts() {
1560
1575
  await this.processorHelper.removeAlternativeFonts(this.doc, this.stylesheets, this.resources.fonts, this.options.fontTests);
1561
1576
  }
@@ -486,6 +486,9 @@ class ProcessorHelperCommon {
486
486
  groupDuplicateFonts() {
487
487
  }
488
488
 
489
+ groupDuplicateImages() {
490
+ }
491
+
489
492
  getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails) {
490
493
  let mediaIndex = 0, supportsIndex = 0, layerIndex = 0;
491
494
  cssRules.forEach(ruleData => {
@@ -127,33 +127,40 @@ function getProcessorHelperClass(utilInstance) {
127
127
  }
128
128
 
129
129
  replaceStylesheets(doc, stylesheets, options) {
130
- doc.querySelectorAll("style").forEach(element => {
130
+ const styleElements = Array.from(doc.querySelectorAll("style"));
131
+ styleElements.forEach(element => {
132
+ const stylesheetInfo = stylesheets.get(element);
133
+ if (stylesheetInfo && !options.inlineStylesheetsRefs.has(element)) {
134
+ element.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
135
+ options.inlineStylesheets.forEach(({ styleElement }, index) => {
136
+ if (styleElement === element) {
137
+ options.inlineStylesheets.set(index, {
138
+ styleElement,
139
+ content: element.textContent
140
+ });
141
+ }
142
+ });
143
+ }
144
+ });
145
+ styleElements.forEach(element => {
131
146
  const stylesheetInfo = stylesheets.get(element);
132
147
  if (stylesheetInfo) {
133
148
  stylesheets.delete(element);
134
149
  const stylesheetRefIndex = options.inlineStylesheetsRefs.get(element);
135
- if (stylesheetRefIndex === undefined) {
136
- element.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
137
- options.inlineStylesheets.forEach(({ styleElement }, index) => {
138
- if (styleElement === element) {
139
- options.inlineStylesheets.set(index, {
140
- styleElement,
141
- content: element.textContent
142
- });
150
+ if (stylesheetRefIndex !== undefined) {
151
+ if (options.groupDuplicateStylesheets) {
152
+ if (!doc.querySelector("style[" + DUPLICATE_STYLESHEET_ATTRIBUTE_NAME + "=\"" + stylesheetRefIndex + "\"]")) {
153
+ const styleElement = doc.createElement("style");
154
+ styleElement.textContent = options.inlineStylesheets.get(stylesheetRefIndex).content;
155
+ styleElement.setAttribute("media", "not all");
156
+ styleElement.setAttribute(DUPLICATE_STYLESHEET_ATTRIBUTE_NAME, stylesheetRefIndex);
157
+ doc.head.appendChild(styleElement);
143
158
  }
144
- });
145
- } else if (options.groupDuplicateStylesheets) {
146
- if (!doc.querySelector("style[" + DUPLICATE_STYLESHEET_ATTRIBUTE_NAME + "=\"" + stylesheetRefIndex + "\"]")) {
147
- const styleElement = doc.createElement("style");
148
- styleElement.textContent = options.inlineStylesheets.get(stylesheetRefIndex).content;
149
- styleElement.setAttribute("media", "not all");
150
- styleElement.setAttribute(DUPLICATE_STYLESHEET_ATTRIBUTE_NAME, stylesheetRefIndex);
151
- doc.head.appendChild(styleElement);
159
+ element.textContent = "/* */";
160
+ element.setAttribute("onload", "this.textContent=document.querySelector('style[" + DUPLICATE_STYLESHEET_ATTRIBUTE_NAME + "=\"" + stylesheetRefIndex + "\"]')?.textContent;this.removeAttribute('onload')");
161
+ } else {
162
+ element.textContent = options.inlineStylesheets.get(stylesheetRefIndex).content;
152
163
  }
153
- element.textContent = "/* */";
154
- element.setAttribute("onload", "this.textContent=document.querySelector('style[" + DUPLICATE_STYLESHEET_ATTRIBUTE_NAME + "=\"" + stylesheetRefIndex + "\"]')?.textContent;this.removeAttribute('onload')");
155
- } else {
156
- element.textContent = options.inlineStylesheets.get(stylesheetRefIndex).content;
157
164
  }
158
165
  if (stylesheetInfo.mediaText) {
159
166
  element.media = stylesheetInfo.mediaText;
@@ -38,6 +38,9 @@ const LOCAL_SOURCE = "local(";
38
38
  const FONT_MAX_LOAD_DELAY = 5000;
39
39
  const SCRIPT_EXTENSION = ".js";
40
40
  const LINK_OWN_ATTRIBUTE_NAMES = ["rel", "type", "href", "media"];
41
+ const SRCSET_ATTRIBUTE_NAME = "srcset";
42
+ const IMAGE_ATTRIBUTE_NAMES = ["src", "href", "data", "poster", "background", "xlink:href", SRCSET_ATTRIBUTE_NAME];
43
+ const IMAGE_ATTRIBUTE_SELECTOR = "[src], [href], [data], [poster], [background], [srcset], image, feImage";
41
44
 
42
45
  let util;
43
46
 
@@ -103,36 +106,49 @@ function getProcessorHelperClass(utilInstance) {
103
106
  const entries = Array.from(stylesheets).reverse();
104
107
  const linkElements = new Map();
105
108
  const sharedStyleElements = new Map();
109
+ const emptyStylesheetRefIndexes = new Set();
106
110
  Array.from(new Set(options.inlineStylesheetsRefs.values())).forEach(stylesheetRefIndex => {
107
- const linkElement = doc.createElement("link");
108
- linkElement.setAttribute("rel", "stylesheet");
109
- linkElement.setAttribute("type", "text/css");
110
- const name = "stylesheet_" + resources.stylesheets.size + ".css";
111
- linkElement.setAttribute("href", name);
112
111
  const { styleElement, content } = options.inlineStylesheets.get(stylesheetRefIndex);
113
112
  const sharedEntry = entries.find(([key]) => key.element == styleElement);
114
113
  const stylesheet = sharedEntry
115
114
  ? sharedEntry[1].stylesheet
116
115
  : cssTree.parse(content, { context: "stylesheet", parseCustomProperty: true });
117
- resources.stylesheets.set(resources.stylesheets.size, { name, stylesheet });
118
- linkElements.set(stylesheetRefIndex, linkElement);
116
+ if (isEmptyStylesheet(stylesheet)) {
117
+ emptyStylesheetRefIndexes.add(stylesheetRefIndex);
118
+ } else {
119
+ const linkElement = doc.createElement("link");
120
+ linkElement.setAttribute("rel", "stylesheet");
121
+ linkElement.setAttribute("type", "text/css");
122
+ const name = "stylesheet_" + resources.stylesheets.size + ".css";
123
+ linkElement.setAttribute("href", name);
124
+ resources.stylesheets.set(resources.stylesheets.size, { name, stylesheet });
125
+ linkElements.set(stylesheetRefIndex, linkElement);
126
+ }
119
127
  sharedStyleElements.set(styleElement, stylesheetRefIndex);
120
128
  });
121
129
  for (const [key, stylesheetInfo] of entries) {
122
130
  if (key.urlNode) {
123
- const name = "stylesheet_" + resources.stylesheets.size + ".css";
124
- if (!isDataURL(stylesheetInfo.url) && options.saveOriginalURLs) {
125
- key.urlNode.type = "Url";
126
- key.urlNode.value = "-sf-url-original(" + JSON.stringify(stylesheetInfo.url) + ") " + name;
131
+ if (isEmptyStylesheet(stylesheetInfo.stylesheet) && !key.importDeclaresLayer) {
132
+ removeImportRule(key.importParent, key.importNode);
127
133
  } else {
128
- key.urlNode.value = name;
134
+ const name = "stylesheet_" + resources.stylesheets.size + ".css";
135
+ if (!isDataURL(stylesheetInfo.url) && options.saveOriginalURLs) {
136
+ key.urlNode.type = "Url";
137
+ key.urlNode.value = "-sf-url-original(" + JSON.stringify(stylesheetInfo.url) + ") " + name;
138
+ } else {
139
+ key.urlNode.value = name;
140
+ }
141
+ resources.stylesheets.set(resources.stylesheets.size, { name, stylesheet: stylesheetInfo.stylesheet, url: stylesheetInfo.url });
129
142
  }
130
- resources.stylesheets.set(resources.stylesheets.size, { name, stylesheet: stylesheetInfo.stylesheet, url: stylesheetInfo.url });
131
143
  } else if (key.element.tagName.toUpperCase() == "LINK") {
132
144
  const linkElement = key.element;
133
- const name = "stylesheet_" + resources.stylesheets.size + ".css";
134
- linkElement.setAttribute("href", name);
135
- resources.stylesheets.set(resources.stylesheets.size, { name, stylesheet: stylesheetInfo.stylesheet, url: stylesheetInfo.url });
145
+ if (isEmptyStylesheet(stylesheetInfo.stylesheet)) {
146
+ linkElement.remove();
147
+ } else {
148
+ const name = "stylesheet_" + resources.stylesheets.size + ".css";
149
+ linkElement.setAttribute("href", name);
150
+ resources.stylesheets.set(resources.stylesheets.size, { name, stylesheet: stylesheetInfo.stylesheet, url: stylesheetInfo.url });
151
+ }
136
152
  } else {
137
153
  const styleElement = key.element;
138
154
  const stylesheetRefIndex = options.inlineStylesheetsRefs.has(styleElement)
@@ -140,6 +156,8 @@ function getProcessorHelperClass(utilInstance) {
140
156
  : sharedStyleElements.get(styleElement);
141
157
  if (stylesheetRefIndex === undefined) {
142
158
  styleElement.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
159
+ } else if (emptyStylesheetRefIndexes.has(stylesheetRefIndex)) {
160
+ styleElement.remove();
143
161
  } else {
144
162
  const linkElement = linkElements.get(stylesheetRefIndex).cloneNode(true);
145
163
  Array.from(styleElement.attributes).forEach(({ name, value }) => {
@@ -200,7 +218,8 @@ function getProcessorHelperClass(utilInstance) {
200
218
  layerName,
201
219
  supportsCondition
202
220
  };
203
- stylesheets.set({ urlNode }, stylesheetInfo);
221
+ const importDeclaresLayer = Boolean(cssTree.find(node, node => node.type == "Layer" || ((node.type == "Identifier" || node.type == "Function") && node.name.toLowerCase() == "layer")));
222
+ stylesheets.set({ urlNode, importNode: node, importParent: stylesheet, importDeclaresLayer }, stylesheetInfo);
204
223
  const requestedURL = resourceURL;
205
224
  const content = await this.getStylesheetContent(resourceURL, options);
206
225
  stylesheetInfo.url = resourceURL = content.resourceURL;
@@ -260,7 +279,7 @@ function getProcessorHelperClass(utilInstance) {
260
279
  } else {
261
280
  frameElement.setAttribute("src", name + "index.html");
262
281
  }
263
- resources.frames.set(frameWindowId, { name, content: pageData.content, resources: pageData.resources, url: frameData.url });
282
+ resources.frames.set(frameWindowId, { name, content: pageData.content, resources: pageData.resources, url: frameData.url, title: pageData.title });
264
283
  }
265
284
 
266
285
  async processFont(resourceURL, urlNode, originalResourceURL, baseURI, options, resources, batchRequest) {
@@ -294,12 +313,54 @@ function getProcessorHelperClass(utilInstance) {
294
313
  if (canonicalNames.size) {
295
314
  stylesheets.forEach(stylesheetInfo => {
296
315
  if (stylesheetInfo.stylesheet) {
297
- getUrlFunctions(stylesheetInfo.stylesheet).forEach(urlNode => replaceFontName(urlNode, canonicalNames));
316
+ getUrlFunctions(stylesheetInfo.stylesheet).forEach(urlNode => replaceResourceName(urlNode, canonicalNames));
298
317
  }
299
318
  });
300
319
  }
301
320
  }
302
321
 
322
+ groupDuplicateImages(doc, stylesheets, styles, images) {
323
+ const canonicalNames = new Map();
324
+ const resourcesBySize = new Map();
325
+ [...images]
326
+ .filter(([, resource]) => resource.content && resource.content.length)
327
+ .sort(([indexResource], [otherIndexResource]) => indexResource - otherIndexResource)
328
+ .forEach(([indexResource, resource]) => {
329
+ const sameSizeResources = resourcesBySize.get(resource.content.length) || [];
330
+ const original = sameSizeResources.find(previousResource => testSameContent(previousResource.content, resource.content));
331
+ if (original) {
332
+ canonicalNames.set(resource.name, original.name);
333
+ images.delete(indexResource);
334
+ } else {
335
+ sameSizeResources.push(resource);
336
+ resourcesBySize.set(resource.content.length, sameSizeResources);
337
+ }
338
+ });
339
+ if (canonicalNames.size) {
340
+ stylesheets.forEach(stylesheetInfo => {
341
+ if (stylesheetInfo.stylesheet) {
342
+ getUrlFunctions(stylesheetInfo.stylesheet).forEach(urlNode => replaceResourceName(urlNode, canonicalNames));
343
+ }
344
+ });
345
+ styles.forEach(declarationList => getUrlFunctions(declarationList).forEach(urlNode => replaceResourceName(urlNode, canonicalNames)));
346
+ doc.querySelectorAll(IMAGE_ATTRIBUTE_SELECTOR).forEach(element => {
347
+ IMAGE_ATTRIBUTE_NAMES.forEach(attributeName => {
348
+ const value = element.getAttribute(attributeName);
349
+ if (value !== null) {
350
+ if (canonicalNames.has(value)) {
351
+ element.setAttribute(attributeName, canonicalNames.get(value));
352
+ } else if (attributeName == SRCSET_ATTRIBUTE_NAME) {
353
+ const srcset = util.parseSrcset(value);
354
+ if (srcset.some(srcsetValue => canonicalNames.has(srcsetValue.url))) {
355
+ element.setAttribute(attributeName, serializeSrcset(srcset.map(srcsetValue => Object.assign({}, srcsetValue, { url: canonicalNames.get(srcsetValue.url) || srcsetValue.url }))));
356
+ }
357
+ }
358
+ }
359
+ });
360
+ });
361
+ }
362
+ }
363
+
303
364
  async processStyle(ruleData, options, resources, batchRequest) {
304
365
  const urls = getUrlFunctions(ruleData);
305
366
  await Promise.all(urls.map(async urlNode => {
@@ -594,6 +655,26 @@ function getProcessorHelperClass(utilInstance) {
594
655
  };
595
656
  }
596
657
 
658
+ function isEmptyStylesheet(stylesheet) {
659
+ return !stylesheet || !stylesheet.children || !stylesheet.children.size;
660
+ }
661
+
662
+ function removeImportRule(parentStylesheet, importNode) {
663
+ let importItem, importList;
664
+ cssTree.walk(parentStylesheet, {
665
+ visit: "Atrule",
666
+ enter(node, item, list) {
667
+ if (node === importNode) {
668
+ importItem = item;
669
+ importList = list;
670
+ }
671
+ }
672
+ });
673
+ if (importItem) {
674
+ importList.remove(importItem);
675
+ }
676
+ }
677
+
597
678
  function testSameContent(content, otherContent) {
598
679
  if (content.length != otherContent.length) {
599
680
  return false;
@@ -606,7 +687,7 @@ function testSameContent(content, otherContent) {
606
687
  return true;
607
688
  }
608
689
 
609
- function replaceFontName(urlNode, canonicalNames) {
690
+ function replaceResourceName(urlNode, canonicalNames) {
610
691
  canonicalNames.forEach((canonicalName, name) => {
611
692
  if (urlNode.value == name) {
612
693
  urlNode.value = canonicalName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.6.6",
3
+ "version": "1.6.7",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",