frond-js 0.4.0

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +247 -0
  3. package/README.md +211 -0
  4. package/dist/abort-BY8vBk0v.d.cts +99 -0
  5. package/dist/abort-BY8vBk0v.d.ts +99 -0
  6. package/dist/adapter-3G46J3CA.cjs +503 -0
  7. package/dist/adapter-3ONQWJVQ.js +501 -0
  8. package/dist/adapter-55QHWRSE.js +124 -0
  9. package/dist/adapter-DF34GBWJ.cjs +19 -0
  10. package/dist/adapter-EXTNILTC.cjs +126 -0
  11. package/dist/adapter-GOFC7TMC.js +284 -0
  12. package/dist/adapter-LRJTQ47I.cjs +286 -0
  13. package/dist/adapter-TWWZML4A.js +17 -0
  14. package/dist/adapter-ZM5FQTJT.js +1415 -0
  15. package/dist/adapter-ZRNSDQUV.cjs +1417 -0
  16. package/dist/chunk-2SUG7YFZ.cjs +108 -0
  17. package/dist/chunk-B2L2YVXD.js +89 -0
  18. package/dist/chunk-D4KWSEZD.js +393 -0
  19. package/dist/chunk-EZTIZO6R.cjs +430 -0
  20. package/dist/chunk-G7DLWGBW.cjs +103 -0
  21. package/dist/chunk-GTGLDLJD.cjs +479 -0
  22. package/dist/chunk-IIV6VIUJ.cjs +83 -0
  23. package/dist/chunk-JDTHZQUK.js +102 -0
  24. package/dist/chunk-JJVXT3AC.js +30 -0
  25. package/dist/chunk-LIXRYQL2.js +473 -0
  26. package/dist/chunk-NABYHI6X.cjs +400 -0
  27. package/dist/chunk-SJVOYNTF.js +425 -0
  28. package/dist/chunk-SLI2YL25.cjs +252 -0
  29. package/dist/chunk-U4264IQH.js +78 -0
  30. package/dist/chunk-UY2YRCFC.js +250 -0
  31. package/dist/chunk-WCZTTQ7Z.cjs +32 -0
  32. package/dist/core/index.cjs +162 -0
  33. package/dist/core/index.d.cts +321 -0
  34. package/dist/core/index.d.ts +321 -0
  35. package/dist/core/index.js +49 -0
  36. package/dist/default-DRLIJX73.js +1183 -0
  37. package/dist/default-UK52WOO5.cjs +1192 -0
  38. package/dist/formats/epub/index.cjs +29 -0
  39. package/dist/formats/epub/index.d.cts +286 -0
  40. package/dist/formats/epub/index.d.ts +286 -0
  41. package/dist/formats/epub/index.js +11 -0
  42. package/dist/index.cjs +655 -0
  43. package/dist/index.d.cts +335 -0
  44. package/dist/index.d.ts +335 -0
  45. package/dist/index.js +600 -0
  46. package/dist/render/index.cjs +2 -0
  47. package/dist/render/index.d.cts +116 -0
  48. package/dist/render/index.d.ts +116 -0
  49. package/dist/render/index.js +1 -0
  50. package/dist/types-B76GOMxj.d.ts +129 -0
  51. package/dist/types-B7mslPBY.d.cts +166 -0
  52. package/dist/types-B7mslPBY.d.ts +166 -0
  53. package/dist/types-BH88rUYt.d.cts +129 -0
  54. package/dist/types-C-5eHSRH.d.ts +379 -0
  55. package/dist/types-CPUqTEPW.d.cts +379 -0
  56. package/dist/types-DQYmArgv.d.cts +17 -0
  57. package/dist/types-DQYmArgv.d.ts +17 -0
  58. package/package.json +115 -0
