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 +34 -13
- package/core/index.js +19 -5
- package/core/lib/processor-helper-common.js +42 -18
- package/core/lib/processor-helper-inline.js +15 -10
- package/core/lib/processor-helper.js +44 -19
- package/doc/singlefile-archive.md +123 -26
- package/modules/css-fonts-minifier.js +191 -33
- package/modules/css-rules-minifier.js +9 -2
- package/package.json +2 -2
- package/processors/compression/compression-display.js +23 -2
- package/processors/compression/compression-packager.js +2 -11
- package/processors/compression/compression.js +33 -9
- package/processors/hooks/content/content-hooks-frames-web.js +27 -4
- package/test/sfz-harness/README.md +5 -2
- package/test/sfz-harness/adopted-stylesheets-hook.js +240 -0
- package/test/sfz-harness/css-fonts-minifier.js +235 -0
- package/test/sfz-harness/css-property-filter.js +85 -0
- package/test/sfz-harness/format-rules.js +31 -1
- package/test/sfz-harness/inlined-functions.js +82 -0
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,
|
|
412
|
-
getUsedFont(getComputedStyle(win, element, ":first-letter"),
|
|
413
|
-
getUsedFont(getComputedStyle(win, element, ":before"),
|
|
414
|
-
getUsedFont(getComputedStyle(win, element, ":after"),
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|