single-file-core 1.6.4 → 1.6.6

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/infobar.js CHANGED
@@ -68,26 +68,6 @@ const INFOBAR_STYLES = `
68
68
  border-color: #eee;
69
69
  border-radius: 16px;
70
70
  z-index: 2147483647;
71
- animation-name: flash;
72
- animation-duration: .5s;
73
- animation-timing-function: cubic-bezier(0.39, 0.58, 0.57, 1);
74
- animation-delay: 1s;
75
- animation-iteration-count: 2;
76
- }
77
-
78
- .infobar:not(:focus-within):not(.infobar-focus)::after {
79
- content: "";
80
- position: absolute;
81
- inset: -2px;
82
- border: 2px solid #dd6a00;
83
- border-radius: inherit;
84
- opacity: 0;
85
- pointer-events: none;
86
- animation-name: ripple;
87
- animation-duration: 3s;
88
- animation-timing-function: ease-out;
89
- animation-delay: 2s;
90
- animation-iteration-count: 3;
91
71
  }
92
72
 
93
73
  .infobar:valid, .infobar:not(:focus-within):not(.infobar-focus) .infobar-content {
@@ -139,33 +119,6 @@ const INFOBAR_STYLES = `
139
119
  min-block-size: 24px;
140
120
  }
141
121
 
