single-file-core 1.1.78 → 1.2.0
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/constants.js +3 -1
- package/core/helper.js +9 -2
- package/core/index.js +61 -751
- package/core/infobar.js +1 -1
- package/core/lib/processor-helper-common.js +323 -0
- package/core/lib/processor-helper-inline.js +876 -0
- package/core/lib/processor-helper.js +801 -0
- package/core/processor-helper.js +34 -0
- package/core/util.js +48 -13
- package/modules/css-fonts-minifier.js +41 -18
- package/modules/index.js +0 -2
- package/package.json +1 -1
- package/processors/compression/compression-display.js +74 -0
- package/processors/compression/compression-extract.js +151 -0
- package/processors/compression/compression.js +305 -0
- package/processors/hooks/content/content-hooks-frames.js +4 -3
- package/processors/index.js +2 -0
- package/single-file-infobar.js +1 -0
- package/single-file.js +24 -2
- package/vendor/index.js +2 -2
- package/vendor/zip/z-worker.js +1 -0
- package/vendor/zip/zip.js +4955 -0
- package/vendor/zip/zip.min.js +1 -0
- package/modules/css-fonts-alt-minifier.js +0 -343
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2010-2022 Gildas Lormeau
|
|
3
|
+
* contact : gildas.lormeau <at> gmail.com
|
|
4
|
+
*
|
|
5
|
+
* This file is part of SingleFile.
|
|
6
|
+
*
|
|
7
|
+
* The code in this file is free software: you can redistribute it and/or
|
|
8
|
+
* modify it under the terms of the GNU Affero General Public License
|
|
9
|
+
* (GNU AGPL) as published by the Free Software Foundation, either version 3
|
|
10
|
+
* of the License, or (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* The code in this file is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
|
|
15
|
+
* General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* As additional permission under GNU AGPL version 3 section 7, you may
|
|
18
|
+
* distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
|
|
19
|
+
* AGPL normally required by section 4, provided you include this license
|
|
20
|
+
* notice and a URL through which recipients can access the Corresponding
|
|
21
|
+
* Source.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/* global globalThis */
|
|
25
|
+
|
|
26
|
+
import * as cssTree from "./../../vendor/css-tree.js";
|
|
27
|
+
import {
|
|
28
|
+
normalizeFontFamily,
|
|
29
|
+
getFontWeight
|
|
30
|
+
} from "./../helper.js";
|
|
31
|
+
|
|
32
|
+
const JSON = globalThis.JSON;
|
|
33
|
+
const FontFace = globalThis.FontFace;
|
|
34
|
+
const Set = globalThis.Set;
|
|
35
|
+
const setTimeout = globalThis.setTimeout;
|
|
36
|
+
const clearTimeout = globalThis.clearTimeout;
|
|
37
|
+
const Image = globalThis.Image;
|
|
38
|
+
|
|
39
|
+
const ABOUT_BLANK_URI = "about:blank";
|
|
40
|
+
const UTF8_CHARSET = "utf-8";
|
|
41
|
+
const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
|
|
42
|
+
const PREFIXES_FORBIDDEN_DATA_URI = ["data:text/"];
|
|
43
|
+
const SCRIPT_TAG_FOUND = /<script/gi;
|
|
44
|
+
const NOSCRIPT_TAG_FOUND = /<noscript/gi;
|
|
45
|
+
const CANVAS_TAG_FOUND = /<canvas/gi;
|
|
46
|
+
const SINGLE_FILE_VARIABLE_NAME_PREFIX = "--sf-img-";
|
|
47
|
+
const SINGLE_FILE_VARIABLE_MAX_SIZE = 512 * 1024;
|
|
48
|
+
|
|
49
|
+
const REGEXP_URL_SIMPLE_QUOTES_FN = /url\s*\(\s*'(.*?)'\s*\)/i;
|
|
50
|
+
const REGEXP_URL_DOUBLE_QUOTES_FN = /url\s*\(\s*"(.*?)"\s*\)/i;
|
|
51
|
+
const REGEXP_URL_NO_QUOTES_FN = /url\s*\(\s*(.*?)\s*\)/i;
|
|
52
|
+
const REGEXP_URL_FUNCTION = /(url|local|-sf-url-original)\(.*?\)\s*(,|$)/g;
|
|
53
|
+
const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
|
|
54
|
+
const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
|
|
55
|
+
const REGEXP_URL_FUNCTION_WOFF = /^url\(\s*["']?data:font\/(woff2?)/;
|
|
56
|
+
const REGEXP_URL_FUNCTION_WOFF_ALT = /^url\(\s*["']?data:application\/x-font-(woff)/;
|
|
57
|
+
const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
|
|
58
|
+
const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
|
|
59
|
+
const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
|
|
60
|
+
const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
|
|
61
|
+
const LOCAL_SOURCE = "local(";
|
|
62
|
+
const MEDIA_ALL = "all";
|
|
63
|
+
const FONT_STRETCHES = {
|
|
64
|
+
"ultra-condensed": "50%",
|
|
65
|
+
"extra-condensed": "62.5%",
|
|
66
|
+
"condensed": "75%",
|
|
67
|
+
"semi-condensed": "87.5%",
|
|
68
|
+
"normal": "100%",
|
|
69
|
+
"semi-expanded": "112.5%",
|
|
70
|
+
"expanded": "125%",
|
|
71
|
+
"extra-expanded": "150%",
|
|
72
|
+
"ultra-expanded": "200%"
|
|
73
|
+
};
|
|
74
|
+
const FONT_MAX_LOAD_DELAY = 5000;
|
|
75
|
+
|
|
76
|
+
let util;
|
|
77
|
+
|
|
78
|
+
import {
|
|
79
|
+
getProcessorHelperCommonClass,
|
|
80
|
+
getUpdatedResourceContent,
|
|
81
|
+
normalizeURL,
|
|
82
|
+
matchCharsetEquals,
|
|
83
|
+
getCharset,
|
|
84
|
+
getUrlFunctions,
|
|
85
|
+
getImportFunctions,
|
|
86
|
+
isDataURL,
|
|
87
|
+
replaceOriginalURLs,
|
|
88
|
+
testIgnoredPath,
|
|
89
|
+
testValidPath,
|
|
90
|
+
testValidURL
|
|
91
|
+
} from "./processor-helper-common.js";
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
getProcessorHelperClass,
|
|
95
|
+
cssTree
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function getProcessorHelperClass(utilInstance) {
|
|
99
|
+
util = utilInstance;
|
|
100
|
+
const ProcessorHelperCommon = getProcessorHelperCommonClass(util, cssTree);
|
|
101
|
+
|
|
102
|
+
return class ProcessorHelper extends ProcessorHelperCommon {
|
|
103
|
+
async processPageResources(doc, baseURI, options, resources, styles, batchRequest) {
|
|
104
|
+
const processAttributeArgs = [
|
|
105
|
+
["link[href][rel*=\"icon\"]", "href", false, true],
|
|
106
|
+
["object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"], object[data*=\".svg\"]", "data"],
|
|
107
|
+
["img[src], input[src][type=image]", "src", true],
|
|
108
|
+
["embed[src*=\".svg\"]", "src"],
|
|
109
|
+
["video[poster]", "poster"],
|
|
110
|
+
["*[background]", "background"],
|
|
111
|
+
["image", "xlink:href"],
|
|
112
|
+
["image", "href"]
|
|
113
|
+
];
|
|
114
|
+
if (options.blockImages) {
|
|
115
|
+
doc.querySelectorAll("svg").forEach(element => element.remove());
|
|
116
|
+
}
|
|
117
|
+
let resourcePromises = processAttributeArgs.map(([selector, attributeName, processDuplicates, removeElementIfMissing]) =>
|
|
118
|
+
this.processAttribute(doc.querySelectorAll(selector), attributeName, baseURI, options, "image", resources, styles, batchRequest, processDuplicates, removeElementIfMissing)
|
|
119
|
+
);
|
|
120
|
+
resourcePromises = resourcePromises.concat([
|
|
121
|
+
this.processXLinks(doc.querySelectorAll("use"), doc, baseURI, options, batchRequest),
|
|
122
|
+
this.processSrcset(doc.querySelectorAll("img[srcset], source[srcset]"), baseURI, options, batchRequest)
|
|
123
|
+
]);
|
|
124
|
+
resourcePromises.push(this.processAttribute(doc.querySelectorAll("object[data*=\".pdf\"]"), "data", baseURI, options, null, resources, styles, batchRequest));
|
|
125
|
+
resourcePromises.push(this.processAttribute(doc.querySelectorAll("embed[src*=\".pdf\"]"), "src", baseURI, options, null, resources, styles, batchRequest));
|
|
126
|
+
resourcePromises.push(this.processAttribute(doc.querySelectorAll("audio[src], audio > source[src]"), "src", baseURI, options, "audio", resources, styles, batchRequest));
|
|
127
|
+
resourcePromises.push(this.processAttribute(doc.querySelectorAll("video[src], video > source[src]"), "src", baseURI, options, "video", resources, styles, batchRequest));
|
|
128
|
+
resourcePromises.push(this.processAttribute(doc.querySelectorAll("model[src]"), "src", baseURI, options, null, resources, styles, batchRequest));
|
|
129
|
+
await Promise.all(resourcePromises);
|
|
130
|
+
if (options.saveFavicon) {
|
|
131
|
+
this.processShortcutIcons(doc);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async processLinkElement(element, stylesheetInfo, stylesheets, baseURI, options, workStyleElement) {
|
|
136
|
+
if (element.tagName.toUpperCase() == "LINK" && element.charset) {
|
|
137
|
+
options.charset = element.charset;
|
|
138
|
+
}
|
|
139
|
+
await this.processStylesheetElement(element, stylesheetInfo, stylesheets, baseURI, options, workStyleElement);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async processStylesheetElement(element, stylesheetInfo, stylesheets, baseURI, options, workStyleElement) {
|
|
143
|
+
let stylesheet;
|
|
144
|
+
stylesheets.set(element, stylesheetInfo);
|
|
145
|
+
if (!options.blockStylesheets) {
|
|
146
|
+
if (element.tagName.toUpperCase() == "LINK") {
|
|
147
|
+
stylesheet = await this.resolveLinkStylesheetURLs(element.href, baseURI, options, workStyleElement);
|
|
148
|
+
} else {
|
|
149
|
+
stylesheet = cssTree.parse(element.textContent, { context: "stylesheet", parseCustomProperty: true });
|
|
150
|
+
const importFound = await this.resolveImportURLs(stylesheet, baseURI, options, workStyleElement);
|
|
151
|
+
if (importFound) {
|
|
152
|
+
stylesheet = cssTree.parse(cssTree.generate(stylesheet), { context: "stylesheet", parseCustomProperty: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (stylesheet && stylesheet.children) {
|
|
157
|
+
if (options.compressCSS) {
|
|
158
|
+
this.removeSingleLineCssComments(stylesheet);
|
|
159
|
+
}
|
|
160
|
+
this.replacePseudoClassDefined(stylesheet);
|
|
161
|
+
stylesheetInfo.stylesheet = stylesheet;
|
|
162
|
+
} else {
|
|
163
|
+
stylesheets.delete(element);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
replaceStylesheets(doc, stylesheets, resources, options) {
|
|
168
|
+
doc.querySelectorAll("style").forEach(styleElement => {
|
|
169
|
+
const stylesheetInfo = stylesheets.get(styleElement);
|
|
170
|
+
if (stylesheetInfo) {
|
|
171
|
+
stylesheets.delete(styleElement);
|
|
172
|
+
styleElement.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
|
|
173
|
+
if (stylesheetInfo.mediaText) {
|
|
174
|
+
styleElement.media = stylesheetInfo.mediaText;
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
styleElement.remove();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
doc.querySelectorAll("link[rel*=stylesheet]").forEach(linkElement => {
|
|
181
|
+
const stylesheetInfo = stylesheets.get(linkElement);
|
|
182
|
+
if (stylesheetInfo) {
|
|
183
|
+
stylesheets.delete(linkElement);
|
|
184
|
+
const styleElement = doc.createElement("style");
|
|
185
|
+
if (stylesheetInfo.mediaText) {
|
|
186
|
+
styleElement.media = stylesheetInfo.mediaText;
|
|
187
|
+
}
|
|
188
|
+
styleElement.textContent = this.generateStylesheetContent(stylesheetInfo.stylesheet, options);
|
|
189
|
+
linkElement.parentElement.replaceChild(styleElement, linkElement);
|
|
190
|
+
} else {
|
|
191
|
+
linkElement.remove();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async resolveImportURLs(stylesheet, baseURI, options, workStylesheet, importedStyleSheets = new Set()) {
|
|
197
|
+
let importFound;
|
|
198
|
+
this.resolveStylesheetURLs(stylesheet, baseURI, workStylesheet);
|
|
199
|
+
const imports = getImportFunctions(stylesheet);
|
|
200
|
+
await Promise.all(imports.map(async node => {
|
|
201
|
+
const urlNode = cssTree.find(node, node => node.type == "Url") || cssTree.find(node, node => node.type == "String");
|
|
202
|
+
if (urlNode) {
|
|
203
|
+
let resourceURL = normalizeURL(urlNode.value);
|
|
204
|
+
if (!testIgnoredPath(resourceURL) && testValidPath(resourceURL)) {
|
|
205
|
+
urlNode.value = util.EMPTY_RESOURCE;
|
|
206
|
+
try {
|
|
207
|
+
resourceURL = util.resolveURL(resourceURL, baseURI);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// ignored
|
|
210
|
+
}
|
|
211
|
+
if (testValidURL(resourceURL) && !importedStyleSheets.has(resourceURL)) {
|
|
212
|
+
options.inline = true;
|
|
213
|
+
const content = await this.getStylesheetContent(resourceURL, options);
|
|
214
|
+
resourceURL = content.resourceURL;
|
|
215
|
+
content.data = getUpdatedResourceContent(resourceURL, content, options);
|
|
216
|
+
if (content.data && content.data.match(/^<!doctype /i)) {
|
|
217
|
+
content.data = "";
|
|
218
|
+
}
|
|
219
|
+
const mediaQueryListNode = cssTree.find(node, node => node.type == "MediaQueryList");
|
|
220
|
+
if (mediaQueryListNode) {
|
|
221
|
+
content.data = this.wrapMediaQuery(content.data, cssTree.generate(mediaQueryListNode));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
content.data = content.data.replace(/:defined/gi, "*");
|
|
225
|
+
|
|
226
|
+
const importedStylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
|
|
227
|
+
const ancestorStyleSheets = new Set(importedStyleSheets);
|
|
228
|
+
ancestorStyleSheets.add(resourceURL);
|
|
229
|
+
await this.resolveImportURLs(importedStylesheet, resourceURL, options, workStylesheet, ancestorStyleSheets);
|
|
230
|
+
for (let keyName of Object.keys(importedStylesheet)) {
|
|
231
|
+
node[keyName] = importedStylesheet[keyName];
|
|
232
|
+
}
|
|
233
|
+
importFound = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}));
|
|
238
|
+
return importFound;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async resolveLinkStylesheetURLs(resourceURL, baseURI, options, workStylesheet) {
|
|
242
|
+
resourceURL = normalizeURL(resourceURL);
|
|
243
|
+
if (resourceURL && resourceURL != baseURI && resourceURL != ABOUT_BLANK_URI) {
|
|
244
|
+
const content = await util.getContent(resourceURL, {
|
|
245
|
+
inline: true,
|
|
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
|
+
return this.resolveLinkStylesheetURLs(resourceURL, baseURI, options, workStylesheet);
|
|
261
|
+
}
|
|
262
|
+
resourceURL = content.resourceURL;
|
|
263
|
+
content.data = getUpdatedResourceContent(content.resourceURL, content, options);
|
|
264
|
+
if (content.data && content.data.match(/^<!doctype /i)) {
|
|
265
|
+
content.data = "";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
content.data = content.data.replace(/:defined/gi, "*");
|
|
269
|
+
|
|
270
|
+
let stylesheet = cssTree.parse(content.data, { context: "stylesheet", parseCustomProperty: true });
|
|
271
|
+
const importFound = await this.resolveImportURLs(stylesheet, resourceURL, options, workStylesheet);
|
|
272
|
+
if (importFound) {
|
|
273
|
+
stylesheet = cssTree.parse(cssTree.generate(stylesheet), { context: "stylesheet", parseCustomProperty: true });
|
|
274
|
+
}
|
|
275
|
+
return stylesheet;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async processFrame(frameElement, pageData) {
|
|
280
|
+
let sandbox = "allow-popups allow-top-navigation allow-top-navigation-by-user-activation";
|
|
281
|
+
if (pageData.content.match(NOSCRIPT_TAG_FOUND) || pageData.content.match(CANVAS_TAG_FOUND) || pageData.content.match(SCRIPT_TAG_FOUND)) {
|
|
282
|
+
sandbox += " allow-scripts allow-same-origin";
|
|
283
|
+
}
|
|
284
|
+
frameElement.setAttribute("sandbox", sandbox);
|
|
285
|
+
if (frameElement.tagName.toUpperCase() == "OBJECT") {
|
|
286
|
+
frameElement.setAttribute("data", "data:text/html," + pageData.content);
|
|
287
|
+
} else {
|
|
288
|
+
if (frameElement.tagName.toUpperCase() == "FRAME") {
|
|
289
|
+
frameElement.setAttribute("src", "data:text/html," + pageData.content.replace(/%/g, "%25").replace(/#/g, "%23"));
|
|
290
|
+
} else {
|
|
291
|
+
frameElement.setAttribute("srcdoc", pageData.content);
|
|
292
|
+
frameElement.removeAttribute("src");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async processStylesheet(cssRules, baseURI, options, resources, batchRequest) {
|
|
298
|
+
const promises = [];
|
|
299
|
+
const removedRules = [];
|
|
300
|
+
for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
|
|
301
|
+
const ruleData = cssRule.data;
|
|
302
|
+
if (ruleData.type == "Atrule" && ruleData.name == "charset") {
|
|
303
|
+
removedRules.push(cssRule);
|
|
304
|
+
} else if (ruleData.block && ruleData.block.children) {
|
|
305
|
+
if (ruleData.type == "Rule") {
|
|
306
|
+
promises.push(this.processStyle(ruleData, options, resources, batchRequest));
|
|
307
|
+
} else if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports")) {
|
|
308
|
+
promises.push(this.processStylesheet(ruleData.block.children, baseURI, options, resources, batchRequest));
|
|
309
|
+
} else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
|
|
310
|
+
promises.push(processFontFaceRule(ruleData));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
removedRules.forEach(cssRule => cssRules.remove(cssRule));
|
|
315
|
+
await Promise.all(promises);
|
|
316
|
+
|
|
317
|
+
async function processFontFaceRule(ruleData) {
|
|
318
|
+
const urls = getUrlFunctions(ruleData);
|
|
319
|
+
await Promise.all(urls.map(async urlNode => {
|
|
320
|
+
const originalResourceURL = urlNode.value;
|
|
321
|
+
if (!options.blockFonts) {
|
|
322
|
+
const resourceURL = normalizeURL(originalResourceURL);
|
|
323
|
+
if (!testIgnoredPath(resourceURL) && testValidURL(resourceURL)) {
|
|
324
|
+
let { content } = await batchRequest.addURL(resourceURL, { asBinary: true, expectedType: "font", baseURI, blockMixedContent: options.blockMixedContent });
|
|
325
|
+
let resourceURLs = resources.fonts.get(urlNode);
|
|
326
|
+
if (!resourceURLs) {
|
|
327
|
+
resourceURLs = [];
|
|
328
|
+
resources.fonts.set(urlNode, resourceURLs);
|
|
329
|
+
}
|
|
330
|
+
resourceURLs.push(resourceURL);
|
|
331
|
+
if (!isDataURL(resourceURL) && options.saveOriginalURLs) {
|
|
332
|
+
urlNode.value = "-sf-url-original(" + JSON.stringify(originalResourceURL) + ") " + content;
|
|
333
|
+
} else {
|
|
334
|
+
urlNode.value = content;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
urlNode.value = util.EMPTY_RESOURCE;
|
|
339
|
+
}
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async processStyle(ruleData, options, { cssVariables }, batchRequest) {
|
|
345
|
+
const urls = getUrlFunctions(ruleData);
|
|
346
|
+
await Promise.all(urls.map(async urlNode => {
|
|
347
|
+
const originalResourceURL = urlNode.value;
|
|
348
|
+
if (!options.blockImages) {
|
|
349
|
+
const resourceURL = normalizeURL(originalResourceURL);
|
|
350
|
+
if (!testIgnoredPath(resourceURL) && testValidURL(resourceURL)) {
|
|
351
|
+
let { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL, { asBinary: true, expectedType: "image", groupDuplicates: options.groupDuplicateImages });
|
|
352
|
+
if (!originalResourceURL.startsWith("#")) {
|
|
353
|
+
const maxSizeDuplicateImages = options.maxSizeDuplicateImages || SINGLE_FILE_VARIABLE_MAX_SIZE;
|
|
354
|
+
if (duplicate && options.groupDuplicateImages && util.getContentSize(content) < maxSizeDuplicateImages) {
|
|
355
|
+
const varNode = cssTree.parse("var(" + SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource + ")", { context: "value" });
|
|
356
|
+
for (let keyName of Object.keys(varNode.children.head.data)) {
|
|
357
|
+
urlNode[keyName] = varNode.children.head.data[keyName];
|
|
358
|
+
}
|
|
359
|
+
cssVariables.set(indexResource, { content, url: originalResourceURL });
|
|
360
|
+
} else {
|
|
361
|
+
if (!isDataURL(resourceURL) && options.saveOriginalURLs) {
|
|
362
|
+
urlNode.value = "-sf-url-original(" + JSON.stringify(originalResourceURL) + ") " + content;
|
|
363
|
+
} else {
|
|
364
|
+
urlNode.value = content;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
urlNode.value = util.EMPTY_RESOURCE;
|
|
371
|
+
}
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async processAttribute(resourceElements, attributeName, baseURI, options, expectedType, { cssVariables }, styles, batchRequest, processDuplicates, removeElementIfMissing) {
|
|
376
|
+
await Promise.all(Array.from(resourceElements).map(async resourceElement => {
|
|
377
|
+
let resourceURL = resourceElement.getAttribute(attributeName);
|
|
378
|
+
if (resourceURL != null) {
|
|
379
|
+
resourceURL = normalizeURL(resourceURL);
|
|
380
|
+
let originURL = resourceElement.dataset.singleFileOriginURL;
|
|
381
|
+
if (options.saveOriginalURLs && !isDataURL(resourceURL)) {
|
|
382
|
+
resourceElement.setAttribute("data-sf-original-" + attributeName, resourceURL);
|
|
383
|
+
}
|
|
384
|
+
delete resourceElement.dataset.singleFileOriginURL;
|
|
385
|
+
if (!options["block" + expectedType.charAt(0).toUpperCase() + expectedType.substring(1) + "s"]) {
|
|
386
|
+
if (!testIgnoredPath(resourceURL)) {
|
|
387
|
+
setAttributeEmpty(resourceElement, attributeName, expectedType);
|
|
388
|
+
if (testValidPath(resourceURL)) {
|
|
389
|
+
try {
|
|
390
|
+
resourceURL = util.resolveURL(resourceURL, baseURI);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
// ignored
|
|
393
|
+
}
|
|
394
|
+
if (testValidURL(resourceURL)) {
|
|
395
|
+
let { content, indexResource, duplicate } = await batchRequest.addURL(
|
|
396
|
+
resourceURL,
|
|
397
|
+
{ asBinary: true, expectedType, groupDuplicates: options.groupDuplicateImages && resourceElement.tagName.toUpperCase() == "IMG" && attributeName == "src" });
|
|
398
|
+
if (originURL) {
|
|
399
|
+
if (this.testEmptyResource(content)) {
|
|
400
|
+
try {
|
|
401
|
+
originURL = util.resolveURL(originURL, baseURI);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
// ignored
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
resourceURL = originURL;
|
|
407
|
+
content = (await util.getContent(resourceURL, {
|
|
408
|
+
asBinary: true,
|
|
409
|
+
inline: true,
|
|
410
|
+
expectedType,
|
|
411
|
+
maxResourceSize: options.maxResourceSize,
|
|
412
|
+
maxResourceSizeEnabled: options.maxResourceSizeEnabled,
|
|
413
|
+
frameId: options.windowId,
|
|
414
|
+
resourceReferrer: options.resourceReferrer,
|
|
415
|
+
acceptHeaders: options.acceptHeaders,
|
|
416
|
+
networkTimeout: options.networkTimeout
|
|
417
|
+
})).data;
|
|
418
|
+
} catch (error) {
|
|
419
|
+
// ignored
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (removeElementIfMissing && this.testEmptyResource(content)) {
|
|
424
|
+
resourceElement.remove();
|
|
425
|
+
} else if (!this.testEmptyResource(content)) {
|
|
426
|
+
let forbiddenPrefixFound = PREFIXES_FORBIDDEN_DATA_URI.filter(prefixDataURI => content.startsWith(prefixDataURI)).length;
|
|
427
|
+
if (expectedType == "image") {
|
|
428
|
+
if (forbiddenPrefixFound && Image) {
|
|
429
|
+
forbiddenPrefixFound = await new Promise((resolve) => {
|
|
430
|
+
const image = new Image();
|
|
431
|
+
const timeoutId = setTimeout(() => resolve(true), 100);
|
|
432
|
+
image.src = content;
|
|
433
|
+
image.onload = () => cleanupAndResolve();
|
|
434
|
+
image.onerror = () => cleanupAndResolve(true);
|
|
435
|
+
|
|
436
|
+
function cleanupAndResolve(value) {
|
|
437
|
+
clearTimeout(timeoutId);
|
|
438
|
+
resolve(value);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (!forbiddenPrefixFound) {
|
|
443
|
+
const isSVG = content.startsWith(PREFIX_DATA_URI_IMAGE_SVG);
|
|
444
|
+
const maxSizeDuplicateImages = options.maxSizeDuplicateImages || SINGLE_FILE_VARIABLE_MAX_SIZE;
|
|
445
|
+
if (processDuplicates && duplicate && !isSVG && util.getContentSize(content) < maxSizeDuplicateImages) {
|
|
446
|
+
if (this.replaceImageSource(resourceElement, SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource, options)) {
|
|
447
|
+
cssVariables.set(indexResource, { content, url: originURL });
|
|
448
|
+
const declarationList = cssTree.parse(resourceElement.getAttribute("style"), { context: "declarationList", parseCustomProperty: true });
|
|
449
|
+
styles.set(resourceElement, declarationList);
|
|
450
|
+
} else {
|
|
451
|
+
resourceElement.setAttribute(attributeName, content);
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
resourceElement.setAttribute(attributeName, content);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
resourceElement.setAttribute(attributeName, content);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
setAttributeEmpty(resourceElement, attributeName, expectedType);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}));
|
|
469
|
+
|
|
470
|
+
function setAttributeEmpty(resourceElement, attributeName, expectedType) {
|
|
471
|
+
if (expectedType == "video" || expectedType == "audio") {
|
|
472
|
+
resourceElement.removeAttribute(attributeName);
|
|
473
|
+
} else {
|
|
474
|
+
resourceElement.setAttribute(attributeName, util.EMPTY_RESOURCE);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async processSrcset(resourceElements, baseURI, options, batchRequest) {
|
|
480
|
+
await Promise.all(Array.from(resourceElements).map(async resourceElement => {
|
|
481
|
+
const originSrcset = resourceElement.getAttribute("srcset");
|
|
482
|
+
const srcset = util.parseSrcset(originSrcset);
|
|
483
|
+
if (options.saveOriginalURLs && !isDataURL(originSrcset)) {
|
|
484
|
+
resourceElement.setAttribute("data-sf-original-srcset", originSrcset);
|
|
485
|
+
}
|
|
486
|
+
if (!options.blockImages) {
|
|
487
|
+
const srcsetValues = await Promise.all(srcset.map(async srcsetValue => {
|
|
488
|
+
let resourceURL = normalizeURL(srcsetValue.url);
|
|
489
|
+
if (!testIgnoredPath(resourceURL)) {
|
|
490
|
+
if (testValidPath(resourceURL)) {
|
|
491
|
+
try {
|
|
492
|
+
resourceURL = util.resolveURL(resourceURL, baseURI);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
// ignored
|
|
495
|
+
}
|
|
496
|
+
if (testValidURL(resourceURL)) {
|
|
497
|
+
const { content } = await batchRequest.addURL(resourceURL, { asBinary: true, expectedType: "image" });
|
|
498
|
+
const forbiddenPrefixFound = PREFIXES_FORBIDDEN_DATA_URI.filter(prefixDataURI => content.startsWith(prefixDataURI)).length;
|
|
499
|
+
if (forbiddenPrefixFound) {
|
|
500
|
+
return "";
|
|
501
|
+
}
|
|
502
|
+
return content + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
|
|
503
|
+
} else {
|
|
504
|
+
return "";
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
return "";
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
return resourceURL + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
|
|
511
|
+
}
|
|
512
|
+
}));
|
|
513
|
+
resourceElement.setAttribute("srcset", srcsetValues.join(", "));
|
|
514
|
+
} else {
|
|
515
|
+
resourceElement.setAttribute("srcset", "");
|
|
516
|
+
}
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
testEmptyResource(resource) {
|
|
521
|
+
return resource == util.EMPTY_RESOURCE;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
generateStylesheetContent(stylesheet, options) {
|
|
525
|
+
let stylesheetContent = cssTree.generate(stylesheet);
|
|
526
|
+
if (options.compressCSS) {
|
|
527
|
+
stylesheetContent = util.compressCSS(stylesheetContent);
|
|
528
|
+
}
|
|
529
|
+
if (options.saveOriginalURLs) {
|
|
530
|
+
stylesheetContent = replaceOriginalURLs(stylesheetContent);
|
|
531
|
+
}
|
|
532
|
+
return stylesheetContent;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
replaceImageSource(imgElement, variableName, options) {
|
|
536
|
+
const attributeValue = imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME);
|
|
537
|
+
if (attributeValue) {
|
|
538
|
+
const imageData = options.images[Number(imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME))];
|
|
539
|
+
if (imageData && imageData.replaceable) {
|
|
540
|
+
imgElement.setAttribute("src", `${PREFIX_DATA_URI_IMAGE_SVG},<svg xmlns="http://www.w3.org/2000/svg" width="${imageData.size.pxWidth}" height="${imageData.size.pxHeight}"><rect fill-opacity="0"/></svg>`);
|
|
541
|
+
const backgroundStyle = {};
|
|
542
|
+
const backgroundSize = (imageData.objectFit == "content" || imageData.objectFit == "cover") && imageData.objectFit;
|
|
543
|
+
if (backgroundSize) {
|
|
544
|
+
backgroundStyle["background-size"] = imageData.objectFit;
|
|
545
|
+
}
|
|
546
|
+
if (imageData.objectPosition) {
|
|
547
|
+
backgroundStyle["background-position"] = imageData.objectPosition;
|
|
548
|
+
}
|
|
549
|
+
if (imageData.backgroundColor) {
|
|
550
|
+
backgroundStyle["background-color"] = imageData.backgroundColor;
|
|
551
|
+
}
|
|
552
|
+
this.setBackgroundImage(imgElement, "var(" + variableName + ")", backgroundStyle);
|
|
553
|
+
imgElement.removeAttribute(util.IMAGE_ATTRIBUTE_NAME);
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
wrapMediaQuery(stylesheetContent, mediaQuery) {
|
|
560
|
+
if (mediaQuery) {
|
|
561
|
+
return "@media " + mediaQuery + "{ " + stylesheetContent + " }";
|
|
562
|
+
} else {
|
|
563
|
+
return stylesheetContent;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
getAdditionalPageData() {
|
|
568
|
+
return { };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
removeAlternativeFonts(doc, stylesheets, fonts, fontTests) {
|
|
572
|
+
return removeAlternativeFonts(doc, stylesheets, fonts, fontTests);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async processScript(element, resourceURL) {
|
|
576
|
+
const content = await util.getContent(resourceURL, {
|
|
577
|
+
asBinary: true,
|
|
578
|
+
inline: true,
|
|
579
|
+
charset: this.charset != UTF8_CHARSET && this.charset,
|
|
580
|
+
maxResourceSize: this.options.maxResourceSize,
|
|
581
|
+
maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
|
|
582
|
+
frameId: this.options.windowId,
|
|
583
|
+
resourceReferrer: this.options.resourceReferrer,
|
|
584
|
+
baseURI: this.options.baseURI,
|
|
585
|
+
blockMixedContent: this.options.blockMixedContent,
|
|
586
|
+
expectedType: "script",
|
|
587
|
+
acceptHeaders: this.options.acceptHeaders,
|
|
588
|
+
networkTimeout: this.options.networkTimeout
|
|
589
|
+
});
|
|
590
|
+
content.data = getUpdatedResourceContent(resourceURL, content, this.options);
|
|
591
|
+
element.setAttribute("src", content.data);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
setMetaCSP(metaElement) {
|
|
595
|
+
metaElement.content = "default-src 'none'; font-src 'self' data:; img-src 'self' data:; style-src 'unsafe-inline'; media-src 'self' data:; script-src 'unsafe-inline' data:; object-src 'self' data:; frame-src 'self' data:;";
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
removeUnusedStylesheets(doc) {
|
|
599
|
+
doc.querySelectorAll("link[rel*=stylesheet][rel*=alternate][title]").forEach(element => element.remove());
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function removeAlternativeFonts(doc, stylesheets, fontDeclarations, fontTests) {
|
|
605
|
+
const fontsDetails = {
|
|
606
|
+
fonts: new Map(),
|
|
607
|
+
medias: new Map(),
|
|
608
|
+
supports: new Map()
|
|
609
|
+
};
|
|
610
|
+
const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
|
|
611
|
+
let sheetIndex = 0;
|
|
612
|
+
stylesheets.forEach(stylesheetInfo => {
|
|
613
|
+
const cssRules = stylesheetInfo.stylesheet.children;
|
|
614
|
+
if (cssRules) {
|
|
615
|
+
stats.rules.processed += cssRules.size;
|
|
616
|
+
stats.rules.discarded += cssRules.size;
|
|
617
|
+
if (stylesheetInfo.mediaText && stylesheetInfo.mediaText != MEDIA_ALL) {
|
|
618
|
+
const mediaFontsDetails = createFontsDetailsInfo();
|
|
619
|
+
fontsDetails.medias.set("media-" + sheetIndex + "-" + stylesheetInfo.mediaText, mediaFontsDetails);
|
|
620
|
+
getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails);
|
|
621
|
+
} else {
|
|
622
|
+
getFontsDetails(doc, cssRules, sheetIndex, fontsDetails);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
sheetIndex++;
|
|
626
|
+
});
|
|
627
|
+
processFontDetails(fontsDetails);
|
|
628
|
+
await Promise.all([...stylesheets].map(async ([, stylesheetInfo], sheetIndex) => {
|
|
629
|
+
const cssRules = stylesheetInfo.stylesheet.children;
|
|
630
|
+
const media = stylesheetInfo.mediaText;
|
|
631
|
+
if (cssRules) {
|
|
632
|
+
if (media && media != MEDIA_ALL) {
|
|
633
|
+
await processFontFaceRules(cssRules, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + media), fontDeclarations, fontTests, stats);
|
|
634
|
+
} else {
|
|
635
|
+
await processFontFaceRules(cssRules, sheetIndex, fontsDetails, fontDeclarations, fontTests, stats);
|
|
636
|
+
}
|
|
637
|
+
stats.rules.discarded -= cssRules.size;
|
|
638
|
+
}
|
|
639
|
+
}));
|
|
640
|
+
return stats;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails) {
|
|
644
|
+
let mediaIndex = 0, supportsIndex = 0;
|
|
645
|
+
cssRules.forEach(ruleData => {
|
|
646
|
+
if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
|
|
647
|
+
const mediaText = cssTree.generate(ruleData.prelude);
|
|
648
|
+
const fontsDetails = createFontsDetailsInfo();
|
|
649
|
+
mediaFontsDetails.medias.set("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText, fontsDetails);
|
|
650
|
+
mediaIndex++;
|
|
651
|
+
getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
|
|
652
|
+
} else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
|
|
653
|
+
const supportsText = cssTree.generate(ruleData.prelude);
|
|
654
|
+
const fontsDetails = createFontsDetailsInfo();
|
|
655
|
+
mediaFontsDetails.supports.set("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText, fontsDetails);
|
|
656
|
+
supportsIndex++;
|
|
657
|
+
getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
|
|
658
|
+
} else if (ruleData.type == "Atrule" && ruleData.name == "font-face" && ruleData.block && ruleData.block.children) {
|
|
659
|
+
const fontKey = getFontKey(ruleData);
|
|
660
|
+
let fontInfo = mediaFontsDetails.fonts.get(fontKey);
|
|
661
|
+
if (!fontInfo) {
|
|
662
|
+
fontInfo = [];
|
|
663
|
+
mediaFontsDetails.fonts.set(fontKey, fontInfo);
|
|
664
|
+
}
|
|
665
|
+
const src = getPropertyValue(ruleData, "src");
|
|
666
|
+
if (src) {
|
|
667
|
+
const fontSources = src.match(REGEXP_URL_FUNCTION);
|
|
668
|
+
if (fontSources) {
|
|
669
|
+
fontSources.forEach(source => fontInfo.unshift(source));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function processFontDetails(fontsDetails) {
|
|
677
|
+
fontsDetails.fonts.forEach((fontInfo, fontKey) => {
|
|
678
|
+
fontsDetails.fonts.set(fontKey, fontInfo.map(fontSource => {
|
|
679
|
+
const fontFormatMatch = fontSource.match(REGEXP_FONT_FORMAT_VALUE);
|
|
680
|
+
let fontFormat;
|
|
681
|
+
const fontUrl = getURL(fontSource);
|
|
682
|
+
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
683
|
+
fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
|
|
684
|
+
}
|
|
685
|
+
if (!fontFormat) {
|
|
686
|
+
const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF);
|
|
687
|
+
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
688
|
+
fontFormat = fontFormatMatch[1];
|
|
689
|
+
} else {
|
|
690
|
+
const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF_ALT);
|
|
691
|
+
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
692
|
+
fontFormat = fontFormatMatch[1];
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (!fontFormat && fontUrl) {
|
|
697
|
+
const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
|
|
698
|
+
if (fontFormatMatch && fontFormatMatch[1]) {
|
|
699
|
+
fontFormat = fontFormatMatch[1];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return { src: fontSource.match(REGEXP_FONT_SRC)[1], fontUrl, format: fontFormat };
|
|
703
|
+
}));
|
|
704
|
+
});
|
|
705
|
+
fontsDetails.medias.forEach(mediaFontsDetails => processFontDetails(mediaFontsDetails));
|
|
706
|
+
fontsDetails.supports.forEach(supportsFontsDetails => processFontDetails(supportsFontsDetails));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function processFontFaceRules(cssRules, sheetIndex, fontsDetails, fontDeclarations, fontTests, stats) {
|
|
710
|
+
const removedRules = [];
|
|
711
|
+
let mediaIndex = 0, supportsIndex = 0;
|
|
712
|
+
for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
|
|
713
|
+
const ruleData = cssRule.data;
|
|
714
|
+
if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
|
|
715
|
+
const mediaText = cssTree.generate(ruleData.prelude);
|
|
716
|
+
await processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText), fontDeclarations, fontTests, stats);
|
|
717
|
+
mediaIndex++;
|
|
718
|
+
} else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
|
|
719
|
+
const supportsText = cssTree.generate(ruleData.prelude);
|
|
720
|
+
await processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.supports.get("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText), fontDeclarations, fontTests, stats);
|
|
721
|
+
supportsIndex++;
|
|
722
|
+
} else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
|
|
723
|
+
const key = getFontKey(ruleData);
|
|
724
|
+
const fontInfo = fontsDetails.fonts.get(key);
|
|
725
|
+
if (fontInfo) {
|
|
726
|
+
const processed = await processFontFaceRule(ruleData, fontInfo, fontDeclarations, fontTests, stats);
|
|
727
|
+
if (processed) {
|
|
728
|
+
fontsDetails.fonts.delete(key);
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
removedRules.push(cssRule);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
removedRules.forEach(cssRule => cssRules.remove(cssRule));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function processFontFaceRule(ruleData, fontInfo, fontDeclarations, fontTests, stats) {
|
|
739
|
+
const removedNodes = [];
|
|
740
|
+
for (let node = ruleData.block.children.head; node; node = node.next) {
|
|
741
|
+
if (node.data.property == "src") {
|
|
742
|
+
removedNodes.push(node);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
removedNodes.pop();
|
|
746
|
+
removedNodes.forEach(node => ruleData.block.children.remove(node));
|
|
747
|
+
const srcDeclaration = ruleData.block.children.filter(node => node.property == "src").tail;
|
|
748
|
+
if (srcDeclaration) {
|
|
749
|
+
await Promise.all(fontInfo.map(async source => {
|
|
750
|
+
if (fontTests.has(source.src)) {
|
|
751
|
+
source.valid = fontTests.get(source.src);
|
|
752
|
+
} else {
|
|
753
|
+
if (FontFace && source.fontUrl) {
|
|
754
|
+
const fontFace = new FontFace("test-font", source.src);
|
|
755
|
+
try {
|
|
756
|
+
let timeout;
|
|
757
|
+
await Promise.race([
|
|
758
|
+
fontFace.load().then(() => fontFace.loaded).then(() => { source.valid = true; globalThis.clearTimeout(timeout); }),
|
|
759
|
+
new Promise(resolve => timeout = globalThis.setTimeout(() => { source.valid = true; resolve(); }, FONT_MAX_LOAD_DELAY))
|
|
760
|
+
]);
|
|
761
|
+
} catch (error) {
|
|
762
|
+
const urlNodes = cssTree.findAll(srcDeclaration.data, node => node.type == "Url");
|
|
763
|
+
const declarationFontURLs = Array.from(fontDeclarations).find(([node]) => urlNodes.includes(node) && node.value == source.fontUrl);
|
|
764
|
+
if (declarationFontURLs && declarationFontURLs[1].length) {
|
|
765
|
+
const fontURL = declarationFontURLs[1][0];
|
|
766
|
+
if (fontURL) {
|
|
767
|
+
const fontFace = new FontFace("test-font", "url(" + fontURL + ")");
|
|
768
|
+
try {
|
|
769
|
+
let timeout;
|
|
770
|
+
await Promise.race([
|
|
771
|
+
fontFace.load().then(() => fontFace.loaded).then(() => { source.valid = true; globalThis.clearTimeout(timeout); }),
|
|
772
|
+
new Promise(resolve => timeout = globalThis.setTimeout(() => { source.valid = true; resolve(); }, FONT_MAX_LOAD_DELAY))
|
|
773
|
+
]);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
// ignored
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
source.valid = true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
source.valid = true;
|
|
784
|
+
}
|
|
785
|
+
fontTests.set(source.src, source.valid);
|
|
786
|
+
}
|
|
787
|
+
}));
|
|
788
|
+
const findSourceByFormat = (fontFormat, testValidity) => fontInfo.find(source => !source.src.match(EMPTY_URL_SOURCE) && source.format == fontFormat && (!testValidity || source.valid));
|
|
789
|
+
const filterSources = fontSource => fontInfo.filter(source => source == fontSource || source.src.startsWith(LOCAL_SOURCE));
|
|
790
|
+
stats.fonts.processed += fontInfo.length;
|
|
791
|
+
stats.fonts.discarded += fontInfo.length;
|
|
792
|
+
const woffFontFound =
|
|
793
|
+
findSourceByFormat("woff2-variations", true) || findSourceByFormat("woff2", true) || findSourceByFormat("woff", true);
|
|
794
|
+
if (woffFontFound) {
|
|
795
|
+
fontInfo = filterSources(woffFontFound);
|
|
796
|
+
} else {
|
|
797
|
+
const ttfFontFound =
|
|
798
|
+
findSourceByFormat("truetype-variations", true) || findSourceByFormat("truetype", true);
|
|
799
|
+
if (ttfFontFound) {
|
|
800
|
+
fontInfo = filterSources(ttfFontFound);
|
|
801
|
+
} else {
|
|
802
|
+
const otfFontFound =
|
|
803
|
+
findSourceByFormat("opentype") || findSourceByFormat("embedded-opentype");
|
|
804
|
+
if (otfFontFound) {
|
|
805
|
+
fontInfo = filterSources(otfFontFound);
|
|
806
|
+
} else {
|
|
807
|
+
fontInfo = fontInfo.filter(source => !source.src.match(EMPTY_URL_SOURCE) && (source.valid) || source.src.startsWith(LOCAL_SOURCE));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
stats.fonts.discarded -= fontInfo.length;
|
|
812
|
+
fontInfo.reverse();
|
|
813
|
+
try {
|
|
814
|
+
srcDeclaration.data.value = cssTree.parse(fontInfo.map(fontSource => fontSource.src).join(","), { context: "value", parseCustomProperty: true });
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
// ignored
|
|
818
|
+
}
|
|
819
|
+
return true;
|
|
820
|
+
} else {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function getPropertyValue(ruleData, propertyName) {
|
|
826
|
+
let property;
|
|
827
|
+
if (ruleData.block.children) {
|
|
828
|
+
property = ruleData.block.children.filter(node => {
|
|
829
|
+
try {
|
|
830
|
+
return node.property == propertyName && !cssTree.generate(node.value).match(/\\9$/);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
return node.property == propertyName;
|
|
833
|
+
}
|
|
834
|
+
}).tail;
|
|
835
|
+
}
|
|
836
|
+
if (property) {
|
|
837
|
+
try {
|
|
838
|
+
return cssTree.generate(property.data.value);
|
|
839
|
+
} catch (error) {
|
|
840
|
+
// ignored
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function getFontKey(ruleData) {
|
|
846
|
+
return JSON.stringify([
|
|
847
|
+
normalizeFontFamily(getPropertyValue(ruleData, "font-family")),
|
|
848
|
+
getFontWeight(getPropertyValue(ruleData, "font-weight") || "400"),
|
|
849
|
+
getPropertyValue(ruleData, "font-style") || "normal",
|
|
850
|
+
getPropertyValue(ruleData, "unicode-range"),
|
|
851
|
+
getFontStretch(getPropertyValue(ruleData, "font-stretch")),
|
|
852
|
+
getPropertyValue(ruleData, "font-variant") || "normal",
|
|
853
|
+
getPropertyValue(ruleData, "font-feature-settings"),
|
|
854
|
+
getPropertyValue(ruleData, "font-variation-settings")
|
|
855
|
+
]);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function getFontStretch(stretch) {
|
|
859
|
+
return FONT_STRETCHES[stretch] || stretch;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function createFontsDetailsInfo() {
|
|
863
|
+
return {
|
|
864
|
+
fonts: new Map(),
|
|
865
|
+
medias: new Map(),
|
|
866
|
+
supports: new Map()
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function getURL(urlFunction) {
|
|
871
|
+
urlFunction = urlFunction.replace(/url\(-sf-url-original\\\(\\"(.*?)\\"\\\)\\ /g, "");
|
|
872
|
+
const urlMatch = urlFunction.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
|
|
873
|
+
urlFunction.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
|
|
874
|
+
urlFunction.match(REGEXP_URL_NO_QUOTES_FN);
|
|
875
|
+
return urlMatch && urlMatch[1];
|
|
876
|
+
}
|