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/eslint.config.mjs CHANGED
@@ -31,6 +31,12 @@ export default [
31
31
  ],
32
32
  "no-console": [
33
33
  "warn"
34
+ ],
35
+ "no-empty": [
36
+ "error",
37
+ {
38
+ "allowEmptyCatch": true
39
+ }
34
40
  ]
35
41
  }
36
42
  },
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.119",
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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;