single-file-core 1.5.97 → 1.5.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/helper.js CHANGED
@@ -81,6 +81,7 @@ const DEFAULT_REPLACEMENT_CHARACTERS = ["~", "+", "?", "%", "*", ":"
81
81
  const CHARACTER_CLASS_SPECIAL_CHARACTERS = ["[", "]", "^", "-", "\\"];
82
82
  const NESTING_TRACK_ID_ATTRIBUTE_NAME = "data-sf-nesting-track-id";
83
83
  const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
84
+ const removeEventListener = (type, listener, options) => globalThis.removeEventListener(type, listener, options);
84
85
  // eslint-disable-next-line no-unused-vars
85
86
  const dispatchEvent = event => { try { globalThis.dispatchEvent(event); } catch (error) { /* ignored */ } };
86
87
  const JSON = globalThis.JSON;
@@ -173,30 +174,29 @@ function onInitUserScript({ detail }) {
173
174
  // ignored
174
175
  }
175
176
  const event = new CustomEvent(eventPrefixName + "-request", { cancelable: true, detail: detailUserScript });
177
+ const responseEventName = eventPrefixName + "-response";
176
178
  let resolvePromiseResponse;
177
- const promiseResponse = new Promise(resolve => {
178
- resolvePromiseResponse = resolve;
179
- addEventListener(eventPrefixName + "-response", event => {
180
- if (event.detail) {
181
- try {
182
- const detail = typeof event.detail == "string" ? JSON.parse(event.detail) : event.detail;
183
- if (detail.options) {
184
- Object.assign(options, detail.options);
185
- }
186
- // eslint-disable-next-line no-unused-vars
187
- } catch (error) {
188
- // ignored
179
+ const promiseResponse = new Promise(resolve => (resolvePromiseResponse = resolve));
180
+ const onResponse = event => {
181
+ if (event.detail) {
182
+ try {
183
+ const detail = typeof event.detail == "string" ? JSON.parse(event.detail) : event.detail;
184
+ if (detail.options) {
185
+ Object.assign(options, detail.options);
189
186
  }
187
+ // eslint-disable-next-line no-unused-vars
188
+ } catch (error) {
189
+ // ignored
190
190
  }
191
- resolve();
192
- });
193
- });
191
+ }
192
+ resolvePromiseResponse();
193
+ };
194
+ addEventListener(responseEventName, onResponse);
194
195
  dispatchEvent(event);
195
196
  if (event.defaultPrevented) {
196
197
  await promiseResponse;
197
- } else {
198
- resolvePromiseResponse();
199
198
  }
199
+ removeEventListener(responseEventName, onResponse);
200
200
  };
201
201
  }
202
202
 
@@ -152,12 +152,15 @@ class ProcessorHelperCommon {
152
152
  const response = await batchRequest.addURL(resourceURL, { expectedType: "image" });
153
153
  const svgDoc = util.parseSVGContent(response.content);
154
154
  if (hashMatch && hashMatch[0]) {
155
- let symbolElement;
156
- try {
157
- symbolElement = svgDoc.querySelector(hashMatch[0]);
158
- // eslint-disable-next-line no-unused-vars
159
- } catch (error) {
160
- // ignored
155
+ const symbolId = hashMatch[0].substring(1);
156
+ let symbolElement = svgDoc.getElementById(symbolId);
157
+ if (!symbolElement) {
158
+ try {
159
+ symbolElement = svgDoc.getElementById(decodeURIComponent(symbolId));
160
+ // eslint-disable-next-line no-unused-vars
161
+ } catch (error) {
162
+ // ignored
163
+ }
161
164
  }
162
165
  if (symbolElement) {
163
166
  resourceElement.setAttribute(attributeName, hashMatch[0]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.97",
3
+ "version": "1.5.99",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -27,7 +27,7 @@ export {
27
27
  extract
28
28
  };
29
29
 
30
- async function extract(content, { password, prompt = () => { }, zipOptions = { useWebWorkers: true }, noBlobURL, entries, pagePath = "" } = {}) {
30
+ async function extract(content, { password, prompt = () => { }, zipOptions = { useWebWorkers: true }, noBlobURL, entries, pagePath = "", excludedPaths } = {}) {
31
31
  const KNOWN_MIMETYPES = {
32
32
  "gif": "image/gif",
33
33
  "jpg": "image/jpeg",
@@ -91,6 +91,8 @@ async function extract(content, { password, prompt = () => { }, zipOptions = { u
91
91
  }
92
92
  if (pagePath) {
93
93
  entries = entries.filter(entry => entry.filename.startsWith(pagePath));
94
+ } else if (excludedPaths) {
95
+ entries = entries.filter(entry => !excludedPaths.some(excludedPath => entry.filename.startsWith(excludedPath)));
94
96
  }
95
97
  const options = { password };
96
98
  let docContent, origDocContent, url, resources = [], indexPages = [], textResources = [];
@@ -0,0 +1,230 @@
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 URL */
25
+
26
+ import {
27
+ configure,
28
+ TextReader,
29
+ Uint8ArrayReader,
30
+ Uint8ArrayWriter,
31
+ ZipReader
32
+ } from "./../../vendor/zip/zip.js";
33
+ import {
34
+ createArchive
35
+ } from "./compression.js";
36
+
37
+ const PAGES_PREFIX = "pages/";
38
+ const PAGES_FILENAME = "sfz-pages.json";
39
+ const TOC_FILENAME = "sfz-toc.html";
40
+ const TOC_TITLE = "Table of contents";
41
+ const TOC_STYLE = "body{font-family:system-ui,sans-serif;margin:2em auto;max-width:40em;padding:0 1em;background-color:#fff;color:#000}" +
42
+ "a{color:#0000ee}a:visited{color:#551a8b}" +
43
+ "summary{cursor:pointer;font-weight:bold;margin:.5em 0}" +
44
+ "details{padding-left:1em}ul{margin:.25em 0;padding-left:1.5em}" +
45
+ "@media(prefers-color-scheme:dark){body{background-color:#111;color:#eee}a{color:#8ab4f8}a:visited{color:#c58af9}}";
46
+ const COMMENT_HEADER = "Page saved with SingleFile";
47
+ const SYMLINK_UNIX_MODE = 0o120777;
48
+
49
+ export { createPagesArchive };
50
+
51
+ async function createPagesArchive(pages, options) {
52
+ configure({ useWebWorkers: false });
53
+ const manifest = {
54
+ pages: pages.map((page, pageIndex) => ({
55
+ path: getPagePath(pageIndex),
56
+ url: page.url,
57
+ originalUrls: page.originalUrls,
58
+ title: page.title
59
+ }))
60
+ };
61
+ if (options.markUnarchivedLinks) {
62
+ manifest.markUnarchivedLinks = true;
63
+ }
64
+ if (options.pageTransitions && options.pageTransitions != "auto") {
65
+ manifest.pageTransitions = options.pageTransitions;
66
+ }
67
+ const pageData = {
68
+ doctype: "<!DOCTYPE html>",
69
+ content: "",
70
+ title: pages[0].title || "",
71
+ comment: options.insertSingleFileComment ? getComment(pages[0].url, options) : undefined,
72
+ tocContent: getTOCContent(pages)
73
+ };
74
+ const archiveOptions = {
75
+ url: pages[0].url,
76
+ multiPageArchive: true,
77
+ selfExtractingArchive: options.selfExtractingArchive,
78
+ extractDataFromPage: options.extractDataFromPage,
79
+ preventAppendedData: options.preventAppendedData,
80
+ includeBOM: options.includeBOM,
81
+ insertMetaCSP: options.insertMetaCSP,
82
+ insertCanonicalLink: options.insertCanonicalLink,
83
+ insertMetaNoIndex: options.insertMetaNoIndex
84
+ };
85
+ const writtenEntries = options.dedupPages ? new Map() : undefined;
86
+ const aliases = {};
87
+ const blob = await createArchive(pageData, archiveOptions, options.zipScript, async zipWriter => {
88
+ for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
89
+ const pagePath = getPagePath(pageIndex);
90
+ const zipReader = new ZipReader(new Uint8ArrayReader(await pages[pageIndex].getData()));
91
+ for (const entry of await zipReader.getEntries()) {
92
+ const filename = pagePath + entry.filename;
93
+ const rawData = await entry.getData(new Uint8ArrayWriter(), { passThrough: true, checkSignature: false });
94
+ const canonicalFilename = writtenEntries && findDuplicate(writtenEntries, filename, entry, rawData);
95
+ if (canonicalFilename === undefined) {
96
+ await zipWriter.add(filename, new Uint8ArrayReader(rawData), {
97
+ passThrough: true,
98
+ compressionMethod: entry.compressionMethod,
99
+ uncompressedSize: entry.uncompressedSize,
100
+ signature: entry.signature,
101
+ comment: entry.comment,
102
+ lastModDate: entry.lastModDate
103
+ });
104
+ } else {
105
+ // the duplicate becomes a symlink entry so that external
106
+ // extractors still produce complete page folders, the router
107
+ // resolves it from the manifest alias map instead
108
+ aliases[filename] = canonicalFilename;
109
+ await zipWriter.add(filename, new TextReader(getRelativePath(filename, canonicalFilename)), {
110
+ msDosCompatible: false,
111
+ unixMode: SYMLINK_UNIX_MODE,
112
+ level: 0,
113
+ comment: entry.comment,
114
+ lastModDate: entry.lastModDate
115
+ });
116
+ }
117
+ }
118
+ await zipReader.close();
119
+ }
120
+ if (Object.keys(aliases).length) {
121
+ manifest.aliases = aliases;
122
+ }
123
+ if (options.tocPage) {
124
+ await zipWriter.add(TOC_FILENAME, new TextReader(getTOCPageContent(manifest.pages)));
125
+ }
126
+ await zipWriter.add(PAGES_FILENAME, new TextReader(JSON.stringify(manifest, null, 2)));
127
+ });
128
+ return new Uint8Array(await blob.arrayBuffer());
129
+ }
130
+
131
+ function findDuplicate(writtenEntries, filename, entry, rawData) {
132
+ if (entry.directory || !entry.uncompressedSize) {
133
+ return;
134
+ }
135
+ const key = [entry.compressionMethod, entry.uncompressedSize, entry.signature, rawData.length].join(":");
136
+ const candidates = writtenEntries.get(key);
137
+ if (candidates) {
138
+ const match = candidates.find(candidate => equalData(candidate.rawData, rawData));
139
+ if (match) {
140
+ return match.filename;
141
+ }
142
+ candidates.push({ filename, rawData });
143
+ } else {
144
+ writtenEntries.set(key, [{ filename, rawData }]);
145
+ }
146
+ }
147
+
148
+ function equalData(dataLeft, dataRight) {
149
+ return dataLeft.length == dataRight.length && dataLeft.every((value, index) => value == dataRight[index]);
150
+ }
151
+
152
+ function getRelativePath(filename, targetFilename) {
153
+ const baseSegments = filename.split("/").slice(0, -1);
154
+ const targetSegments = targetFilename.split("/");
155
+ while (baseSegments.length && targetSegments.length > 1 && baseSegments[0] == targetSegments[0]) {
156
+ baseSegments.shift();
157
+ targetSegments.shift();
158
+ }
159
+ return "../".repeat(baseSegments.length) + targetSegments.join("/");
160
+ }
161
+
162
+ function getPagePath(pageIndex) {
163
+ return pageIndex == 0 ? "" : PAGES_PREFIX + (pageIndex + 1) + "/";
164
+ }
165
+
166
+ function getComment(url, options) {
167
+ return "\n " + COMMENT_HEADER +
168
+ " \n url: " + url +
169
+ (options.removeSavedDate ? " " : " \n saved date: " + new Date()) + "\n";
170
+ }
171
+
172
+ function getTOCPageContent(pages) {
173
+ const origins = new Set(pages.map(page => new URL(page.url).origin));
174
+ const rootGroup = { groups: new Map(), pages: [] };
175
+ pages.forEach(page => {
176
+ const url = new URL(page.url);
177
+ const segments = url.pathname.split("/").slice(1, -1);
178
+ if (origins.size > 1) {
179
+ segments.unshift(url.origin);
180
+ }
181
+ let group = rootGroup;
182
+ segments.forEach(segment => {
183
+ if (!group.groups.has(segment)) {
184
+ group.groups.set(segment, { groups: new Map(), pages: [] });
185
+ }
186
+ group = group.groups.get(segment);
187
+ });
188
+ group.pages.push(page);
189
+ });
190
+ const title = pages[0].title ? TOC_TITLE + " - " + pages[0].title : TOC_TITLE;
191
+ return "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" +
192
+ "<title>" + escapeUnicodeHTML(title) + "</title><style>" + TOC_STYLE + "</style></head><body><main><h1>" +
193
+ escapeUnicodeHTML(TOC_TITLE) + "</h1>" + getTOCGroupContent(rootGroup) + "</main></body></html>";
194
+ }
195
+
196
+ // nested details/summary groups stay collapsible without scripts on purpose,
197
+ // the page must remain usable after a plain unzip
198
+ function getTOCGroupContent(group) {
199
+ let content = "";
200
+ if (group.pages.length) {
201
+ content += "<ul>" + group.pages.map(page =>
202
+ "<li><a href=\"" + escapeUnicodeHTML(page.path + "index.html") + "\">" + escapeUnicodeHTML(page.title || page.url) + "</a></li>").join("") + "</ul>";
203
+ }
204
+ group.groups.forEach((childGroup, segment) => {
205
+ content += "<details open><summary>" + escapeUnicodeHTML(segment) + "</summary>" + getTOCGroupContent(childGroup) + "</details>";
206
+ });
207
+ return content;
208
+ }
209
+
210
+ // unlike the prelude TOC below, the stored page is a UTF-8 entry: only the
211
+ // markup delimiters need escaping, but crawled titles remain untrusted
212
+ function escapeUnicodeHTML(value) {
213
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
214
+ }
215
+
216
+ function getTOCContent(pages) {
217
+ return "<nav><ul>" +
218
+ pages.map(page => "<li><a href=\"" + escapeHTML(page.url) + "\">" + escapeHTML(page.title || page.url) + "</a></li>").join("") +
219
+ "</ul></nav>";
220
+ }
221
+
222
+ // the prelude declares the windows-1252 charset, non-ASCII characters must be
223
+ // encoded as HTML entities to survive it
224
+ function escapeHTML(value) {
225
+ return Array.from(value).map(character => {
226
+ const codePoint = character.codePointAt(0);
227
+ return codePoint < 32 || codePoint > 126 || character == "&" || character == "<" || character == ">" || character == "\"" ?
228
+ "&#" + codePoint + ";" : character;
229
+ }).join("");
230
+ }
@@ -0,0 +1,26 @@
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
+ export * from "./processors/compression/compression.js";
25
+ export * from "./processors/compression/compression-packager.js";
26
+ export * from "./vendor/zip/zip.js";