single-file-core 1.5.119 → 1.5.120
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/index.js +4 -0
- package/doc/singlefile-archive.md +323 -135
- package/eslint.config.mjs +6 -0
- package/package.json +2 -2
- package/processors/compression/compression-display.js +0 -11
- package/processors/compression/compression-extract.js +0 -4
- package/processors/compression/compression-packager.js +0 -12
- package/processors/compression/compression-router.js +0 -35
- package/processors/compression/compression.js +73 -176
- package/processors/hooks/content/content-hooks-frames-web.js +2 -1
- package/processors/lazy/content/content-lazy-loader.js +21 -18
- package/test/sfz-harness/README.md +10 -1
- package/test/sfz-harness/byte-map.js +137 -0
- package/test/sfz-harness/format-rules.js +132 -2
- package/test/sfz-harness/option-wiring.js +0 -3
- package/test/sfz-harness/zip64.js +77 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
BlobReader,
|
|
28
28
|
TextReader,
|
|
29
29
|
ZipWriter,
|
|
30
|
+
Uint8ArrayReader,
|
|
30
31
|
Uint8ArrayWriter
|
|
31
32
|
} from "./../../vendor/zip/zip.js";
|
|
32
33
|
import {
|
|
@@ -41,29 +42,12 @@ import {
|
|
|
41
42
|
|
|
42
43
|
const { Blob, fetch, TextEncoder, TextDecoder, DOMParser } = globalThis;
|
|
43
44
|
|
|
44
|
-
// windows-1252 never decodes bytes >= 0x80 into the ASCII range, the scanned patterns are all ASCII
|
|
45
45
|
const TEXT_DECODER = new TextDecoder("windows-1252");
|
|
46
46
|
|
|
47
|
-
// the extension is only a guess when it comes from the URL, and a wrong one costs the whole
|
|
48
|
-
// gain: a script served as text/javascript from a ".ts" URL was stored uncompressed at 2260
|
|
49
|
-
// bytes where the same bytes named ".js" deflate to 70. A textual content type is authoritative
|
|
50
|
-
// when the server sent one, the extension list decides everything else
|
|
51
47
|
const COMPRESSIBLE_CONTENT_TYPES = ["application/javascript", "application/x-javascript", "application/ecmascript", "application/json", "application/ld+json", "application/manifest+json", "application/xml", "application/xhtml+xml", "application/rss+xml", "application/atom+xml", "image/svg+xml"];
|
|
52
48
|
const TEXT_CONTENT_TYPE_PREFIX = "text/";
|
|
53
49
|
const NO_COMPRESSION_EXTENSIONS = [".jpg", ".jpeg", ".png", ".apng", ".gif", ".webp", ".avif", ".heif", ".heic", ".jxl", ".pdf", ".woff", ".woff2", ".mp4", ".webm", ".avi", ".mpeg", ".mov", ".ts", ".ogv", ".mp3", ".ogg", ".oga", ".weba", ".m4a", ".aac", ".opus", ".flac"];
|
|
54
50
|
const SCRIPT_PATH = "/lib/single-file-zip.min.js";
|
|
55
|
-
// <noscript> is excluded: it is the only tag whose content is raw text when scripting is
|
|
56
|
-
// enabled and markup when it is not, so the archive bytes would be parsed on a page opened
|
|
57
|
-
// without scripting
|
|
58
|
-
// the script and style rungs come first because text extractors drop their content the way
|
|
59
|
-
// they drop a comment: macOS Spotlight indexes the content of every other rung, and textutil
|
|
60
|
-
// also reads the last two. Neither is applied nor executed, the type is neither CSS nor
|
|
61
|
-
// JavaScript. <plaintext> stays last, it is the only rung that cannot be closed
|
|
62
|
-
// the CDATA section sits second to last, and it is the one rung a payload is unlikely to hold
|
|
63
|
-
// the terminator of: every other rung ends on a sequence that real documents carry, which is
|
|
64
|
-
// also why each level of self-nesting burns one. It is low in the ladder only because text
|
|
65
|
-
// extractors read its content; nothing about the parse is weaker. A CDATA section is only a
|
|
66
|
-
// CDATA section in foreign content, hence the <svg> element around it
|
|
67
51
|
const EXTRA_DATA_TAGS = [
|
|
68
52
|
["<script type=sfz-data>", "</script>"],
|
|
69
53
|
["<style type=sfz-data>", "</style>"],
|
|
@@ -78,8 +62,6 @@ const EMBEDDED_DATA_TAGS = [
|
|
|
78
62
|
["<!--", "-->"],
|
|
79
63
|
...EXTRA_DATA_TAGS,
|
|
80
64
|
];
|
|
81
|
-
// the identifier the extractor addresses the zip data with; the faces hidden by the same
|
|
82
|
-
// wrapper ladder must never carry it, they are located by byte structure instead
|
|
83
65
|
const DATA_IDENTIFIER = "sfz-data";
|
|
84
66
|
const EXTRA_DATA_REGEXPS = [
|
|
85
67
|
[/<script/i, /<\/script[\t\n\f\r />]/i],
|
|
@@ -88,17 +70,9 @@ const EXTRA_DATA_REGEXPS = [
|
|
|
88
70
|
[/<noembed/i, /<\/noembed[\t\n\f\r />]/i],
|
|
89
71
|
[/<iframe/i, /<\/iframe[\t\n\f\r />]/i],
|
|
90
72
|
[/<xmp/i, /<\/xmp[\t\n\f\r />]/i],
|
|
91
|
-
// a CDATA section ends on "]]>" and nothing else, so the terminator is the whole test; the
|
|
92
|
-
// start pattern is the same conservatism the raw text rungs get, since a nested "<![CDATA["
|
|
93
|
-
// is text like any other. Trailing brackets are safe: a payload ending "]]" against the
|
|
94
|
-
// writer's "]]>" gives "]]]]>", and the tokenizer emits the payload's own two before closing
|
|
95
73
|
[/<!\[CDATA\[/i, /\]\]>/],
|
|
96
74
|
[/<plaintext/i, /<\/plaintext[\t\n\f\r />]/i]
|
|
97
75
|
];
|
|
98
|
-
// a comment must also not end with "<!-", the last of the restrictions HTML puts on comment
|
|
99
|
-
// text. The remaining one, that it must not start with ">" or "->", is not a matter of what
|
|
100
|
-
// the payload contains: the zip data starts with the identifier and the PDF with a signature,
|
|
101
|
-
// while the image data starts with a checksum, tested where that checksum is computed
|
|
102
76
|
const EMBEDDED_DATA_REGEXPS = [
|
|
103
77
|
[/<!--/i, /--!?>|<!-$/i],
|
|
104
78
|
...EXTRA_DATA_REGEXPS,
|
|
@@ -117,7 +91,8 @@ const PNG_IHDR_LENGTH = 25;
|
|
|
117
91
|
const COMMENT_LENGTH_FIELD_LENGTH = 2;
|
|
118
92
|
const MAX_APPENDED_DATA_LENGTH = 65535;
|
|
119
93
|
const PDF_ENTRY_FILENAME = "page.pdf";
|
|
120
|
-
const
|
|
94
|
+
const PRESCAN_WINDOW_LENGTH = 1024;
|
|
95
|
+
const PNG_TEXT_CHUNK_HEADER_LENGTH = 12;
|
|
121
96
|
const MINIMAL_DOCTYPE = "<!DOCTYPE html>";
|
|
122
97
|
const UNHIDDEN_FACE_WARNING_MESSAGE = "SingleFile: the page data contains every HTML tag that could hide an embedded file, the archive was written without its";
|
|
123
98
|
const EMBEDDED_IMAGE_LABEL = "PNG image";
|
|
@@ -127,14 +102,10 @@ const CENTRAL_FILE_HEADER_SIGNATURE = 0x02014b50;
|
|
|
127
102
|
const END_OF_CENTRAL_DIR_SIGNATURE = 0x06054b50;
|
|
128
103
|
const ZIP64_END_OF_CENTRAL_DIR_SIGNATURE = 0x06064b50;
|
|
129
104
|
const ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE = 0x07064b50;
|
|
105
|
+
const LANGUAGE_ENCODING_FLAG = 0x0800;
|
|
130
106
|
|
|
131
107
|
const browser = globalThis.browser;
|
|
132
108
|
|
|
133
|
-
// the options process() accepts from its caller. single-file.js builds its argument from this
|
|
134
|
-
// list instead of a literal, because an option added here and forgotten there is undefined at
|
|
135
|
-
// every call site and the feature silently does nothing: that is how declareAppendedData and
|
|
136
|
-
// includeBOM both shipped inert. Options the module sets on itself between passes, and options
|
|
137
|
-
// the packager supplies, are deliberately absent
|
|
138
109
|
const PROCESS_OPTION_NAMES = [
|
|
139
110
|
"createRootDirectory",
|
|
140
111
|
"declareAppendedData",
|
|
@@ -163,12 +134,6 @@ export {
|
|
|
163
134
|
|
|
164
135
|
async function process(pageData, options, lastModDate = new Date()) {
|
|
165
136
|
let script;
|
|
166
|
-
// The worker is configured before anything else, and outside the extension it is turned off
|
|
167
|
-
// rather than left alone. Given no address, zip.js resolves its default one against the page
|
|
168
|
-
// being saved, so the browser asks the CAPTURED SITE for a file that site has never heard of:
|
|
169
|
-
// three 404s in the user's own server logs for every archive, and then a fallback to the main
|
|
170
|
-
// thread anyway, which is where the work was always going to happen. Choosing the fallback
|
|
171
|
-
// costs nothing that was ever gained and asks the site for nothing.
|
|
172
137
|
const extensionContext = Boolean(browser && browser.runtime && browser.runtime.getURL);
|
|
173
138
|
if (extensionContext) {
|
|
174
139
|
configure({ workerURI: "/lib/single-file-z-worker.js" });
|
|
@@ -188,15 +153,21 @@ async function process(pageData, options, lastModDate = new Date()) {
|
|
|
188
153
|
}
|
|
189
154
|
|
|
190
155
|
async function createArchive(pageData, options, script, writeEntries, lastModDate = new Date()) {
|
|
156
|
+
const zipWriterOptions = { bufferedWrite: true, keepOrder: true, lastModDate, useCompressionStream: true };
|
|
157
|
+
const entriesWriter = new ZipWriter(new Uint8ArrayWriter(), zipWriterOptions);
|
|
158
|
+
await writeEntries(entriesWriter);
|
|
159
|
+
const entriesData = await entriesWriter.close();
|
|
160
|
+
return buildArchive(pageData, options, script, entriesData, zipWriterOptions);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function buildArchive(pageData, options, script, entriesData, zipWriterOptions) {
|
|
164
|
+
const { lastModDate } = zipWriterOptions;
|
|
191
165
|
const zipDataWriter = new Uint8ArrayWriter();
|
|
192
166
|
zipDataWriter.init();
|
|
193
167
|
let extraDataOffset, extraData, embeddedImageDataOffset, endTag, pdfEntry;
|
|
194
168
|
if (options.embeddedImage) {
|
|
195
169
|
options.embeddedImage = new Uint8Array(options.embeddedImage);
|
|
196
170
|
}
|
|
197
|
-
// the whole chunk is built before the first byte of the image is written, because building it
|
|
198
|
-
// is what settles the rung, and the search can end with no rung at all: the image is then left
|
|
199
|
-
// out altogether rather than written unwrapped
|
|
200
171
|
let imageChunk;
|
|
201
172
|
if (options.embeddedImage && options.selfExtractingArchive) {
|
|
202
173
|
imageChunk = getImageHTMLChunk(pageData, options, lastModDate);
|
|
@@ -211,8 +182,7 @@ async function createArchive(pageData, options, script, writeEntries, lastModDat
|
|
|
211
182
|
endTag = imageChunk.endTag;
|
|
212
183
|
if (imageChunk.startHTMLData.pdfEntry) {
|
|
213
184
|
pdfEntry = imageChunk.startHTMLData.pdfEntry;
|
|
214
|
-
|
|
215
|
-
pdfEntry.offset += zipDataWriter.offset + 12;
|
|
185
|
+
pdfEntry.offset += zipDataWriter.offset + PNG_TEXT_CHUNK_HEADER_LENGTH;
|
|
216
186
|
}
|
|
217
187
|
await writeData(zipDataWriter.writable, imageChunk.htmlData);
|
|
218
188
|
await writeData(zipDataWriter.writable, imageChunk.htmlDataCRC);
|
|
@@ -236,53 +206,35 @@ async function createArchive(pageData, options, script, writeEntries, lastModDat
|
|
|
236
206
|
} else if (!options.embeddedImage && options.embeddedPdf) {
|
|
237
207
|
await writeData(zipDataWriter.writable, new Uint8Array(options.embeddedPdf));
|
|
238
208
|
}
|
|
239
|
-
// a WritableWriter object is passed instead of the writer so that the ZipWriter
|
|
240
|
-
// never takes ownership of the stream: preventClose is only honored when the
|
|
241
|
-
// caller owns the writable, and the HTML suffix still gets written after it;
|
|
242
|
-
// its size property tells the ZipWriter the offset of the data written so far
|
|
243
209
|
const startOffset = zipDataWriter.offset;
|
|
244
|
-
const zipWriter = new ZipWriter({ writable: zipDataWriter.writable, size: startOffset },
|
|
245
|
-
await
|
|
210
|
+
const zipWriter = new ZipWriter({ writable: zipDataWriter.writable, size: startOffset }, zipWriterOptions);
|
|
211
|
+
await zipWriter.appendZip(new Uint8ArrayReader(entriesData));
|
|
246
212
|
if (pdfEntry) {
|
|
247
|
-
// the record is written where the central directory will start so that the PDF is listed
|
|
248
|
-
// first; the ZipWriter is unaware of these bytes, so the central directory offset it
|
|
249
|
-
// stores in the end of central directory record points here
|
|
250
213
|
new DataView(pdfEntry.centralRecord.buffer).setUint32(42, pdfEntry.offset, true);
|
|
251
214
|
await writeData(zipDataWriter.writable, pdfEntry.centralRecord);
|
|
252
215
|
}
|
|
253
216
|
await zipWriter.close(undefined, { preventClose: true });
|
|
254
217
|
if (pdfEntry && !patchEndOfCentralDirectory(zipDataWriter, pdfEntry.centralRecord.length)) {
|
|
255
|
-
// the record cannot be declared in the end of central directory record: rebuild the
|
|
256
|
-
// archive without it rather than leave a record the directory does not count
|
|
257
218
|
options.preventEmbeddedPdfEntry = true;
|
|
258
|
-
return
|
|
219
|
+
return buildArchive(pageData, options, script, entriesData, zipWriterOptions);
|
|
259
220
|
}
|
|
260
221
|
const data = zipDataWriter.getData();
|
|
261
|
-
// the last two bytes of the archive are the comment length field of the end of central
|
|
262
|
-
// directory record: they are left out of the data the extraction payload describes, so that
|
|
263
|
-
// declaring the appended data as the archive comment cannot invalidate a payload computed
|
|
264
|
-
// before that length is known
|
|
265
222
|
const zipDataEnd = data.length - COMMENT_LENGTH_FIELD_LENGTH;
|
|
266
223
|
if (options.selfExtractingArchive) {
|
|
267
224
|
const lfCodes = [];
|
|
268
225
|
let crc32 = -1;
|
|
269
|
-
// the wrapper must not be closed by the zip data itself, whether or not the page
|
|
270
|
-
// carries the data: a premature closer parses the rest of the archive as markup
|
|
271
226
|
if (!options.extractDataFromPageTags || options.extractDataFromPageTags[0] != "<plaintext>") {
|
|
272
227
|
const textContent = TEXT_DECODER.decode(data.subarray(startOffset));
|
|
273
228
|
if (options.extractDataFromPageTags) {
|
|
274
|
-
// the rung is matched on its start tag, not on the identity of the array holding it:
|
|
275
|
-
// the option is set from EXTRA_DATA_TAGS internally, but a caller passing an equal
|
|
276
|
-
// pair of its own would otherwise index the regexps with -1
|
|
277
229
|
const tagIndex = getExtraDataTagIndex(options.extractDataFromPageTags);
|
|
278
230
|
const regExpsTag = EXTRA_DATA_REGEXPS[tagIndex];
|
|
279
231
|
if (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1])) {
|
|
280
|
-
return findExtraDataTags(textContent, pageData, options, script,
|
|
232
|
+
return findExtraDataTags(textContent, pageData, options, script, entriesData, zipWriterOptions, tagIndex + 1);
|
|
281
233
|
}
|
|
282
234
|
} else {
|
|
283
235
|
const [startRegExp, endRegExp] = EMBEDDED_DATA_REGEXPS[0];
|
|
284
236
|
if (textContent.match(startRegExp) || textContent.match(endRegExp)) {
|
|
285
|
-
return findExtraDataTags(textContent, pageData, options, script,
|
|
237
|
+
return findExtraDataTags(textContent, pageData, options, script, entriesData, zipWriterOptions);
|
|
286
238
|
}
|
|
287
239
|
}
|
|
288
240
|
}
|
|
@@ -314,35 +266,23 @@ async function createArchive(pageData, options, script, writeEntries, lastModDat
|
|
|
314
266
|
}
|
|
315
267
|
const endTags = options.preventAppendedData || options.embeddedImage ? "" : "</body></html>";
|
|
316
268
|
if (options.extractDataFromPage) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// window readers scan backward to find the EOCD record
|
|
269
|
+
const words = new Uint32Array(3 + Math.ceil(lfCodes.length / 16));
|
|
270
|
+
words[0] = crc32;
|
|
271
|
+
words[1] = zipDataEnd - startOffset;
|
|
272
|
+
words[2] = lfCodes.length;
|
|
273
|
+
lfCodes.forEach((lfCode, indexLFCode) => words[3 + (indexLFCode >> 4)] |= lfCode << ((indexLFCode & 15) * 2));
|
|
274
|
+
const payload = new Uint8Array(words.length * 4);
|
|
275
|
+
const payloadView = new DataView(payload.buffer);
|
|
276
|
+
words.forEach((word, indexWord) => payloadView.setUint32(indexWord * 4, word, true));
|
|
277
|
+
extraData = "<sfz-extra-data>" + base64Encode(deflateRaw(payload)) + "</sfz-extra-data>";
|
|
327
278
|
if (options.preventAppendedData || extraData.length > MAX_APPENDED_DATA_LENGTH - pageContent.length - endTags.length - (options.embeddedImage ? PNG_IEND_LENGTH + PNG_CHUNK_CRC_LENGTH : 0)) {
|
|
328
279
|
if (!options.extraDataSize) {
|
|
280
|
+
options.preventAppendedData = true;
|
|
329
281
|
options.extraDataSize = getReservationSize(extraData.length);
|
|
330
|
-
return
|
|
282
|
+
return buildArchive(pageData, options, script, entriesData, zipWriterOptions);
|
|
331
283
|
}
|
|
332
284
|
} else {
|
|
333
|
-
|
|
334
|
-
// dropping the reservation moves the archive back, which changes the payload
|
|
335
|
-
// that made it necessary: a payload sitting on the boundary would be too large
|
|
336
|
-
// appended and small enough relocated, forever. The reservation is dropped at
|
|
337
|
-
// most once, so the build cannot oscillate between the two placements
|
|
338
|
-
if (!options.extraDataSizeDropped) {
|
|
339
|
-
options.extraDataSizeDropped = true;
|
|
340
|
-
options.extraDataSize = undefined;
|
|
341
|
-
return createArchive(pageData, options, script, writeEntries, lastModDate);
|
|
342
|
-
}
|
|
343
|
-
} else {
|
|
344
|
-
pageContent += extraData;
|
|
345
|
-
}
|
|
285
|
+
pageContent += extraData;
|
|
346
286
|
}
|
|
347
287
|
}
|
|
348
288
|
pageContent += endTags;
|
|
@@ -354,14 +294,11 @@ async function createArchive(pageData, options, script, writeEntries, lastModDat
|
|
|
354
294
|
if (options.extraDataSize >= extraData.length) {
|
|
355
295
|
pageContent.set(new TextEncoder().encode(extraData), startOffset - extraDataOffset);
|
|
356
296
|
} else {
|
|
357
|
-
options.extraData = extraData;
|
|
358
297
|
options.extraDataSize = getReservationSize(extraData.length);
|
|
359
|
-
return
|
|
298
|
+
return buildArchive(pageData, options, script, entriesData, zipWriterOptions);
|
|
360
299
|
}
|
|
361
300
|
}
|
|
362
301
|
if (options.declareAppendedData) {
|
|
363
|
-
// readers that reject undeclared bytes after the end of central directory record, notably
|
|
364
|
-
// java.util.zip, accept the file when the same bytes are declared as the archive comment
|
|
365
302
|
const appendedDataLength = pageContent.length - data.length +
|
|
366
303
|
(options.embeddedImage ? PNG_CHUNK_CRC_LENGTH + PNG_IEND_LENGTH : 0);
|
|
367
304
|
if (appendedDataLength && appendedDataLength <= MAX_APPENDED_DATA_LENGTH) {
|
|
@@ -403,6 +340,7 @@ function getPDFEntry(embeddedPdf, lastModDate = new Date()) {
|
|
|
403
340
|
const localHeaderView = new DataView(localHeader.buffer);
|
|
404
341
|
localHeaderView.setUint32(0, LOCAL_FILE_HEADER_SIGNATURE, true);
|
|
405
342
|
localHeaderView.setUint16(4, 20, true);
|
|
343
|
+
localHeaderView.setUint16(6, LANGUAGE_ENCODING_FLAG, true);
|
|
406
344
|
localHeaderView.setUint16(10, dosTime, true);
|
|
407
345
|
localHeaderView.setUint16(12, dosDate, true);
|
|
408
346
|
localHeaderView.setUint32(14, crc32, true);
|
|
@@ -415,6 +353,7 @@ function getPDFEntry(embeddedPdf, lastModDate = new Date()) {
|
|
|
415
353
|
centralRecordView.setUint32(0, CENTRAL_FILE_HEADER_SIGNATURE, true);
|
|
416
354
|
centralRecordView.setUint16(4, 0x0300, true);
|
|
417
355
|
centralRecordView.setUint16(6, 20, true);
|
|
356
|
+
centralRecordView.setUint16(8, LANGUAGE_ENCODING_FLAG, true);
|
|
418
357
|
centralRecordView.setUint16(12, dosTime, true);
|
|
419
358
|
centralRecordView.setUint16(14, dosDate, true);
|
|
420
359
|
centralRecordView.setUint32(16, crc32, true);
|
|
@@ -438,7 +377,6 @@ function patchEndOfCentralDirectory(zipDataWriter, centralRecordLength) {
|
|
|
438
377
|
const offsetLocator = offsetEOCD - 20;
|
|
439
378
|
let offsetZip64EOCD;
|
|
440
379
|
if (offsetLocator >= 0 && view.getUint32(offsetLocator, true) == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE) {
|
|
441
|
-
// the offset stored in the locator does not account for the PDF record either
|
|
442
380
|
offsetZip64EOCD = Number(view.getBigUint64(offsetLocator + 8, true)) + centralRecordLength;
|
|
443
381
|
if (view.getUint32(offsetZip64EOCD, true) != ZIP64_END_OF_CENTRAL_DIR_SIGNATURE) {
|
|
444
382
|
return false;
|
|
@@ -464,16 +402,12 @@ function patchEndOfCentralDirectory(zipDataWriter, centralRecordLength) {
|
|
|
464
402
|
return true;
|
|
465
403
|
}
|
|
466
404
|
|
|
467
|
-
// the functions inlined in the archive lose their newlines, so a line comment would swallow
|
|
468
|
-
// the rest of the script: whole-line comments are removed before the newlines are
|
|
469
405
|
function inlineFunction(bootstrapFunction) {
|
|
470
406
|
return bootstrapFunction.toString().replace(/^[ \t]*\/\/.*$/gm, "").replace(/\n|\t/g, "");
|
|
471
407
|
}
|
|
472
408
|
|
|
473
|
-
// the reservation must be strictly larger than the payload it was computed from, otherwise
|
|
474
|
-
// the retry loop can be handed the same size again and oscillate instead of converging
|
|
475
409
|
function getReservationSize(length) {
|
|
476
|
-
return Math.
|
|
410
|
+
return Math.ceil(length * 1.01) + 32;
|
|
477
411
|
}
|
|
478
412
|
|
|
479
413
|
function getLength(length) {
|
|
@@ -513,14 +447,11 @@ async function prependHTMLData(pageData, zipDataWriter, script, options, lastMod
|
|
|
513
447
|
if (pageData.tocContent) {
|
|
514
448
|
pageContent += pageData.tocContent;
|
|
515
449
|
}
|
|
516
|
-
// the text body repeats the page content outside the archive, where no password reaches it
|
|
517
450
|
if (options.insertTextBody && !options.password) {
|
|
518
451
|
const doc = (new DOMParser()).parseFromString(pageData.content, "text/html");
|
|
519
452
|
doc.body.querySelectorAll("style, script, noscript").forEach(element => element.remove());
|
|
520
453
|
let textBody = "";
|
|
521
454
|
if (options.extractDataFromPage) {
|
|
522
|
-
// the text body is read as raw bytes by text tools, so the title goes in unencoded;
|
|
523
|
-
// the < and > escaping below covers it
|
|
524
455
|
textBody += (pageData.title || "") + "\n\n";
|
|
525
456
|
}
|
|
526
457
|
textBody += doc.body.innerText;
|
|
@@ -534,16 +465,12 @@ async function prependHTMLData(pageData, zipDataWriter, script, options, lastMod
|
|
|
534
465
|
textBody = textBody.replace(/</g, "<").replace(/>/g, ">").replace(/\n +/g, "\n").replace(/\n\n\n+/g, "\n\n").trim();
|
|
535
466
|
pageContent += "\n<main hidden>\n" + textBody + "\n</main>\n";
|
|
536
467
|
}
|
|
537
|
-
const displayOptions = {
|
|
538
|
-
insertEmbeddedImage: Boolean(options.embeddedImage),
|
|
539
|
-
insertEmbeddedScreenshotImage: Boolean(options.embeddedScreenshotImage)
|
|
540
|
-
};
|
|
541
468
|
const bootstrapBody = options.multiPageArchive ?
|
|
542
469
|
"(" + inlineFunction(router) + ")(content,{extract:" +
|
|
543
470
|
inlineFunction(extract) + ",display:" +
|
|
544
471
|
inlineFunction(display) + "})" :
|
|
545
472
|
"(" + inlineFunction(extract) + ")(content,{prompt}).then(({docContent}) => " +
|
|
546
|
-
inlineFunction(display) + "(document,docContent
|
|
473
|
+
inlineFunction(display) + "(document,docContent))";
|
|
547
474
|
script = "<script>" +
|
|
548
475
|
script +
|
|
549
476
|
"document.currentScript.remove();" +
|
|
@@ -571,14 +498,10 @@ async function prependHTMLData(pageData, zipDataWriter, script, options, lastMod
|
|
|
571
498
|
return { extraDataOffset, pdfEntry };
|
|
572
499
|
}
|
|
573
500
|
|
|
574
|
-
// the extractor finds the zip data by identifier instead of by its position in the tree: an
|
|
575
|
-
// element carries it as an attribute, a comment as the first characters of its data
|
|
576
501
|
function getDataStartTag([startTag]) {
|
|
577
502
|
if (startTag == "<!--") {
|
|
578
503
|
return startTag + DATA_IDENTIFIER;
|
|
579
504
|
}
|
|
580
|
-
// the attribute belongs to the element that opens the wrapper, which is not always the whole
|
|
581
|
-
// start tag: the CDATA rung opens with an <svg> and then a markup declaration that takes none
|
|
582
505
|
const tagEnd = startTag.indexOf(">");
|
|
583
506
|
return startTag.slice(0, tagEnd) + " id=" + DATA_IDENTIFIER + startTag.slice(tagEnd);
|
|
584
507
|
}
|
|
@@ -591,14 +514,13 @@ function getStartHTMLArray(pageData, options, lastModDate, startTag = "") {
|
|
|
591
514
|
const doctype = options.embeddedImage ? "" : pageData.doctype;
|
|
592
515
|
const charset = options.extractDataFromPage ? "windows-1252" : "utf-8";
|
|
593
516
|
const documentStart = "<html data-sfz><meta charset=" + charset + ">";
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const comment = pageData.comment && !options.embeddedImage && !options.password ? "<!--" + pageData.comment + "-->" : "";
|
|
517
|
+
const startOffset = options.embeddedImage ?
|
|
518
|
+
PNG_SIGNATURE_LENGTH + PNG_IHDR_LENGTH + PNG_TEXT_CHUNK_HEADER_LENGTH : 0;
|
|
519
|
+
let html = bom + doctype + documentStart;
|
|
520
|
+
if (startOffset + new TextEncoder().encode(html).length > PRESCAN_WINDOW_LENGTH) {
|
|
521
|
+
html = bom + MINIMAL_DOCTYPE + documentStart;
|
|
522
|
+
}
|
|
523
|
+
const comment = pageData.comment && !options.embeddedImage && !options.password ? "<!--" + escapeCommentData(pageData.comment) + "-->" : "";
|
|
602
524
|
const htmlHeadData = getHTMLHeadData(pageData, options);
|
|
603
525
|
let htmlArray, pdfEntry;
|
|
604
526
|
if (options.embeddedPdf) {
|
|
@@ -613,10 +535,7 @@ function getStartHTMLArray(pageData, options, lastModDate, startTag = "") {
|
|
|
613
535
|
} else {
|
|
614
536
|
const [pdfStartTag, pdfEndTag] = EMBEDDED_DATA_TAGS[pdfTagIndex];
|
|
615
537
|
let htmlArray1 = new TextEncoder().encode(html + pdfStartTag);
|
|
616
|
-
if (htmlArray1.length + localHeader.length >
|
|
617
|
-
// PDF readers only scan the start of the file for the %PDF- header, and the page
|
|
618
|
-
// doctype is copied verbatim: it is the one part of the prefix with no bound, so a
|
|
619
|
-
// long one is replaced rather than pushing the header out of the scan window
|
|
538
|
+
if (startOffset + htmlArray1.length + localHeader.length > PRESCAN_WINDOW_LENGTH) {
|
|
620
539
|
htmlArray1 = new TextEncoder().encode(bom + MINIMAL_DOCTYPE + documentStart + pdfStartTag);
|
|
621
540
|
}
|
|
622
541
|
const htmlArray2 = new TextEncoder().encode(pdfEndTag + comment + htmlHeadData + startTag);
|
|
@@ -638,23 +557,19 @@ function getStartHTMLArray(pageData, options, lastModDate, startTag = "") {
|
|
|
638
557
|
|
|
639
558
|
function getHTMLHeadData(pageData, options) {
|
|
640
559
|
let pageContent = "";
|
|
641
|
-
// the title is left out of a password-protected archive: manifest.json carries it too and
|
|
642
|
-
// is encrypted, so emitting it here would publish what the password is meant to cover
|
|
643
560
|
const title = options.password ? "" : escapeHTML(pageData.title || "");
|
|
644
561
|
pageContent += "<title>" + title + "</title>";
|
|
645
|
-
// the canonical link publishes the URL the archive was saved from, for the same reason
|
|
646
|
-
// the title above is left out of a password-protected archive
|
|
647
562
|
if (options.insertCanonicalLink && !options.password) {
|
|
648
|
-
pageContent += "<link rel=canonical href=\"" + options.url + "\">";
|
|
563
|
+
pageContent += "<link rel=canonical href=\"" + escapeHTML(options.url) + "\">";
|
|
649
564
|
}
|
|
650
565
|
if (options.insertMetaNoIndex) {
|
|
651
566
|
pageContent += "<meta name=robots content=noindex>";
|
|
652
567
|
}
|
|
653
568
|
if (pageData.viewport) {
|
|
654
|
-
pageContent += "<meta name=viewport content
|
|
569
|
+
pageContent += "<meta name=viewport content=\"" + escapeHTML(pageData.viewport) + "\">";
|
|
655
570
|
}
|
|
656
571
|
if (options.insertMetaCSP) {
|
|
657
|
-
const cspContent = "default-src 'none';connect-src 'self' data: blob:;font-src 'self' data: blob:;img-src 'self' data: blob:;style-src 'self' 'unsafe-inline' data: blob:;frame-src 'self' data: blob:;media-src 'self' data: blob:;script-src 'self' 'unsafe-inline' data: blob:;object-src 'self' data: blob
|
|
572
|
+
const cspContent = "default-src 'none';connect-src 'self' data: blob:;font-src 'self' data: blob:;img-src 'self' data: blob:;style-src 'self' 'unsafe-inline' data: blob:;frame-src 'self' data: blob:;media-src 'self' data: blob:;script-src 'self' 'unsafe-inline' data: blob:;object-src 'self' data: blob:;form-action 'none';base-uri 'none'";
|
|
658
573
|
pageContent += `<meta http-equiv=content-security-policy content=${JSON.stringify(cspContent)}>`;
|
|
659
574
|
}
|
|
660
575
|
pageContent += "<style>@keyframes display-wait-message{0%{opacity:0}100%{opacity:1}}body{color:transparent}div{color:initial}body>:not(#sfz-wait-message,#sfz-error-message){display:none}</style>";
|
|
@@ -662,9 +577,17 @@ function getHTMLHeadData(pageData, options) {
|
|
|
662
577
|
return pageContent;
|
|
663
578
|
}
|
|
664
579
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
580
|
+
function escapeCommentData(value) {
|
|
581
|
+
let data = value.replace(/--(!?)>/g, "--$1 >");
|
|
582
|
+
if (data.startsWith(">") || data.startsWith("->")) {
|
|
583
|
+
data = " " + data;
|
|
584
|
+
}
|
|
585
|
+
if (data.endsWith("<!-")) {
|
|
586
|
+
data += " ";
|
|
587
|
+
}
|
|
588
|
+
return data;
|
|
589
|
+
}
|
|
590
|
+
|
|
668
591
|
function escapeHTML(value) {
|
|
669
592
|
return Array.from(value).map(character => {
|
|
670
593
|
const codePoint = character.codePointAt(0);
|
|
@@ -681,46 +604,26 @@ function getExtraDataTagIndex(extractDataFromPageTags) {
|
|
|
681
604
|
return tagIndex;
|
|
682
605
|
}
|
|
683
606
|
|
|
684
|
-
function findExtraDataTags(textContent, pageData, options, script,
|
|
607
|
+
function findExtraDataTags(textContent, pageData, options, script, entriesData, zipWriterOptions, indexExtractDataFromPageTags = 0) {
|
|
685
608
|
const regExpsTag = EXTRA_DATA_REGEXPS[indexExtractDataFromPageTags];
|
|
686
609
|
const plaintextTag = EXTRA_DATA_TAGS[indexExtractDataFromPageTags][0] == "<plaintext>";
|
|
687
610
|
const matchTag = !plaintextTag && (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1]));
|
|
688
611
|
if (matchTag) {
|
|
689
|
-
|
|
690
|
-
return findExtraDataTags(textContent, pageData, options, script, writeEntries, lastModDate, indexExtractDataFromPageTags + 1);
|
|
691
|
-
} else {
|
|
692
|
-
options.extractDataFromPage = false;
|
|
693
|
-
return createArchive(pageData, options, script, writeEntries, lastModDate);
|
|
694
|
-
}
|
|
612
|
+
return findExtraDataTags(textContent, pageData, options, script, entriesData, zipWriterOptions, indexExtractDataFromPageTags + 1);
|
|
695
613
|
} else {
|
|
696
614
|
options.extractDataFromPageTags = EXTRA_DATA_TAGS[indexExtractDataFromPageTags];
|
|
697
615
|
if (options.extractDataFromPageTags[0] == "<plaintext>") {
|
|
698
|
-
// <plaintext> cannot be closed, the file must end with the zip data
|
|
699
616
|
options.preventAppendedData = true;
|
|
700
617
|
}
|
|
701
|
-
return
|
|
618
|
+
return buildArchive(pageData, options, script, entriesData, zipWriterOptions);
|
|
702
619
|
}
|
|
703
620
|
}
|
|
704
621
|
|
|
705
|
-
// a rung is rejected on its end pattern, which terminates the wrapper, and on its start
|
|
706
|
-
// pattern: script data has escape states the raw text rungs do not have, where "<!--"
|
|
707
|
-
// followed by "<script" leaves "</script>" unable to close the element at all
|
|
708
622
|
function findEmbeddedDataTagIndex(text, fromIndex = 0) {
|
|
709
623
|
const tagIndex = EMBEDDED_DATA_REGEXPS.slice(fromIndex, -1).findIndex(([startRegExp, endRegExp]) => !text.match(startRegExp) && !text.match(endRegExp));
|
|
710
624
|
return tagIndex == -1 ? -1 : tagIndex + fromIndex;
|
|
711
625
|
}
|
|
712
626
|
|
|
713
|
-
// a face exists only while a rung can hide it. When the payload names every rung, the older
|
|
714
|
-
// fallback emitted it unwrapped, on the grounds that the page still rendered: but the payload's
|
|
715
|
-
// markup then joins the document, and a payload that is itself an archive contributes an
|
|
716
|
-
// sfz-data node ahead of this file's own. A reader takes that one and extracts it, checksum and
|
|
717
|
-
// all, with nothing to say the archive it returned is not the archive the file was built around.
|
|
718
|
-
// Dropping the face costs a picture; keeping it costs the archive
|
|
719
|
-
// what follows the start tag is the checksum of the chunk carrying it, four bytes only known once
|
|
720
|
-
// the tag is chosen: a comment they open with ">" or "->" is closed by the parser there and then,
|
|
721
|
-
// leaving the image data to be read as markup. Stepping past the comment rung means searching from
|
|
722
|
-
// the next one, not taking it — a rung qualifies on the payload, and the payload had no say in
|
|
723
|
-
// which rung the checksum sent the writer to
|
|
724
627
|
function getImageHTMLChunk(pageData, options, lastModDate) {
|
|
725
628
|
const embeddedImageText = TEXT_DECODER.decode(getEmbeddedImageData(options.embeddedImage));
|
|
726
629
|
let tagIndex = findEmbeddedDataTagIndex(embeddedImageText);
|
|
@@ -729,7 +632,9 @@ function getImageHTMLChunk(pageData, options, lastModDate) {
|
|
|
729
632
|
const startHTMLData = getStartHTMLArray(pageData, options, lastModDate, startTag);
|
|
730
633
|
const htmlData = new Uint8Array([...getLength(startHTMLData.htmlArray.length + 4), ...[0x74, 0x45, 0x58, 0x74, 0x50, 0x4e, 0x47, 0], ...startHTMLData.htmlArray]);
|
|
731
634
|
const htmlDataCRC = getCRC32(htmlData, 4);
|
|
732
|
-
|
|
635
|
+
const wrappedText = TEXT_DECODER.decode(htmlDataCRC) + embeddedImageText;
|
|
636
|
+
if ((tagIndex == 0 && (htmlDataCRC[0] == 0x3e || (htmlDataCRC[0] == 0x2d && htmlDataCRC[1] == 0x3e))) ||
|
|
637
|
+
findEmbeddedDataTagIndex(wrappedText, tagIndex) != tagIndex) {
|
|
733
638
|
tagIndex = findEmbeddedDataTagIndex(embeddedImageText, tagIndex + 1);
|
|
734
639
|
} else {
|
|
735
640
|
return { endTag, startHTMLData, htmlData, htmlDataCRC };
|
|
@@ -791,8 +696,6 @@ async function addFile(zipWriter, prefixName, data, disableCompression) {
|
|
|
791
696
|
const dataReader = typeof data.content == "string" ? new TextReader(data.content) : new BlobReader(new Blob([new Uint8Array(data.content)]));
|
|
792
697
|
const options = { password: data.password, bufferedWrite: true };
|
|
793
698
|
if (!data.password) {
|
|
794
|
-
// entry comments are stored in the central directory, which is never encrypted: with a
|
|
795
|
-
// password the resource URLs would be readable while the same map in manifest.json is not
|
|
796
699
|
options.comment = data.url && data.url.startsWith("data:") ? "data:" : data.url;
|
|
797
700
|
}
|
|
798
701
|
if (disableCompression || (!isCompressibleContentType(data.contentType) && NO_COMPRESSION_EXTENSIONS.includes(data.extension))) {
|
|
@@ -807,7 +710,6 @@ function isCompressibleContentType(contentType) {
|
|
|
807
710
|
|
|
808
711
|
async function getContent() {
|
|
809
712
|
const BASE64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
810
|
-
// the function is inlined in the archive as source, it cannot close over the module scope
|
|
811
713
|
const DATA_IDENTIFIER = "sfz-data";
|
|
812
714
|
const { Blob, XMLHttpRequest, NodeFilter, document, zip, location } = globalThis;
|
|
813
715
|
const characterMap = new Map([
|
|
@@ -849,8 +751,6 @@ async function getContent() {
|
|
|
849
751
|
const xhr = new XMLHttpRequest();
|
|
850
752
|
xhr.responseType = "blob";
|
|
851
753
|
xhr.open("GET", "");
|
|
852
|
-
// a failure of the full download is recoverable when the page carries the data:
|
|
853
|
-
// the wait message left the document intact, so the page text can still be read
|
|
854
754
|
xhr.onerror = () => extractDataFromDocument();
|
|
855
755
|
xhr.send();
|
|
856
756
|
xhr.onreadystatechange = () => {
|
|
@@ -869,7 +769,6 @@ async function getContent() {
|
|
|
869
769
|
getPageData();
|
|
870
770
|
}
|
|
871
771
|
} else {
|
|
872
|
-
// an HTTP error status fires no error event; fall back like a network failure
|
|
873
772
|
xhr.abort();
|
|
874
773
|
extractDataFromDocument();
|
|
875
774
|
}
|
|
@@ -913,13 +812,12 @@ async function getContent() {
|
|
|
913
812
|
const zipDataElement = document.querySelector("sfz-extra-data");
|
|
914
813
|
if (zipDataElement) {
|
|
915
814
|
const inflatedPayload = zip.inflateRaw(base64Decode(zipDataElement.textContent));
|
|
916
|
-
const payload = new
|
|
917
|
-
// the zip data is identified, not located: its node can be moved before this runs
|
|
815
|
+
const payload = new DataView(inflatedPayload.buffer, inflatedPayload.byteOffset, inflatedPayload.length & -4);
|
|
918
816
|
const dataElement = document.getElementById(DATA_IDENTIFIER);
|
|
919
817
|
if (dataElement) {
|
|
920
818
|
return decodeZipData(dataElement, payload, 0);
|
|
921
819
|
}
|
|
922
|
-
const walker = document.createTreeWalker(document
|
|
820
|
+
const walker = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
|
|
923
821
|
while (walker.nextNode()) {
|
|
924
822
|
if (walker.currentNode.data.startsWith(DATA_IDENTIFIER)) {
|
|
925
823
|
return decodeZipData(walker.currentNode, payload, DATA_IDENTIFIER.length);
|
|
@@ -930,12 +828,9 @@ async function getContent() {
|
|
|
930
828
|
}
|
|
931
829
|
|
|
932
830
|
function decodeZipData(dataNode, payload, startIndex) {
|
|
933
|
-
const expectedCRC32 = payload
|
|
934
|
-
const zipDataLength = payload
|
|
935
|
-
const lfCodesLength = payload
|
|
936
|
-
// the two extra bytes are the comment length field of the end of central directory
|
|
937
|
-
// record, which the payload does not describe: left at zero they declare no comment,
|
|
938
|
-
// which is what the recovered data holds
|
|
831
|
+
const expectedCRC32 = payload.getUint32(0, true);
|
|
832
|
+
const zipDataLength = payload.getUint32(4, true);
|
|
833
|
+
const lfCodesLength = payload.getUint32(8, true);
|
|
939
834
|
const zipData = new Uint8Array(zipDataLength + 2);
|
|
940
835
|
const { textContent } = dataNode;
|
|
941
836
|
let offset = 0;
|
|
@@ -944,9 +839,11 @@ async function getContent() {
|
|
|
944
839
|
for (let index = startIndex; index < textContent.length && offset < zipDataLength; index++) {
|
|
945
840
|
const charCode = textContent.charCodeAt(index);
|
|
946
841
|
if (charCode == 10) {
|
|
947
|
-
const lfCode = (payload
|
|
842
|
+
const lfCode = (payload.getUint32(12 + (indexLFCode >> 4) * 4, true) >>> ((indexLFCode & 15) * 2)) & 3;
|
|
948
843
|
indexLFCode++;
|
|
949
|
-
if (lfCode ==
|
|
844
|
+
if (lfCode == 3) {
|
|
845
|
+
throw new Error("Unsupported newline code in the extracted zip data");
|
|
846
|
+
} else if (lfCode == 0) {
|
|
950
847
|
writeByte(10);
|
|
951
848
|
} else {
|
|
952
849
|
writeByte(13);
|
|
@@ -322,7 +322,7 @@
|
|
|
322
322
|
const rootBounds = getBoundingClientRectDefined ? rootBoundingRect : docBoundingRect;
|
|
323
323
|
const time = 0;
|
|
324
324
|
return { target, intersectionRatio, boundingClientRect, intersectionRect: boundingClientRect, isIntersecting, rootBounds, time };
|
|
325
|
-
}).filter(params => params.boundingClientRect.width && params.boundingClientRect.height);
|
|
325
|
+
}).filter(params => params.boundingClientRect.width && params.boundingClientRect.height > 1);
|
|
326
326
|
if (params.length) {
|
|
327
327
|
observer.callback.call(intersectionObserver, params, intersectionObserver);
|
|
328
328
|
}
|
|
@@ -349,6 +349,7 @@
|
|
|
349
349
|
delete globalThis._singleFileImage;
|
|
350
350
|
}
|
|
351
351
|
if (!keepZoomLevel) {
|
|
352
|
+
resetScreenSize();
|
|
352
353
|
dispatchResizeEvent();
|
|
353
354
|
}
|
|
354
355
|
}
|
|
@@ -33,6 +33,8 @@ const helper = {
|
|
|
33
33
|
|
|
34
34
|
const MAX_IDLE_TIMEOUT_CALLS = 10;
|
|
35
35
|
const ATTRIBUTES_MUTATION_TYPE = "attributes";
|
|
36
|
+
const CHILD_LIST_MUTATION_TYPE = "childList";
|
|
37
|
+
const STYLESHEET_TAG_NAMES = ["STYLE", "LINK"];
|
|
36
38
|
|
|
37
39
|
const browser = globalThis.browser;
|
|
38
40
|
const document = globalThis.document;
|
|
@@ -88,24 +90,25 @@ function triggerLazyLoading(options) {
|
|
|
88
90
|
let loadingImages;
|
|
89
91
|
const pendingImages = new Set();
|
|
90
92
|
const observer = new MutationObserver(async mutations => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
93
|
+
const attributeMutations = mutations.filter(mutation => mutation.type == ATTRIBUTES_MUTATION_TYPE);
|
|
94
|
+
const insertedStylesheets = mutations.filter(mutation => mutation.type == CHILD_LIST_MUTATION_TYPE &&
|
|
95
|
+
Array.from(mutation.addedNodes).some(node => node.tagName && STYLESHEET_TAG_NAMES.includes(node.tagName.toUpperCase()) &&
|
|
96
|
+
(!node.classList || !node.classList.contains(helper.SINGLE_FILE_UI_ELEMENT_CLASS))));
|
|
97
|
+
const updated = attributeMutations.filter(mutation => {
|
|
98
|
+
if (mutation.attributeName == "src") {
|
|
99
|
+
mutation.target.setAttribute(helper.LAZY_SRC_ATTRIBUTE_NAME, mutation.target.src);
|
|
100
|
+
mutation.target.addEventListener("load", onResourceLoad);
|
|
101
|
+
}
|
|
102
|
+
if (mutation.attributeName == "src" || mutation.attributeName == "srcset" ||
|
|
103
|
+
(mutation.target.tagName && mutation.target.tagName.toUpperCase() == "SOURCE")) {
|
|
104
|
+
return !mutation.target.classList || !mutation.target.classList.contains(helper.SINGLE_FILE_UI_ELEMENT_CLASS);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
if (updated.length || insertedStylesheets.length) {
|
|
108
|
+
loadingImages = true;
|
|
109
|
+
await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
|
|
110
|
+
if (!pendingImages.size) {
|
|
111
|
+
await deferLazyLoadEnd(observer, options, cleanupAndResolve);
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
});
|