142
- @keyframes flash {
143
- 0%, 100% {
144
- background-color: #737373;
145
- }
146
- 50% {
147
- background-color: #dd6a00;
148
- }
149
- }
150
-
151
- @keyframes ripple {
152
- 0% {
153
- transform: scale(1);
154
- opacity: 1;
155
- }
156
- 45%, 100% {
157
- transform: scale(2);
158
- opacity: 0;
159
- }
160
- }
161
-
162
- @media (prefers-reduced-motion: reduce) {
163
- .infobar,
164
- .infobar:not(:focus-within):not(.infobar-focus)::after {
165
- animation-name: none;
166
- }
167
- }
168
-
169
122
  .infobar:focus-within .infobar-icon, .infobar.infobar-focus .infobar-icon {
170
123
  z-index: -1;
171
124
  background-image: none;
@@ -195,6 +148,57 @@ const INFOBAR_STYLES = `
195
148
  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAABhmlDQ1BJQ0MgcHJvZmlsZQAAKJF9kj1Iw0AYht+mSkUrDnYQcchQnSyIijqWKhbBQmkrtOpgcukfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfEydFJ0UVK/C4ptIjx4LiH9+59+e67A4RGhalm1wSgapaRisfEbG5VDLyiDwEAvZiVmKkn0osZeI6ve/j4ehfhWd7n/hz9St5kgE8kjjLdsIg3iGc2LZ3zPnGIlSSF+Jx43KACiR+5Lrv8xrnosMAzQ0YmNU8cIhaLHSx3MCsZKvE0cVhRNcoXsi4rnLc4q5Uaa9XJbxjMaytprtMcQRxLSCAJETJqKKMCCxFaNVJMpGg/5uEfdvxJcsnkKoORYwFVqJAcP/gb/O6tWZiadJOCMaD7xbY/RoHALtCs2/b3sW03TwD/M3Cltf3VBjD3SXq9rYWPgIFt4OK6rcl7wOUOMPSkS4bkSH6aQqEAvJ/RM+WAwVv6EGtu31r7OH0AMtSr5Rvg4BAYK1L2use9ezr79u+ZVv9+AFlNcp0UUpiqAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH5AsHAB8VC4EQ6QAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAJUExURQAAAICHi4qKioTuJAkAAAABdFJOUwBA5thmAAAAAWJLR0QCZgt8ZAAAAJtJREFUOI3NkrsBgCAMRLFwBPdxBArcfxXFkO8rbKWAAJfHJ9faf9vuYX/749T5NmShm3bEwbe2SxeuM4+2oxDL1cDoKtVUjRy+tH78Cv2CS+wIiQNC1AEhk4AQeUTMWUJMfUJMSEJMSEY8kIx4IONroaYAimNxsXp1PA7PxwfVL8QnowwoVC0lig07wDDVUjAdbAnjwtow/z/bDW7eI4M2KruJAAAAAElFTkSuQmCC);
196
149
  }
197
150
  `;
151
+ const INFOBAR_ANIMATIONS_STYLES = `
152
+ .infobar {
153
+ animation-name: flash;
154
+ animation-duration: .5s;
155
+ animation-timing-function: cubic-bezier(0.39, 0.58, 0.57, 1);
156
+ animation-delay: 1s;
157
+ animation-iteration-count: 2;
158
+ }
159
+
160
+ .infobar:not(:focus-within):not(.infobar-focus)::after {
161
+ content: "";
162
+ position: absolute;
163
+ inset: -2px;
164
+ border: 2px solid #dd6a00;
165
+ border-radius: inherit;
166
+ opacity: 0;
167
+ pointer-events: none;
168
+ animation-name: ripple;
169
+ animation-duration: 3s;
170
+ animation-timing-function: ease-out;
171
+ animation-delay: 2s;
172
+ animation-iteration-count: 3;
173
+ }
174
+
175
+ @keyframes flash {
176
+ 0%, 100% {
177
+ background-color: #737373;
178
+ }
179
+ 50% {
180
+ background-color: #dd6a00;
181
+ }
182
+ }
183
+
184
+ @keyframes ripple {
185
+ 0% {
186
+ transform: scale(1);
187
+ opacity: 1;
188
+ }
189
+ 45%, 100% {
190
+ transform: scale(2);
191
+ opacity: 0;
192
+ }
193
+ }
194
+
195
+ @media (prefers-reduced-motion: reduce) {
196
+ .infobar,
197
+ .infobar:not(:focus-within):not(.infobar-focus)::after {
198
+ animation-name: none;
199
+ }
200
+ }
201
+ `;
198
202
 
199
203
  export { displayIcon, appendInfobar, refreshInfobarInfo, extractInfobarData, INFOBAR_TAGNAME };
200
204
 
@@ -221,6 +225,9 @@ function appendInfobar(doc, options, useShadowRoot) {
221
225
  const shadowRootContent = doc.createElement("div");
222
226
  const styleElement = doc.createElement("style");
223
227
  styleElement.textContent = INFOBAR_STYLES;
228
+ if (options.animateInfobar) {
229
+ styleElement.textContent += INFOBAR_ANIMATIONS_STYLES;
230
+ }
224
231
  if (options.infobarPositionAbsolute) {
225
232
  styleElement.textContent += ".infobar { position: absolute; }";
226
233
  const parentElementStyle = getComputedStyle(parentElement);
@@ -332,6 +339,7 @@ function displayIcon(doc, useShadowRoot, options = {}) {
332
339
  const infoData = extractInfobarData(doc);
333
340
  if (infoData.saveUrl) {
334
341
  infoData.openInfobar = options.openInfobar;
342
+ infoData.animateInfobar = options.animateInfobar;
335
343
  infoData.infobarPositionAbsolute = options.infobarPositionAbsolute;
336
344
  infoData.infobarPositionTop = options.infobarPositionTop;
337
345
  infoData.infobarPositionRight = options.infobarPositionRight;
@@ -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,9 @@ class ProcessorHelperCommon {
469
483
  removedRules.forEach(cssRule => cssRules.remove(cssRule));
470
484
  }
471
485
 
486
+ groupDuplicateFonts() {
487
+ }
488
+
472
489
  getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails) {
473
490
  let mediaIndex = 0, supportsIndex = 0, layerIndex = 0;
474
491
  cssRules.forEach(ruleData => {
@@ -516,7 +533,7 @@ class ProcessorHelperCommon {
516
533
  medias: new Map(),
517
534
  supports: new Map(),
518
535
  layers: new Map(),
519
- emittedFonts: new Set()
536
+ repeatedFonts: new Set()
520
537
  };
521
538
  }
522
539
 
@@ -529,7 +546,11 @@ class ProcessorHelperCommon {
529
546
  getFontStretch(this.getPropertyValue(ruleData, "font-stretch")),
530
547
  this.getPropertyValue(ruleData, "font-variant") || "normal",
531
548
  this.getPropertyValue(ruleData, "font-feature-settings"),
532
- this.getPropertyValue(ruleData, "font-variation-settings")
549
+ this.getPropertyValue(ruleData, "font-variation-settings"),
550
+ this.getPropertyValue(ruleData, "ascent-override"),
551
+ this.getPropertyValue(ruleData, "descent-override"),
552
+ this.getPropertyValue(ruleData, "line-gap-override"),
553
+ this.getPropertyValue(ruleData, "size-adjust")
533
554
  ]);
534
555
  }
535
556
 
@@ -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,
@@ -569,6 +577,97 @@ function getProcessorHelperClass(utilInstance) {
569
577
  doc.querySelectorAll("link[rel*=stylesheet][rel*=alternate][title]").forEach(element => element.remove());
570
578
  }
571
579
 
580
+ groupDuplicateFonts(stylesheets, fonts, options) {
581
+ if (options.usedFonts && options.usedFonts.length) {
582
+ const fontFaces = [];
583
+ stylesheets.forEach(stylesheetInfo => {
584
+ if (stylesheetInfo.stylesheet && stylesheetInfo.stylesheet.children) {
585
+ getFontFaces(stylesheetInfo.stylesheet.children, "", fontFaces);
586
+ }
587
+ });
588
+ const families = new Map();
589
+ fontFaces.forEach(fontFace => {
590
+ const fontKey = JSON.parse(this.getFontKey(fontFace.ruleData));
591
+ const familyKey = fontFace.scope + JSON.stringify([fontKey[FONT_KEY_FAMILY_INDEX],
592
+ fontKey[FONT_KEY_STYLE_INDEX], fontKey[FONT_KEY_STRETCH_INDEX]]);
593
+ let family = families.get(familyKey);
594
+ if (!family) {
595
+ family = { name: fontKey[FONT_KEY_FAMILY_INDEX], groups: new Map(), mergeable: true };
596
+ families.set(familyKey, family);
597
+ }
598
+ const source = this.getPropertyValue(fontFace.ruleData, "src");
599
+ const range = this.getFontWeightRange(fontFace.ruleData);
600
+ if (source && source.includes(DATA_URI_PREFIX) && range) {
601
+ fontKey[FONT_KEY_WEIGHT_INDEX] = null;
602
+ const groupKey = JSON.stringify(fontKey) + " " + source;
603
+ const group = family.groups.get(groupKey);
604
+ if (group) {
605
+ group.push({ fontFace, range });
606
+ } else {
607
+ family.groups.set(groupKey, [{ fontFace, range }]);
608
+ }
609
+ } else {
610
+ family.mergeable = false;
611
+ }
612
+ });
613
+ families.forEach(family => this.mergeFontFaceWeights(family, options.usedFonts));
614
+ }
615
+ }
616
+
617
+ // Every group of the family has to be merged or none of it: two groups that differ only in
618
+ // unicode-range are one composite face, and merging one of them alone would leave a weight
619
+ // matched by two faces that are no longer a composite, which CSS Fonts 4 §5.2 resolves in a way
620
+ // that "can differ between multiple user agents". Requiring every group to span the same set of
621
+ // weights is what keeps the merged faces a composite, and it also blocks the case where some
622
+ // other face of the family sits inside the interval, since that face is a group of its own.
623
+ mergeFontFaceWeights(family, usedFonts) {
624
+ const groups = Array.from(family.groups.values());
625
+ const weightKey = group => group.map(({ range }) => range.join("-")).sort().join(",");
626
+ if (family.mergeable && groups.some(group => group.length > 1) &&
627
+ groups.every(group => weightKey(group) == weightKey(groups[0]))) {
628
+ const ranges = groups[0].map(({ range }) => range);
629
+ const minWeight = Math.min(...ranges.map(([min]) => min));
630
+ const maxWeight = Math.max(...ranges.map(([, max]) => max));
631
+ const declared = weight => ranges.some(([min, max]) => weight >= min && weight <= max);
632
+ const usedWeightInRange = usedFonts.some(([usedFamily, usedWeight]) => {
633
+ const weight = Number(usedWeight);
634
+ return usedFamily == family.name && weight > minWeight && weight < maxWeight && !declared(weight);
635
+ });
636
+ if (!usedWeightInRange) {
637
+ groups.forEach(group => {
638
+ const kept = group[group.length - 1];
639
+ this.setFontWeightRange(kept.fontFace.ruleData, minWeight, maxWeight);
640
+ group.forEach(({ fontFace }) => {
641
+ if (fontFace != kept.fontFace) {
642
+ fontFace.cssRules.remove(fontFace.cssRule);
643
+ }
644
+ });
645
+ });
646
+ }
647
+ }
648
+ }
649
+
650
+ getFontWeightRange(ruleData) {
651
+ const value = this.getPropertyValue(ruleData, "font-weight");
652
+ if (value === undefined) {
653
+ return [DEFAULT_FONT_WEIGHT, DEFAULT_FONT_WEIGHT];
654
+ }
655
+ const weights = value.trim().split(/\s+/).map(weight => Number(getFontWeight(util.removeQuotes(weight))));
656
+ if (weights.length <= 2 && weights.every(weight => Number.isFinite(weight))) {
657
+ return [Math.min(...weights), Math.max(...weights)];
658
+ }
659
+ }
660
+
661
+ setFontWeightRange(ruleData, minWeight, maxWeight) {
662
+ const value = cssTree.parse(minWeight == maxWeight ? String(minWeight) : minWeight + " " + maxWeight, { context: "value" });
663
+ const declaration = ruleData.block.children.filter(node => node.property == "font-weight").tail;
664
+ if (declaration) {
665
+ declaration.data.value = value;
666
+ } else {
667
+ ruleData.block.children.appendData({ type: "Declaration", property: "font-weight", important: false, value });
668
+ }
669
+ }
670
+
572
671
  async processFontFaceRule(ruleData, fontInfo, fontDeclarations, fontTests, stats) {
573
672
  const removedNodes = [];
574
673
  for (let node = ruleData.block.children.head; node; node = node.next) {
@@ -662,4 +761,20 @@ function getProcessorHelperClass(utilInstance) {
662
761
  return true;
663
762
  }
664
763
  };
764
+ }
765
+
766
+ function getFontFaces(cssRules, scope, fontFaces) {
767
+ for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
768
+ const ruleData = cssRule.data;
769
+ if (ruleData.type == "Atrule" && ruleData.name == "import" && ruleData.prelude && ruleData.prelude.children &&
770
+ ruleData.prelude.children.head.data.importedChildren) {
771
+ getFontFaces(ruleData.prelude.children.head.data.importedChildren,
772
+ scope + "|import " + cssTree.generate(ruleData.prelude), fontFaces);
773
+ } else if (ruleData.type == "Atrule" && NESTED_AT_RULE_NAMES.includes(ruleData.name) && ruleData.block && ruleData.block.children) {
774
+ getFontFaces(ruleData.block.children,
775
+ scope + "|" + ruleData.name + " " + (ruleData.prelude ? cssTree.generate(ruleData.prelude) : ""), fontFaces);
776
+ } else if (ruleData.type == "Atrule" && ruleData.name == "font-face" && ruleData.block && ruleData.block.children) {
777
+ fontFaces.push({ ruleData, cssRule, cssRules, scope });
778
+ }
779
+ }
665
780
  }
@@ -114,7 +114,7 @@ function getProcessorHelperClass(utilInstance) {
114
114
  const stylesheet = sharedEntry
115
115
  ? sharedEntry[1].stylesheet
116
116
  : cssTree.parse(content, { context: "stylesheet", parseCustomProperty: true });
117
- resources.stylesheets.set(resources.stylesheets.size, { name, content: this.generateStylesheetContent(stylesheet, options) });
117
+ resources.stylesheets.set(resources.stylesheets.size, { name, stylesheet });
118
118
  linkElements.set(stylesheetRefIndex, linkElement);
119
119
  sharedStyleElements.set(styleElement, stylesheetRefIndex);
120
120
  });
@@ -194,35 +194,23 @@ function getProcessorHelperClass(utilInstance) {
194
194
  if (supportsNode) {
195
195
  supportsCondition = cssTree.generate(supportsNode);
196
196
  }
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;
197
+ const stylesheetInfo = {
198
+ scoped,
199
+ mediaText,
200
+ layerName,
201
+ supportsCondition
202
+ };
203
+ stylesheets.set({ urlNode }, stylesheetInfo);
204
+ const requestedURL = resourceURL;
205
+ const content = await this.getStylesheetContent(resourceURL, options);
206
+ stylesheetInfo.url = resourceURL = content.resourceURL;
207
+ content.data = getUpdatedResourceContent(resourceURL, options) || content.data;
208
+ stylesheetInfo.stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
209
+ const ancestorStyleSheets = new Set(importedStyleSheets);
210
+ ancestorStyleSheets.add(requestedURL);
211
+ ancestorStyleSheets.add(resourceURL);
212
+ await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets, ancestorStyleSheets);
213
+ urlNode.importedChildren = stylesheetInfo.stylesheet.children;
226
214
  urlNode.importedMediaText = mediaText;
227
215
  urlNode.importedLayerName = layerName;
228
216
  urlNode.importedSupportsCondition = supportsCondition;
@@ -235,36 +223,27 @@ function getProcessorHelperClass(utilInstance) {
235
223
  async resolveLinkStylesheetURLs(stylesheetInfo, element, resourceURL, baseURI, options, workStylesheet, resources, stylesheets) {
236
224
  resourceURL = normalizeURL(resourceURL);
237
225
  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
- });
226
+ const content = await util.getContent(resourceURL, {
227
+ maxResourceSize: options.maxResourceSize,
228
+ maxResourceSizeEnabled: options.maxResourceSizeEnabled,
229
+ charset: options.charset,
230
+ frameId: options.frameId,
231
+ resourceReferrer: options.resourceReferrer,
232
+ validateTextContentType: true,
233
+ baseURI: baseURI,
234
+ blockMixedContent: options.blockMixedContent,
235
+ expectedType: "stylesheet",
236
+ acceptHeaders: options.acceptHeaders,
237
+ networkTimeout: options.networkTimeout
238
+ });
239
+ if (!(matchCharsetEquals(content.data, content.charset) || matchCharsetEquals(content.data, options.charset))) {
240
+ options = Object.assign({}, options, { charset: getCharset(content.data) });
241
+ await this.resolveLinkStylesheetURLs(stylesheetInfo, element, resourceURL, baseURI, options, workStylesheet, resources, stylesheets);
245
242
  } 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
- }
243
+ resourceURL = content.resourceURL;
244
+ content.data = getUpdatedResourceContent(content.resourceURL, options) || content.data;
245
+ stylesheetInfo.stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
246
+ await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets);
268
247
  }
