single-file-core 1.0.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.
@@ -0,0 +1,412 @@
1
+ /*
2
+ * Copyright 2010-2020 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, URLSearchParams */
25
+
26
+ import * as vendor from "./vendor/index.js";
27
+ import * as modules from "./modules/index.js";
28
+ import * as helper from "./single-file-helper.js";
29
+
30
+ const DEBUG = false;
31
+ const ONE_MB = 1024 * 1024;
32
+ const PREFIX_CONTENT_TYPE_TEXT = "text/";
33
+ const DEFAULT_REPLACED_CHARACTERS = ["~", "+", "\\\\", "?", "%", "*", ":", "|", "\"", "<", ">", "\x00-\x1f", "\x7F"];
34
+ const DEFAULT_REPLACEMENT_CHARACTER = "_";
35
+
36
+ const URL = globalThis.URL;
37
+ const DOMParser = globalThis.DOMParser;
38
+ const Blob = globalThis.Blob;
39
+ const FileReader = globalThis.FileReader;
40
+ const fetch = (url, options) => globalThis.fetch(url, options);
41
+ const crypto = globalThis.crypto;
42
+ const TextDecoder = globalThis.TextDecoder;
43
+ const TextEncoder = globalThis.TextEncoder;
44
+
45
+ export {
46
+ getInstance
47
+ };
48
+
49
+ function getInstance(utilOptions) {
50
+ utilOptions = utilOptions || {};
51
+ utilOptions.fetch = utilOptions.fetch || fetch;
52
+ utilOptions.frameFetch = utilOptions.frameFetch || utilOptions.fetch || fetch;
53
+ return {
54
+ getContent,
55
+ parseURL(resourceURL, baseURI) {
56
+ if (baseURI === undefined) {
57
+ return new URL(resourceURL);
58
+ } else {
59
+ return new URL(resourceURL, baseURI);
60
+ }
61
+ },
62
+ resolveURL(resourceURL, baseURI) {
63
+ return this.parseURL(resourceURL, baseURI).href;
64
+ },
65
+ getSearchParams(searchParams) {
66
+ return Array.from(new URLSearchParams(searchParams));
67
+ },
68
+ getValidFilename(filename, replacedCharacters = DEFAULT_REPLACED_CHARACTERS, replacementCharacter = DEFAULT_REPLACEMENT_CHARACTER) {
69
+ replacedCharacters.forEach(replacedCharacter => filename = filename.replace(new RegExp("[" + replacedCharacter + "]+", "g"), replacementCharacter));
70
+ filename = filename
71
+ .replace(/\.\.\//g, "")
72
+ .replace(/^\/+/, "")
73
+ .replace(/\/+/g, "/")
74
+ .replace(/\/$/, "")
75
+ .replace(/\.$/, "")
76
+ .replace(/\.\//g, "." + replacementCharacter)
77
+ .replace(/\/\./g, "/" + replacementCharacter);
78
+ return filename;
79
+ },
80
+ parseDocContent(content, baseURI) {
81
+ const doc = (new DOMParser()).parseFromString(content, "text/html");
82
+ if (!doc.head) {
83
+ doc.documentElement.insertBefore(doc.createElement("HEAD"), doc.body);
84
+ }
85
+ let baseElement = doc.querySelector("base");
86
+ if (!baseElement || !baseElement.getAttribute("href")) {
87
+ if (baseElement) {
88
+ baseElement.remove();
89
+ }
90
+ baseElement = doc.createElement("base");
91
+ baseElement.setAttribute("href", baseURI);
92
+ doc.head.insertBefore(baseElement, doc.head.firstChild);
93
+ }
94
+ return doc;
95
+ },
96
+ parseXMLContent(content) {
97
+ return (new DOMParser()).parseFromString(content, "text/xml");
98
+ },
99
+ parseSVGContent(content) {
100
+ const doc = (new DOMParser()).parseFromString(content, "image/svg+xml");
101
+ if (doc.querySelector("parsererror")) {
102
+ return (new DOMParser()).parseFromString(content, "text/html");
103
+ } else {
104
+ return doc;
105
+ }
106
+ },
107
+ async digest(algo, text) {
108
+ try {
109
+ const hash = await crypto.subtle.digest(algo, new TextEncoder("utf-8").encode(text));
110
+ return hex(hash);
111
+ } catch (error) {
112
+ return "";
113
+ }
114
+ },
115
+ getContentSize(content) {
116
+ return new Blob([content]).size;
117
+ },
118
+ truncateText(content, maxSize) {
119
+ const blob = new Blob([content]);
120
+ const reader = new FileReader();
121
+ reader.readAsText(blob.slice(0, maxSize));
122
+ return new Promise((resolve, reject) => {
123
+ reader.addEventListener("load", () => {
124
+ if (content.startsWith(reader.result)) {
125
+ resolve(reader.result);
126
+ } else {
127
+ this.truncateText(content, maxSize - 1).then(resolve).catch(reject);
128
+ }
129
+ }, false);
130
+ reader.addEventListener("error", reject, false);
131
+ });
132
+ },
133
+ minifyHTML(doc, options) {
134
+ return modules.htmlMinifier.process(doc, options);
135
+ },
136
+ minifyCSSRules(stylesheets, styles, mediaAllInfo) {
137
+ return modules.cssRulesMinifier.process(stylesheets, styles, mediaAllInfo);
138
+ },
139
+ removeUnusedFonts(doc, stylesheets, styles, options) {
140
+ return modules.fontsMinifier.process(doc, stylesheets, styles, options);
141
+ },
142
+ removeAlternativeFonts(doc, stylesheets, fontDeclarations, fontTests) {
143
+ return modules.fontsAltMinifier.process(doc, stylesheets, fontDeclarations, fontTests);
144
+ },
145
+ getMediaAllInfo(doc, stylesheets, styles) {
146
+ return modules.matchedRules.getMediaAllInfo(doc, stylesheets, styles);
147
+ },
148
+ compressCSS(content, options) {
149
+ return vendor.cssMinifier.processString(content, options);
150
+ },
151
+ minifyMedias(stylesheets) {
152
+ return modules.mediasAltMinifier.process(stylesheets);
153
+ },
154
+ removeAlternativeImages(doc) {
155
+ return modules.imagesAltMinifier.process(doc);
156
+ },
157
+ parseSrcset(srcset) {
158
+ return vendor.srcsetParser.process(srcset);
159
+ },
160
+ preProcessDoc(doc, win, options) {
161
+ return helper.preProcessDoc(doc, win, options);
162
+ },
163
+ postProcessDoc(doc, markedElements, invalidElements) {
164
+ helper.postProcessDoc(doc, markedElements, invalidElements);
165
+ },
166
+ serialize(doc, compressHTML) {
167
+ return modules.serializer.process(doc, compressHTML);
168
+ },
169
+ removeQuotes(string) {
170
+ return helper.removeQuotes(string);
171
+ },
172
+ ON_BEFORE_CAPTURE_EVENT_NAME: helper.ON_BEFORE_CAPTURE_EVENT_NAME,
173
+ ON_AFTER_CAPTURE_EVENT_NAME: helper.ON_AFTER_CAPTURE_EVENT_NAME,
174
+ WIN_ID_ATTRIBUTE_NAME: helper.WIN_ID_ATTRIBUTE_NAME,
175
+ REMOVED_CONTENT_ATTRIBUTE_NAME: helper.REMOVED_CONTENT_ATTRIBUTE_NAME,
176
+ HIDDEN_CONTENT_ATTRIBUTE_NAME: helper.HIDDEN_CONTENT_ATTRIBUTE_NAME,
177
+ HIDDEN_FRAME_ATTRIBUTE_NAME: helper.HIDDEN_FRAME_ATTRIBUTE_NAME,
178
+ IMAGE_ATTRIBUTE_NAME: helper.IMAGE_ATTRIBUTE_NAME,
179
+ POSTER_ATTRIBUTE_NAME: helper.POSTER_ATTRIBUTE_NAME,
180
+ VIDEO_ATTRIBUTE_NAME: helper.VIDEO_ATTRIBUTE_NAME,
181
+ CANVAS_ATTRIBUTE_NAME: helper.CANVAS_ATTRIBUTE_NAME,
182
+ HTML_IMPORT_ATTRIBUTE_NAME: helper.HTML_IMPORT_ATTRIBUTE_NAME,
183
+ STYLE_ATTRIBUTE_NAME: helper.STYLE_ATTRIBUTE_NAME,
184
+ INPUT_VALUE_ATTRIBUTE_NAME: helper.INPUT_VALUE_ATTRIBUTE_NAME,
185
+ SHADOW_ROOT_ATTRIBUTE_NAME: helper.SHADOW_ROOT_ATTRIBUTE_NAME,
186
+ PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME: helper.PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME,
187
+ STYLESHEET_ATTRIBUTE_NAME: helper.STYLESHEET_ATTRIBUTE_NAME,
188
+ SELECTED_CONTENT_ATTRIBUTE_NAME: helper.SELECTED_CONTENT_ATTRIBUTE_NAME,
189
+ COMMENT_HEADER: helper.COMMENT_HEADER,
190
+ COMMENT_HEADER_LEGACY: helper.COMMENT_HEADER_LEGACY,
191
+ SINGLE_FILE_UI_ELEMENT_CLASS: helper.SINGLE_FILE_UI_ELEMENT_CLASS,
192
+ EMPTY_RESOURCE: helper.EMPTY_RESOURCE
193
+ };
194
+
195
+ async function getContent(resourceURL, options) {
196
+ let response, startTime, networkTimeoutId, networkTimeoutPromise, resolveNetworkTimeoutPromise;
197
+ const fetchResource = utilOptions.fetch;
198
+ const fetchFrameResource = utilOptions.frameFetch;
199
+ if (DEBUG) {
200
+ startTime = Date.now();
201
+ log(" // STARTED download url =", resourceURL, "asBinary =", options.asBinary);
202
+ }
203
+ if (options.blockMixedContent && /^https:/i.test(options.baseURI) && !/^https:/i.test(resourceURL)) {
204
+ return getFetchResponse(resourceURL, options);
205
+ }
206
+ if (options.networkTimeout) {
207
+ networkTimeoutPromise = new Promise((resolve, reject) => {
208
+ resolveNetworkTimeoutPromise = resolve;
209
+ networkTimeoutId = globalThis.setTimeout(() => reject(new Error("network timeout")), options.networkTimeout);
210
+ });
211
+ } else {
212
+ networkTimeoutPromise = new Promise(resolve => {
213
+ resolveNetworkTimeoutPromise = resolve;
214
+ });
215
+ }
216
+ try {
217
+ const accept = options.acceptHeaders ? options.acceptHeaders[options.expectedType] : "*/*";
218
+ if (options.frameId) {
219
+ try {
220
+ response = await Promise.race([
221
+ fetchFrameResource(resourceURL, { frameId: options.frameId, referrer: options.resourceReferrer, headers: { accept } }),
222
+ networkTimeoutPromise
223
+ ]);
224
+ } catch (error) {
225
+ response = await Promise.race([
226
+ fetchResource(resourceURL, { headers: { accept } }),
227
+ networkTimeoutPromise
228
+ ]);
229
+ }
230
+ } else {
231
+ response = await Promise.race([
232
+ fetchResource(resourceURL, { referrer: options.resourceReferrer, headers: { accept } }),
233
+ networkTimeoutPromise
234
+ ]);
235
+ }
236
+ } catch (error) {
237
+ return getFetchResponse(resourceURL, options);
238
+ } finally {
239
+ resolveNetworkTimeoutPromise();
240
+ if (options.networkTimeout) {
241
+ globalThis.clearTimeout(networkTimeoutId);
242
+ }
243
+ }
244
+ let buffer;
245
+ try {
246
+ buffer = await response.arrayBuffer();
247
+ } catch (error) {
248
+ return { data: options.asBinary ? helper.EMPTY_RESOURCE : "", resourceURL };
249
+ }
250
+ resourceURL = response.url || resourceURL;
251
+ let contentType = "", charset;
252
+ try {
253
+ const mimeType = new vendor.MIMEType(response.headers.get("content-type"));
254
+ contentType = mimeType.type + "/" + mimeType.subtype;
255
+ charset = mimeType.parameters.get("charset");
256
+ } catch (error) {
257
+ // ignored
258
+ }
259
+ if (!contentType) {
260
+ contentType = guessMIMEType(options.expectedType, buffer);
261
+ }
262
+ if (!charset && options.charset) {
263
+ charset = options.charset;
264
+ }
265
+ if (options.asBinary) {
266
+ if (response.status >= 400) {
267
+ return getFetchResponse(resourceURL, options);
268
+ }
269
+ try {
270
+ if (DEBUG) {
271
+ log(" // ENDED download url =", resourceURL, "delay =", Date.now() - startTime);
272
+ }
273
+ if (options.maxResourceSizeEnabled && buffer.byteLength > options.maxResourceSize * ONE_MB) {
274
+ return getFetchResponse(resourceURL, options);
275
+ } else {
276
+ return getFetchResponse(resourceURL, options, buffer, null, contentType);
277
+ }
278
+ } catch (error) {
279
+ return getFetchResponse(resourceURL, options);
280
+ }
281
+ } else {
282
+ if (response.status >= 400 || (options.validateTextContentType && contentType && !contentType.startsWith(PREFIX_CONTENT_TYPE_TEXT))) {
283
+ return getFetchResponse(resourceURL, options);
284
+ }
285
+ if (!charset) {
286
+ charset = "utf-8";
287
+ }
288
+ if (DEBUG) {
289
+ log(" // ENDED download url =", resourceURL, "delay =", Date.now() - startTime);
290
+ }
291
+ if (options.maxResourceSizeEnabled && buffer.byteLength > options.maxResourceSize * ONE_MB) {
292
+ return getFetchResponse(resourceURL, options, null, charset);
293
+ } else {
294
+ try {
295
+ return getFetchResponse(resourceURL, options, buffer, charset, contentType);
296
+ } catch (error) {
297
+ return getFetchResponse(resourceURL, options, null, charset);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ async function getFetchResponse(resourceURL, options, data, charset, contentType) {
305
+ if (data) {
306
+ if (options.asBinary) {
307
+ const reader = new FileReader();
308
+ reader.readAsDataURL(new Blob([data], { type: contentType + (options.charset ? ";charset=" + options.charset : "") }));
309
+ data = await new Promise((resolve, reject) => {
310
+ reader.addEventListener("load", () => resolve(reader.result), false);
311
+ reader.addEventListener("error", reject, false);
312
+ });
313
+ } else {
314
+ const firstBytes = new Uint8Array(data.slice(0, 4));
315
+ if (firstBytes[0] == 132 && firstBytes[1] == 49 && firstBytes[2] == 149 && firstBytes[3] == 51) {
316
+ charset = "gb18030";
317
+ } else if (firstBytes[0] == 255 && firstBytes[1] == 254) {
318
+ charset = "utf-16le";
319
+ } else if (firstBytes[0] == 254 && firstBytes[1] == 255) {
320
+ charset = "utf-16be";
321
+ }
322
+ try {
323
+ data = new TextDecoder(charset).decode(data);
324
+ } catch (error) {
325
+ charset = "utf-8";
326
+ data = new TextDecoder(charset).decode(data);
327
+ }
328
+ }
329
+ } else {
330
+ data = options.asBinary ? helper.EMPTY_RESOURCE : "";
331
+ }
332
+ return { data, resourceURL, charset };
333
+ }
334
+
335
+ function guessMIMEType(expectedType, buffer) {
336
+ if (expectedType == "image") {
337
+ if (compareBytes([255, 255, 255, 255], [0, 0, 1, 0])) {
338
+ return "image/x-icon";
339
+ }
340
+ if (compareBytes([255, 255, 255, 255], [0, 0, 2, 0])) {
341
+ return "image/x-icon";
342
+ }
343
+ if (compareBytes([255, 255], [78, 77])) {
344
+ return "image/bmp";
345
+ }
346
+ if (compareBytes([255, 255, 255, 255, 255, 255], [71, 73, 70, 56, 57, 97])) {
347
+ return "image/gif";
348
+ }
349
+ if (compareBytes([255, 255, 255, 255, 255, 255], [71, 73, 70, 56, 59, 97])) {
350
+ return "image/gif";
351
+ }
352
+ if (compareBytes([255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255], [82, 73, 70, 70, 0, 0, 0, 0, 87, 69, 66, 80, 86, 80])) {
353
+ return "image/webp";
354
+ }
355
+ if (compareBytes([255, 255, 255, 255, 255, 255, 255, 255], [137, 80, 78, 71, 13, 10, 26, 10])) {
356
+ return "image/png";
357
+ }
358
+ if (compareBytes([255, 255, 255], [255, 216, 255])) {
359
+ return "image/jpeg";
360
+ }
361
+ }
362
+ if (expectedType == "font") {
363
+ if (compareBytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255],
364
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 76, 80])) {
365
+ return "application/vnd.ms-fontobject";
366
+ }
367
+ if (compareBytes([255, 255, 255, 255], [0, 1, 0, 0])) {
368
+ return "font/ttf";
369
+ }
370
+ if (compareBytes([255, 255, 255, 255], [79, 84, 84, 79])) {
371
+ return "font/otf";
372
+ }
373
+ if (compareBytes([255, 255, 255, 255], [116, 116, 99, 102])) {
374
+ return "font/collection";
375
+ }
376
+ if (compareBytes([255, 255, 255, 255], [119, 79, 70, 70])) {
377
+ return "font/woff";
378
+ }
379
+ if (compareBytes([255, 255, 255, 255], [119, 79, 70, 50])) {
380
+ return "font/woff2";
381
+ }
382
+ }
383
+
384
+ function compareBytes(mask, pattern) {
385
+ let patternMatch = true, index = 0;
386
+ if (buffer.byteLength >= pattern.length) {
387
+ const value = new Uint8Array(buffer, 0, mask.length);
388
+ for (index = 0; index < mask.length && patternMatch; index++) {
389
+ patternMatch = patternMatch && ((value[index] & mask[index]) == pattern[index]);
390
+ }
391
+ return patternMatch;
392
+ }
393
+ }
394
+ }
395
+
396
+ // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
397
+ function hex(buffer) {
398
+ const hexCodes = [];
399
+ const view = new DataView(buffer);
400
+ for (let i = 0; i < view.byteLength; i += 4) {
401
+ const value = view.getUint32(i);
402
+ const stringValue = value.toString(16);
403
+ const padding = "00000000";
404
+ const paddedValue = (padding + stringValue).slice(-padding.length);
405
+ hexCodes.push(paddedValue);
406
+ }
407
+ return hexCodes.join("");
408
+ }
409
+
410
+ function log(...args) {
411
+ console.log("S-File <browser>", ...args); // eslint-disable-line no-console
412
+ }