single-file-core 1.5.90 → 1.5.91

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.
@@ -2,7 +2,11 @@
2
2
 
3
3
  name: Vendor
4
4
 
5
- on: [push, pull_request]
5
+ # tag pushes are excluded: creating a release would otherwise re-run the check already done on the branch
6
+ on:
7
+ push:
8
+ branches: ['**']
9
+ pull_request:
6
10
 
7
11
  jobs:
8
12
  rebuild:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.90",
3
+ "version": "1.5.91",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -27,8 +27,13 @@ export {
27
27
 
28
28
  async function router(content, { extract, display }) {
29
29
  const PAGES_PREFIX = "pages/";
30
+ const RESERVED_PREFIX = "sfz-";
30
31
  const PAGES_FILENAME = "sfz-pages.json";
32
+ const TOC_FILENAME = "sfz-toc.html";
31
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
+ const TOC_ROUTE = "?toc";
32
37
  const TARGET_ATTRIBUTE = "data-sfz-target";
33
38
  const TARGET_PSEUDO_CLASS = /:target(?![\w-])/g;
34
39
  const VISITED_ATTRIBUTE = "data-sfz-visited";
@@ -41,7 +46,7 @@ async function router(content, { extract, display }) {
41
46
  const UNARCHIVED_TITLE = "Not saved in this archive";
42
47
  const UNARCHIVED_PROTOCOLS = ["http:", "https:"];
43
48
  const PREFETCH_DELAY = 100;
44
- const { zip, document, location, history, CSS, setTimeout, clearTimeout } = globalThis;
49
+ const { zip, document, location, history, CSS, URL, setTimeout, clearTimeout } = globalThis;
45
50
  const cache = new Map();
46
51
  const urlToPath = new Map();
47
52
  const scrollStates = new Map();
@@ -62,6 +67,7 @@ async function router(content, { extract, display }) {
62
67
  throw new Error("Pages data not found");
63
68
  }
64
69
  const manifest = JSON.parse(await pagesEntry.getData(new zip.TextWriter()));
70
+ const tocEntry = entries.find(entry => entry.filename == TOC_FILENAME);
65
71
  const { pages } = manifest;
66
72
  const aliases = new Map(Object.entries(manifest.aliases || {}));
67
73
  pages.forEach(page => {
@@ -97,6 +103,11 @@ async function router(content, { extract, display }) {
97
103
  const node = findAnchor(event.target);
98
104
  if (node && node.href) {
99
105
  const fragment = getFragment(node.href);
106
+ // rewritten TOC links are already hash routes, default navigation
107
+ // triggers hashchange and modified clicks open the deep link natively
108
+ if (fragment && fragment.startsWith(ROUTE_PREFIX) && stripFragment(node.href) == stripFragment(location.href)) {
109
+ return;
110
+ }
100
111
  let path = urlToPath.get(stripFragment(node.href));
101
112
  if (path === undefined && fragment && stripFragment(node.href) == stripFragment(location.href)) {
102
113
  path = currentPath;
@@ -127,7 +138,7 @@ async function router(content, { extract, display }) {
127
138
  const continuityScrolls = captureElementScrolls();
128
139
  const entryId = getEntryId();
129
140
  const { routed, path, fragment } = parseRoute();
130
- const willRender = routed && path != currentPath && Boolean(pages.find(page => page.path == path));
141
+ const willRender = routed && path != currentPath && isRenderablePath(path);
131
142
  // the whole render and scroll sequence runs inside the view transition
132
143
  // so that the crossfade ends on the final scroll position
133
144
  if (willRender && document.startViewTransition && !prefersReducedMotion()) {
@@ -177,13 +188,16 @@ async function router(content, { extract, display }) {
177
188
  if (!routed && !initial) {
178
189
  return false;
179
190
  }
180
- if (path == currentPath || !pages.find(page => page.path == path)) {
191
+ if (path == currentPath || !isRenderablePath(path)) {
181
192
  return false;
182
193
  }
183
194
  const docContent = await getPageContent(path);
184
195
  currentPath = path;
185
196
  await display(document, docContent, { inPlace: true });
186
197
  attachListeners();
198
+ if (path == TOC_ROUTE) {
199
+ rewriteTocLinks();
200
+ }
187
201
  visitedPaths.add(path);
188
202
  markVisitedLinks();
189
203
  if (manifest.markUnarchivedLinks) {
@@ -205,14 +219,24 @@ async function router(content, { extract, display }) {
205
219
  let path = pages[0].path;
206
220
  let fragment;
207
221
  if (routed && hash) {
208
- const route = hash.substring(ROUTE_PREFIX.length);
209
- const indexFragment = route.indexOf("#");
210
- path = decodeURIComponent(indexFragment == -1 ? route : route.substring(0, indexFragment));
211
- fragment = indexFragment == -1 ? undefined : route.substring(indexFragment);
222
+ ({ path, fragment } = parseRouteHash(hash));
212
223
  }
213
224
  return { routed, path, fragment };
214
225
  }
215
226
 
227
+ function parseRouteHash(hash) {
228
+ const route = hash.substring(ROUTE_PREFIX.length);
229
+ const indexFragment = route.indexOf("#");
230
+ return {
231
+ path: decodeURIComponent(indexFragment == -1 ? route : route.substring(0, indexFragment)),
232
+ fragment: indexFragment == -1 ? undefined : route.substring(indexFragment)
233
+ };
234
+ }
235
+
236
+ function isRenderablePath(path) {
237
+ return path == TOC_ROUTE ? Boolean(tocEntry) : Boolean(pages.find(page => page.path == path));
238
+ }
239
+
216
240
  // the cache stores promises so that a click during a hover prefetch awaits
217
241
  // the extraction in flight instead of starting a second one
218
242
  function getPageContent(path) {
@@ -225,6 +249,9 @@ async function router(content, { extract, display }) {
225
249
  }
226
250
 
227
251
  async function extractPageContent(path) {
252
+ if (path == TOC_ROUTE) {
253
+ return tocEntry.getData(new zip.TextWriter());
254
+ }
228
255
  const pageEntries = entries
229
256
  .filter(entry => belongsToPage(entry.filename, path) && !aliases.has(entry.filename))
230
257
  .concat(getAliasEntries(path));
@@ -252,14 +279,37 @@ async function router(content, { extract, display }) {
252
279
 
253
280
  function belongsToPage(filename, path) {
254
281
  return path == "" ?
255
- !filename.startsWith(PAGES_PREFIX) && filename != PAGES_FILENAME :
282
+ !filename.startsWith(PAGES_PREFIX) && !filename.startsWith(RESERVED_PREFIX) :
256
283
  filename.startsWith(path);
257
284
  }
258
285
 
286
+ // the stored TOC links relative page paths so that it works once unzipped,
287
+ // in the archive they are rewritten to hash routes: unrewritten links would
288
+ // resolve against the archive URL and get stamped as unarchived
289
+ function rewriteTocLinks() {
290
+ const pathsByUrl = new Map();
291
+ pages.forEach(page => pathsByUrl.set(new URL(page.path + "index.html", stripFragment(location.href)).href, page.path));
292
+ document.querySelectorAll("a[href]").forEach(anchorElement => {
293
+ const fragment = getFragment(anchorElement.href);
294
+ const path = pathsByUrl.get(stripFragment(anchorElement.href));
295
+ if (path !== undefined) {
296
+ anchorElement.setAttribute("href", ROUTE_PREFIX + path + (fragment || ""));
297
+ }
298
+ });
299
+ }
300
+
301
+ function getLinkPath(href) {
302
+ const fragment = getFragment(href);
303
+ if (fragment && fragment.startsWith(ROUTE_PREFIX) && stripFragment(href) == stripFragment(location.href)) {
304
+ return parseRouteHash(fragment).path;
305
+ }
306
+ return urlToPath.get(stripFragment(href));
307
+ }
308
+
259
309
  function prefetchOnHover(event) {
260
310
  const node = findAnchor(event.target);
261
311
  if (node && node.href) {
262
- const path = urlToPath.get(stripFragment(node.href));
312
+ const path = getLinkPath(node.href);
263
313
  if (path !== undefined && path != currentPath && !cache.has(path)) {
264
314
  clearTimeout(prefetchTimeout);
265
315
  prefetchTimeout = setTimeout(() => getPageContent(path).catch(() => { }), PREFETCH_DELAY);
@@ -380,7 +430,7 @@ async function router(content, { extract, display }) {
380
430
  getPseudoRules(VISITED_PSEUDO_CLASS, VISITED_ATTRIBUTE);
381
431
  document.head.appendChild(styleElement);
382
432
  document.querySelectorAll("a[href]").forEach(anchorElement => {
383
- const path = urlToPath.get(stripFragment(anchorElement.href));
433
+ const path = getLinkPath(anchorElement.href);
384
434
  if (path !== undefined && visitedPaths.has(path)) {
385
435
  anchorElement.setAttribute(VISITED_ATTRIBUTE, "");
386
436
  }
@@ -112,7 +112,6 @@ async function process(pageData, options, lastModDate = new Date()) {
112
112
  async function createArchive(pageData, options, script, writeEntries, lastModDate = new Date()) {
113
113
  const zipDataWriter = new Uint8ArrayWriter();
114
114
  zipDataWriter.init();
115
- zipDataWriter.writable.size = 0;
116
115
  let extraDataOffset, extraData, embeddedImageDataOffset, endTag;
117
116
  if (options.embeddedImage) {
118
117
  options.embeddedImage = new Uint8Array(options.embeddedImage);
@@ -145,10 +144,15 @@ async function createArchive(pageData, options, script, writeEntries, lastModDat
145
144
  } else if (!options.embeddedImage && options.embeddedPdf) {
146
145
  await writeData(zipDataWriter.writable, new Uint8Array(options.embeddedPdf));
147
146
  }
148
- const zipWriter = new ZipWriter(zipDataWriter, { bufferedWrite: true, keepOrder: true, lastModDate, useCompressionStream: true });
147
+ // a WritableWriter object is passed instead of the writer so that the ZipWriter
148
+ // never takes ownership of the stream: preventClose is only honored when the
149
+ // caller owns the writable, and the HTML suffix still gets written after it;
150
+ // its size property tells the ZipWriter the offset of the data written so far
149
151
  const startOffset = zipDataWriter.offset;
152
+ const zipWriter = new ZipWriter({ writable: zipDataWriter.writable, size: startOffset }, { bufferedWrite: true, keepOrder: true, lastModDate, useCompressionStream: true });
150
153
  await writeEntries(zipWriter);
151
- const data = await zipWriter.close(null, { preventClose: true });
154
+ await zipWriter.close(undefined, { preventClose: true });
155
+ const data = zipDataWriter.getData();
152
156
  if (options.selfExtractingArchive) {
153
157
  const lfCodes = [];
154
158
  let crc32 = -1;
@@ -418,7 +422,6 @@ function findExtraDataTags(textContent, pageData, options, script, writeEntries,
418
422
  async function writeData(writable, array) {
419
423
  const streamWriter = writable.getWriter();
420
424
  await streamWriter.ready;
421
- writable.size += array.length;
422
425
  await streamWriter.write(array);
423
426
  streamWriter.releaseLock();
424
427
  }