single-file-core 1.5.90 → 1.5.92

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/core/helper.js CHANGED
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import * as cssUnescape from "./../vendor/css-unescape.js";
25
+ import * as sha from "./lib/sha.js";
25
26
  import * as hooksFrames from "./../processors/hooks/content/content-hooks-frames.js";
26
27
  import * as infobar from "./infobar.js";
27
28
  import {
@@ -793,7 +794,8 @@ function getContentSize(content) {
793
794
 
794
795
  async function digest(algo, text) {
795
796
  try {
796
- const hash = await crypto.subtle.digest(algo, new TextEncoder("utf-8").encode(text));
797
+ const data = new TextEncoder("utf-8").encode(text);
798
+ const hash = globalThis.crypto && crypto.subtle ? await crypto.subtle.digest(algo, data) : sha.digest(algo, data);
797
799
  return hex(hash);
798
800
  // eslint-disable-next-line no-unused-vars
799
801
  } catch (error) {
@@ -0,0 +1,242 @@
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
+ const H1 = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
25
+ const H256 = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
26
+ const K256 = [
27
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
28
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
29
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
30
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
31
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
32
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
33
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
34
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
35
+ ];
36
+ const H384 = [
37
+ 0xcbbb9d5dc1059ed8n, 0x629a292a367cd507n, 0x9159015a3070dd17n, 0x152fecd8f70e5939n,
38
+ 0x67332667ffc00b31n, 0x8eb44a8768581511n, 0xdb0c2e0d64f98fa7n, 0x47b5481dbefa4fa4n
39
+ ];
40
+ const H512 = [
41
+ 0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n,
42
+ 0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n
43
+ ];
44
+ const K512 = [
45
+ 0x428a2f98d728ae22n, 0x7137449123ef65cdn, 0xb5c0fbcfec4d3b2fn, 0xe9b5dba58189dbbcn,
46
+ 0x3956c25bf348b538n, 0x59f111f1b605d019n, 0x923f82a4af194f9bn, 0xab1c5ed5da6d8118n,
47
+ 0xd807aa98a3030242n, 0x12835b0145706fben, 0x243185be4ee4b28cn, 0x550c7dc3d5ffb4e2n,
48
+ 0x72be5d74f27b896fn, 0x80deb1fe3b1696b1n, 0x9bdc06a725c71235n, 0xc19bf174cf692694n,
49
+ 0xe49b69c19ef14ad2n, 0xefbe4786384f25e3n, 0x0fc19dc68b8cd5b5n, 0x240ca1cc77ac9c65n,
50
+ 0x2de92c6f592b0275n, 0x4a7484aa6ea6e483n, 0x5cb0a9dcbd41fbd4n, 0x76f988da831153b5n,
51
+ 0x983e5152ee66dfabn, 0xa831c66d2db43210n, 0xb00327c898fb213fn, 0xbf597fc7beef0ee4n,
52
+ 0xc6e00bf33da88fc2n, 0xd5a79147930aa725n, 0x06ca6351e003826fn, 0x142929670a0e6e70n,
53
+ 0x27b70a8546d22ffcn, 0x2e1b21385c26c926n, 0x4d2c6dfc5ac42aedn, 0x53380d139d95b3dfn,
54
+ 0x650a73548baf63den, 0x766a0abb3c77b2a8n, 0x81c2c92e47edaee6n, 0x92722c851482353bn,
55
+ 0xa2bfe8a14cf10364n, 0xa81a664bbc423001n, 0xc24b8b70d0f89791n, 0xc76c51a30654be30n,
56
+ 0xd192e819d6ef5218n, 0xd69906245565a910n, 0xf40e35855771202an, 0x106aa07032bbd1b8n,
57
+ 0x19a4c116b8d2d0c8n, 0x1e376c085141ab53n, 0x2748774cdf8eeb99n, 0x34b0bcb5e19b48a8n,
58
+ 0x391c0cb3c5c95a63n, 0x4ed8aa4ae3418acbn, 0x5b9cca4f7763e373n, 0x682e6ff3d6b2b8a3n,
59
+ 0x748f82ee5defb2fcn, 0x78a5636f43172f60n, 0x84c87814a1f0ab72n, 0x8cc702081a6439ecn,
60
+ 0x90befffa23631e28n, 0xa4506cebde82bde9n, 0xbef9a3f7b2c67915n, 0xc67178f2e372532bn,
61
+ 0xca273eceea26619cn, 0xd186b8c721c0c207n, 0xeada7dd6cde0eb1en, 0xf57d4f7fee6ed178n,
62
+ 0x06f067aa72176fban, 0x0a637dc5a2c898a6n, 0x113f9804bef90daen, 0x1b710b35131c471bn,
63
+ 0x28db77f523047d84n, 0x32caab7b40c72493n, 0x3c9ebe0a15c9bebcn, 0x431d67c49c100d4cn,
64
+ 0x4cc5d4becb3e42b6n, 0x597f299cfc657e2an, 0x5fcb6fab3ad6faecn, 0x6c44198c4a475817n
65
+ ];
66
+ const MASK_64 = 0xffffffffffffffffn;
67
+
68
+ export {
69
+ digest
70
+ };
71
+
72
+ function digest(algorithm, data) {
73
+ if (algorithm == "SHA-1") {
74
+ return digestSha1(data);
75
+ } else if (algorithm == "SHA-256") {
76
+ return digestSha256(data);
77
+ } else if (algorithm == "SHA-384") {
78
+ return digestSha512(data, H384, 6);
79
+ } else if (algorithm == "SHA-512") {
80
+ return digestSha512(data, H512, 8);
81
+ } else {
82
+ throw new Error("Unsupported algorithm: " + algorithm);
83
+ }
84
+ }
85
+
86
+ function digestSha1(data) {
87
+ const state = Int32Array.from(H1);
88
+ const schedule = new Int32Array(80);
89
+ const view = padMessage(data, 64);
90
+ for (let blockOffset = 0; blockOffset < view.byteLength; blockOffset += 64) {
91
+ for (let index = 0; index < 16; index++) {
92
+ schedule[index] = view.getUint32(blockOffset + index * 4);
93
+ }
94
+ for (let index = 16; index < 80; index++) {
95
+ const value = schedule[index - 3] ^ schedule[index - 8] ^ schedule[index - 14] ^ schedule[index - 16];
96
+ schedule[index] = (value << 1) | (value >>> 31);
97
+ }
98
+ let [a, b, c, d, e] = state;
99
+ for (let index = 0; index < 80; index++) {
100
+ let mix, constant;
101
+ if (index < 20) {
102
+ mix = (b & c) | (~b & d);
103
+ constant = 0x5a827999;
104
+ } else if (index < 40) {
105
+ mix = b ^ c ^ d;
106
+ constant = 0x6ed9eba1;
107
+ } else if (index < 60) {
108
+ mix = (b & c) | (b & d) | (c & d);
109
+ constant = 0x8f1bbcdc;
110
+ } else {
111
+ mix = b ^ c ^ d;
112
+ constant = 0xca62c1d6;
113
+ }
114
+ const value = (((a << 5) | (a >>> 27)) + mix + e + constant + schedule[index]) | 0;
115
+ e = d;
116
+ d = c;
117
+ c = (b << 30) | (b >>> 2);
118
+ b = a;
119
+ a = value;
120
+ }
121
+ state[0] += a;
122
+ state[1] += b;
123
+ state[2] += c;
124
+ state[3] += d;
125
+ state[4] += e;
126
+ }
127
+ return serializeState32(state);
128
+ }
129
+
130
+ function digestSha256(data) {
131
+ const state = Int32Array.from(H256);
132
+ const schedule = new Int32Array(64);
133
+ const view = padMessage(data, 64);
134
+ for (let blockOffset = 0; blockOffset < view.byteLength; blockOffset += 64) {
135
+ for (let index = 0; index < 16; index++) {
136
+ schedule[index] = view.getUint32(blockOffset + index * 4);
137
+ }
138
+ for (let index = 16; index < 64; index++) {
139
+ const value0 = schedule[index - 15];
140
+ const value1 = schedule[index - 2];
141
+ const sigma0 = rotateRight32(value0, 7) ^ rotateRight32(value0, 18) ^ (value0 >>> 3);
142
+ const sigma1 = rotateRight32(value1, 17) ^ rotateRight32(value1, 19) ^ (value1 >>> 10);
143
+ schedule[index] = schedule[index - 16] + sigma0 + schedule[index - 7] + sigma1;
144
+ }
145
+ let [a, b, c, d, e, f, g, h] = state;
146
+ for (let index = 0; index < 64; index++) {
147
+ const sum1 = rotateRight32(e, 6) ^ rotateRight32(e, 11) ^ rotateRight32(e, 25);
148
+ const choice = (e & f) ^ (~e & g);
149
+ const value1 = (h + sum1 + choice + K256[index] + schedule[index]) | 0;
150
+ const sum0 = rotateRight32(a, 2) ^ rotateRight32(a, 13) ^ rotateRight32(a, 22);
151
+ const majority = (a & b) ^ (a & c) ^ (b & c);
152
+ const value2 = (sum0 + majority) | 0;
153
+ h = g;
154
+ g = f;
155
+ f = e;
156
+ e = (d + value1) | 0;
157
+ d = c;
158
+ c = b;
159
+ b = a;
160
+ a = (value1 + value2) | 0;
161
+ }
162
+ state[0] += a;
163
+ state[1] += b;
164
+ state[2] += c;
165
+ state[3] += d;
166
+ state[4] += e;
167
+ state[5] += f;
168
+ state[6] += g;
169
+ state[7] += h;
170
+ }
171
+ return serializeState32(state);
172
+ }
173
+
174
+ function digestSha512(data, initialState, stateLength) {
175
+ const state = Array.from(initialState);
176
+ const schedule = new Array(80);
177
+ const view = padMessage(data, 128);
178
+ for (let blockOffset = 0; blockOffset < view.byteLength; blockOffset += 128) {
179
+ for (let index = 0; index < 16; index++) {
180
+ schedule[index] = (BigInt(view.getUint32(blockOffset + index * 8)) << 32n) | BigInt(view.getUint32(blockOffset + index * 8 + 4));
181
+ }
182
+ for (let index = 16; index < 80; index++) {
183
+ const value0 = schedule[index - 15];
184
+ const value1 = schedule[index - 2];
185
+ const sigma0 = rotateRight64(value0, 1n) ^ rotateRight64(value0, 8n) ^ (value0 >> 7n);
186
+ const sigma1 = rotateRight64(value1, 19n) ^ rotateRight64(value1, 61n) ^ (value1 >> 6n);
187
+ schedule[index] = (schedule[index - 16] + sigma0 + schedule[index - 7] + sigma1) & MASK_64;
188
+ }
189
+ let [a, b, c, d, e, f, g, h] = state;
190
+ for (let index = 0; index < 80; index++) {
191
+ const sum1 = rotateRight64(e, 14n) ^ rotateRight64(e, 18n) ^ rotateRight64(e, 41n);
192
+ const choice = (e & f) ^ (~e & g);
193
+ const value1 = (h + sum1 + choice + K512[index] + schedule[index]) & MASK_64;
194
+ const sum0 = rotateRight64(a, 28n) ^ rotateRight64(a, 34n) ^ rotateRight64(a, 39n);
195
+ const majority = (a & b) ^ (a & c) ^ (b & c);
196
+ const value2 = (sum0 + majority) & MASK_64;
197
+ h = g;
198
+ g = f;
199
+ f = e;
200
+ e = (d + value1) & MASK_64;
201
+ d = c;
202
+ c = b;
203
+ b = a;
204
+ a = (value1 + value2) & MASK_64;
205
+ }
206
+ const values = [a, b, c, d, e, f, g, h];
207
+ values.forEach((value, index) => state[index] = (state[index] + value) & MASK_64);
208
+ }
209
+ const buffer = new ArrayBuffer(stateLength * 8);
210
+ const outputView = new DataView(buffer);
211
+ for (let index = 0; index < stateLength; index++) {
212
+ outputView.setUint32(index * 8, Number(state[index] >> 32n));
213
+ outputView.setUint32(index * 8 + 4, Number(state[index] & 0xffffffffn));
214
+ }
215
+ return buffer;
216
+ }
217
+
218
+ function padMessage(data, blockLength) {
219
+ const paddedLength = Math.ceil((data.length + 1 + blockLength / 8) / blockLength) * blockLength;
220
+ const padded = new Uint8Array(paddedLength);
221
+ padded.set(data);
222
+ padded[data.length] = 0x80;
223
+ const view = new DataView(padded.buffer);
224
+ view.setUint32(paddedLength - 8, Math.floor(data.length / 0x20000000));
225
+ view.setUint32(paddedLength - 4, data.length << 3);
226
+ return view;
227
+ }
228
+
229
+ function rotateRight32(value, count) {
230
+ return (value >>> count) | (value << (32 - count));
231
+ }
232
+
233
+ function rotateRight64(value, count) {
234
+ return ((value >> count) | (value << (64n - count))) & MASK_64;
235
+ }
236
+
237
+ function serializeState32(state) {
238
+ const buffer = new ArrayBuffer(state.length * 4);
239
+ const view = new DataView(buffer);
240
+ state.forEach((value, index) => view.setUint32(index * 4, value));
241
+ return buffer;
242
+ }
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.92",
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
  }