single-file-core 1.5.85 → 1.5.87
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/package.json
CHANGED
|
@@ -27,7 +27,7 @@ export {
|
|
|
27
27
|
extract
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
async function extract(content, { password, prompt = () => { }, zipOptions = { useWebWorkers: true }, noBlobURL } = {}) {
|
|
30
|
+
async function extract(content, { password, prompt = () => { }, zipOptions = { useWebWorkers: true }, noBlobURL, entries, pagePath = "" } = {}) {
|
|
31
31
|
const KNOWN_MIMETYPES = {
|
|
32
32
|
"gif": "image/gif",
|
|
33
33
|
"jpg": "image/jpeg",
|
|
@@ -74,22 +74,28 @@ async function extract(content, { password, prompt = () => { }, zipOptions = { u
|
|
|
74
74
|
const REGEXP_MATCH_MANIFEST = /manifest\.json$/;
|
|
75
75
|
const CHARSET_UTF8 = ";charset=utf-8";
|
|
76
76
|
const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
|
|
77
|
-
let
|
|
77
|
+
let zipReader;
|
|
78
78
|
zip.configure(zipOptions);
|
|
79
|
-
if (
|
|
80
|
-
reader
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
if (!entries) {
|
|
80
|
+
let reader;
|
|
81
|
+
if (content.readUint8Array) {
|
|
82
|
+
reader = content;
|
|
83
|
+
} else {
|
|
84
|
+
if (Array.isArray(content)) {
|
|
85
|
+
content = new Blob([new Uint8Array(content)]);
|
|
86
|
+
}
|
|
87
|
+
reader = new zip.BlobReader(content);
|
|
84
88
|
}
|
|
85
|
-
|
|
89
|
+
zipReader = new zip.ZipReader(reader);
|
|
90
|
+
entries = await zipReader.getEntries();
|
|
91
|
+
}
|
|
92
|
+
if (pagePath) {
|
|
93
|
+
entries = entries.filter(entry => entry.filename.startsWith(pagePath));
|
|
86
94
|
}
|
|
87
|
-
const zipReader = new zip.ZipReader(reader);
|
|
88
|
-
const entries = await zipReader.getEntries();
|
|
89
95
|
const options = { password };
|
|
90
96
|
let docContent, origDocContent, url, resources = [], indexPages = [], textResources = [];
|
|
91
97
|
await Promise.all(entries.map(async entry => {
|
|
92
|
-
const
|
|
98
|
+
const filename = entry.filename.substring(pagePath.length);
|
|
93
99
|
let dataWriter, content, textContent, mimeType;
|
|
94
100
|
const resourceInfo = {};
|
|
95
101
|
if (!options.password && entry.encrypted) {
|
|
@@ -131,7 +137,7 @@ async function extract(content, { password, prompt = () => { }, zipOptions = { u
|
|
|
131
137
|
content = URL.createObjectURL(blob);
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
|
-
const name =
|
|
140
|
+
const name = filename.match(/^([0-9_]+\/)?(.*)$/)[2];
|
|
135
141
|
let prefixPath = "";
|
|
136
142
|
const prefixPathMatch = filename.match(/(.*\/)[^/]+$/);
|
|
137
143
|
if (prefixPathMatch && prefixPathMatch[1]) {
|
|
@@ -139,7 +145,7 @@ async function extract(content, { password, prompt = () => { }, zipOptions = { u
|
|
|
139
145
|
}
|
|
140
146
|
Object.assign(resourceInfo, {
|
|
141
147
|
prefixPath,
|
|
142
|
-
filename
|
|
148
|
+
filename,
|
|
143
149
|
name,
|
|
144
150
|
url: entry.comment,
|
|
145
151
|
content,
|
|
@@ -148,7 +154,9 @@ async function extract(content, { password, prompt = () => { }, zipOptions = { u
|
|
|
148
154
|
parentResources: []
|
|
149
155
|
});
|
|
150
156
|
}));
|
|
151
|
-
|
|
157
|
+
if (zipReader) {
|
|
158
|
+
await zipReader.close();
|
|
159
|
+
}
|
|
152
160
|
indexPages.sort(sortByFilenameLengthDec);
|
|
153
161
|
textResources.sort(sortByFilenameLengthInc);
|
|
154
162
|
resources = resources.sort(sortByFilenameLengthDec).concat(...textResources).concat(...indexPages);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2010-2026 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
|
+
export {
|
|
27
|
+
router
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async function router(content, { extract, display }) {
|
|
31
|
+
const PAGES_PREFIX = "pages/";
|
|
32
|
+
const PAGES_FILENAME = "sfz-pages.json";
|
|
33
|
+
const ROUTE_PREFIX = "#sfz/";
|
|
34
|
+
const { zip, document, location } = globalThis;
|
|
35
|
+
const cache = new Map();
|
|
36
|
+
const urlToPath = new Map();
|
|
37
|
+
let currentPath;
|
|
38
|
+
zip.configure({ useWebWorkers: true });
|
|
39
|
+
const zipReader = new zip.ZipReader(content.readUint8Array ? content : new zip.BlobReader(content));
|
|
40
|
+
const entries = await zipReader.getEntries();
|
|
41
|
+
const pagesEntry = entries.find(entry => entry.filename == PAGES_FILENAME);
|
|
42
|
+
if (!pagesEntry) {
|
|
43
|
+
throw new Error("Pages data not found");
|
|
44
|
+
}
|
|
45
|
+
const { pages } = JSON.parse(await pagesEntry.getData(new zip.TextWriter()));
|
|
46
|
+
pages.forEach(page => {
|
|
47
|
+
urlToPath.set(stripFragment(page.url), page.path);
|
|
48
|
+
if (page.originalUrls) {
|
|
49
|
+
page.originalUrls.forEach(url => urlToPath.set(stripFragment(url), page.path));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
attachListeners();
|
|
53
|
+
return renderRoute();
|
|
54
|
+
|
|
55
|
+
// document.open() removes the listeners of the document and of its window,
|
|
56
|
+
// re-attaching identical function references is idempotent
|
|
57
|
+
function attachListeners() {
|
|
58
|
+
globalThis.addEventListener("click", interceptClick, true);
|
|
59
|
+
globalThis.addEventListener("hashchange", onHashChange);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onHashChange() {
|
|
63
|
+
if (location.hash.startsWith(ROUTE_PREFIX) || !location.hash) {
|
|
64
|
+
renderRoute().catch(error => globalThis.console.error(error));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function interceptClick(event) {
|
|
69
|
+
let node = event.target;
|
|
70
|
+
while (node && node.tagName != "A") {
|
|
71
|
+
node = node.parentNode;
|
|
72
|
+
}
|
|
73
|
+
if (node && node.href) {
|
|
74
|
+
const path = urlToPath.get(stripFragment(node.href));
|
|
75
|
+
if (path !== undefined) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
location.hash = ROUTE_PREFIX + path;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function renderRoute() {
|
|
83
|
+
const hash = decodeURIComponent(location.hash);
|
|
84
|
+
const path = hash.startsWith(ROUTE_PREFIX) ? hash.substring(ROUTE_PREFIX.length) : pages[0].path;
|
|
85
|
+
if (path == currentPath || !pages.find(page => page.path == path)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const docContent = await getPageContent(path);
|
|
89
|
+
currentPath = path;
|
|
90
|
+
await display(document, docContent);
|
|
91
|
+
attachListeners();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function getPageContent(path) {
|
|
95
|
+
if (!cache.has(path)) {
|
|
96
|
+
const pageEntries = entries.filter(entry => path == "" ?
|
|
97
|
+
!entry.filename.startsWith(PAGES_PREFIX) && entry.filename != PAGES_FILENAME :
|
|
98
|
+
entry.filename.startsWith(path));
|
|
99
|
+
const { docContent } = await extract(null, { entries: pageEntries, pagePath: path });
|
|
100
|
+
cache.set(path, docContent);
|
|
101
|
+
}
|
|
102
|
+
return cache.get(path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stripFragment(url) {
|
|
106
|
+
const indexFragment = url.indexOf("#");
|
|
107
|
+
return indexFragment == -1 ? url : url.substring(0, indexFragment);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -37,6 +37,9 @@ import {
|
|
|
37
37
|
import {
|
|
38
38
|
display
|
|
39
39
|
} from "./compression-display.js";
|
|
40
|
+
import {
|
|
41
|
+
router
|
|
42
|
+
} from "./compression-router.js";
|
|
40
43
|
|
|
41
44
|
const { Blob, fetch, TextEncoder, TextDecoder, DOMParser } = globalThis;
|
|
42
45
|
|
|
@@ -87,7 +90,8 @@ const PNG_IHDR_LENGTH = 25;
|
|
|
87
90
|
const browser = globalThis.browser;
|
|
88
91
|
|
|
89
92
|
export {
|
|
90
|
-
process
|
|
93
|
+
process,
|
|
94
|
+
createArchive
|
|
91
95
|
};
|
|
92
96
|
|
|
93
97
|
async function process(pageData, options, lastModDate = new Date()) {
|
|
@@ -98,16 +102,24 @@ async function process(pageData, options, lastModDate = new Date()) {
|
|
|
98
102
|
configure({ workerURI: "/lib/single-file-z-worker.js" });
|
|
99
103
|
script = await (await fetch(browser.runtime.getURL(SCRIPT_PATH))).text();
|
|
100
104
|
}
|
|
105
|
+
return createArchive(pageData, options, script, zipWriter => {
|
|
106
|
+
pageData.url = options.url;
|
|
107
|
+
pageData.archiveTime = (new Date()).toISOString();
|
|
108
|
+
return addPageResources(zipWriter, pageData, { password: options.password, disableCompression: options.disableCompression }, options.createRootDirectory ? String(Date.now()) + "_" + (options.tabId || 0) + "/" : "", options.url);
|
|
109
|
+
}, lastModDate);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function createArchive(pageData, options, script, writeEntries, lastModDate = new Date()) {
|
|
101
113
|
const zipDataWriter = new Uint8ArrayWriter();
|
|
102
114
|
zipDataWriter.init();
|
|
103
115
|
zipDataWriter.writable.size = 0;
|
|
104
116
|
let extraDataOffset, extraData, embeddedImageDataOffset, endTag;
|
|
105
117
|
if (options.embeddedImage) {
|
|
106
|
-
options.embeddedImage =
|
|
118
|
+
options.embeddedImage = new Uint8Array(options.embeddedImage);
|
|
107
119
|
const embeddedImageData = options.embeddedImage.slice(PNG_SIGNATURE_LENGTH + PNG_IHDR_LENGTH, options.embeddedImage.length - PNG_IEND_LENGTH);
|
|
108
120
|
await writeData(zipDataWriter.writable, options.embeddedImage.slice(0, PNG_SIGNATURE_LENGTH + PNG_IHDR_LENGTH));
|
|
109
121
|
if (options.selfExtractingArchive) {
|
|
110
|
-
const embeddedImageText = TEXT_DECODER.decode(
|
|
122
|
+
const embeddedImageText = TEXT_DECODER.decode(embeddedImageData);
|
|
111
123
|
const tagIndex = EMBEDDED_DATA_REGEXPS.slice(0, -1).findIndex(tests => !embeddedImageText.match(tests[1]));
|
|
112
124
|
let startTag;
|
|
113
125
|
[startTag, endTag] = tagIndex == -1 ? ["", ""] : EMBEDDED_DATA_TAGS[tagIndex];
|
|
@@ -135,9 +147,7 @@ async function process(pageData, options, lastModDate = new Date()) {
|
|
|
135
147
|
}
|
|
136
148
|
const zipWriter = new ZipWriter(zipDataWriter, { bufferedWrite: true, keepOrder: true, lastModDate, useCompressionStream: true });
|
|
137
149
|
const startOffset = zipDataWriter.offset;
|
|
138
|
-
|
|
139
|
-
pageData.archiveTime = (new Date()).toISOString();
|
|
140
|
-
await addPageResources(zipWriter, pageData, { password: options.password, disableCompression: options.disableCompression }, options.createRootDirectory ? String(Date.now()) + "_" + (options.tabId || 0) + "/" : "", options.url);
|
|
150
|
+
await writeEntries(zipWriter);
|
|
141
151
|
const data = await zipWriter.close(null, { preventClose: true });
|
|
142
152
|
if (options.selfExtractingArchive) {
|
|
143
153
|
const lfCodes = [];
|
|
@@ -149,12 +159,12 @@ async function process(pageData, options, lastModDate = new Date()) {
|
|
|
149
159
|
const tagIndex = EXTRA_DATA_TAGS.indexOf(options.extractDataFromPageTags);
|
|
150
160
|
const regExpsTag = EXTRA_DATA_REGEXPS[tagIndex];
|
|
151
161
|
if (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1])) {
|
|
152
|
-
return findExtraDataTags(textContent, pageData, options, lastModDate, tagIndex + 1);
|
|
162
|
+
return findExtraDataTags(textContent, pageData, options, script, writeEntries, lastModDate, tagIndex + 1);
|
|
153
163
|
}
|
|
154
164
|
} else {
|
|
155
165
|
const matchCommentTags = textContent.match(/<!--/i) || textContent.match(/--!?>/i);
|
|
156
166
|
if (matchCommentTags) {
|
|
157
|
-
return findExtraDataTags(textContent, pageData, options, lastModDate);
|
|
167
|
+
return findExtraDataTags(textContent, pageData, options, script, writeEntries, lastModDate);
|
|
158
168
|
}
|
|
159
169
|
}
|
|
160
170
|
}
|
|
@@ -195,12 +205,12 @@ async function process(pageData, options, lastModDate = new Date()) {
|
|
|
195
205
|
if (options.preventAppendedData || extraData.length > 65535 - endTags.length - (options.embeddedImage ? PNG_IEND_LENGTH : 0)) {
|
|
196
206
|
if (!options.extraDataSize) {
|
|
197
207
|
options.extraDataSize = Math.floor(extraData.length * 1.001);
|
|
198
|
-
return
|
|
208
|
+
return createArchive(pageData, options, script, writeEntries, lastModDate);
|
|
199
209
|
}
|
|
200
210
|
} else {
|
|
201
211
|
if (options.extraDataSize) {
|
|
202
212
|
options.extraDataSize = undefined;
|
|
203
|
-
return
|
|
213
|
+
return createArchive(pageData, options, script, writeEntries, lastModDate);
|
|
204
214
|
} else {
|
|
205
215
|
pageContent += extraData;
|
|
206
216
|
}
|
|
@@ -217,7 +227,7 @@ async function process(pageData, options, lastModDate = new Date()) {
|
|
|
217
227
|
} else {
|
|
218
228
|
options.extraData = extraData;
|
|
219
229
|
options.extraDataSize = Math.floor(extraData.length * 1.001);
|
|
220
|
-
return
|
|
230
|
+
return createArchive(pageData, options, script, writeEntries, lastModDate);
|
|
221
231
|
}
|
|
222
232
|
}
|
|
223
233
|
if (options.embeddedImage) {
|
|
@@ -225,7 +235,7 @@ async function process(pageData, options, lastModDate = new Date()) {
|
|
|
225
235
|
return new Blob([
|
|
226
236
|
pageContent,
|
|
227
237
|
getCRC32(pageContent, embeddedImageDataOffset),
|
|
228
|
-
|
|
238
|
+
options.embeddedImage.slice(options.embeddedImage.length - PNG_IEND_LENGTH)
|
|
229
239
|
], { type: "application/octet-stream" });
|
|
230
240
|
} else {
|
|
231
241
|
return new Blob([pageContent], { type: "application/octet-stream" });
|
|
@@ -271,6 +281,9 @@ async function prependHTMLData(pageData, zipDataWriter, script, options) {
|
|
|
271
281
|
pageContent += "<li style='margin-bottom:10px'><strong>Chrome/Edge/Brave</strong>: Install <a href='https://www.getsinglefile.com'>SingleFile</a> and enable the option \"Allow access to file URLs\" in the details page of the extension.</li>";
|
|
272
282
|
pageContent += "<li><strong>Safari</strong>: Select \"Security > Disable Local File Restrictions\" in the \"Develop > Developer settings\" menu.</li></ul></div>";
|
|
273
283
|
}
|
|
284
|
+
if (pageData.tocContent) {
|
|
285
|
+
pageContent += pageData.tocContent;
|
|
286
|
+
}
|
|
274
287
|
if (options.insertTextBody) {
|
|
275
288
|
const doc = (new DOMParser()).parseFromString(pageData.content, "text/html");
|
|
276
289
|
doc.body.querySelectorAll("style, script, noscript").forEach(element => element.remove());
|
|
@@ -293,12 +306,17 @@ async function prependHTMLData(pageData, zipDataWriter, script, options) {
|
|
|
293
306
|
insertEmbeddedImage: Boolean(options.embeddedImage),
|
|
294
307
|
insertEmbeddedScreenshotImage: Boolean(options.embeddedScreenshotImage)
|
|
295
308
|
};
|
|
309
|
+
const bootstrapBody = options.multiPageArchive ?
|
|
310
|
+
"(" + router.toString().replace(/\n|\t/g, "") + ")(content,{extract:" +
|
|
311
|
+
extract.toString().replace(/\n|\t/g, "") + ",display:" +
|
|
312
|
+
display.toString().replace(/\n|\t/g, "") + "})" :
|
|
313
|
+
"(" + extract.toString().replace(/\n|\t/g, "") + ")(content,{prompt}).then(({docContent}) => " +
|
|
314
|
+
display.toString().replace(/\n|\t/g, "") + "(document,docContent," + JSON.stringify(displayOptions) + "))";
|
|
296
315
|
script = "<script>" +
|
|
297
316
|
script +
|
|
298
317
|
"document.currentScript.remove();" +
|
|
299
|
-
"globalThis.bootstrap=(()=>{let bootstrapStarted;return async content=>{if (bootstrapStarted) return bootstrapStarted; bootstrapStarted =
|
|
300
|
-
|
|
301
|
-
display.toString().replace(/\n|\t/g, "") + "(document,docContent," + JSON.stringify(displayOptions) + "));return bootstrapStarted;}})();(" +
|
|
318
|
+
"globalThis.bootstrap=(()=>{let bootstrapStarted;return async content=>{if (bootstrapStarted) return bootstrapStarted; bootstrapStarted = " +
|
|
319
|
+
bootstrapBody + ";return bootstrapStarted;}})();(" +
|
|
302
320
|
getContent.toString().replace(/\n|\t/g, "") + ")().then(globalThis.bootstrap).then(() => document.dispatchEvent(new CustomEvent(\"single-file-display-infobar\"))).catch(error => {" +
|
|
303
321
|
"console.error(error);" +
|
|
304
322
|
"const waitMessage = document.getElementById(\"sfz-wait-message\");" +
|
|
@@ -376,16 +394,16 @@ function getPageTitle(pageData) {
|
|
|
376
394
|
return pageData.title.replace(/</g, "<").replace(/>/g, ">") || "";
|
|
377
395
|
}
|
|
378
396
|
|
|
379
|
-
function findExtraDataTags(textContent, pageData, options, lastModDate, indexExtractDataFromPageTags = 0) {
|
|
397
|
+
function findExtraDataTags(textContent, pageData, options, script, writeEntries, lastModDate, indexExtractDataFromPageTags = 0) {
|
|
380
398
|
const regExpsTag = EXTRA_DATA_REGEXPS[indexExtractDataFromPageTags];
|
|
381
399
|
const plaintextTag = EXTRA_DATA_TAGS[indexExtractDataFromPageTags][0] == "<plaintext>";
|
|
382
400
|
const matchTag = !plaintextTag && (textContent.match(regExpsTag[0]) || textContent.match(regExpsTag[1]));
|
|
383
401
|
if (matchTag) {
|
|
384
402
|
if (indexExtractDataFromPageTags < EXTRA_DATA_TAGS.length - 1) {
|
|
385
|
-
return findExtraDataTags(textContent, pageData, options, lastModDate, indexExtractDataFromPageTags + 1);
|
|
403
|
+
return findExtraDataTags(textContent, pageData, options, script, writeEntries, lastModDate, indexExtractDataFromPageTags + 1);
|
|
386
404
|
} else {
|
|
387
405
|
options.extractDataFromPage = false;
|
|
388
|
-
return
|
|
406
|
+
return createArchive(pageData, options, script, writeEntries, lastModDate);
|
|
389
407
|
}
|
|
390
408
|
} else {
|
|
391
409
|
options.extractDataFromPageTags = EXTRA_DATA_TAGS[indexExtractDataFromPageTags];
|
|
@@ -393,7 +411,7 @@ function findExtraDataTags(textContent, pageData, options, lastModDate, indexExt
|
|
|
393
411
|
// <plaintext> cannot be closed, the file must end with the zip data
|
|
394
412
|
options.preventAppendedData = true;
|
|
395
413
|
}
|
|
396
|
-
return
|
|
414
|
+
return createArchive(pageData, options, script, writeEntries, lastModDate);
|
|
397
415
|
}
|
|
398
416
|
}
|
|
399
417
|
|