single-file-core 1.5.115 → 1.5.117

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
@@ -408,10 +408,10 @@ function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map
408
408
  }
409
409
  }
410
410
  if (options.removeUnusedFonts) {
411
- getUsedFont(computedStyle, options, data.usedFonts);
412
- getUsedFont(getComputedStyle(win, element, ":first-letter"), options, data.usedFonts);
413
- getUsedFont(getComputedStyle(win, element, ":before"), options, data.usedFonts);
414
- getUsedFont(getComputedStyle(win, element, ":after"), options, data.usedFonts);
411
+ getUsedFont(computedStyle, data.usedFonts);
412
+ getUsedFont(getComputedStyle(win, element, ":first-letter"), data.usedFonts);
413
+ getUsedFont(getComputedStyle(win, element, ":before"), data.usedFonts);
414
+ getUsedFont(getComputedStyle(win, element, ":after"), data.usedFonts);
415
415
  }
416
416
  }
417
417
  }
@@ -496,17 +496,22 @@ function getStylesheetsContent(styleSheets, adoptedStyleSheetsCache = new Map())
496
496
  function getResourcesInfo(win, doc, element, options, data, elementHidden, computedStyle) {
497
497
  const tagName = element.tagName && element.tagName.toUpperCase();
498
498
  if (tagName == "CANVAS") {
499
+ const canvasComputedStyle = computedStyle || getComputedStyle(win, element);
500
+ const canvasData = {
501
+ backgroundColor: canvasComputedStyle && canvasComputedStyle.getPropertyValue("background-color")
502
+ };
499
503
  try {
500
- data.canvases.push({
501
- dataURI: element.toDataURL("image/png"),
502
- backgroundColor: computedStyle.getPropertyValue("background-color")
503
- });
504
- element.setAttribute(CANVAS_ATTRIBUTE_NAME, data.canvases.length - 1);
505
- data.markedElements.push(element);
504
+ canvasData.dataURI = element.toDataURL("image/png");
506
505
  // eslint-disable-next-line no-unused-vars
507
506
  } catch (error) {
508
- // ignored
507
+ // a canvas painted with a cross-origin resource is tainted and toDataURL throws for the
508
+ // whole element, its own drawing included. The entry is pushed and the element marked
509
+ // anyway: the loss is then counted and reported instead of being silent, and an
510
+ // out-of-page fallback re-rendering the element has something to find it by
509
511
  }
512
+ data.canvases.push(canvasData);
513
+ element.setAttribute(CANVAS_ATTRIBUTE_NAME, data.canvases.length - 1);
514
+ data.markedElements.push(element);
510
515
  }
511
516
  if (tagName == "IMG") {
512
517
  const imageData = {
@@ -606,12 +611,28 @@ function getResourcesInfo(win, doc, element, options, data, elementHidden, compu
606
611
  }
607
612
  }
608
613
 
609
- function getUsedFont(computedStyle, options, usedFonts) {
614
+ // Every family in the computed font-family is recorded, and the set of faces the document actually
615
+ // resolved is deliberately NOT consulted. Narrowing the list to the loaded faces is the obvious
616
+ // idea — a fallback stack names every family the page might need and only some are used — and it
617
+ // was written in 2019, wired to an option nothing ever set, and dead until it was reconnected here.
618
+ // Reconnected, it saved nothing measurable on nine real pages and cost fidelity twice: a face
619
+ // matched on its declared style is not found when the browser SYNTHESIZED that style, so a family
620
+ // drawn in an italic it declares no face for was dropped from the page that draws it; and a webfont
621
+ // that merely failed to load on the capturing machine would be dropped from the archive for good.
622
+ // It belongs with the unicode-range work, which needs the same information and can weigh a measured
623
+ // benefit against those, not here.
624
+ function getUsedFont(computedStyle, usedFonts) {
610
625
  if (computedStyle) {
611
626
  const fontStyle = computedStyle.getPropertyValue("font-style") || "normal";
612
627
  computedStyle.getPropertyValue("font-family").split(",").forEach(fontFamilyName => {
613
628
  fontFamilyName = normalizeFontFamily(fontFamilyName);
614
- if (!options.loadedFonts || options.loadedFonts.find(font => normalizeFontFamily(font.family) == fontFamilyName && font.style == fontStyle)) {
629
+ // an element with no computed font-family at all is one whose styles could not be read,
630
+ // not one drawn in a family with no name. It happens to every element of a frame that
631
+ // was re-parsed from its srcdoc, because the computed style is asked of the parent
632
+ // window and the document it belongs to is not the one being rendered. Recorded, it
633
+ // turns "nothing is known here" into a list of length one, which reads downstream as a
634
+ // complete answer and prunes every face the frame declares
635
+ if (fontFamilyName) {
615
636
  const fontWeight = getFontWeight(computedStyle.getPropertyValue("font-weight"));
616
637
  const fontVariant = computedStyle.getPropertyValue("font-variant") || "normal";
617
638
  const value = [fontFamilyName, fontWeight, fontStyle, fontVariant];
package/core/index.js CHANGED
@@ -422,6 +422,7 @@ const SHADOWROOT_CLONABLE = "shadowrootclonable";
422
422
  const SHADOWROOT_SERIALIZABLE = "shadowrootserializable";
423
423
  const SCRIPT_OPTIONS = "data-single-file-options";
424
424
  const UTF8_CHARSET = "utf-8";
425
+ const TAINTED_CANVAS_WARNING_MESSAGE = "SingleFile: canvas elements tainted by a cross-origin resource, dropped from the page:";
425
426
 
426
427
  class Processor {
427
428
  constructor(options, processorHelper, batchRequest) {
@@ -1055,20 +1056,29 @@ class Processor {
1055
1056
 
1056
1057
  replaceCanvasElements() {
1057
1058
  if (this.options.canvases) {
1059
+ let discardedCount = 0;
1058
1060
  this.doc.querySelectorAll("canvas").forEach(canvasElement => {
1059
1061
  const attributeValue = canvasElement.getAttribute(util.CANVAS_ATTRIBUTE_NAME);
1060
1062
  if (attributeValue) {
1061
1063
  const canvasData = this.options.canvases[Number(attributeValue)];
1062
1064
  if (canvasData) {
1063
- const backgroundStyle = {};
1064
- if (canvasData.backgroundColor) {
1065
- backgroundStyle["background-color"] = canvasData.backgroundColor;
1065
+ if (canvasData.dataURI) {
1066
+ const backgroundStyle = {};
1067
+ if (canvasData.backgroundColor) {
1068
+ backgroundStyle["background-color"] = canvasData.backgroundColor;
1069
+ }
1070
+ this.processorHelper.setBackgroundImage(canvasElement, "url(" + canvasData.dataURI + ")", backgroundStyle);
1071
+ this.stats.add("processed", "canvas", 1);
1072
+ } else {
1073
+ discardedCount++;
1074
+ this.stats.add("discarded", "canvas", 1);
1066
1075
  }
1067
- this.processorHelper.setBackgroundImage(canvasElement, "url(" + canvasData.dataURI + ")", backgroundStyle);
1068
- this.stats.add("processed", "canvas", 1);
1069
1076
  }
1070
1077
  }
1071
1078
  });
1079
+ if (discardedCount) {
1080
+ warn(TAINTED_CANVAS_WARNING_MESSAGE, discardedCount);
1081
+ }
1072
1082
  }
1073
1083
  }
1074
1084
 
@@ -1726,6 +1736,10 @@ function log(...args) {
1726
1736
  console.log("S-File <core> ", ...args); // eslint-disable-line no-console
1727
1737
  }
1728
1738
 
1739
+ function warn(...args) {
1740
+ console.warn("S-File <core> ", ...args); // eslint-disable-line no-console
1741
+ }
1742
+
1729
1743
  // -----
1730
1744
  // Stats
1731
1745
  // -----
@@ -102,9 +102,10 @@ class ProcessorHelperCommon {
102
102
  ["image, feImage", "xlink:href"],
103
103
  ["image, feImage", "href"]
104
104
  ];
105
- if (options.blockImages) {
106
- doc.querySelectorAll("svg").forEach(element => element.remove());
107
- }
105
+ // an inline svg is document content, not a fetched resource: blocking images empties the
106
+ // references it makes outwards (image, feImage and use are all processed below) and leaves
107
+ // the markup itself alone. Removing it deleted JS-drawn charts, inline icons, and the
108
+ // <defs> holding gradients and filters that CSS applies to ordinary HTML elements
108
109
  let resourcePromises = processAttributeArgs.map(([selector, attributeName, removeElementIfMissing, processDuplicates]) =>
109
110
  this.processAttribute(doc, doc.querySelectorAll(selector), attributeName, baseURI, options, "image", resources, removeElementIfMissing, batchRequest, styles, processDuplicates)
110
111
  );
@@ -136,9 +137,12 @@ class ProcessorHelperCommon {
136
137
  resourceElement.setAttribute("data-sf-original-href", originalResourceURL);
137
138
  }
138
139
  let resourceURL = normalizeURL(originalResourceURL);
139
- if (!options.blockImages) {
140
- if (testValidPath(resourceURL) && !testIgnoredPath(resourceURL)) {
141
- resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
140
+ // a reference into the page itself costs no request, so blocking images must not empty
141
+ // it: normalizeURL drops the fragment, which leaves a bare "#symbol" as the empty path
142
+ // below and a same-document URL matching options.url
143
+ if (testValidPath(resourceURL) && !testIgnoredPath(resourceURL)) {
144
+ resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
145
+ if (!options.blockImages) {
142
146
  try {
143
147
  resourceURL = util.resolveURL(resourceURL, baseURI);
144
148
  // eslint-disable-next-line no-unused-vars
@@ -172,11 +176,9 @@ class ProcessorHelperCommon {
172
176
  }
173
177
  }
174
178
  }
175
- } else if (resourceURL == options.url) {
176
- resourceElement.setAttribute(attributeName, originalResourceURL.substring(resourceURL.length));
177
179
  }
178
- } else {
179
- resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
180
+ } else if (resourceURL == options.url) {
181
+ resourceElement.setAttribute(attributeName, originalResourceURL.substring(resourceURL.length));
180
182
  }
181
183
  }));
182
184
  }
@@ -255,6 +257,17 @@ class ProcessorHelperCommon {
255
257
  }));
256
258
  }
257
259
 
260
+ // a video or an audio element gets the attribute REMOVED rather than emptied: util.EMPTY_RESOURCE
261
+ // is inert on an image but not on a media element, which would attempt the load, fail, and sit
262
+ // in an error state where removing the attribute leaves the poster showing cleanly
263
+ setAttributeEmpty(resourceElement, attributeName, expectedType) {
264
+ if (expectedType == "video" || expectedType == "audio") {
265
+ resourceElement.removeAttribute(attributeName);
266
+ } else {
267
+ resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
268
+ }
269
+ }
270
+
258
271
  setBackgroundImage(element, url, style) {
259
272
  element.style.setProperty("background-blend-mode", "normal", "important");
260
273
  element.style.setProperty("background-clip", "content-box", "important");
@@ -382,12 +395,7 @@ class ProcessorHelperCommon {
382
395
  }
383
396
 
384
397
  async removeAlternativeFonts(doc, stylesheets, fonts, fontTests) {
385
- const fontsDetails = {
386
- fonts: new Map(),
387
- medias: new Map(),
388
- supports: new Map(),
389
- layers: new Map()
390
- };
398
+ const fontsDetails = this.createFontsDetailsInfo();
391
399
  const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
392
400
  let sheetIndex = 0;
393
401
  stylesheets.forEach(stylesheetInfo => {
@@ -446,7 +454,18 @@ class ProcessorHelperCommon {
446
454
  const key = this.getFontKey(ruleData);
447
455
  const fontInfo = fontsDetails.fonts.get(key);
448
456
  if (fontInfo) {
449
- await this.processFontFaceRule(ruleData, fontInfo, fonts, fontTests, stats);
457
+ // a face declaring the same descriptors and the same sources as one already kept
458
+ // cannot change what that one did, and its sources are embedded a second time. A
459
+ // stylesheet reached from two <link> elements, or imported by two sheets, repeats
460
+ // every face it carries. The test is made before the await: the sources are read
461
+ // as they stand for both rules, and processing them is what would interleave
462
+ const ruleKey = key + " " + this.getPropertyValue(ruleData, "src");
463
+ if (fontsDetails.emittedFonts.has(ruleKey)) {
464
+ removedRules.push(cssRule);
465
+ } else {
466
+ fontsDetails.emittedFonts.add(ruleKey);
467
+ await this.processFontFaceRule(ruleData, fontInfo, fonts, fontTests, stats);
468
+ }
450
469
  } else {
451
470
  removedRules.push(cssRule);
452
471
  }
@@ -504,7 +523,12 @@ class ProcessorHelperCommon {
504
523
  fonts: new Map(),
505
524
  medias: new Map(),
506
525
  supports: new Map(),
507
- layers: new Map()
526
+ layers: new Map(),
527
+ // the faces already emitted in this cascade context, so a second declaration of one of
528
+ // them can be dropped instead of embedding the same bytes again. Each media, supports
529
+ // and layer block gets its own info, so only rules that apply under the same conditions
530
+ // are ever compared
531
+ emittedFonts: new Set()
508
532
  };
509
533
  }
510
534
 
@@ -44,6 +44,10 @@ const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
44
44
  const LOCAL_SOURCE = "local(";
45
45
  const FONT_MAX_LOAD_DELAY = 5000;
46
46
  const DUPLICATE_STYLESHEET_ATTRIBUTE_NAME = "data-sf-duplicate-stylesheet-ref";
47
+ // the attributes that say how the link fetched its stylesheet, which the style element now holding
48
+ // that stylesheet has no use for. Everything else identified the element in the page
49
+ const LINK_FETCH_ATTRIBUTE_NAMES = ["rel", "href", "type", "media", "as", "crossorigin", "integrity",
50
+ "referrerpolicy", "hreflang", "sizes", "imagesrcset", "imagesizes", "fetchpriority"];
47
51
 
48
52
  let util;
49
53
 
@@ -162,6 +166,15 @@ function getProcessorHelperClass(utilInstance) {
162
166
  if (stylesheetInfo) {
163
167
  stylesheets.delete(linkElement);
164
168
  const styleElement = doc.createElement("style");
169
+ // the element is replaced rather than rewritten, so whatever identified it in
170
+ // the page has to be carried over: an id a script still looks up, a class a
171
+ // selector still matches. The attributes left out are the ones that describe
172
+ // the fetch the style element no longer performs
173
+ Array.from(linkElement.attributes).forEach(({ name, value }) => {
174
+ if (!LINK_FETCH_ATTRIBUTE_NAMES.includes(name.toLowerCase())) {
175
+ styleElement.setAttribute(name, value);
176
+ }
177
+ });
165
178
  if (stylesheetInfo.mediaText) {
166
179
  styleElement.media = stylesheetInfo.mediaText;
167
180
  }
@@ -348,7 +361,7 @@ function getProcessorHelperClass(utilInstance) {
348
361
  delete resourceElement.dataset.singleFileOriginURL;
349
362
  if (!expectedType || !options["block" + expectedType.charAt(0).toUpperCase() + expectedType.substring(1) + "s"]) {
350
363
  if (!testIgnoredPath(resourceURL)) {
351
- setAttributeEmpty(resourceElement, attributeName, expectedType);
364
+ this.setAttributeEmpty(resourceElement, attributeName, expectedType);
352
365
  if (testValidPath(resourceURL)) {
353
366
  try {
354
367
  resourceURL = util.resolveURL(resourceURL, baseURI);
@@ -435,18 +448,10 @@ function getProcessorHelperClass(utilInstance) {
435
448
  }
436
449
  }
437
450
  } else {
438
- setAttributeEmpty(resourceElement, attributeName, expectedType);
451
+ this.setAttributeEmpty(resourceElement, attributeName, expectedType);
439
452
  }
440
453
  }
441
454
  }));
442
-
443
- function setAttributeEmpty(resourceElement, attributeName, expectedType) {
444
- if (expectedType == "video" || expectedType == "audio") {
445
- resourceElement.removeAttribute(attributeName);
446
- } else {
447
- resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
448
- }
449
- }
450
455
  }
451
456
 
452
457
  async processImageSrcset(resourceURL, srcsetValue, resources, batchRequest) {
@@ -36,6 +36,7 @@ const CANVAS_TAG_FOUND = /<canvas/gi;
36
36
  const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
37
37
  const LOCAL_SOURCE = "local(";
38
38
  const FONT_MAX_LOAD_DELAY = 5000;
39
+ const LINK_OWN_ATTRIBUTE_NAMES = ["rel", "type", "href", "media"];
39
40
 
40
41
  let util;
41
42
 
@@ -100,18 +101,29 @@ function getProcessorHelperClass(utilInstance) {
100
101
  replaceStylesheets(doc, stylesheets, options, resources) {
101
102
  const entries = Array.from(stylesheets).reverse();
102
103
  const linkElements = new Map();
104
+ const sharedStyleElements = new Map();
103
105
  Array.from(new Set(options.inlineStylesheetsRefs.values())).forEach(stylesheetRefIndex => {
104
106
  const linkElement = doc.createElement("link");
105
107
  linkElement.setAttribute("rel", "stylesheet");
106
108
  linkElement.setAttribute("type", "text/css");
107
109
  const name = "stylesheet_" + resources.stylesheets.size + ".css";
108
110
  linkElement.setAttribute("href", name);
109
- let { content } = options.inlineStylesheets.get(stylesheetRefIndex);
110
- const stylesheet = cssTree.parse(content, { context: "stylesheet", parseCustomProperty: true });
111
- this.replacePseudoClassDefined(stylesheet);
112
- content = this.generateStylesheetContent(stylesheet, options);
113
- resources.stylesheets.set(resources.stylesheets.size, { name, content });
111
+ // the shared copy is generated from the stylesheet of the element the duplicates were
112
+ // folded into, not from the text that was captured: that text still names the resources
113
+ // by their addresses in the original page, which resolve to nothing once the page is
114
+ // inside the archive
115
+ const { styleElement, content } = options.inlineStylesheets.get(stylesheetRefIndex);
116
+ const sharedEntry = entries.find(([key]) => key.element == styleElement);
117
+ const stylesheet = sharedEntry
118
+ ? sharedEntry[1].stylesheet
119
+ : cssTree.parse(content, { context: "stylesheet", parseCustomProperty: true });
120
+ resources.stylesheets.set(resources.stylesheets.size, { name, content: this.generateStylesheetContent(stylesheet, options) });
114
121
  linkElements.set(stylesheetRefIndex, linkElement);
122
+ // the element the duplicates were folded into is not itself a duplicate, so it is
123
+ // absent from inlineStylesheetsRefs and would keep its content inline: the archive
124
+ // would then hold the same stylesheet twice, once as the entry the links point at
125
+ // and once in the page
126
+ sharedStyleElements.set(styleElement, stylesheetRefIndex);
115
127
  });
116
128
  for (const [key, stylesheetInfo] of entries) {
117
129
  if (key.urlNode) {
@@ -130,11 +142,22 @@ function getProcessorHelperClass(utilInstance) {
130
142
  resources.stylesheets.set(resources.stylesheets.size, { name, stylesheet: stylesheetInfo.stylesheet, url: stylesheetInfo.url });
131
143
  } else {
132
144
  const styleElement = key.element;
133
- const stylesheetRefIndex = options.inlineStylesheetsRefs.get(styleElement);
145
+ const stylesheetRefIndex = options.inlineStylesheetsRefs.has(styleElement)
146
+ ? options.inlineStylesheetsRefs.get(styleElement)
147
+ : sharedStyleElements.get(styleElement);
134
148
  if (stylesheetRefIndex === undefined) {
135
149
  styleElement.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
136
150
  } else {
137
151
  const linkElement = linkElements.get(stylesheetRefIndex).cloneNode(true);
152
+ // the element is replaced rather than rewritten, so whatever identified it in
153
+ // the page has to be carried over: an id a script still looks up, a class a
154
+ // selector still matches. The attributes left out are the ones that describe
155
+ // the link itself, which the code around here sets
156
+ Array.from(styleElement.attributes).forEach(({ name, value }) => {
157
+ if (!LINK_OWN_ATTRIBUTE_NAMES.includes(name.toLowerCase())) {
158
+ linkElement.setAttribute(name, value);
159
+ }
160
+ });
138
161
  if (stylesheetInfo.mediaText) {
139
162
  linkElement.media = stylesheetInfo.mediaText;
140
163
  }
@@ -151,7 +174,7 @@ function getProcessorHelperClass(utilInstance) {
151
174
  }
152
175
  }
153
176
 
154
- async resolveImportURLs(stylesheetInfo, baseURI, options, workStylesheet, resources, stylesheets) {
177
+ async resolveImportURLs(stylesheetInfo, baseURI, options, workStylesheet, resources, stylesheets, importedStyleSheets = new Set()) {
155
178
  const stylesheet = stylesheetInfo.stylesheet;
156
179
  const scoped = stylesheetInfo.scoped;
157
180
  this.resolveStylesheetURLs(stylesheet, baseURI, workStylesheet);
@@ -168,7 +191,10 @@ function getProcessorHelperClass(utilInstance) {
168
191
  } catch (error) {
169
192
  // ignored
170
193
  }
171
- if (testValidURL(resourceURL)) {
194
+ // a sheet already open higher in this import chain must not be entered again: the
195
+ // ancestors are carried down the branch, not accumulated across the whole document,
196
+ // so two sibling imports of one sheet are still both resolved
197
+ if (testValidURL(resourceURL) && !importedStyleSheets.has(resourceURL)) {
172
198
  const mediaQueryListNode = cssTree.find(node, node => node.type == "MediaQueryList");
173
199
  let mediaText, layerName, supportsCondition;
174
200
  if (mediaQueryListNode) {
@@ -198,12 +224,19 @@ function getProcessorHelperClass(utilInstance) {
198
224
  layerName,
199
225
  supportsCondition
200
226
  };
227
+ const requestedURL = resourceURL;
201
228
  const content = await this.getStylesheetContent(resourceURL, options);
202
229
  stylesheetInfo.url = resourceURL = content.resourceURL;
203
230
  content.data = getUpdatedResourceContent(resourceURL, options) || content.data;
204
231
  stylesheetInfo.stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
205
232
  stylesheet = stylesheetInfo.stylesheet;
206
- await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets);
233
+ const ancestorStyleSheets = new Set(importedStyleSheets);
234
+ // both identities of the sheet are remembered: a redirect makes the URL that was
235
+ // requested and the URL that answered differ, and an import of either one is the
236
+ // same cycle
237
+ ancestorStyleSheets.add(requestedURL);
238
+ ancestorStyleSheets.add(resourceURL);
239
+ await this.resolveImportURLs(stylesheetInfo, resourceURL, options, workStylesheet, resources, stylesheets, ancestorStyleSheets);
207
240
  stylesheets.set({ urlNode }, stylesheetInfo);
208
241
  }
209
242
  urlNode.importedChildren = stylesheet.children;
@@ -319,7 +352,7 @@ function getProcessorHelperClass(utilInstance) {
319
352
  delete resourceElement.dataset.singleFileOriginURL;
320
353
  if (!expectedType || !options["block" + expectedType.charAt(0).toUpperCase() + expectedType.substring(1) + "s"]) {
321
354
  if (!testIgnoredPath(resourceURL)) {
322
- setAttributeEmpty(resourceElement, attributeName, expectedType);
355
+ this.setAttributeEmpty(resourceElement, attributeName, expectedType);
323
356
  if (testValidPath(resourceURL)) {
324
357
  try {
325
358
  resourceURL = util.resolveURL(resourceURL, baseURI);
@@ -373,18 +406,10 @@ function getProcessorHelperClass(utilInstance) {
373
406
  }
374
407
  }
375
408
  } else {
376
- setAttributeEmpty(resourceElement, attributeName, expectedType);
409
+ this.setAttributeEmpty(resourceElement, attributeName, expectedType);
377
410
  }
378
411
  }
379
412
  }));
380
-
381
- function setAttributeEmpty(resourceElement, attributeName, expectedType) {
382
- if (expectedType == "video" || expectedType == "audio") {
383
- resourceElement.removeAttribute(attributeName);
384
- } else {
385
- resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
386
- }
387
- }
388
413
  }
389
414
 
390
415
  async processImageSrcset(resourceURL, srcsetValue, resources, batchRequest) {