@@ -0,0 +1,1415 @@
1
+ import { throwIfAborted, FormatError, ParseError } from './chunk-B2L2YVXD.js';
2
+
3
+ // src/formats/mobi/anchors.ts
4
+ var LATIN1 = new TextDecoder("latin1");
5
+ var ASCII = new TextEncoder();
6
+ var ANCHOR_ID_PREFIX = "mobi-toc-";
7
+ function isWithinTag(raw, offset) {
8
+ const open = raw.lastIndexOf("<", offset - 1);
9
+ if (open === -1) return false;
10
+ const close = raw.indexOf(">", open);
11
+ return close === -1 || close >= offset;
12
+ }
13
+ function openTagInsertionPoint(raw, offset) {
14
+ const first = raw.charCodeAt(offset + 1);
15
+ const isLetter = first >= 65 && first <= 90 || first >= 97 && first <= 122;
16
+ if (!isLetter) return -1;
17
+ for (let i = offset + 2; i < raw.length; i++) {
18
+ const code = raw.charCodeAt(i);
19
+ if (code === 32 || code === 9 || code === 10 || code === 13 || code === 47 || code === 62) {
20
+ return i;
21
+ }
22
+ }
23
+ return -1;
24
+ }
25
+ function landingAt(raw, offset) {
26
+ const miss = (kind) => ({ kind, insertionPoint: -1 });
27
+ if (!Number.isInteger(offset) || offset <= 0 || offset >= raw.length) return miss("out-of-range");
28
+ if (isWithinTag(raw, offset)) return miss("inside-tag");
29
+ if (raw[offset] !== "<") return miss("in-text");
30
+ const insertionPoint = openTagInsertionPoint(raw, offset);
31
+ return insertionPoint === -1 ? miss("not-open-tag") : { kind: "open-tag", insertionPoint };
32
+ }
33
+ function injectAnchors(bytes, offsets) {
34
+ const raw = LATIN1.decode(bytes);
35
+ const targets = [...new Set(offsets)].sort((a, b) => a - b);
36
+ const placements = [];
37
+ const edits = [];
38
+ const pieces = [];
39
+ let cursor = 0;
40
+ for (const at of targets) {
41
+ const landing = landingAt(raw, at);
42
+ if (landing.kind !== "open-tag") {
43
+ placements.push({ at, kind: landing.kind });
44
+ continue;
45
+ }
46
+ const id = `${ANCHOR_ID_PREFIX}${String(at)}`;
47
+ const insertion = ASCII.encode(` id="${id}"`);
48
+ pieces.push(bytes.subarray(cursor, landing.insertionPoint), insertion);
49
+ edits.push({ at: landing.insertionPoint, inserted: insertion.byteLength });
50
+ placements.push({ at, kind: "open-tag", id });
51
+ cursor = landing.insertionPoint;
52
+ }
53
+ if (edits.length === 0) return { bytes, edits, placements };
54
+ pieces.push(bytes.subarray(cursor));
55
+ const total = pieces.reduce((sum, piece) => sum + piece.byteLength, 0);
56
+ const out = new Uint8Array(total);
57
+ let offset = 0;
58
+ for (const piece of pieces) {
59
+ out.set(piece, offset);
60
+ offset += piece.byteLength;
61
+ }
62
+ return { bytes: out, edits, placements };
63
+ }
64
+
65
+ // src/formats/mobi/label.ts
66
+ function sourceSuffix(label) {
67
+ return label === void 0 ? "" : `\uFF08\u6E90 ${label}\uFF09`;
68
+ }
69
+
70
+ // src/formats/mobi/header.ts
71
+ var PALMDOC_HEADER_BYTES = 16;
72
+ var EXTH_MAGIC = "EXTH";
73
+ var MOBI6_FILE_VERSION = 6;
74
+ var KF8_FILE_VERSION = 8;
75
+ var UTF8_CODEPAGE = 65001;
76
+ var MOBI_MAGIC = "MOBI";
77
+ var EXTH_BLOCK_HEADER_BYTES = 12;
78
+ var EXTH_ENTRY_HEADER_BYTES = 8;
79
+ var AT = {
80
+ headerLength: 20,
81
+ codepage: 28,
82
+ fileVersion: 36,
83
+ fullNameOffset: 84,
84
+ fullNameLength: 88,
85
+ firstResourceRecord: 108,
86
+ huffOffset: 112,
87
+ huffCount: 116,
88
+ fdstOffset: 192,
89
+ fdstFlowCount: 196,
90
+ ncxIndex: 244,
91
+ fragmentIndex: 248
92
+ };
93
+ var MIN_KF8_HEADER_BYTES = AT.fdstFlowCount + 4;
94
+ var NO_INDEX = 4294967295;
95
+ var MIN_INDEX_HEADER_BYTES = AT.ncxIndex + 4 - PALMDOC_HEADER_BYTES;
96
+ var MIN_FRAGMENT_HEADER_BYTES = AT.fragmentIndex + 4 - PALMDOC_HEADER_BYTES;
97
+ var MIN_MOBI_HEADER_BYTES = AT.huffCount + 4;
98
+ function readIndexes(view, headerLength, fileVersion) {
99
+ if (headerLength < MIN_INDEX_HEADER_BYTES) return {};
100
+ const ncxIndex = view.getUint32(AT.ncxIndex, false);
101
+ const indexes = ncxIndex === NO_INDEX ? {} : { ncxIndex };
102
+ if (fileVersion !== KF8_FILE_VERSION) return indexes;
103
+ if (headerLength < MIN_FRAGMENT_HEADER_BYTES) return indexes;
104
+ const fragmentIndex = view.getUint32(AT.fragmentIndex, false);
105
+ return fragmentIndex === NO_INDEX ? indexes : { ...indexes, fragmentIndex };
106
+ }
107
+ function decoderFor(codepage) {
108
+ return new TextDecoder(codepage === UTF8_CODEPAGE ? "utf-8" : "windows-1252", {
109
+ ignoreBOM: true
110
+ });
111
+ }
112
+ function record0(palmdb, label) {
113
+ if (palmdb.recordCount < 1) {
114
+ throw new FormatError(`MOBI \u7F3A\u5C11\u8BB0\u5F55 0\uFF08\u5934\u8BB0\u5F55\uFF09\uFF1A\u5BB9\u5668\u91CC\u4E00\u6761\u8BB0\u5F55\u90FD\u6CA1\u6709${sourceSuffix(label)}`);
115
+ }
116
+ const bytes = palmdb.record(0);
117
+ if (bytes.byteLength < PALMDOC_HEADER_BYTES) {
118
+ throw new FormatError(
119
+ `MOBI \u8BB0\u5F55 0 \u4E0D\u8DB3 ${String(PALMDOC_HEADER_BYTES)} \u5B57\u8282\uFF08PalmDOC \u5934\uFF09\uFF1A\u5B9E\u9645 ${String(bytes.byteLength)} \u5B57\u8282${sourceSuffix(label)}`
120
+ );
121
+ }
122
+ return bytes;
123
+ }
124
+ function asciiAt(bytes, at, length) {
125
+ return String.fromCharCode(...bytes.subarray(at, at + length));
126
+ }
127
+ function parsePalmDocHeader(palmdb, label) {
128
+ const bytes = record0(palmdb, label);
129
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
130
+ return {
131
+ compression: view.getUint16(0, false),
132
+ textLength: view.getUint32(4, false),
133
+ textRecordCount: view.getUint16(8, false),
134
+ maxSectionSize: view.getUint16(10, false)
135
+ };
136
+ }
137
+ function parseExth(record, exthAt, label) {
138
+ const table = /* @__PURE__ */ new Map();
139
+ if (exthAt + EXTH_BLOCK_HEADER_BYTES > record.byteLength) return table;
140
+ if (asciiAt(record, exthAt, EXTH_MAGIC.length) !== EXTH_MAGIC) return table;
141
+ const view = new DataView(record.buffer, record.byteOffset, record.byteLength);
142
+ const declared = view.getUint32(exthAt + 4, false);
143
+ const end = exthAt + declared;
144
+ if (end > record.byteLength) {
145
+ throw new FormatError(
146
+ `EXTH \u5757\u957F\u5EA6\u8D8A\u754C\uFF1A\u81EA\u8FF0 ${String(declared)} \u5B57\u8282\uFF08\u5230 ${String(end)}\uFF09\uFF0C\u4F46\u8BB0\u5F55 0 \u53EA\u6709 ${String(record.byteLength)} \u5B57\u8282${sourceSuffix(label)}`
147
+ );
148
+ }
149
+ let cursor = exthAt + EXTH_BLOCK_HEADER_BYTES;
150
+ while (cursor + EXTH_ENTRY_HEADER_BYTES <= end) {
151
+ const type = view.getUint32(cursor, false);
152
+ const length = view.getUint32(cursor + 4, false);
153
+ if (length < EXTH_ENTRY_HEADER_BYTES) {
154
+ throw new FormatError(
155
+ `EXTH \u6761\u76EE\u957F\u5EA6\u975E\u6CD5\uFF1Atype ${String(type)} \u81EA\u8FF0 ${String(length)} \u5B57\u8282\uFF0C\u5C0F\u4E8E\u6761\u76EE\u5934\u81EA\u8EAB\u7684 ${String(EXTH_ENTRY_HEADER_BYTES)} \u5B57\u8282${sourceSuffix(label)}`
156
+ );
157
+ }
158
+ if (cursor + length > end) {
159
+ throw new FormatError(
160
+ `EXTH \u6761\u76EE\u8D8A\u51FA\u5757\u5C3E\uFF1Atype ${String(type)} \u5230 ${String(cursor + length)}\uFF0C\u5757\u5C3E\u5728 ${String(end)}${sourceSuffix(label)}`
161
+ );
162
+ }
163
+ const data = record.subarray(cursor + EXTH_ENTRY_HEADER_BYTES, cursor + length);
164
+ const list = table.get(type);
165
+ if (list === void 0) {
166
+ table.set(type, [data]);
167
+ } else {
168
+ list.push(data);
169
+ }
170
+ cursor += length;
171
+ }
172
+ return table;
173
+ }
174
+ function parseMobiHeader(palmdb, label) {
175
+ const bytes = record0(palmdb, label);
176
+ const magic = asciiAt(bytes, PALMDOC_HEADER_BYTES, MOBI_MAGIC.length);
177
+ if (magic !== MOBI_MAGIC) {
178
+ throw new FormatError(
179
+ `MOBI \u5934\u9B54\u6570\u4E0D\u7B26\uFF1A\u671F\u671B "${MOBI_MAGIC}"\uFF0C\u8BFB\u5230 "${magic}"${sourceSuffix(label)}`
180
+ );
181
+ }
182
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
183
+ const headerLength = view.getUint32(AT.headerLength, false);
184
+ const exthAt = PALMDOC_HEADER_BYTES + headerLength;
185
+ if (headerLength < EXTH_ENTRY_HEADER_BYTES || exthAt > bytes.byteLength) {
186
+ throw new FormatError(
187
+ `MOBI \u5934\u957F\u5EA6\u975E\u6CD5\uFF1AheaderLength = ${String(headerLength)}\uFF08EXTH \u8D77\u70B9 ${String(exthAt)}\uFF09\uFF0C\u8BB0\u5F55 0 \u53EA\u6709 ${String(bytes.byteLength)} \u5B57\u8282${sourceSuffix(label)}`
188
+ );
189
+ }
190
+ if (bytes.byteLength < PALMDOC_HEADER_BYTES + MIN_MOBI_HEADER_BYTES) {
191
+ throw new FormatError(
192
+ `MOBI \u5934\u8FC7\u77ED\uFF1A\u8BB0\u5F55 0 \u53EA\u6709 ${String(bytes.byteLength)} \u5B57\u8282\uFF0C\u800C 0x${MIN_MOBI_HEADER_BYTES.toString(16)} \u4E4B\u524D\u7684\u5B57\u6BB5\u9700\u8981 ${String(PALMDOC_HEADER_BYTES + MIN_MOBI_HEADER_BYTES)} \u5B57\u8282${sourceSuffix(label)}`
193
+ );
194
+ }
195
+ const fileVersion = view.getUint32(AT.fileVersion, false);
196
+ if (fileVersion !== MOBI6_FILE_VERSION && fileVersion !== KF8_FILE_VERSION) {
197
+ throw new FormatError(
198
+ `MOBI \u6587\u4EF6\u7248\u672C ${String(fileVersion)} \u4E0D\u53D7\u652F\u6301\uFF1A\u53EA\u652F\u6301 MOBI6\uFF08${String(MOBI6_FILE_VERSION)}\uFF09\u4E0E KF8 / AZW3\uFF08${String(KF8_FILE_VERSION)}\uFF09${sourceSuffix(label)}`
199
+ );
200
+ }
201
+ return {
202
+ headerLength,
203
+ fileVersion,
204
+ codepage: view.getUint32(AT.codepage, false),
205
+ firstResourceRecord: view.getUint32(AT.firstResourceRecord, false),
206
+ fullNameOffset: view.getUint32(AT.fullNameOffset, false),
207
+ fullNameLength: view.getUint32(AT.fullNameLength, false),
208
+ huff: {
209
+ offset: view.getUint32(AT.huffOffset, false),
210
+ count: view.getUint32(AT.huffCount, false)
211
+ },
212
+ // ⚠️ **只在 KF8 上读**:MOBI6 的同一偏移是 `first_content` / `last_content`(事实 3),
213
+ // 按 `u32` 读会得到越界的巨大数(实测 65746)。「没有」用 `undefined` 表达,
214
+ // 而不是读出一个 0 再让调用方去猜。
215
+ ...fileVersion === KF8_FILE_VERSION ? { fdst: readFdst(view, headerLength, label) } : {},
216
+ // 索引表位置(③b)。`ncx_index` 两个版本都读;`fragment_index` 仅 KF8(见 `readIndexes`)。
217
+ ...readIndexes(view, headerLength, fileVersion),
218
+ exth: parseExth(bytes, exthAt, label)
219
+ };
220
+ }
221
+ function readFdst(view, headerLength, label) {
222
+ if (headerLength < MIN_KF8_HEADER_BYTES) {
223
+ throw new FormatError(
224
+ `KF8 \u5934\u957F\u5EA6\u4E0D\u8DB3\uFF1AheaderLength = ${String(headerLength)}\uFF0C\u8BFB fdst \u5B57\u6BB5\u9700\u8981\u81F3\u5C11 ${String(MIN_KF8_HEADER_BYTES)} \u5B57\u8282${sourceSuffix(label)}`
225
+ );
226
+ }
227
+ return {
228
+ offset: view.getUint32(AT.fdstOffset, false),
229
+ flowCount: view.getUint32(AT.fdstFlowCount, false)
230
+ };
231
+ }
232
+ function exthTexts(exth, type, codepage) {
233
+ const decoder = decoderFor(codepage);
234
+ return (exth.get(type) ?? []).map((value) => decoder.decode(value));
235
+ }
236
+
237
+ // src/formats/mobi/kf8.ts
238
+ var FDST_MAGIC = "FDST";
239
+ var FDST_COUNT_AT = 8;
240
+ var FDST_SPAN_BYTES = 8;
241
+ var XML_DECL = [60, 63, 120, 109, 108];
242
+ var HTML_CLOSE = [60, 47, 104, 116, 109, 108, 62];
243
+ var LATIN12 = new TextDecoder("latin1");
244
+ var AID_TAG = /<([a-zA-Z][\w:-]*)\b[^>]*\said\s*=\s*["'][^"']*["'][^>]*>/g;
245
+ var KINDLE_URI = /kindle:(flow|embed):([0-9A-Za-z]+)(?:\?mime=([^"')\s>]+))?/g;
246
+ function base32(text) {
247
+ if (text === void 0 || text.length === 0 || text.length > 7) return void 0;
248
+ let value = 0;
249
+ for (const char of text) {
250
+ const code = char.charCodeAt(0);
251
+ const digit = code >= 48 && code <= 57 ? code - 48 : code >= 65 && code <= 86 ? code - 65 + 10 : -1;
252
+ if (digit < 0) return void 0;
253
+ value = value * 32 + digit;
254
+ }
255
+ return value;
256
+ }
257
+ function indexOfBytes(haystack, needle, from = 0) {
258
+ const limit = haystack.byteLength - needle.length;
259
+ outer: for (let at = Math.max(from, 0); at <= limit; at += 1) {
260
+ for (let offset = 0; offset < needle.length; offset += 1) {
261
+ if (haystack[at + offset] !== needle[offset]) continue outer;
262
+ }
263
+ return at;
264
+ }
265
+ return -1;
266
+ }
267
+ function insertPoint(skeleton) {
268
+ const text = LATIN12.decode(skeleton);
269
+ let last = -1;
270
+ for (const match of text.matchAll(AID_TAG)) {
271
+ if (match[0].endsWith("/>")) continue;
272
+ last = match.index + match[0].length;
273
+ }
274
+ return last;
275
+ }
276
+ function asciiBytes(text) {
277
+ const out = new Uint8Array(text.length);
278
+ for (let index = 0; index < text.length; index += 1) {
279
+ out[index] = text.charCodeAt(index) & 255;
280
+ }
281
+ return out;
282
+ }
283
+ function parseFlows(palmdb, header, textLength, label) {
284
+ const where = sourceSuffix(label);
285
+ const at = header.fdst;
286
+ if (at === void 0) {
287
+ throw new FormatError(`\u8FD9\u672C\u4E66\u6CA1\u6709 FDST \u8868\uFF1A\`fdst\` \u53EA\u5728 KF8\uFF08fileVersion = 8\uFF09\u4E0A\u5B58\u5728${where}`);
288
+ }
289
+ if (at.offset < 1 || at.offset >= palmdb.recordCount) {
290
+ throw new FormatError(
291
+ `FDST \u8BB0\u5F55\u53F7\u8D8A\u754C\uFF1A${String(at.offset)}\uFF0C\u5BB9\u5668\u53EA\u6709 ${String(palmdb.recordCount)} \u6761\u8BB0\u5F55${where}`
292
+ );
293
+ }
294
+ const record = palmdb.record(at.offset);
295
+ if (indexOfBytes(record, [70, 68, 83, 84]) !== 0) {
296
+ throw new FormatError(`FDST \u9B54\u6570\u4E0D\u7B26\uFF1A\u8BB0\u5F55 ${String(at.offset)} \u4E0D\u662F "${FDST_MAGIC}"${where}`);
297
+ }
298
+ const needed = FDST_COUNT_AT + 4;
299
+ if (record.byteLength < needed) {
300
+ throw new FormatError(
301
+ `FDST \u8BB0\u5F55\u592A\u77ED\uFF1A\u53EA\u6709 ${String(record.byteLength)} \u5B57\u8282\uFF0C\u8FDE\u6BB5\u6570\u5B57\u6BB5\uFF08\u5230 ${String(needed)}\uFF09\u90FD\u653E\u4E0D\u4E0B${where}`
302
+ );
303
+ }
304
+ const view = new DataView(record.buffer, record.byteOffset, record.byteLength);
305
+ const count = view.getUint32(FDST_COUNT_AT, false);
306
+ if (count !== at.flowCount) {
307
+ throw new FormatError(
308
+ `FDST \u6BB5\u6570\u4E0D\u7B26\uFF1A\u5934\u81EA\u8FF0 ${String(at.flowCount)}\uFF0C\u8868\u91CC\u662F ${String(count)}${where}`
309
+ );
310
+ }
311
+ if (count === 0) {
312
+ throw new FormatError(`FDST \u6BB5\u6570\u4E3A 0\uFF1A\u6B63\u6587\u4E0D\u53EF\u80FD\u6CA1\u6709\u6BB5${where}`);
313
+ }
314
+ const tableEnd = needed + count * FDST_SPAN_BYTES;
315
+ if (record.byteLength < tableEnd) {
316
+ throw new FormatError(
317
+ `FDST \u8868\u88AB\u622A\u65AD\uFF1A${String(count)} \u6BB5\u9700\u8981 ${String(tableEnd)} \u5B57\u8282\uFF0C\u8BB0\u5F55\u53EA\u6709 ${String(record.byteLength)} \u5B57\u8282${where}`
318
+ );
319
+ }
320
+ const spans = [];
321
+ let expected = 0;
322
+ for (let index = 0; index < count; index += 1) {
323
+ const at2 = needed + index * FDST_SPAN_BYTES;
324
+ const start = view.getUint32(at2, false);
325
+ const end = view.getUint32(at2 + 4, false);
326
+ if (start !== expected) {
327
+ throw new FormatError(
328
+ `FDST \u7B2C ${String(index)} \u6BB5\u4E0D\u4ECE ${String(expected)} \u5F00\u59CB\uFF08\u8BFB\u5230 ${String(start)}\uFF09\uFF1A\u5404\u6BB5\u5FC5\u987B\u8FDE\u7EED\u8986\u76D6 [0, textLength)${where}`
329
+ );
330
+ }
331
+ if (end <= start) {
332
+ throw new FormatError(
333
+ `FDST \u7B2C ${String(index)} \u6BB5\u4E3A\u7A7A\u6216\u5012\u5E8F\uFF1A[${String(start)}, ${String(end)})${where}`
334
+ );
335
+ }
336
+ spans.push({ start, end });
337
+ expected = end;
338
+ }
339
+ if (expected !== textLength) {
340
+ throw new FormatError(
341
+ `FDST \u672B\u6BB5\u7ED3\u675F\u4E8E ${String(expected)}\uFF0C\u4E0E textLength = ${String(textLength)} \u4E0D\u7B26${where}`
342
+ );
343
+ }
344
+ return spans;
345
+ }
346
+ function splitKf8Documents(flow0, label) {
347
+ const where = sourceSuffix(label);
348
+ const starts = [];
349
+ for (let at = indexOfBytes(flow0, XML_DECL); at !== -1; at = indexOfBytes(flow0, XML_DECL, at + 1)) {
350
+ starts.push(at);
351
+ }
352
+ if (starts.length === 0) {
353
+ throw new FormatError(`KF8 \u6B63\u6587\u91CC\u627E\u4E0D\u5230\u4EFB\u4F55 \`<?xml\`\uFF0C\u65E0\u6CD5\u5207\u5206\u6587\u6863${where}`);
354
+ }
355
+ const documents = [];
356
+ for (const [index, skelStart] of starts.entries()) {
357
+ const htmlAt = indexOfBytes(flow0, HTML_CLOSE, skelStart);
358
+ if (htmlAt === -1) {
359
+ throw new FormatError(`KF8 \u7B2C ${String(index)} \u4E2A skeleton \u6CA1\u6709 \`</html>\`${where}`);
360
+ }
361
+ const skelEnd = htmlAt + HTML_CLOSE.length;
362
+ const skeleton = flow0.subarray(skelStart, skelEnd);
363
+ const at = insertPoint(skeleton);
364
+ if (at < 0) {
365
+ throw new FormatError(
366
+ `KF8 \u7B2C ${String(index)} \u4E2A skeleton \u91CC\u6CA1\u6709\u5E26 \`aid\` \u7684\u975E\u81EA\u95ED\u5408\u6807\u7B7E\uFF0C\u63D2\u5165\u70B9\u4E0D\u6210\u7ACB${where}`
367
+ );
368
+ }
369
+ const fragment = flow0.subarray(skelEnd, starts[index + 1] ?? flow0.byteLength);
370
+ const merged = new Uint8Array(skeleton.byteLength + fragment.byteLength);
371
+ merged.set(skeleton.subarray(0, at), 0);
372
+ merged.set(fragment, at);
373
+ merged.set(skeleton.subarray(at), at + fragment.byteLength);
374
+ documents.push(merged);
375
+ }
376
+ return documents;
377
+ }
378
+ function rewriteKindleUris(bytes, resolve, label) {
379
+ const where = sourceSuffix(label);
380
+ const text = LATIN12.decode(bytes);
381
+ const pieces = [];
382
+ let cursor = 0;
383
+ for (const match of text.matchAll(KINDLE_URI)) {
384
+ const [whole, kind, digits, mime] = match;
385
+ const index = base32(digits);
386
+ if (index === void 0) {
387
+ throw new FormatError(
388
+ `KF8 \u79C1\u6709 URI \u7684\u7F16\u53F7\u4E0D\u662F\u5408\u6CD5\u57FA 32\uFF1A"${String(digits)}"\uFF08\u5B57\u7B26\u96C6\u662F 0-9 \u4E0E A-V\uFF09${where}`
389
+ );
390
+ }
391
+ pieces.push(bytes.subarray(cursor, match.index));
392
+ pieces.push(asciiBytes(resolve(kind, index, mime)));
393
+ cursor = match.index + whole.length;
394
+ }
395
+ if (pieces.length === 0) return bytes;
396
+ pieces.push(bytes.subarray(cursor));
397
+ const total = pieces.reduce((sum, piece) => sum + piece.byteLength, 0);
398
+ const out = new Uint8Array(total);
399
+ let offset = 0;
400
+ for (const piece of pieces) {
401
+ out.set(piece, offset);
402
+ offset += piece.byteLength;
403
+ }
404
+ return out;
405
+ }
406
+
407
+ // src/formats/mobi/indx.ts
408
+ var INDX_MAGIC = 1229866072;
409
+ var TAGX_MAGIC = 1413564248;
410
+ var ORDT_HACK_CODE = 65002;
411
+ var ORDT_COUNT_AT = 164;
412
+ var IDXT_MARKER_BYTES = 4;
413
+ var IDXT_SLOT_BYTES = 2;
414
+ function u32At(bytes, at) {
415
+ return ((bytes[at] ?? 0) << 24 | (bytes[at + 1] ?? 0) << 16 | (bytes[at + 2] ?? 0) << 8 | (bytes[at + 3] ?? 0)) >>> 0;
416
+ }
417
+ function u16At(bytes, at) {
418
+ return ((bytes[at] ?? 0) << 8 | (bytes[at + 1] ?? 0)) >>> 0;
419
+ }
420
+ function parseIndxHeader(bytes, label) {
421
+ if (u32At(bytes, 0) !== INDX_MAGIC) return null;
422
+ const code = u32At(bytes, 28);
423
+ if (code === ORDT_HACK_CODE || u32At(bytes, ORDT_COUNT_AT) !== 0 || u32At(bytes, ORDT_COUNT_AT + 4) > 0) {
424
+ throw new FormatError(
425
+ `\u89E6\u53D1\u4E86\u672C\u6279\u4E0D\u652F\u6301\u7684 ORDT / ESP \u7D22\u5F15\u5206\u652F\uFF08code=0x${code.toString(16)}\uFF09\u2014\u2014 \u8BE5\u4E66\u7684\u76EE\u5F55\u6587\u672C\u8981\u4E24\u5F20 ORDT \u8868\u624D\u80FD\u89E3\u5BF9\uFF0C\u672C\u5305\u4F1A\u89E3\u51FA\u9519\u503C\uFF0C\u6545\u76F4\u63A5\u62A5\u9519${sourceSuffix(label)}`
426
+ );
427
+ }
428
+ return {
429
+ len: u32At(bytes, 4),
430
+ start: u32At(bytes, 20),
431
+ count: u32At(bytes, 24),
432
+ nctoc: u32At(bytes, 52)
433
+ };
434
+ }
435
+ function readTagSection(bytes, at, label) {
436
+ if (u32At(bytes, at) !== TAGX_MAGIC) return null;
437
+ const firstEntryOffset = u32At(bytes, at + 4);
438
+ if (at + firstEntryOffset > bytes.length) {
439
+ throw new FormatError(
440
+ `TAGX \u7684 firstEntryOffset=${String(firstEntryOffset)} \u8D8A\u51FA\u8BB0\u5F55\uFF08TAGX \u5728 ${String(at)}\uFF0C\u8BB0\u5F55\u5171 ${String(bytes.length)} \u5B57\u8282\uFF09${sourceSuffix(label)}`
441
+ );
442
+ }
443
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
444
+ const tags = [];
445
+ for (let i = 12; i < firstEntryOffset; i += 4) {
446
+ tags.push({
447
+ tag: view.getUint8(at + i),
448
+ valuesPerEntry: view.getUint8(at + i + 1),
449
+ mask: view.getUint8(at + i + 2),
450
+ endFlag: view.getUint8(at + i + 3)
451
+ });
452
+ }
453
+ return { controlByteCount: u32At(bytes, at + 8), tags };
454
+ }
455
+ function countSetBits(value) {
456
+ let count = 0;
457
+ let rest = value;
458
+ while (rest !== 0) {
459
+ if ((rest & 1) === 1) count += 1;
460
+ rest >>= 1;
461
+ }
462
+ return count;
463
+ }
464
+ function readTagMap(controlByteCount, specs, data, from, to, label) {
465
+ const found = [];
466
+ let controlByteIndex = 0;
467
+ let valuesAt = from + controlByteCount;
468
+ for (const { tag, valuesPerEntry, mask: rawMask, endFlag } of specs) {
469
+ if (endFlag === 1) {
470
+ controlByteIndex += 1;
471
+ continue;
472
+ }
473
+ let mask = rawMask;
474
+ let value = (data[from + controlByteIndex] ?? 0) & mask;
475
+ if (value === 0) continue;
476
+ if (value === mask) {
477
+ if (countSetBits(mask) > 1) {
478
+ const [consumed, byteCount] = readVariableWidthInt(data, valuesAt, label);
479
+ valuesAt += consumed;
480
+ found.push({ tag, valueCount: null, valueBytes: byteCount, valuesPerEntry });
481
+ } else {
482
+ found.push({ tag, valueCount: 1, valueBytes: 0, valuesPerEntry });
483
+ }
484
+ } else {
485
+ while ((mask & 1) === 0) {
486
+ mask >>= 1;
487
+ value >>= 1;
488
+ }
489
+ found.push({ tag, valueCount: value, valueBytes: 0, valuesPerEntry });
490
+ }
491
+ }
492
+ const tags = /* @__PURE__ */ new Map();
493
+ for (const { tag, valueCount, valueBytes, valuesPerEntry } of found) {
494
+ const values = [];
495
+ if (valueCount === null) {
496
+ let consumedBytes = 0;
497
+ while (consumedBytes < valueBytes) {
498
+ const [consumed, value] = readVariableWidthInt(data, valuesAt, label);
499
+ valuesAt += consumed;
500
+ consumedBytes += consumed;
501
+ values.push(value);
502
+ }
503
+ } else {
504
+ for (let i = 0; i < valueCount * valuesPerEntry; i += 1) {
505
+ const [consumed, value] = readVariableWidthInt(data, valuesAt, label);
506
+ valuesAt += consumed;
507
+ values.push(value);
508
+ }
509
+ }
510
+ tags.set(tag, values);
511
+ }
512
+ let leftover = 0;
513
+ for (let i = valuesAt; i < to; i += 1) if (data[i] !== 0) leftover += 1;
514
+ return { tags, leftover };
515
+ }
516
+ function readCtoc(bytes, label) {
517
+ const text = /* @__PURE__ */ new Map();
518
+ let at = 0;
519
+ while (at < bytes.length && bytes[at] !== 0) {
520
+ const [consumed, length] = readVariableWidthInt(bytes, at, label);
521
+ const from = at + consumed;
522
+ const to = from + length;
523
+ if (to > bytes.length) {
524
+ throw new FormatError(
525
+ `CTOC \u6587\u672C\u8D8A\u754C\uFF1A\u504F\u79FB ${String(at)} \u5904\u58F0\u660E\u957F\u5EA6 ${String(length)}\uFF0C\u4F46\u4ECE ${String(from)} \u8D77\u53EA\u5269 ${String(bytes.length - from)} \u5B57\u8282${sourceSuffix(label)}`
526
+ );
527
+ }
528
+ text.set(at, bytes.subarray(from, to));
529
+ at = to;
530
+ }
531
+ return text;
532
+ }
533
+ function loadRecord(palmdb, index, what, label) {
534
+ if (index < 0 || index >= palmdb.recordCount) {
535
+ throw new FormatError(
536
+ `${what}\u7684\u8BB0\u5F55\u53F7 ${String(index)} \u8D8A\u754C\uFF08\u5171 ${String(palmdb.recordCount)} \u6761\u8BB0\u5F55\uFF09${sourceSuffix(label)}`
537
+ );
538
+ }
539
+ return palmdb.record(index);
540
+ }
541
+ function readVariableWidthInt(data, at, label) {
542
+ let value = 0;
543
+ let consumed = 0;
544
+ for (; ; ) {
545
+ const byte = data[at + consumed];
546
+ if (byte === void 0) {
547
+ throw new FormatError(
548
+ `\u53D8\u5BBD\u503C\u5728 ${String(at + consumed)} \u5904\u8D8A\u754C\uFF1A\u8BFB\u6EE1 ${String(consumed)} \u5B57\u8282\u4ECD\u672A\u89C1\u7ED3\u675F\u6807\u5FD7\uFF08\u53D8\u5BBD\u503C\u7684**\u9AD8\u4F4D**\u662F\u7ED3\u675F\u6807\u5FD7\uFF09${sourceSuffix(label)}`
549
+ );
550
+ }
551
+ consumed += 1;
552
+ value = value * 128 + (byte & 127);
553
+ if ((byte & 128) !== 0) return [consumed, value];
554
+ }
555
+ }
556
+ function parseIndx(palmdb, at, label) {
557
+ const head = loadRecord(palmdb, at, "INDX \u5934", label);
558
+ const header = parseIndxHeader(head, label);
559
+ if (header === null) {
560
+ throw new FormatError(`\u8BB0\u5F55 ${String(at)} \u4E0D\u662F INDX\uFF08\u9B54\u6570\u4E0D\u7B26\uFF09${sourceSuffix(label)}`);
561
+ }
562
+ const tagSection = readTagSection(head, header.len, label);
563
+ if (tagSection === null) {
564
+ throw new FormatError(
565
+ `\u8BB0\u5F55 ${String(at)} \u7684 INDX \u5934\u91CC\u6CA1\u6709 TAGX \u6807\u7B7E\u8868\uFF08\u504F\u79FB ${String(header.len)}\uFF09${sourceSuffix(label)}`
566
+ );
567
+ }
568
+ const ctoc = /* @__PURE__ */ new Map();
569
+ for (let j = 0; j < header.nctoc; j += 1) {
570
+ const block = loadRecord(palmdb, at + header.count + 1 + j, "CTOC \u6587\u672C\u6BB5", label);
571
+ for (const [key, value] of readCtoc(block, label)) ctoc.set(key + j * 65536, value);
572
+ }
573
+ const entries = [];
574
+ let leftover = 0;
575
+ for (let i = 1; i <= header.count; i += 1) {
576
+ const data = loadRecord(palmdb, at + i, "INDX \u6570\u636E\u6BB5", label);
577
+ const sub = parseIndxHeader(data, label);
578
+ if (sub === null) {
579
+ throw new FormatError(
580
+ `\u8BB0\u5F55 ${String(at + i)} \u4E0D\u662F INDX \u6570\u636E\u6BB5\uFF08\u9B54\u6570\u4E0D\u7B26\uFF09${sourceSuffix(label)}`
581
+ );
582
+ }
583
+ const bounds = [];
584
+ let cursor;
585
+ const maxSlots = Math.floor(data.byteLength / IDXT_SLOT_BYTES);
586
+ const slots = Math.min(sub.count, maxSlots);
587
+ for (let j = 0; j < slots; j += 1) {
588
+ const from = u16At(data, sub.start + IDXT_MARKER_BYTES + IDXT_SLOT_BYTES * j);
589
+ if (cursor !== void 0) bounds.push([cursor, from]);
590
+ cursor = from;
591
+ }
592
+ if (cursor !== void 0) bounds.push([cursor, sub.start]);
593
+ for (const [startPos, endPos] of bounds) {
594
+ const textLength = data[startPos] ?? 0;
595
+ const text = data.subarray(startPos + 1, startPos + 1 + textLength);
596
+ const parsed = readTagMap(
597
+ tagSection.controlByteCount,
598
+ tagSection.tags,
599
+ data,
600
+ startPos + 1 + textLength,
601
+ endPos,
602
+ label
603
+ );
604
+ leftover += parsed.leftover;
605
+ entries.push({ text, tags: parsed.tags });
606
+ }
607
+ }
608
+ return { count: header.count, nctoc: header.nctoc, entries, ctoc, leftover };
609
+ }
610
+
611
+ // src/formats/mobi/kf8toc.ts
612
+ var LATIN13 = new TextDecoder("latin1");
613
+ var XML_DECL_TEXT = "<?xml";
614
+ var TAG_PATTERN = /<[^>]*>/g;
615
+ var ANCHOR_PATTERN = /\s(?:id|name)\s*=\s*["']([^"']*)["']/i;
616
+ var NCX_NOFFS = 3;
617
+ var NCX_HLVL = 4;
618
+ var NCX_POS_FID = 6;
619
+ var NCX_CHILD1 = 22;
620
+ var NCX_CHILDN = 23;
621
+ var FRAG_CHAPTER = 3;
622
+ function scanChapterStarts(flow0) {
623
+ const text = LATIN13.decode(flow0);
624
+ const starts = [];
625
+ for (let at = text.indexOf(XML_DECL_TEXT); at !== -1; at = text.indexOf(XML_DECL_TEXT, at + 1)) {
626
+ starts.push(at);
627
+ }
628
+ return starts;
629
+ }
630
+ function backSearchId(part, q) {
631
+ if (q < 0 || q > part.length) return void 0;
632
+ const text = LATIN13.decode(part);
633
+ const gt = text.indexOf(">", q);
634
+ const lt = text.indexOf("<", q);
635
+ let at = q;
636
+ if (lt === q || gt !== -1 && gt < lt) at = gt + 1;
637
+ const tags = [...text.slice(0, at).matchAll(TAG_PATTERN)];
638
+ for (const match of tags.reverse()) {
639
+ const tag = match[0];
640
+ if (tag.startsWith("<body ")) return void 0;
641
+ if (tag.startsWith("<meta ")) continue;
642
+ const named = ANCHOR_PATTERN.exec(tag);
643
+ if (named !== null && named[1] !== "") return named[1];
644
+ }
645
+ return void 0;
646
+ }
647
+ function readFragments(table) {
648
+ return table.entries.map((entry) => ({
649
+ insertpos: Number(LATIN13.decode(entry.text)),
650
+ chapter: entry.tags.get(FRAG_CHAPTER)?.[0] ?? 0
651
+ }));
652
+ }
653
+ function readNcxRow(entry, ncx, fragments, documents, starts, decodeLabel2, where) {
654
+ const level = entry.tags.get(NCX_HLVL)?.[0] ?? -1;
655
+ const child1 = entry.tags.get(NCX_CHILD1)?.[0] ?? -1;
656
+ const childn = entry.tags.get(NCX_CHILDN)?.[0] ?? -1;
657
+ const noffs = entry.tags.get(NCX_NOFFS)?.[0];
658
+ const fid = entry.tags.get(NCX_POS_FID)?.[0] ?? -1;
659
+ const off = entry.tags.get(NCX_POS_FID)?.[1] ?? 0;
660
+ const fragmentRow = fragments[fid];
661
+ if (fragmentRow === void 0) {
662
+ throw new FormatError(
663
+ `KF8 \u7684\u76EE\u5F55\u9879\u6307\u5411\u7247\u6BB5 ${String(fid)}\uFF0C\u4F46\u7247\u6BB5\u8868\u53EA\u6709 ${String(fragments.length)} \u6761${where}`
664
+ );
665
+ }
666
+ const chapter = fragmentRow.chapter;
667
+ const document = documents[chapter];
668
+ const start = starts[chapter];
669
+ if (document === void 0 || start === void 0) {
670
+ throw new FormatError(
671
+ `KF8 \u7684\u76EE\u5F55\u9879\u843D\u5728\u7AE0 ${String(chapter)}\uFF0C\u4F46\u53EA\u6709 ${String(documents.length)} \u7AE0${where}`
672
+ );
673
+ }
674
+ const label = decodeLabel2(ncx.ctoc.get(noffs ?? -1) ?? new Uint8Array(0));
675
+ return {
676
+ label,
677
+ level,
678
+ child1,
679
+ childn,
680
+ // ⚠️ 这个名字必须与 `open.ts::openKf8` 产出的章节名**逐字一致**,否则目录跳转落空。
681
+ href: `text/${String(chapter).padStart(4, "0")}.xhtml`,
682
+ fragment: backSearchId(document, fragmentRow.insertpos + off - start)
683
+ };
684
+ }
685
+ function buildLevel(rows, start, end, level) {
686
+ const from = start <= 0 ? 0 : start;
687
+ const to = end <= 0 ? rows.length : end;
688
+ const items = [];
689
+ for (let i = from; i < to; i += 1) {
690
+ const row = rows[i];
691
+ if (row === void 0 || row.level !== level) continue;
692
+ items.push({
693
+ label: row.label,
694
+ href: row.href,
695
+ ...row.fragment === void 0 ? {} : { fragment: row.fragment },
696
+ children: row.child1 >= 0 ? buildLevel(rows, row.child1, row.childn + 1, level + 1) : []
697
+ });
698
+ }
699
+ return items;
700
+ }
701
+ function countItems(items) {
702
+ let count = 0;
703
+ for (const item of items) count += 1 + countItems(item.children);
704
+ return count;
705
+ }
706
+ function parseKf8Toc(palmdb, header, flow0, documents, label) {
707
+ const where = sourceSuffix(label);
708
+ const { ncxIndex, fragmentIndex } = header;
709
+ if (ncxIndex === void 0) {
710
+ return [];
711
+ }
712
+ if (fragmentIndex === void 0) {
713
+ throw new FormatError(
714
+ `KF8 \u7684\u5934\u91CC\u6709 NCX \u7D22\u5F15\u8868\u5374\u6CA1\u6709\u7247\u6BB5\u8868\uFF08\`fragment_index\`\uFF09\u21D2 \u76EE\u5F55\u76EE\u6807\u65E0\u6CD5\u5B9A\u4F4D${where}`
715
+ );
716
+ }
717
+ const ncx = parseIndx(palmdb, ncxIndex, label);
718
+ const fragments = readFragments(parseIndx(palmdb, fragmentIndex, label));
719
+ const starts = scanChapterStarts(flow0);
720
+ if (starts.length !== documents.length) {
721
+ throw new FormatError(
722
+ `KF8 \u7684\u7AE0\u8282\u8D77\u70B9\u6570\uFF08${String(starts.length)}\uFF09\u4E0E\u5DF2\u5207\u5206\u7684\u6587\u6863\u6570\uFF08${String(documents.length)}\uFF09\u4E0D\u4E00\u81F4 \u2014\u2014 \u4E24\u4E2A\u6765\u6E90\u5DF2\u7ECF\u5206\u53C9${where}`
723
+ );
724
+ }
725
+ const decoder = decoderFor(header.codepage);
726
+ const decodeLabel2 = (bytes) => decoder.decode(bytes);
727
+ const rows = ncx.entries.map(
728
+ (entry) => readNcxRow(entry, ncx, fragments, documents, starts, decodeLabel2, where)
729
+ );
730
+ const toc = buildLevel(rows, -1, -1, 0);
731
+ const emitted = countItems(toc);
732
+ if (emitted !== rows.length) {
733
+ throw new FormatError(
734
+ `KF8 \u7684\u76EE\u5F55\u5C42\u7EA7\u6811\u53EA\u8986\u76D6\u4E86 ${String(emitted)} / ${String(rows.length)} \u4E2A\u6761\u76EE \u2014\u2014 \`hlvl\` \u4E0D\u8FDE\u7EED\u6216 \`child1\`/\`childn\` \u81EA\u76F8\u77DB\u76FE\u65F6\u4F1A\u9759\u9ED8\u5C11\u7ED9\u76EE\u5F55${where}`
735
+ );
736
+ }
737
+ return toc;
738
+ }
739
+
740
+ // src/formats/mobi/palmdb.ts
741
+ var HEADER_BYTES = 78;
742
+ var RECORD_ENTRY_BYTES = 8;
743
+ var NUM_RECORDS_AT = 76;
744
+ function parsePalmDb(bytes, label) {
745
+ const where = sourceSuffix(label);
746
+ if (bytes.byteLength < HEADER_BYTES) {
747
+ throw new FormatError(
748
+ `PalmDB \u5BB9\u5668\u4E0D\u5B8C\u6574\uFF1A\u6587\u4EF6\u53EA\u6709 ${String(bytes.byteLength)} \u5B57\u8282\uFF0C\u4E0D\u8DB3\u4EE5\u5BB9\u7EB3 ${String(HEADER_BYTES)} \u5B57\u8282\u7684\u6587\u4EF6\u5934${where}`
749
+ );
750
+ }
751
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
752
+ const recordCount = view.getUint16(NUM_RECORDS_AT, false);
753
+ const tableEnd = HEADER_BYTES + recordCount * RECORD_ENTRY_BYTES;
754
+ if (bytes.byteLength < tableEnd) {
755
+ throw new FormatError(
756
+ `PalmDB \u5BB9\u5668\u4E0D\u5B8C\u6574\uFF1A\u5934\u58F0\u79F0\u6709 ${String(recordCount)} \u6761\u8BB0\u5F55\uFF0C\u504F\u79FB\u8868\u9700\u8981 ${String(tableEnd)} \u5B57\u8282\uFF0C\u4F46\u6587\u4EF6\u53EA\u6709 ${String(bytes.byteLength)} \u5B57\u8282${where}`
757
+ );
758
+ }
759
+ let floor = tableEnd;
760
+ for (let index = 0; index < recordCount; index += 1) {
761
+ const offset = view.getUint32(HEADER_BYTES + index * RECORD_ENTRY_BYTES, false);
762
+ if (offset < floor) {
763
+ throw new FormatError(
764
+ `PalmDB \u8BB0\u5F55\u504F\u79FB\u975E\u6CD5\uFF1A\u7B2C ${String(index)} \u6761\u8BB0\u5F55\u8D77\u70B9\u4E3A ${String(offset)}\uFF0C\u5C0F\u4E8E\u5141\u8BB8\u7684\u6700\u5C0F\u503C ${String(floor)}${where}`
765
+ );
766
+ }
767
+ if (offset > bytes.byteLength) {
768
+ throw new FormatError(
769
+ `PalmDB \u8BB0\u5F55\u504F\u79FB\u8D8A\u754C\uFF1A\u7B2C ${String(index)} \u6761\u8BB0\u5F55\u8D77\u70B9\u4E3A ${String(offset)}\uFF0C\u8D85\u51FA\u6587\u4EF6\u957F\u5EA6 ${String(bytes.byteLength)}${where}`
770
+ );
771
+ }
772
+ floor = offset + 1;
773
+ }
774
+ return {
775
+ recordCount,
776
+ record(index) {
777
+ if (!Number.isInteger(index) || index < 0 || index >= recordCount) {
778
+ throw new RangeError(
779
+ `PalmDB \u8BB0\u5F55\u4E0B\u6807\u8D8A\u754C\uFF1A${String(index)}\uFF08\u5408\u6CD5\u8303\u56F4 0..${String(recordCount - 1)}\uFF09`
780
+ );
781
+ }
782
+ const start = view.getUint32(HEADER_BYTES + index * RECORD_ENTRY_BYTES, false);
783
+ const end = index + 1 < recordCount ? view.getUint32(HEADER_BYTES + (index + 1) * RECORD_ENTRY_BYTES, false) : bytes.byteLength;
784
+ return bytes.subarray(start, end);
785
+ }
786
+ };
787
+ }
788
+
789
+ // src/formats/mobi/recindex.ts
790
+ var LATIN14 = new TextDecoder("latin1");
791
+ var IMG_TAG = /<img\b[^>]*>/gi;
792
+ var RECINDEX_ATTR = /\brecindex\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/i;
793
+ var DECIMAL = /^[0-9]+$/;
794
+ var ASCII2 = new TextEncoder();
795
+ function rewriteRecindex(bytes, resolve, label) {
796
+ const raw = LATIN14.decode(bytes);
797
+ const edits = [];
798
+ const pieces = [];
799
+ let cursor = 0;
800
+ for (const match of raw.matchAll(IMG_TAG)) {
801
+ const [tag] = match;
802
+ const attribute = RECINDEX_ATTR.exec(tag);
803
+ if (attribute === null) continue;
804
+ const [, captured] = attribute;
805
+ const quoted = captured;
806
+ const value = quoted.startsWith('"') || quoted.startsWith("'") ? quoted.slice(1, -1) : quoted;
807
+ if (!DECIMAL.test(value)) {
808
+ throw new FormatError(
809
+ `MOBI \u7684 recindex \u4E0D\u662F\u5341\u8FDB\u5236\u6570\uFF1A"${value}"\uFF08\u57FA 32 \u7684\u503C\u542B A-V\uFF0C\u6309\u5341\u8FDB\u5236\u8BFB\u4F1A**\u9759\u9ED8**\u5F97\u5230\u9519\u503C\uFF09${sourceSuffix(label)}`
810
+ );
811
+ }
812
+ const at = match.index + attribute.index;
813
+ const replacement = ASCII2.encode(`src="${resolve(Number(value))}"`);
814
+ pieces.push(bytes.subarray(cursor, at), replacement);
815
+ edits.push({ at, removed: attribute[0].length, inserted: replacement.byteLength });
816
+ cursor = at + attribute[0].length;
817
+ }
818
+ if (edits.length === 0) return { bytes, edits };
819
+ pieces.push(bytes.subarray(cursor));
820
+ const total = pieces.reduce((sum, piece) => sum + piece.byteLength, 0);
821
+ const out = new Uint8Array(total);
822
+ let offset = 0;
823
+ for (const piece of pieces) {
824
+ out.set(piece, offset);
825
+ offset += piece.byteLength;
826
+ }
827
+ return { bytes: out, edits };
828
+ }
829
+ function shiftOffset(edits, offset) {
830
+ let shifted = offset;
831
+ for (const edit of edits) {
832
+ if (offset >= edit.at + edit.removed) shifted += edit.inserted - edit.removed;
833
+ }
834
+ return shifted;
835
+ }
836
+
837
+ // src/formats/mobi/huffcdic.ts
838
+ var HUFF_MAGIC = "HUFF";
839
+ var CDIC_MAGIC = "CDIC";
840
+ var HUFF_HEADER_BYTES = 24;
841
+ var CDIC_HEADER_BYTES = 16;
842
+ var DICT1_ENTRIES = 256;
843
+ var MAX_CODE_LENGTH = 32;
844
+ var TAIL_PADDING_BYTES = 8;
845
+ var TWO_POW_32 = 4294967296;
846
+ function magicAt(record, length) {
847
+ return String.fromCharCode(...record.subarray(0, length));
848
+ }
849
+ function requireWithin(record, at, size, what, where) {
850
+ if (at + size > record.byteLength) {
851
+ throw new FormatError(
852
+ `${what} \u8D8A\u754C\uFF1A\u9700\u8981\u5230 ${String(at + size)} \u5B57\u8282\uFF0C\u8BB0\u5F55\u53EA\u6709 ${String(record.byteLength)} \u5B57\u8282${where}`
853
+ );
854
+ }
855
+ }
856
+ function openRecord(record, magic, headerBytes, where) {
857
+ if (magicAt(record, magic.length) !== magic) {
858
+ throw new FormatError(
859
+ `${magic} \u8BB0\u5F55\u9B54\u6570\u4E0D\u7B26\uFF1A\u671F\u671B "${magic}"\uFF0C\u8BFB\u5230 "${magicAt(record, magic.length)}"${where}`
860
+ );
861
+ }
862
+ requireWithin(record, 0, headerBytes, `${magic} \u8BB0\u5F55\u5934`, where);
863
+ const view = new DataView(record.buffer, record.byteOffset, record.byteLength);
864
+ const declared = view.getUint32(4, false);
865
+ if (declared < headerBytes) {
866
+ throw new FormatError(
867
+ `${magic} \u8BB0\u5F55\u5934\u957F\u5EA6\u975E\u6CD5\uFF1A\u81EA\u8FF0 ${String(declared)} \u5B57\u8282\uFF0C\u5C0F\u4E8E\u8868\u5934\u81EA\u8EAB\u7684 ${String(headerBytes)} \u5B57\u8282${where}`
868
+ );
869
+ }
870
+ return view;
871
+ }
872
+ function windowAt(view, bitPos) {
873
+ const at = bitPos >>> 3;
874
+ const shift = bitPos & 7;
875
+ const wide = view.getUint8(at) * TWO_POW_32 + view.getUint8(at + 1) * 16777216 + view.getUint8(at + 2) * 65536 + view.getUint8(at + 3) * 256 + view.getUint8(at + 4);
876
+ return Math.floor(wide / 2 ** (8 - shift)) % TWO_POW_32;
877
+ }
878
+ function loadHuffCdic(palmdb, huff, label) {
879
+ const where = sourceSuffix(label);
880
+ if (huff.count < 1) {
881
+ throw new FormatError(
882
+ `HUFF/CDIC \u8868\u4F4D\u7F6E\u975E\u6CD5\uFF1Ahuff_num = ${String(huff.count)}\uFF08\u81F3\u5C11\u8981\u6709 HUFF \u8BB0\u5F55\u672C\u8EAB \u2014\u2014 \u8BE5\u5B57\u6BB5**\u542B** HUFF \u8BB0\u5F55\uFF09${where}`
883
+ );
884
+ }
885
+ const end = huff.offset + huff.count;
886
+ if (end > palmdb.recordCount) {
887
+ throw new FormatError(
888
+ `HUFF/CDIC \u8868\u8D8A\u754C\uFF1A\u4ECE\u8BB0\u5F55 ${String(huff.offset)} \u8D77\u8981\u88C5 ${String(huff.count)} \u6761\uFF08\u5230 ${String(end)}\uFF09\uFF0C\u5BB9\u5668\u53EA\u6709 ${String(palmdb.recordCount)} \u6761${where}`
889
+ );
890
+ }
891
+ const huffRecord = palmdb.record(huff.offset);
892
+ const huffView = openRecord(huffRecord, HUFF_MAGIC, HUFF_HEADER_BYTES, where);
893
+ const dict1At = huffView.getUint32(8, false);
894
+ const dict2At = huffView.getUint32(12, false);
895
+ requireWithin(huffRecord, dict1At, DICT1_ENTRIES * 4, "HUFF \u7684 dict1", where);
896
+ requireWithin(huffRecord, dict2At, MAX_CODE_LENGTH * 8, "HUFF \u7684 dict2", where);
897
+ const minCodeAt = (length) => huffView.getUint32(dict2At + (length - 1) * 8, false) * 2 ** (32 - length);
898
+ const maxCodeAt = (length) => (huffView.getUint32(dict2At + (length - 1) * 8 + 4, false) + 1) * 2 ** (32 - length) - 1;
899
+ const phrases = /* @__PURE__ */ new Map();
900
+ for (let index = huff.offset + 1; index < end; index += 1) {
901
+ const cdic = palmdb.record(index);
902
+ const view = openRecord(cdic, CDIC_MAGIC, CDIC_HEADER_BYTES, where);
903
+ const total = view.getUint32(8, false);
904
+ const bits = view.getUint32(12, false);
905
+ const slots = Math.min(2 ** bits, total - phrases.size);
906
+ requireWithin(cdic, CDIC_HEADER_BYTES, slots * 2, "CDIC \u7684\u504F\u79FB\u8868", where);
907
+ for (let slot = 0; slot < slots; slot += 1) {
908
+ const at = CDIC_HEADER_BYTES + view.getUint16(CDIC_HEADER_BYTES + slot * 2, false);
909
+ const length = view.getUint16(at, false);
910
+ const size = length & 32767;
911
+ requireWithin(cdic, at + 2, size, "CDIC \u7684\u6761\u76EE\u4F53", where);
912
+ phrases.set(phrases.size, {
913
+ body: cdic.subarray(at + 2, at + 2 + size),
914
+ expanded: (length & 32768) !== 0
915
+ });
916
+ }
917
+ }
918
+ const cache = /* @__PURE__ */ new Map();
919
+ const pending = /* @__PURE__ */ new Set();
920
+ function decodeStream(stream, budget) {
921
+ const padded = new Uint8Array(stream.byteLength + TAIL_PADDING_BYTES);
922
+ padded.set(stream);
923
+ const view = new DataView(padded.buffer);
924
+ const chunks = [];
925
+ let bitPos = 0;
926
+ let bitsLeft = stream.byteLength * 8;
927
+ let total = 0;
928
+ let cut = false;
929
+ const emit = (chunk) => {
930
+ chunks.push(chunk);
931
+ total += chunk.byteLength;
932
+ budget.left -= chunk.byteLength;
933
+ };
934
+ for (; ; ) {
935
+ if (budget.left <= 0) {
936
+ cut = true;
937
+ break;
938
+ }
939
+ const code = windowAt(view, bitPos);
940
+ const top = code >>> 24;
941
+ const packed = huffView.getUint32(dict1At + top * 4, false);
942
+ let length = packed & 31;
943
+ let limit;
944
+ if (length === 0) {
945
+ throw new ParseError(
946
+ `HUFF/CDIC \u7801\u957F\u4E3A 0\uFF1Adict1[${String(top)}] = ${String(packed)}${where}`
947
+ );
948
+ }
949
+ if ((packed & 128) !== 0) {
950
+ limit = ((packed >>> 8) + 1) * 2 ** (32 - length) - 1;
951
+ } else {
952
+ while (code < minCodeAt(length)) {
953
+ length += 1;
954
+ if (length > MAX_CODE_LENGTH) {
955
+ throw new ParseError(
956
+ `HUFF/CDIC \u7801\u957F\u8865\u6B63\u8D85\u8FC7 ${String(MAX_CODE_LENGTH)}\uFF1Adict1[${String(top)}] \u62A5 ${String(packed & 31)}\uFF0C\u4F46 mincode \u4E00\u76F4\u6CA1\u6EE1\u8DB3${where}`
957
+ );
958
+ }
959
+ }
960
+ limit = maxCodeAt(length);
961
+ }
962
+ bitPos += length;
963
+ bitsLeft -= length;
964
+ if (bitsLeft < 0) break;
965
+ const index = Math.floor((limit - code) / 2 ** (32 - length));
966
+ const phrase = phrases.get(index);
967
+ if (phrase === void 0) {
968
+ throw new ParseError(
969
+ `HUFF/CDIC \u4F4D\u6D41\u5F15\u7528\u4E86\u5B57\u5178\u5916\u7684\u4E0B\u6807\uFF1A${String(index)}\uFF08\u5B57\u5178 ${String(phrases.size)} \u6761\uFF09${where}`
970
+ );
971
+ }
972
+ if (phrase.expanded) {
973
+ emit(phrase.body);
974
+ continue;
975
+ }
976
+ const cached = cache.get(index);
977
+ if (cached !== void 0) {
978
+ emit(cached);
979
+ continue;
980
+ }
981
+ if (pending.has(index)) {
982
+ throw new ParseError(`HUFF/CDIC \u77ED\u8BED\u6761\u76EE\u6210\u73AF\uFF1A\u5B57\u5178\u4E0B\u6807 ${String(index)}${where}`);
983
+ }
984
+ pending.add(index);
985
+ const expanded = decodeStream(phrase.body, { left: budget.left });
986
+ pending.delete(index);
987
+ if (!expanded.cut) cache.set(index, expanded.bytes);
988
+ emit(expanded.bytes);
989
+ }
990
+ const out = new Uint8Array(total);
991
+ let at = 0;
992
+ for (const chunk of chunks) {
993
+ out.set(chunk, at);
994
+ at += chunk.byteLength;
995
+ }
996
+ return { bytes: out, cut };
997
+ }
998
+ return {
999
+ unpack(record, limitBytes) {
1000
+ const budget = { left: limitBytes ?? Number.POSITIVE_INFINITY };
1001
+ const { bytes } = decodeStream(record, budget);
1002
+ return limitBytes === void 0 || bytes.byteLength <= limitBytes ? bytes : bytes.subarray(0, limitBytes);
1003
+ }
1004
+ };
1005
+ }
1006
+
1007
+ // src/formats/mobi/palmdoc.ts
1008
+ var RUN_MAX = 8;
1009
+ var LITERAL_MAX = 127;
1010
+ var SPACE_MIN = 192;
1011
+ var PAIR_LENGTH_BASE = 3;
1012
+ var PAIR_DISTANCE_MASK = 2047;
1013
+ var SPACE = 32;
1014
+ var MAX_EXPANSION = 5;
1015
+ function decompressPalmDoc(input) {
1016
+ const inputView = new DataView(input.buffer, input.byteOffset, input.byteLength);
1017
+ const output = new Uint8Array(input.byteLength * MAX_EXPANSION + 2);
1018
+ const outputView = new DataView(output.buffer);
1019
+ let produced = 0;
1020
+ let cursor = 0;
1021
+ while (cursor < input.byteLength) {
1022
+ const byte = inputView.getUint8(cursor);
1023
+ cursor += 1;
1024
+ if (byte >= 1 && byte <= RUN_MAX) {
1025
+ for (let taken = 0; taken < byte && cursor < input.byteLength; taken += 1) {
1026
+ outputView.setUint8(produced, inputView.getUint8(cursor));
1027
+ produced += 1;
1028
+ cursor += 1;
1029
+ }
1030
+ continue;
1031
+ }
1032
+ if (byte <= LITERAL_MAX) {
1033
+ outputView.setUint8(produced, byte);
1034
+ produced += 1;
1035
+ continue;
1036
+ }
1037
+ if (byte >= SPACE_MIN) {
1038
+ outputView.setUint8(produced, SPACE);
1039
+ outputView.setUint8(produced + 1, byte ^ 128);
1040
+ produced += 2;
1041
+ continue;
1042
+ }
1043
+ if (cursor >= input.byteLength) break;
1044
+ const pair = byte << 8 | inputView.getUint8(cursor);
1045
+ cursor += 1;
1046
+ const distance = pair >> 3 & PAIR_DISTANCE_MASK;
1047
+ const length = (pair & 7) + PAIR_LENGTH_BASE;
1048
+ if (distance > produced) {
1049
+ throw new ParseError(
1050
+ `PalmDOC \u8BB0\u5F55\u635F\u574F\uFF1A\u53CC\u5B57\u8282\u5BF9\u5F15\u7528\u4E86\u5C1A\u672A\u4EA7\u51FA\u7684\u5B57\u8282\uFF08\u8DDD\u79BB ${String(distance)}\uFF0C\u5DF2\u4EA7\u51FA ${String(produced)} \u5B57\u8282\uFF09`
1051
+ );
1052
+ }
1053
+ for (let copied = 0; copied < length; copied += 1) {
1054
+ outputView.setUint8(produced, outputView.getUint8(produced - distance));
1055
+ produced += 1;
1056
+ }
1057
+ }
1058
+ return output.subarray(0, produced);
1059
+ }
1060
+
1061
+ // src/formats/mobi/text.ts
1062
+ var COMPRESSION_NONE = 1;
1063
+ var COMPRESSION_PALMDOC = 2;
1064
+ var COMPRESSION_HUFFCDIC = 17480;
1065
+ function decompressFor(compression, palmdb, mobi, label) {
1066
+ if (compression === COMPRESSION_PALMDOC) return decompressPalmDoc;
1067
+ if (compression === COMPRESSION_HUFFCDIC) {
1068
+ const decoder = loadHuffCdic(palmdb, mobi.huff, label);
1069
+ return (record, wantBytes) => decoder.unpack(record, wantBytes);
1070
+ }
1071
+ return (record) => record;
1072
+ }
1073
+ function readMobiText(palmdb, palmdoc, mobi, label) {
1074
+ const where = sourceSuffix(label);
1075
+ const { compression } = palmdoc;
1076
+ if (compression !== COMPRESSION_NONE && compression !== COMPRESSION_PALMDOC && compression !== COMPRESSION_HUFFCDIC) {
1077
+ throw new FormatError(
1078
+ `MOBI \u538B\u7F29\u65B9\u5F0F ${String(compression)} \u4E0D\u53D7\u652F\u6301\uFF1A\u672C\u6279\u652F\u6301 1\uFF08\u65E0\u538B\u7F29\uFF09\u30012\uFF08PalmDOC\uFF09\u4E0E 17480\uFF08HUFF/CDIC\uFF09${where}`
1079
+ );
1080
+ }
1081
+ const needed = 1 + palmdoc.textRecordCount;
1082
+ if (palmdb.recordCount < needed) {
1083
+ throw new FormatError(
1084
+ `MOBI \u6B63\u6587\u8BB0\u5F55\u4E0D\u8DB3\uFF1A\u5934\u58F0\u79F0 ${String(palmdoc.textRecordCount)} \u6761\u6B63\u6587\u8BB0\u5F55\uFF08\u9700\u8981\u5BB9\u5668\u6709 ${String(needed)} \u6761\uFF09\uFF0C\u5B9E\u9645\u53EA\u6709 ${String(palmdb.recordCount)} \u6761${where}`
1085
+ );
1086
+ }
1087
+ const decode = decompressFor(compression, palmdb, mobi, label);
1088
+ const capacity = palmdoc.textRecordCount * palmdoc.maxSectionSize;
1089
+ if (palmdoc.textLength > capacity) {
1090
+ throw new FormatError(
1091
+ `MOBI \u6B63\u6587\u957F\u5EA6\u4E0D\u53EF\u8FBE\uFF1A\u5934\u58F0\u79F0 ${String(palmdoc.textLength)} \u5B57\u8282\uFF0C\u4F46 ${String(palmdoc.textRecordCount)} \u6761\u6B63\u6587\u8BB0\u5F55\u6BCF\u6BB5\u81F3\u591A\u4EA7\u51FA ${String(palmdoc.maxSectionSize)} \u5B57\u8282 \u21D2 \u81F3\u591A ${String(capacity)} \u5B57\u8282${where}`
1092
+ );
1093
+ }
1094
+ const out = new Uint8Array(palmdoc.textLength);
1095
+ let produced = 0;
1096
+ for (let index = 1; index <= palmdoc.textRecordCount; index += 1) {
1097
+ const remaining = palmdoc.textLength - produced;
1098
+ if (remaining <= 0) break;
1099
+ const want = Math.min(palmdoc.maxSectionSize, remaining);
1100
+ const decoded = decode(palmdb.record(index), want);
1101
+ const take = Math.min(want, decoded.byteLength);
1102
+ out.set(decoded.subarray(0, take), produced);
1103
+ produced += take;
1104
+ }
1105
+ if (produced !== palmdoc.textLength) {
1106
+ throw new FormatError(
1107
+ `MOBI \u6B63\u6587\u957F\u5EA6\u4E0D\u7B26\uFF1A\u5934\u58F0\u79F0 ${String(palmdoc.textLength)} \u5B57\u8282\uFF0C\u4F46 ${String(palmdoc.textRecordCount)} \u6761\u8BB0\u5F55\u53EA\u62FC\u51FA ${String(produced)} \u5B57\u8282${where}`
1108
+ );
1109
+ }
1110
+ return out;
1111
+ }
1112
+
1113
+ // src/formats/mobi/toc.ts
1114
+ var MOBI_TEXT_NAME = "text.html";
1115
+ var LATIN15 = new TextDecoder("latin1");
1116
+ var GUIDE_TAG = /<reference\b[^>]*\btype\s*=\s*"toc"[^>]*>/i;
1117
+ var FILEPOS_IN_TAG = /\bfilepos\s*=\s*"?([0-9]+)/i;
1118
+ var INLINE_TAG = /<[^>]*>/g;
1119
+ function collectAnchors(raw, from, to) {
1120
+ const region = raw.slice(from, to);
1121
+ const starts = /<a[\s>]/gi;
1122
+ const hits = [];
1123
+ for (let match = starts.exec(region); match !== null; match = starts.exec(region)) {
1124
+ const tagEnd = region.indexOf(">", match.index);
1125
+ if (tagEnd === -1) break;
1126
+ const digits = FILEPOS_IN_TAG.exec(region.slice(match.index, tagEnd))?.[1];
1127
+ if (digits === void 0) continue;
1128
+ const closeAt = region.indexOf("</a>", tagEnd);
1129
+ if (closeAt === -1) continue;
1130
+ hits.push({
1131
+ filepos: Number(digits),
1132
+ labelStart: from + tagEnd + 1,
1133
+ labelEnd: from + closeAt
1134
+ });
1135
+ }
1136
+ return hits;
1137
+ }
1138
+ function decodeLabel(bytes, decoder) {
1139
+ return decoder.decode(bytes).replace(INLINE_TAG, "").trim();
1140
+ }
1141
+ function parseMobiToc(text, codepage = UTF8_CODEPAGE, label) {
1142
+ if (text.byteLength === 0) return [];
1143
+ const raw = LATIN15.decode(text);
1144
+ const guideTag = GUIDE_TAG.exec(raw)?.[0];
1145
+ const fileposText = guideTag === void 0 ? void 0 : FILEPOS_IN_TAG.exec(guideTag)?.[1];
1146
+ if (fileposText === void 0) return [];
1147
+ const guideAt = Number(fileposText);
1148
+ if (guideAt > text.byteLength) {
1149
+ throw new FormatError(
1150
+ `MOBI \u76EE\u5F55\u7EBF\u7D22\u8D8A\u754C\uFF1Aguide \u7684 <reference type="toc"> \u6307\u5411 filepos ${String(guideAt)}\uFF0C\u8D85\u51FA\u6B63\u6587\u957F\u5EA6 ${String(text.byteLength)}${sourceSuffix(label)}`
1151
+ );
1152
+ }
1153
+ const pagebreakAt = raw.indexOf("<mbp:pagebreak", guideAt);
1154
+ const end = pagebreakAt === -1 ? text.byteLength : pagebreakAt;
1155
+ const decoder = decoderFor(codepage);
1156
+ return collectAnchors(raw, guideAt, end).map((hit) => ({
1157
+ label: decodeLabel(text.subarray(hit.labelStart, hit.labelEnd), decoder),
1158
+ href: MOBI_TEXT_NAME,
1159
+ fragment: String(hit.filepos),
1160
+ children: []
1161
+ }));
1162
+ }
1163
+
1164
+ // src/formats/mobi/open.ts
1165
+ var EXTH_AUTHOR = 100;
1166
+ var EXTH_ISBN = 104;
1167
+ var EXTH_ASIN = 113;
1168
+ var EXTH_TITLE = 503;
1169
+ var EXTH_LANGUAGE = 524;
1170
+ var TEXT_MEDIA_TYPE = "text/html";
1171
+ var KF8_CHAPTER_MEDIA_TYPE = "application/xhtml+xml";
1172
+ var UNKNOWN_MEDIA_TYPE = "application/octet-stream";
1173
+ var MEDIA_EXTENSIONS = /* @__PURE__ */ new Map([
1174
+ ["text/css", "css"],
1175
+ ["image/jpeg", "jpg"],
1176
+ ["image/png", "png"],
1177
+ ["image/gif", "gif"],
1178
+ ["image/svg", "svg"],
1179
+ ["image/svg+xml", "svg"]
1180
+ ]);
1181
+ function extensionFor(mediaType) {
1182
+ return MEDIA_EXTENSIONS.get(mediaType) ?? "bin";
1183
+ }
1184
+ var IMAGE_MAGICS = [
1185
+ ["image/jpeg", [255, 216, 255]],
1186
+ ["image/png", [137, 80, 78, 71]],
1187
+ ["image/gif", [71, 73, 70, 56]]
1188
+ ];
1189
+ function imageKind(bytes) {
1190
+ for (const [mediaType, magic] of IMAGE_MAGICS) {
1191
+ if (magic.every((byte, index) => bytes[index] === byte)) {
1192
+ return mediaType;
1193
+ }
1194
+ }
1195
+ return void 0;
1196
+ }
1197
+ function imageName(record, mediaType) {
1198
+ return `images/${String(record)}.${extensionFor(mediaType)}`;
1199
+ }
1200
+ function archiveRootReference(name) {
1201
+ return `/${name}`;
1202
+ }
1203
+ function assertRecordInRange(palmdb, record, describe, label) {
1204
+ if (record < 1 || record >= palmdb.recordCount) {
1205
+ throw new FormatError(
1206
+ `${describe} \u6307\u5411\u8BB0\u5F55 ${String(record)}\uFF0C\u8D8A\u51FA\u5BB9\u5668\uFF08\u5171 ${String(palmdb.recordCount)} \u6761\uFF09${sourceSuffix(label)}`
1207
+ );
1208
+ }
1209
+ }
1210
+ function collectImages(palmdb, header) {
1211
+ const images = [];
1212
+ const from = Math.max(header.firstResourceRecord, 1);
1213
+ for (let index = from; index < palmdb.recordCount; index += 1) {
1214
+ const bytes = palmdb.record(index);
1215
+ const mediaType = imageKind(bytes);
1216
+ if (mediaType === void 0) continue;
1217
+ images.push({
1218
+ record: index,
1219
+ href: imageName(index, mediaType),
1220
+ mediaType,
1221
+ bytes
1222
+ });
1223
+ }
1224
+ return images;
1225
+ }
1226
+ function readFullName(palmdb, header, label) {
1227
+ if (header.fullNameLength === 0) return void 0;
1228
+ const record = palmdb.record(0);
1229
+ const end = header.fullNameOffset + header.fullNameLength;
1230
+ if (end > record.byteLength) {
1231
+ throw new FormatError(
1232
+ `MOBI \u5B8C\u6574\u4E66\u540D\u8D8A\u754C\uFF1A${String(header.fullNameOffset)}..${String(end)} \u8D85\u51FA\u8BB0\u5F55 0 \u7684 ${String(record.byteLength)} \u5B57\u8282${sourceSuffix(label)}`
1233
+ );
1234
+ }
1235
+ return decoderFor(header.codepage).decode(record.subarray(header.fullNameOffset, end));
1236
+ }
1237
+ function readMetadata(palmdb, header, label) {
1238
+ const where = sourceSuffix(label);
1239
+ const { exth, codepage } = header;
1240
+ const title = exthTexts(exth, EXTH_TITLE, codepage)[0] ?? readFullName(palmdb, header, label);
1241
+ if (title === void 0 || title.length === 0) {
1242
+ throw new FormatError(
1243
+ `MOBI \u7F3A\u5C11\u4E66\u540D\uFF1AEXTH ${String(EXTH_TITLE)} \u4E0E MOBI \u5B8C\u6574\u4E66\u540D\u90FD\u6CA1\u6709${where}`
1244
+ );
1245
+ }
1246
+ const language = exthTexts(exth, EXTH_LANGUAGE, codepage)[0];
1247
+ if (language === void 0 || language.length === 0) {
1248
+ throw new FormatError(
1249
+ `MOBI \u7F3A\u5C11\u8BED\u8A00\uFF1AEXTH ${String(EXTH_LANGUAGE)} \u4E0D\u5B58\u5728\uFF0C\u800C\u6A21\u578B\u91CC language \u662F\u5FC5\u586B${where}`
1250
+ );
1251
+ }
1252
+ const identifier = exthTexts(exth, EXTH_ISBN, codepage)[0] ?? exthTexts(exth, EXTH_ASIN, codepage)[0] ?? "";
1253
+ return {
1254
+ title,
1255
+ creators: exthTexts(exth, EXTH_AUTHOR, codepage),
1256
+ language,
1257
+ identifier
1258
+ };
1259
+ }
1260
+ function createContent(entries, codepage) {
1261
+ const decoder = decoderFor(codepage);
1262
+ const read = (name, readOptions) => (
1263
+ // 同步体推进微任务队列 ⇒ 抛出的错误自然成为拒绝(不依赖调用方套 `try`)。
1264
+ Promise.resolve().then(() => {
1265
+ throwIfAborted(readOptions?.signal);
1266
+ const bytes = entries.get(name);
1267
+ if (bytes === void 0) {
1268
+ throw new FormatError(`MOBI \u5185\u5BB9\u6E90\u91CC\u6CA1\u6709 "${name}"`);
1269
+ }
1270
+ return bytes;
1271
+ })
1272
+ );
1273
+ const readText = async (name, readOptions) => {
1274
+ const decoded = decoder.decode(await read(name, readOptions));
1275
+ return decoded.startsWith("\uFEFF") ? decoded.slice(1) : decoded;
1276
+ };
1277
+ return { read, readText };
1278
+ }
1279
+ function openSync(bytes, source) {
1280
+ const palmdb = parsePalmDb(bytes, source);
1281
+ const palmdoc = parsePalmDocHeader(palmdb, source);
1282
+ const header = parseMobiHeader(palmdb, source);
1283
+ const text = readMobiText(palmdb, palmdoc, header, source);
1284
+ return header.fileVersion === KF8_FILE_VERSION ? openKf8(palmdb, header, text, source) : openMobi6(palmdb, header, text, source);
1285
+ }
1286
+ function openMobi6(palmdb, header, text, source) {
1287
+ const where = sourceSuffix(source);
1288
+ const toc = parseMobiToc(text, header.codepage, source);
1289
+ const images = collectImages(palmdb, header);
1290
+ const byRecord = new Map(images.map((image) => [image.record, image]));
1291
+ const rewrite = rewriteRecindex(
1292
+ text,
1293
+ (index) => {
1294
+ const record = header.firstResourceRecord + index - 1;
1295
+ const describe = `MOBI \u7684 \`recindex="${String(index)}"\``;
1296
+ assertRecordInRange(palmdb, record, describe, source);
1297
+ const image = byRecord.get(record);
1298
+ if (image === void 0) {
1299
+ throw new FormatError(`${describe} \u6307\u5411\u8BB0\u5F55 ${String(record)}\uFF0C\u4F46\u8BE5\u8BB0\u5F55\u4E0D\u662F\u56FE\u7247${where}`);
1300
+ }
1301
+ return archiveRootReference(image.href);
1302
+ },
1303
+ source
1304
+ );
1305
+ const resolved = toc.map((item) => ({
1306
+ item,
1307
+ at: shiftOffset(rewrite.edits, Number(item.fragment))
1308
+ }));
1309
+ const anchors = injectAnchors(
1310
+ rewrite.bytes,
1311
+ resolved.map((entry) => entry.at)
1312
+ );
1313
+ const idAt = /* @__PURE__ */ new Map();
1314
+ for (const placement of anchors.placements) {
1315
+ if (placement.kind === "open-tag") idAt.set(placement.at, placement.id);
1316
+ }
1317
+ const entries = /* @__PURE__ */ new Map([[MOBI_TEXT_NAME, anchors.bytes]]);
1318
+ for (const image of images) entries.set(image.href, image.bytes);
1319
+ return {
1320
+ content: createContent(entries, header.codepage),
1321
+ book: {
1322
+ format: "mobi",
1323
+ metadata: readMetadata(palmdb, header, source),
1324
+ resources: images.map((image) => ({ href: image.href, mediaType: image.mediaType })),
1325
+ // MOBI6 的正文是**单个** HTML 文档(事实 6)⇒ 阅读顺序只有一条,
1326
+ // 分节靠目录项的 `fragment`(元素 id,见 `anchors.ts`)。
1327
+ chapters: [{ href: MOBI_TEXT_NAME, mediaType: TEXT_MEDIA_TYPE, linear: true }],
1328
+ // 以下为 EPUB 专属:非 OPF 格式取空值(ADR-0014 的字段分层)。
1329
+ manifest: [],
1330
+ spine: [],
1331
+ manifestIndex: /* @__PURE__ */ new Map()
1332
+ },
1333
+ // 落点是开标签 ⇒ `fragment` 是该元素的 **id**(渲染层 `getElementById` 命中);
1334
+ // 降级时保留换算后的**字节偏移**(渲染层查不到 ⇒ 回退章首),与 ④ 之前逐字相同。
1335
+ toc: resolved.map(({ item, at }) => ({ ...item, fragment: idAt.get(at) ?? String(at) }))
1336
+ };
1337
+ }
1338
+ function openKf8(palmdb, header, text, source) {
1339
+ const where = sourceSuffix(source);
1340
+ const flows = parseFlows(palmdb, header, text.byteLength, source);
1341
+ const entries = /* @__PURE__ */ new Map();
1342
+ const resources = [];
1343
+ const register = (name, mediaType, bytes) => {
1344
+ if (!entries.has(name)) {
1345
+ entries.set(name, bytes);
1346
+ resources.push({ href: name, mediaType });
1347
+ }
1348
+ return name;
1349
+ };
1350
+ const resolve = (kind, index, mime) => {
1351
+ const mediaType = mime ?? UNKNOWN_MEDIA_TYPE;
1352
+ if (kind === "flow") {
1353
+ const span = flows[index];
1354
+ if (span === void 0) {
1355
+ throw new FormatError(
1356
+ `KF8 \u7684 \`kindle:flow:${String(index)}\` \u8D8A\u51FA FDST \u7684 ${String(flows.length)} \u6BB5${where}`
1357
+ );
1358
+ }
1359
+ return archiveRootReference(
1360
+ register(
1361
+ `flows/${String(index).padStart(4, "0")}.${extensionFor(mediaType)}`,
1362
+ mediaType,
1363
+ text.subarray(span.start, span.end)
1364
+ )
1365
+ );
1366
+ }
1367
+ const record = header.firstResourceRecord + index - 1;
1368
+ assertRecordInRange(palmdb, record, `KF8 \u7684 \`kindle:embed:${String(index)}\``, source);
1369
+ return archiveRootReference(
1370
+ register(imageName(record, mediaType), mediaType, palmdb.record(record))
1371
+ );
1372
+ };
1373
+ const first = flows[0];
1374
+ const flow0 = text.subarray(first.start, first.end);
1375
+ const documents = splitKf8Documents(flow0, source);
1376
+ const chapters = [];
1377
+ documents.forEach((document, index) => {
1378
+ const href = `text/${String(index).padStart(4, "0")}.xhtml`;
1379
+ entries.set(href, rewriteKindleUris(document, resolve, source));
1380
+ chapters.push({ href, mediaType: KF8_CHAPTER_MEDIA_TYPE, linear: true });
1381
+ });
1382
+ return {
1383
+ content: createContent(entries, header.codepage),
1384
+ book: {
1385
+ format: "mobi",
1386
+ metadata: readMetadata(palmdb, header, source),
1387
+ resources,
1388
+ chapters,
1389
+ // 以下为 EPUB 专属:非 OPF 格式取空值(ADR-0014 的字段分层)。
1390
+ manifest: [],
1391
+ spine: [],
1392
+ manifestIndex: /* @__PURE__ */ new Map()
1393
+ },
1394
+ // ⚠️ 反搜元素 id 必须用 `documents`(`kindle:` URI **重写之前**的字节):
1395
+ // 重写会把 `kindle:flow:0001` 换成更长的归档路径、**字节长度变了**,
1396
+ // 于是目录项的字节偏移 `q` 不再指向原处(见 `kf8toc.ts` 文件头)。
1397
+ toc: parseKf8Toc(palmdb, header, flow0, documents, source)
1398
+ };
1399
+ }
1400
+ function openMobi(bytes, options) {
1401
+ return Promise.resolve().then(() => {
1402
+ throwIfAborted(options?.signal);
1403
+ return openSync(bytes, options?.source);
1404
+ });
1405
+ }
1406
+
1407
+ // src/formats/mobi/adapter.ts
1408
+ var mobiAdapter = {
1409
+ format: "mobi",
1410
+ async open(bytes, options) {
1411
+ return openMobi(bytes, options);
1412
+ }
1413
+ };
1414
+
1415
+ export { mobiAdapter };