single-file-core 1.6.5 → 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.
@@ -414,6 +414,7 @@ class ProcessorHelperCommon {
414
414
  sheetIndex++;
415
415
  });
416
416
  processFontDetails(fontsDetails, fonts);
417
+ this.markRepeatedFontFaceRules(fontsDetails);
417
418
  await Promise.all([...stylesheets].map(async ([, stylesheetInfo], sheetIndex) => {
418
419
  if (stylesheetInfo.stylesheet) {
419
420
  const cssRules = stylesheetInfo.stylesheet.children;
@@ -431,6 +432,21 @@ class ProcessorHelperCommon {
431
432
  return stats;
432
433
  }
433
434
 
435
+ markRepeatedFontFaceRules(fontsDetails) {
436
+ const lastRules = new Map();
437
+ fontsDetails.fonts.forEach((fontInfo, ruleData) => {
438
+ const ruleKey = this.getFontKey(ruleData) + " " + this.getPropertyValue(ruleData, "src");
439
+ const previousRuleData = lastRules.get(ruleKey);
440
+ if (previousRuleData) {
441
+ fontsDetails.repeatedFonts.add(previousRuleData);
442
+ }
443
+ lastRules.set(ruleKey, ruleData);
444
+ });
445
+ fontsDetails.medias.forEach(mediaFontsDetails => this.markRepeatedFontFaceRules(mediaFontsDetails));
446
+ fontsDetails.supports.forEach(supportsFontsDetails => this.markRepeatedFontFaceRules(supportsFontsDetails));
447
+ fontsDetails.layers.forEach(layerFontsDetails => this.markRepeatedFontFaceRules(layerFontsDetails));
448
+ }
449
+
434
450
  async processFontFaceRules(cssRules, sheetIndex, fontsDetails, fonts, fontTests, stats) {
435
451
  const removedRules = [];
436
452
  let mediaIndex = 0, supportsIndex = 0, layerIndex = 0;
@@ -451,11 +467,9 @@ class ProcessorHelperCommon {
451
467
  } else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
452
468
  const fontInfo = fontsDetails.fonts.get(ruleData);
453
469
  if (fontInfo) {
454
- const ruleKey = this.getFontKey(ruleData) + " " + this.getPropertyValue(ruleData, "src");
455
- if (fontsDetails.emittedFonts.has(ruleKey)) {
470
+ if (fontsDetails.repeatedFonts.has(ruleData)) {
456
471
  removedRules.push(cssRule);
457
472
  } else {
458
- fontsDetails.emittedFonts.add(ruleKey);
459
473
  const keptRule = await this.processFontFaceRule(ruleData, fontInfo, fonts, fontTests, stats);
460
474
  if (!keptRule) {
461
475
  removedRules.push(cssRule);
@@ -469,6 +483,12 @@ class ProcessorHelperCommon {
469
483
  removedRules.forEach(cssRule => cssRules.remove(cssRule));
470
484
  }
471
485
 
486
+ groupDuplicateFonts() {
487
+ }
488
+
489
+ groupDuplicateImages() {
490
+ }
491
+
472
492
  getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails) {
473
493
  let mediaIndex = 0, supportsIndex = 0, layerIndex = 0;
474
494
  cssRules.forEach(ruleData => {
@@ -516,7 +536,7 @@ class ProcessorHelperCommon {
516
536
  medias: new Map(),
517
537
  supports: new Map(),
518
538
  layers: new Map(),
519
- emittedFonts: new Set()
539
+ repeatedFonts: new Set()
520
540
  };
521
541
  }
522
542
 
@@ -529,7 +549,11 @@ class ProcessorHelperCommon {
529
549
  getFontStretch(this.getPropertyValue(ruleData, "font-stretch")),
530
550
  this.getPropertyValue(ruleData, "font-variant") || "normal",
531
551
  this.getPropertyValue(ruleData, "font-feature-settings"),
532
- this.getPropertyValue(ruleData, "font-variation-settings")
552
+ this.getPropertyValue(ruleData, "font-variation-settings"),
553
+ this.getPropertyValue(ruleData, "ascent-override"),
554
+ this.getPropertyValue(ruleData, "descent-override"),
555
+ this.getPropertyValue(ruleData, "line-gap-override"),
556
+ this.getPropertyValue(ruleData, "size-adjust")
533
557
  ]);
534
558
  }
535
559
 
@@ -33,6 +33,13 @@ const Image = globalThis.Image;
33
33
 
34
34
  const ABOUT_BLANK_URI = "about:blank";
35
35
  const UTF8_CHARSET = "utf-8";
36
+ const DATA_URI_PREFIX = "data:";
37
+ const DEFAULT_FONT_WEIGHT = 400;
38
+ const FONT_KEY_FAMILY_INDEX = 0;
39
+ const FONT_KEY_WEIGHT_INDEX = 1;
40
+ const FONT_KEY_STYLE_INDEX = 2;
41
+ const FONT_KEY_STRETCH_INDEX = 4;
42
+ const NESTED_AT_RULE_NAMES = ["media", "supports", "layer", "container"];
36
43
  const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
37
44
  const PREFIXES_FORBIDDEN_DATA_URI = ["data:text/"];
38
45
  const SCRIPT_TAG_FOUND = /<script/gi;
@@ -65,6 +72,7 @@ import {
65
72
  resizeImage,
66
73
  toDataURI
67
74
  } from "./processor-helper-common.js";
75
+ import { getFontWeight } from "./../helper.js";
68
76
 
69
77
  export {
70
78
  getProcessorHelperClass,
@@ -119,33 +127,40 @@ function getProcessorHelperClass(utilInstance) {
119
127
  }
120
128
 
121
129
  replaceStylesheets(doc, stylesheets, options) {
122
- 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 => {
123
146
  const stylesheetInfo = stylesheets.get(element);
124
147
  if (stylesheetInfo) {
125
148
  stylesheets.delete(element);
126
149
  const stylesheetRefIndex = options.inlineStylesheetsRefs.get(element);
127
- if (stylesheetRefIndex === undefined) {
128
- element.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
129
- options.inlineStylesheets.forEach(({ styleElement }, index) => {
130
- if (styleElement === element) {
131
- options.inlineStylesheets.set(index, {
132
- styleElement,
133
- content: element.textContent
134
- });
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);
135
158
  }
136
- });
137
- } else if (options.groupDuplicateStylesheets) {
138
- if (!doc.querySelector("style[" + DUPLICATE_STYLESHEET_ATTRIBUTE_NAME + "=\"" + stylesheetRefIndex + "\"]")) {
139
- const styleElement = doc.createElement("style");
140
- styleElement.textContent = options.inlineStylesheets.get(stylesheetRefIndex).content;
141
- styleElement.setAttribute("media", "not all");
142
- styleElement.setAttribute(DUPLICATE_STYLESHEET_ATTRIBUTE_NAME, stylesheetRefIndex);
143
- 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;
144
163
  }
145
- element.textContent = "/* */";
146
- element.setAttribute("onload", "this.textContent=document.querySelector('style[" + DUPLICATE_STYLESHEET_ATTRIBUTE_NAME + "=\"" + stylesheetRefIndex + "\"]')?.textContent;this.removeAttribute('onload')");
147
- } else {
148
- element.textContent = options.inlineStylesheets.get(stylesheetRefIndex).content;
149
164
  }
150
165
  if (stylesheetInfo.mediaText) {
151
166
  element.media = stylesheetInfo.mediaText;
@@ -569,6 +584,97 @@ function getProcessorHelperClass(utilInstance) {
569
584
  doc.querySelectorAll("link[rel*=stylesheet][rel*=alternate][title]").forEach(element => element.remove());
570
585
  }
571
586
 
587
+ groupDuplicateFonts(stylesheets, fonts, options) {
588
+ if (options.usedFonts && options.usedFonts.length) {
589
+ const fontFaces = [];
590
+ stylesheets.forEach(stylesheetInfo => {
591
+ if (stylesheetInfo.stylesheet && stylesheetInfo.stylesheet.children) {
592
+ getFontFaces(stylesheetInfo.stylesheet.children, "", fontFaces);
593
+ }
594
+ });
595
+ const families = new Map();
596
+ fontFaces.forEach(fontFace => {
597
+ const fontKey = JSON.parse(this.getFontKey(fontFace.ruleData));
598
+ const familyKey = fontFace.scope + JSON.stringify([fontKey[FONT_KEY_FAMILY_INDEX],
599
+ fontKey[FONT_KEY_STYLE_INDEX], fontKey[FONT_KEY_STRETCH_INDEX]]);
600
+ let family = families.get(familyKey);
601
+ if (!family) {
602
+ family = { name: fontKey[FONT_KEY_FAMILY_INDEX], groups: new Map(), mergeable: true };
603
+ families.set(familyKey, family);
604
+ }
605
+ const source = this.getPropertyValue(fontFace.ruleData, "src");
606
+ const range = this.getFontWeightRange(fontFace.ruleData);
607
+ if (source && source.includes(DATA_URI_PREFIX) && range) {
608
+ fontKey[FONT_KEY_WEIGHT_INDEX] = null;
609
+ const groupKey = JSON.stringify(fontKey) + " " + source;
610
+ const group = family.groups.get(groupKey);
611
+ if (group) {
612
+ group.push({ fontFace, range });
613
+ } else {
614
+ family.groups.set(groupKey, [{ fontFace, range }]);
615
+ }
616
+ } else {
617
+ family.mergeable = false;
618
+ }
619
+ });
620
+ families.forEach(family => this.mergeFontFaceWeights(family, options.usedFonts));
621
+ }
622
+ }
623
+
624
+ // Every group of the family has to be merged or none of it: two groups that differ only in
625
+ // unicode-range are one composite face, and merging one of them alone would leave a weight
626
+ // matched by two faces that are no longer a composite, which CSS Fonts 4 §5.2 resolves in a way
627
+ // that "can differ between multiple user agents". Requiring every group to span the same set of
628
+ // weights is what keeps the merged faces a composite, and it also blocks the case where some
629
+ // other face of the family sits inside the interval, since that face is a group of its own.
630
+ mergeFontFaceWeights(family, usedFonts) {
631
+ const groups = Array.from(family.groups.values());
632
+ const weightKey = group => group.map(({ range }) => range.join("-")).sort().join(",");
633
+ if (family.mergeable && groups.some(group => group.length > 1) &&
634
+ groups.every(group => weightKey(group) == weightKey(groups[0]))) {
635
+ const ranges = groups[0].map(({ range }) => range);
636
+ const minWeight = Math.min(...ranges.map(([min]) => min));
637
+ const maxWeight = Math.max(...ranges.map(([, max]) => max));
638
+ const declared = weight => ranges.some(([min, max]) => weight >= min && weight <= max);
639
+ const usedWeightInRange = usedFonts.some(([usedFamily, usedWeight]) => {
640
+ const weight = Number(usedWeight);
641
+ return usedFamily == family.name && weight > minWeight && weight < maxWeight && !declared(weight);
642
+ });
643
+ if (!usedWeightInRange) {
644
+ groups.forEach(group => {
645
+ const kept = group[group.length - 1];
646
+ this.setFontWeightRange(kept.fontFace.ruleData, minWeight, maxWeight);
647
+ group.forEach(({ fontFace }) => {
648
+ if (fontFace != kept.fontFace) {
649
+ fontFace.cssRules.remove(fontFace.cssRule);
650
+ }
651
+ });
652
+ });
653
+ }
654
+ }
655
+ }
656
+
657
+ getFontWeightRange(ruleData) {
658
+ const value = this.getPropertyValue(ruleData, "font-weight");
659
+ if (value === undefined) {
660
+ return [DEFAULT_FONT_WEIGHT, DEFAULT_FONT_WEIGHT];
661
+ }
662
+ const weights = value.trim().split(/\s+/).map(weight => Number(getFontWeight(util.removeQuotes(weight))));
663
+ if (weights.length <= 2 && weights.every(weight => Number.isFinite(weight))) {
664
+ return [Math.min(...weights), Math.max(...weights)];
665
+ }
666
+ }
667
+
668
+ setFontWeightRange(ruleData, minWeight, maxWeight) {
669
+ const value = cssTree.parse(minWeight == maxWeight ? String(minWeight) : minWeight + " " + maxWeight, { context: "value" });
670
+ const declaration = ruleData.block.children.filter(node => node.property == "font-weight").tail;
671
+ if (declaration) {
672
+ declaration.data.value = value;
673
+ } else {
674
+ ruleData.block.children.appendData({ type: "Declaration", property: "font-weight", important: false, value });
675
+ }
676
+ }
677
+
572
678
  async processFontFaceRule(ruleData, fontInfo, fontDeclarations, fontTests, stats) {
573
679
  const removedNodes = [];
574
680
  for (let node = ruleData.block.children.head; node; node = node.next) {
@@ -662,4 +768,20 @@ function getProcessorHelperClass(utilInstance) {
662
768
  return true;
663
769
  }
664
770
  };
771
+ }
772
+
773
+ function getFontFaces(cssRules, scope, fontFaces) {
774
+ for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
775
+ const ruleData = cssRule.data;
776
+ if (ruleData.type == "Atrule" && ruleData.name == "import" && ruleData.prelude && ruleData.prelude.children &&
777
+ ruleData.prelude.children.head.data.importedChildren) {
778
+ getFontFaces(ruleData.prelude.children.head.data.importedChildren,
779
+ scope + "|import " + cssTree.generate(ruleData.prelude), fontFaces);
780
+ } else if (ruleData.type == "Atrule" && NESTED_AT_RULE_NAMES.includes(ruleData.name) && ruleData.block && ruleData.block.children) {
781
+ getFontFaces(ruleData.block.children,
782
+ scope + "|" + ruleData.name + " " + (ruleData.prelude ? cssTree.generate(ruleData.prelude) : ""), fontFaces);
783
+ } else if (ruleData.type == "Atrule" && ruleData.name == "font-face" && ruleData.block && ruleData.block.children) {
784
+ fontFaces.push({ ruleData, cssRule, cssRules, scope });
785
+ }
786
+ }
665
787
  }
@@ -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, content: this.generateStylesheetContent(stylesheet, options) });
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 }) => {
@@ -194,35 +212,24 @@ function getProcessorHelperClass(utilInstance) {
194
212
  if (supportsNode) {
195
213
  supportsCondition = cssTree.generate(supportsNode);
196
214
  }
197
- const existingStylesheet = Array.from(stylesheets).find(([, stylesheetInfo]) => stylesheetInfo.resourceURL == resourceURL);
198
- let stylesheet;
199
- if (existingStylesheet) {
200
- stylesheet = existingStylesheet[1].stylesheet;
201
- stylesheets.set({ urlNode }, {
202
- url: resourceURL,
203
- stylesheet,
204
- scoped
205
- });
206
- } else {
207
- const stylesheetInfo = {
208
- scoped,
209
- mediaText,
210
- layerName,
211
- supportsCondition
212
- };
213
- const requestedURL = resourceURL;
214
- const content = await this.getStylesheetContent(resourceURL, options);
215
- stylesheetInfo.url = resourceURL = content.resourceURL;
216
- content.data = getUpdatedResourceContent(resourceURL, options) || content.data;
217
- stylesheetInfo.stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
218
- stylesheet = stylesheetInfo.stylesheet;
219
- const ancestorStyleSheets = new Set(importedStyleSheets);
220
- ancestorStyleSheets.add(requestedURL);
221
- ancestorStyleSheets.add(resourceURL);
222
- await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets, ancestorStyleSheets);
223
- stylesheets.set({ urlNode }, stylesheetInfo);
224
- }
225
- urlNode.importedChildren = stylesheet.children;
215
+ const stylesheetInfo = {
216
+ scoped,
217
+ mediaText,
218
+ layerName,
219
+ supportsCondition
220
+ };
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);
223
+ const requestedURL = resourceURL;
224
+ const content = await this.getStylesheetContent(resourceURL, options);
225
+ stylesheetInfo.url = resourceURL = content.resourceURL;
226
+ content.data = getUpdatedResourceContent(resourceURL, options) || content.data;
227
+ stylesheetInfo.stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
228
+ const ancestorStyleSheets = new Set(importedStyleSheets);
229
+ ancestorStyleSheets.add(requestedURL);
230
+ ancestorStyleSheets.add(resourceURL);
231
+ await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets, ancestorStyleSheets);
232
+ urlNode.importedChildren = stylesheetInfo.stylesheet.children;
226
233
  urlNode.importedMediaText = mediaText;
227
234
  urlNode.importedLayerName = layerName;
228
235
  urlNode.importedSupportsCondition = supportsCondition;
@@ -235,36 +242,27 @@ function getProcessorHelperClass(utilInstance) {
235
242
  async resolveLinkStylesheetURLs(stylesheetInfo, element, resourceURL, baseURI, options, workStylesheet, resources, stylesheets) {
236
243
  resourceURL = normalizeURL(resourceURL);
237
244
  if (resourceURL && resourceURL != baseURI && resourceURL != ABOUT_BLANK_URI) {
238
- const existingStylesheet = Array.from(stylesheets).find(([, otherStylesheetInfo]) => otherStylesheetInfo.resourceURL == resourceURL);
239
- if (existingStylesheet) {
240
- stylesheets.set({ element }, {
241
- url: resourceURL,
242
- stylesheet: existingStylesheet[1].stylesheet,
243
- mediaText: stylesheetInfo.mediaText
244
- });
245
+ const content = await util.getContent(resourceURL, {
246
+ maxResourceSize: options.maxResourceSize,
247
+ maxResourceSizeEnabled: options.maxResourceSizeEnabled,
248
+ charset: options.charset,
249
+ frameId: options.frameId,
250
+ resourceReferrer: options.resourceReferrer,
251
+ validateTextContentType: true,
252
+ baseURI: baseURI,
253
+ blockMixedContent: options.blockMixedContent,
254
+ expectedType: "stylesheet",
255
+ acceptHeaders: options.acceptHeaders,
256
+ networkTimeout: options.networkTimeout
257
+ });
258
+ if (!(matchCharsetEquals(content.data, content.charset) || matchCharsetEquals(content.data, options.charset))) {
259
+ options = Object.assign({}, options, { charset: getCharset(content.data) });
260
+ await this.resolveLinkStylesheetURLs(stylesheetInfo, element, resourceURL, baseURI, options, workStylesheet, resources, stylesheets);
245
261
  } else {
246
- const content = await util.getContent(resourceURL, {
247
- maxResourceSize: options.maxResourceSize,
248
- maxResourceSizeEnabled: options.maxResourceSizeEnabled,
249
- charset: options.charset,
250
- frameId: options.frameId,
251
- resourceReferrer: options.resourceReferrer,
252
- validateTextContentType: true,
253
- baseURI: baseURI,
254
- blockMixedContent: options.blockMixedContent,
255
- expectedType: "stylesheet",
256
- acceptHeaders: options.acceptHeaders,
257
- networkTimeout: options.networkTimeout
258
- });
259
- if (!(matchCharsetEquals(content.data, content.charset) || matchCharsetEquals(content.data, options.charset))) {
260
- options = Object.assign({}, options, { charset: getCharset(content.data) });
261
- await this.resolveLinkStylesheetURLs(stylesheetInfo, element, resourceURL, baseURI, options, workStylesheet, resources, stylesheets);
262
- } else {
263
- resourceURL = content.resourceURL;
264
- content.data = getUpdatedResourceContent(content.resourceURL, options) || content.data;
265
- stylesheetInfo.stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
266
- await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets);
267
- }
262
+ resourceURL = content.resourceURL;
263
+ content.data = getUpdatedResourceContent(content.resourceURL, options) || content.data;
264
+ stylesheetInfo.stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
265
+ await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets);
268
266
  }
269
267
  }
270
268
  }
@@ -281,7 +279,7 @@ function getProcessorHelperClass(utilInstance) {
281
279
  } else {
282
280
  frameElement.setAttribute("src", name + "index.html");
283
281
  }
284
- 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 });
285
283
  }
286
284
 
287
285
  async processFont(resourceURL, urlNode, originalResourceURL, baseURI, options, resources, batchRequest) {
@@ -300,6 +298,69 @@ function getProcessorHelperClass(utilInstance) {
300
298
  resources.fonts.set(indexResource, { name, content, extension, contentType, url: resourceURL });
301
299
  }
302
300
 
301
+ groupDuplicateFonts(stylesheets, fonts) {
302
+ const canonicalNames = new Map();
303
+ const fontResources = [...fonts]
304
+ .filter(([, resource]) => resource.content)
305
+ .sort(([indexResource], [otherIndexResource]) => indexResource - otherIndexResource);
306
+ fontResources.forEach(([indexResource, resource], index) => {
307
+ const original = fontResources.slice(0, index).find(([, previousResource]) => testSameContent(previousResource.content, resource.content));
308
+ if (original) {
309
+ canonicalNames.set(resource.name, original[1].name);
310
+ fonts.delete(indexResource);
311
+ }
312
+ });
313
+ if (canonicalNames.size) {
314
+ stylesheets.forEach(stylesheetInfo => {
315
+ if (stylesheetInfo.stylesheet) {
316
+ getUrlFunctions(stylesheetInfo.stylesheet).forEach(urlNode => replaceResourceName(urlNode, canonicalNames));
317
+ }
318
+ });
319
+ }
320
+ }
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 => {
@@ -428,7 +489,11 @@ function getProcessorHelperClass(utilInstance) {
428
489
  Object.keys(pageResources).forEach(resourceType => {
429
490
  const unusedResources = Array.from(pageResources[resourceType]).filter(([, value]) => !textContent.includes(value.name));
430
491
  unusedResources.forEach(([indexResource]) => pageResources[resourceType].delete(indexResource));
431
- resources[resourceType] = Array.from(pageResources[resourceType].values());
492
+ const orderedResources = Array.from(pageResources[resourceType]);
493
+ if (orderedResources.every(([indexResource]) => typeof indexResource == "number")) {
494
+ orderedResources.sort(([indexResource], [otherIndexResource]) => indexResource - otherIndexResource);
495
+ }
496
+ resources[resourceType] = orderedResources.map(([, resource]) => resource);
432
497
  });
433
498
  const viewportElement = doc.head.querySelector("meta[name=viewport]");
434
499
  const viewport = viewportElement ? viewportElement.content : null;
@@ -588,4 +653,46 @@ function getProcessorHelperClass(utilInstance) {
588
653
  return true;
589
654
  }
590
655
  };
656
+ }
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
+
678
+ function testSameContent(content, otherContent) {
679
+ if (content.length != otherContent.length) {
680
+ return false;
681
+ }
682
+ for (let index = 0; index < content.length; index++) {
683
+ if (content[index] != otherContent[index]) {
684
+ return false;
685
+ }
686
+ }
687
+ return true;
688
+ }
689
+
690
+ function replaceResourceName(urlNode, canonicalNames) {
691
+ canonicalNames.forEach((canonicalName, name) => {
692
+ if (urlNode.value == name) {
693
+ urlNode.value = canonicalName;
694
+ } else if (urlNode.value.endsWith(" " + name)) {
695
+ urlNode.value = urlNode.value.substring(0, urlNode.value.length - name.length) + canonicalName;
696
+ }
697
+ });
591
698
  }