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.
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
|
|
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) {
|
package/core/lib/sha.js
ADDED
|
@@ -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
|
@@ -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 &&
|
|
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 || !
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|