single-file-core 1.5.119 → 1.5.121
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/core/lib/processor-helper-inline.js +3 -3
- package/core/util.js +3 -1
- package/doc/assets/singlefile-archive-byte-map.svg +195 -217
- package/doc/singlefile-archive.md +439 -180
- 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 +105 -181
- 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
package/eslint.config.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "single-file-core",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.121",
|
|
4
4
|
"description": "SingleFile Core",
|
|
5
5
|
"author": "Gildas Lormeau",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "deno run --allow-read test/sfz-harness/format-rules.js && deno run --allow-read test/sfz-harness/stored-trigger.js && deno run --allow-read test/sfz-harness/check-determinism.js && deno run --allow-read test/sfz-harness/option-wiring.js && deno run --allow-read test/sfz-harness/css-property-filter.js && deno run --allow-read test/sfz-harness/adopted-stylesheets-hook.js && deno run --allow-read test/sfz-harness/css-fonts-minifier.js && deno run --allow-read test/sfz-harness/inlined-functions.js && deno run --allow-read test/sfz-harness/pages-archive.js && deno run --allow-read test/sfz-harness/filename-max-length.js && deno run --allow-read test/sfz-harness/entry-compression.js && deno run --allow-read test/sfz-harness/filename-characters.js",
|
|
8
|
+
"test": "deno run --allow-read test/sfz-harness/format-rules.js && deno run --allow-read test/sfz-harness/stored-trigger.js && deno run --allow-read test/sfz-harness/check-determinism.js && deno run --allow-read test/sfz-harness/option-wiring.js && deno run --allow-read test/sfz-harness/css-property-filter.js && deno run --allow-read test/sfz-harness/adopted-stylesheets-hook.js && deno run --allow-read test/sfz-harness/css-fonts-minifier.js && deno run --allow-read test/sfz-harness/inlined-functions.js && deno run --allow-read test/sfz-harness/pages-archive.js && deno run --allow-read test/sfz-harness/filename-max-length.js && deno run --allow-read test/sfz-harness/entry-compression.js && deno run --allow-read test/sfz-harness/filename-characters.js && deno run --allow-read test/sfz-harness/byte-map.js && deno run --allow-read test/sfz-harness/zip64.js",
|
|
9
9
|
"bump-patch": "npm version patch --no-git-tag-version && npm run bump-commit",
|
|
10
10
|
"bump-minor": "npm version minor --no-git-tag-version && npm run bump-commit",
|
|
11
11
|
"bump-major": "npm version major --no-git-tag-version && npm run bump-commit",
|
|
@@ -27,9 +27,6 @@ export {
|
|
|
27
27
|
display
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
// the functions below are serialized with toString() and pasted into the page the archive
|
|
31
|
-
// generates, where module scope does not exist: everything they call must be declared inside
|
|
32
|
-
// them. An import would survive bundling and then be an undefined free variable at runtime
|
|
33
30
|
async function display(document, docContent, { disableFramePointerEvents, inPlace } = {}) {
|
|
34
31
|
function getDoctypeString(doc) {
|
|
35
32
|
const docType = doc.doctype;
|
|
@@ -61,15 +58,7 @@ async function display(document, docContent, { disableFramePointerEvents, inPlac
|
|
|
61
58
|
element.style.setProperty(pointerEvents, "none", "important");
|
|
62
59
|
});
|
|
63
60
|
}
|
|
64
|
-
// the in-place swap avoids the document churn of document.open() but cannot
|
|
65
|
-
// change the compat mode and does not execute script elements; it morphs
|
|
66
|
-
// documentElement instead of replacing it so that document-level childList
|
|
67
|
-
// observers (e.g. extension content scripts watching for document.open())
|
|
68
|
-
// do not fire on every rendered page
|
|
69
61
|
if (inPlace && doc.compatMode == document.compatMode && !doc.querySelector("script")) {
|
|
70
|
-
// stylesheet links inserted from script are not render-blocking; preload
|
|
71
|
-
// them while the previous page is still displayed and keep the new page
|
|
72
|
-
// hidden until they are applied to avoid a flash of unstyled content
|
|
73
62
|
await Promise.all(Array.from(doc.querySelectorAll("link[rel~=stylesheet][href]")).map(linkElement => new Promise(resolve => {
|
|
74
63
|
const preloadElement = document.createElement("link");
|
|
75
64
|
preloadElement.rel = "preload";
|
|
@@ -88,8 +88,6 @@ async function extract(content, { password, prompt = () => { }, zipOptions = { u
|
|
|
88
88
|
zipReader = new zip.ZipReader(reader, zipOptions);
|
|
89
89
|
entries = await zipReader.getEntries();
|
|
90
90
|
}
|
|
91
|
-
// page.pdf data lies in the HTML head, outside the zip region recovered in
|
|
92
|
-
// universal mode, and is never referenced by the displayed page
|
|
93
91
|
entries = entries.filter(entry => entry.filename != "page.pdf");
|
|
94
92
|
if (aliases) {
|
|
95
93
|
const allEntries = entries;
|
|
@@ -110,8 +108,6 @@ async function extract(content, { password, prompt = () => { }, zipOptions = { u
|
|
|
110
108
|
} else if (excludedPaths) {
|
|
111
109
|
entries = entries.filter(entry => !excludedPaths.some(excludedPath => entry.filename.startsWith(excludedPath)));
|
|
112
110
|
}
|
|
113
|
-
// the options are applied per reader and per entry: a global zip.configure() would leak
|
|
114
|
-
// them to every other caller in the page, and the default would re-enable web workers
|
|
115
111
|
const options = Object.assign({ password }, zipOptions);
|
|
116
112
|
let docContent, origDocContent, url, resources = [], indexPages = [], textResources = [];
|
|
117
113
|
await Promise.all(entries.map(async entry => {
|
|
@@ -85,8 +85,6 @@ async function createPagesArchive(pages, options) {
|
|
|
85
85
|
extractDataFromPage: options.extractDataFromPage,
|
|
86
86
|
preventAppendedData: options.preventAppendedData,
|
|
87
87
|
declareAppendedData: options.declareAppendedData,
|
|
88
|
-
// the faces belong to the archive and are supplied by the caller: one rendered from a
|
|
89
|
-
// single page would misrepresent every other page, so nothing is generated here
|
|
90
88
|
embeddedPdf: options.embeddedPdf,
|
|
91
89
|
embeddedImage: options.embeddedImage,
|
|
92
90
|
includeBOM: options.includeBOM,
|
|
@@ -114,9 +112,6 @@ async function createPagesArchive(pages, options) {
|
|
|
114
112
|
lastModDate: entry.lastModDate
|
|
115
113
|
});
|
|
116
114
|
} else {
|
|
117
|
-
// the duplicate becomes a symlink entry so that external
|
|
118
|
-
// extractors still produce complete page folders, the router
|
|
119
|
-
// resolves it from the manifest alias map instead
|
|
120
115
|
aliases[filename] = canonicalFilename;
|
|
121
116
|
await zipWriter.add(filename, new TextReader(getRelativePath(filename, canonicalFilename)), {
|
|
122
117
|
msDosCompatible: false,
|
|
@@ -205,8 +200,6 @@ function getTOCPageContent(pages) {
|
|
|
205
200
|
escapeUnicodeHTML(TOC_TITLE) + "</h1>" + getTOCGroupContent(rootGroup) + "</main></body></html>";
|
|
206
201
|
}
|
|
207
202
|
|
|
208
|
-
// nested details/summary groups stay collapsible without scripts on purpose,
|
|
209
|
-
// the page must remain usable after a plain unzip
|
|
210
203
|
function getTOCGroupContent(group) {
|
|
211
204
|
let content = "";
|
|
212
205
|
if (group.pages.length) {
|
|
@@ -219,15 +212,10 @@ function getTOCGroupContent(group) {
|
|
|
219
212
|
return content;
|
|
220
213
|
}
|
|
221
214
|
|
|
222
|
-
// unlike the prelude TOC below, the stored page is a UTF-8 entry: only the
|
|
223
|
-
// markup delimiters need escaping, but crawled titles remain untrusted
|
|
224
215
|
function escapeUnicodeHTML(value) {
|
|
225
216
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
226
217
|
}
|
|
227
218
|
|
|
228
|
-
// the list of pages in the prelude is the only description of the archive readable without
|
|
229
|
-
// decompressing an entry, for indexing tools that never extract it; the table of contents
|
|
230
|
-
// page stored in the archive is what serves navigation
|
|
231
219
|
function getTOCContent(pages) {
|
|
232
220
|
return "<nav><ul>" +
|
|
233
221
|
pages.map(page => "<li><a href=\"" + escapeHTML(page.url) + "\">" + escapeHTML(page.title || page.url) + "</a></li>").join("") +
|
|
@@ -31,8 +31,6 @@ async function router(content, { extract, display }) {
|
|
|
31
31
|
const PAGES_FILENAME = "sfz-pages.json";
|
|
32
32
|
const TOC_FILENAME = "sfz-toc.html";
|
|
33
33
|
const ROUTE_PREFIX = "#sfz/";
|
|
34
|
-
// "?" cannot start a zip entry path written by the packager, so reserved
|
|
35
|
-
// routes never collide with page paths
|
|
36
34
|
const TOC_ROUTE = "?toc";
|
|
37
35
|
const TARGET_ATTRIBUTE = "data-sfz-target";
|
|
38
36
|
const TARGET_PSEUDO_CLASS = /:target(?![\w-])/g;
|
|
@@ -40,8 +38,6 @@ async function router(content, { extract, display }) {
|
|
|
40
38
|
const VISITED_PSEUDO_CLASS = /:visited(?![\w-])/g;
|
|
41
39
|
const VISITED_DEFAULT_COLOR = "#551a8b";
|
|
42
40
|
const UNARCHIVED_ATTRIBUTE = "data-sfz-unarchived";
|
|
43
|
-
// relative units, currentColor and opacity keep the marker legible on any
|
|
44
|
-
// page theme, and \2197 stays ASCII in the windows-1252 prelude
|
|
45
41
|
const UNARCHIVED_STYLE = "a[" + UNARCHIVED_ATTRIBUTE + "]::after{content:\" \\2197\";font-size:.75em;opacity:.7}";
|
|
46
42
|
const UNARCHIVED_TITLE = "Not saved in this archive";
|
|
47
43
|
const UNARCHIVED_PROTOCOLS = ["http:", "https:"];
|
|
@@ -57,7 +53,6 @@ async function router(content, { extract, display }) {
|
|
|
57
53
|
try {
|
|
58
54
|
history.scrollRestoration = "manual";
|
|
59
55
|
} catch {
|
|
60
|
-
// ignored
|
|
61
56
|
}
|
|
62
57
|
zip.configure({ useWebWorkers: true });
|
|
63
58
|
const zipReader = new zip.ZipReader(content.readUint8Array ? content : new zip.BlobReader(content));
|
|
@@ -84,8 +79,6 @@ async function router(content, { extract, display }) {
|
|
|
84
79
|
}
|
|
85
80
|
return renderRoute(true);
|
|
86
81
|
|
|
87
|
-
// document.open() removes the listeners of the document and of its window,
|
|
88
|
-
// re-attaching identical function references is idempotent
|
|
89
82
|
function attachListeners() {
|
|
90
83
|
globalThis.addEventListener("click", interceptClick, true);
|
|
91
84
|
globalThis.addEventListener("auxclick", interceptClick, true);
|
|
@@ -104,8 +97,6 @@ async function router(content, { extract, display }) {
|
|
|
104
97
|
const node = findAnchor(event.target);
|
|
105
98
|
if (node && node.href) {
|
|
106
99
|
const fragment = getFragment(node.href);
|
|
107
|
-
// rewritten TOC links are already hash routes, default navigation
|
|
108
|
-
// triggers hashchange and modified clicks open the deep link natively
|
|
109
100
|
if (fragment && fragment.startsWith(ROUTE_PREFIX) && stripFragment(node.href) == stripFragment(location.href)) {
|
|
110
101
|
return;
|
|
111
102
|
}
|
|
@@ -115,13 +106,9 @@ async function router(content, { extract, display }) {
|
|
|
115
106
|
}
|
|
116
107
|
if (path !== undefined) {
|
|
117
108
|
event.preventDefault();
|
|
118
|
-
// modified and middle clicks open the archive deep link in a new
|
|
119
|
-
// tab instead of letting the browser open the live site URL
|
|
120
109
|
if (event.type == "auxclick" || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
121
110
|
globalThis.open(stripFragment(location.href) + ROUTE_PREFIX + path + (fragment || ""));
|
|
122
111
|
} else {
|
|
123
|
-
// the fragment stays encoded inside the route hash so that
|
|
124
|
-
// reloading or sharing the URL comes back to the same page
|
|
125
112
|
const previousHash = location.hash;
|
|
126
113
|
location.hash = ROUTE_PREFIX + path + (fragment || "");
|
|
127
114
|
if (location.hash == previousHash && fragment) {
|
|
@@ -140,8 +127,6 @@ async function router(content, { extract, display }) {
|
|
|
140
127
|
const entryId = getEntryId();
|
|
141
128
|
const { routed, path, fragment } = parseRoute();
|
|
142
129
|
const willRender = routed && path != currentPath && isRenderablePath(path);
|
|
143
|
-
// the whole render and scroll sequence runs inside the view transition
|
|
144
|
-
// so that the crossfade ends on the final scroll position
|
|
145
130
|
if (willRender && document.startViewTransition && pageTransitionEnabled() && !prefersReducedMotion()) {
|
|
146
131
|
await document.startViewTransition(update).updateCallbackDone;
|
|
147
132
|
} else {
|
|
@@ -238,8 +223,6 @@ async function router(content, { extract, display }) {
|
|
|
238
223
|
return path == TOC_ROUTE ? Boolean(tocEntry) : Boolean(pages.find(page => page.path == path));
|
|
239
224
|
}
|
|
240
225
|
|
|
241
|
-
// the cache stores promises so that a click during a hover prefetch awaits
|
|
242
|
-
// the extraction in flight instead of starting a second one
|
|
243
226
|
function getPageContent(path) {
|
|
244
227
|
if (!cache.has(path)) {
|
|
245
228
|
const contentPromise = extractPageContent(path);
|
|
@@ -260,8 +243,6 @@ async function router(content, { extract, display }) {
|
|
|
260
243
|
return docContent;
|
|
261
244
|
}
|
|
262
245
|
|
|
263
|
-
// deduplicated resources exist in the zip as symlink stand-ins for external
|
|
264
|
-
// extractors, they are resolved from the manifest alias map when extracting
|
|
265
246
|
function getAliasEntries(path) {
|
|
266
247
|
return Array.from(aliases)
|
|
267
248
|
.filter(([filename]) => belongsToPage(filename, path))
|
|
@@ -284,9 +265,6 @@ async function router(content, { extract, display }) {
|
|
|
284
265
|
filename.startsWith(path);
|
|
285
266
|
}
|
|
286
267
|
|
|
287
|
-
// the stored TOC links relative page paths so that it works once unzipped,
|
|
288
|
-
// in the archive they are rewritten to hash routes: unrewritten links would
|
|
289
|
-
// resolve against the archive URL and get stamped as unarchived
|
|
290
268
|
function rewriteTocLinks() {
|
|
291
269
|
const pathsByUrl = new Map();
|
|
292
270
|
pages.forEach(page => pathsByUrl.set(new URL(page.path + "index.html", stripFragment(location.href)).href, page.path));
|
|
@@ -325,9 +303,6 @@ async function router(content, { extract, display }) {
|
|
|
325
303
|
return node;
|
|
326
304
|
}
|
|
327
305
|
|
|
328
|
-
// "auto" approximates the cross-document opt-in of live sites: the transition
|
|
329
|
-
// runs when the displayed page itself contains an @view-transition rule set
|
|
330
|
-
// to navigation: auto, so pages without transitions stay instant
|
|
331
306
|
function pageTransitionEnabled() {
|
|
332
307
|
if (pageTransitions == "fade") {
|
|
333
308
|
return true;
|
|
@@ -366,7 +341,6 @@ async function router(content, { extract, display }) {
|
|
|
366
341
|
try {
|
|
367
342
|
history.replaceState({ sfzSession: sessionKey, sfzEntry: entryId }, "");
|
|
368
343
|
} catch {
|
|
369
|
-
// ignored
|
|
370
344
|
}
|
|
371
345
|
return entryId;
|
|
372
346
|
}
|
|
@@ -394,7 +368,6 @@ async function router(content, { extract, display }) {
|
|
|
394
368
|
try {
|
|
395
369
|
element = document.querySelector(path);
|
|
396
370
|
} catch {
|
|
397
|
-
// ignored
|
|
398
371
|
}
|
|
399
372
|
if (element) {
|
|
400
373
|
element.scrollTop = top;
|
|
@@ -417,9 +390,6 @@ async function router(content, { extract, display }) {
|
|
|
417
390
|
return segments.join(">");
|
|
418
391
|
}
|
|
419
392
|
|
|
420
|
-
// the fragment is scrolled to from script because the real fragment is the
|
|
421
|
-
// route hash, which also means :target never matches in rendered pages; the
|
|
422
|
-
// :target rules of the page are cloned against a marker attribute instead
|
|
423
393
|
function scrollToFragment(fragment) {
|
|
424
394
|
const target = findFragmentTarget(fragment);
|
|
425
395
|
if (target) {
|
|
@@ -447,8 +417,6 @@ async function router(content, { extract, display }) {
|
|
|
447
417
|
}
|
|
448
418
|
}
|
|
449
419
|
|
|
450
|
-
// :visited itself is privacy-gated, so visited routes are stamped with an
|
|
451
|
-
// attribute styled by the page's own :visited rules over a default color
|
|
452
420
|
function markVisitedLinks() {
|
|
453
421
|
const styleElement = document.createElement("style");
|
|
454
422
|
styleElement.textContent = "a[" + VISITED_ATTRIBUTE + "]{color:" + VISITED_DEFAULT_COLOR + "}" +
|
|
@@ -462,8 +430,6 @@ async function router(content, { extract, display }) {
|
|
|
462
430
|
});
|
|
463
431
|
}
|
|
464
432
|
|
|
465
|
-
// links that leave the archive are stamped so that the reader knows before
|
|
466
|
-
// clicking; opt-in at packaging time because the marker alters the rendering
|
|
467
433
|
function markUnarchivedLinks() {
|
|
468
434
|
const styleElement = document.createElement("style");
|
|
469
435
|
styleElement.textContent = UNARCHIVED_STYLE;
|
|
@@ -497,7 +463,6 @@ async function router(content, { extract, display }) {
|
|
|
497
463
|
try {
|
|
498
464
|
cssText += getPseudoRulesText(styleSheet.cssRules, pseudoRegExp, attributeName);
|
|
499
465
|
} catch {
|
|
500
|
-
// ignored
|
|
501
466
|
}
|
|
502
467
|
});
|
|
503
468
|
return cssText;
|