269
248
  }
270
249
  }
@@ -300,6 +279,27 @@ function getProcessorHelperClass(utilInstance) {
300
279
  resources.fonts.set(indexResource, { name, content, extension, contentType, url: resourceURL });
301
280
  }
302
281
 
282
+ groupDuplicateFonts(stylesheets, fonts) {
283
+ const canonicalNames = new Map();
284
+ const fontResources = [...fonts]
285
+ .filter(([, resource]) => resource.content)
286
+ .sort(([indexResource], [otherIndexResource]) => indexResource - otherIndexResource);
287
+ fontResources.forEach(([indexResource, resource], index) => {
288
+ const original = fontResources.slice(0, index).find(([, previousResource]) => testSameContent(previousResource.content, resource.content));
289
+ if (original) {
290
+ canonicalNames.set(resource.name, original[1].name);
291
+ fonts.delete(indexResource);
292
+ }
293
+ });
294
+ if (canonicalNames.size) {
295
+ stylesheets.forEach(stylesheetInfo => {
296
+ if (stylesheetInfo.stylesheet) {
297
+ getUrlFunctions(stylesheetInfo.stylesheet).forEach(urlNode => replaceFontName(urlNode, canonicalNames));
298
+ }
299
+ });
300
+ }
301
+ }
302
+
303
303
  async processStyle(ruleData, options, resources, batchRequest) {
304
304
  const urls = getUrlFunctions(ruleData);
305
305
  await Promise.all(urls.map(async urlNode => {
@@ -428,7 +428,11 @@ function getProcessorHelperClass(utilInstance) {
428
428
  Object.keys(pageResources).forEach(resourceType => {
429
429
  const unusedResources = Array.from(pageResources[resourceType]).filter(([, value]) => !textContent.includes(value.name));
430
430
  unusedResources.forEach(([indexResource]) => pageResources[resourceType].delete(indexResource));
431
- resources[resourceType] = Array.from(pageResources[resourceType].values());
431
+ const orderedResources = Array.from(pageResources[resourceType]);
432
+ if (orderedResources.every(([indexResource]) => typeof indexResource == "number")) {
433
+ orderedResources.sort(([indexResource], [otherIndexResource]) => indexResource - otherIndexResource);
434
+ }
435
+ resources[resourceType] = orderedResources.map(([, resource]) => resource);
432
436
  });
433
437
  const viewportElement = doc.head.querySelector("meta[name=viewport]");
434
438
  const viewport = viewportElement ? viewportElement.content : null;
@@ -588,4 +592,26 @@ function getProcessorHelperClass(utilInstance) {
588
592
  return true;
589
593
  }
590
594
  };
595
+ }
596
+
597
+ function testSameContent(content, otherContent) {
598
+ if (content.length != otherContent.length) {
599
+ return false;
600
+ }
601
+ for (let index = 0; index < content.length; index++) {
602
+ if (content[index] != otherContent[index]) {
603
+ return false;
604
+ }
605
+ }
606
+ return true;
607
+ }
608
+
609
+ function replaceFontName(urlNode, canonicalNames) {
610
+ canonicalNames.forEach((canonicalName, name) => {
611
+ if (urlNode.value == name) {
612
+ urlNode.value = canonicalName;
613
+ } else if (urlNode.value.endsWith(" " + name)) {
614
+ urlNode.value = urlNode.value.substring(0, urlNode.value.length - name.length) + canonicalName;
615
+ }
616
+ });
591
617
  }