UNPKG

vue-book-reader

Version:

<div align="center"> <img width=250 src="https://raw.githubusercontent.com/jinhuan138/vue--book-reader/master/public/logo.png" /> <h1>VueReader</h1> </div>

1,279 lines (1,278 loc) 42.9 kB
const unescapeHTML = (str) => { if (!str) return ""; const textarea = document.createElement("textarea"); textarea.innerHTML = str; return textarea.value; }; const MIME = { XML: "application/xml", XHTML: "application/xhtml+xml", HTML: "text/html", CSS: "text/css", SVG: "image/svg+xml" }; const PDB_HEADER = { name: [0, 32, "string"], type: [60, 4, "string"], creator: [64, 4, "string"], numRecords: [76, 2, "uint"] }; const PALMDOC_HEADER = { compression: [0, 2, "uint"], numTextRecords: [8, 2, "uint"], recordSize: [10, 2, "uint"], encryption: [12, 2, "uint"] }; const MOBI_HEADER = { magic: [16, 4, "string"], length: [20, 4, "uint"], type: [24, 4, "uint"], encoding: [28, 4, "uint"], uid: [32, 4, "uint"], version: [36, 4, "uint"], titleOffset: [84, 4, "uint"], titleLength: [88, 4, "uint"], localeRegion: [94, 1, "uint"], localeLanguage: [95, 1, "uint"], resourceStart: [108, 4, "uint"], huffcdic: [112, 4, "uint"], numHuffcdic: [116, 4, "uint"], exthFlag: [128, 4, "uint"], trailingFlags: [240, 4, "uint"], indx: [244, 4, "uint"] }; const KF8_HEADER = { resourceStart: [108, 4, "uint"], fdst: [192, 4, "uint"], numFdst: [196, 4, "uint"], frag: [248, 4, "uint"], skel: [252, 4, "uint"], guide: [260, 4, "uint"] }; const EXTH_HEADER = { magic: [0, 4, "string"], length: [4, 4, "uint"], count: [8, 4, "uint"] }; const INDX_HEADER = { magic: [0, 4, "string"], length: [4, 4, "uint"], type: [8, 4, "uint"], idxt: [20, 4, "uint"], numRecords: [24, 4, "uint"], encoding: [28, 4, "uint"], language: [32, 4, "uint"], total: [36, 4, "uint"], ordt: [40, 4, "uint"], ligt: [44, 4, "uint"], numLigt: [48, 4, "uint"], numCncx: [52, 4, "uint"] }; const TAGX_HEADER = { magic: [0, 4, "string"], length: [4, 4, "uint"], numControlBytes: [8, 4, "uint"] }; const HUFF_HEADER = { magic: [0, 4, "string"], offset1: [8, 4, "uint"], offset2: [12, 4, "uint"] }; const CDIC_HEADER = { magic: [0, 4, "string"], length: [4, 4, "uint"], numEntries: [8, 4, "uint"], codeLength: [12, 4, "uint"] }; const FDST_HEADER = { magic: [0, 4, "string"], numEntries: [8, 4, "uint"] }; const FONT_HEADER = { flags: [8, 4, "uint"], dataStart: [12, 4, "uint"], keyLength: [16, 4, "uint"], keyStart: [20, 4, "uint"] }; const MOBI_ENCODING = { 1252: "windows-1252", 65001: "utf-8" }; const EXTH_RECORD_TYPE = { 100: ["creator", "string", true], 101: ["publisher"], 103: ["description"], 104: ["isbn"], 105: ["subject", "string", true], 106: ["date"], 108: ["contributor", "string", true], 109: ["rights"], 110: ["subjectCode", "string", true], 112: ["source", "string", true], 113: ["asin"], 121: ["boundary", "uint"], 122: ["fixedLayout"], 125: ["numResources", "uint"], 126: ["originalResolution"], 127: ["zeroGutter"], 128: ["zeroMargin"], 129: ["coverURI"], 132: ["regionMagnification"], 201: ["coverOffset", "uint"], 202: ["thumbnailOffset", "uint"], 503: ["title"], 524: ["language", "string", true], 527: ["pageProgressionDirection"] }; const MOBI_LANG = { 1: [ "ar", "ar-SA", "ar-IQ", "ar-EG", "ar-LY", "ar-DZ", "ar-MA", "ar-TN", "ar-OM", "ar-YE", "ar-SY", "ar-JO", "ar-LB", "ar-KW", "ar-AE", "ar-BH", "ar-QA" ], 2: ["bg"], 3: ["ca"], 4: ["zh", "zh-TW", "zh-CN", "zh-HK", "zh-SG"], 5: ["cs"], 6: ["da"], 7: ["de", "de-DE", "de-CH", "de-AT", "de-LU", "de-LI"], 8: ["el"], 9: [ "en", "en-US", "en-GB", "en-AU", "en-CA", "en-NZ", "en-IE", "en-ZA", "en-JM", null, "en-BZ", "en-TT", "en-ZW", "en-PH" ], 10: [ "es", "es-ES", "es-MX", null, "es-GT", "es-CR", "es-PA", "es-DO", "es-VE", "es-CO", "es-PE", "es-AR", "es-EC", "es-CL", "es-UY", "es-PY", "es-BO", "es-SV", "es-HN", "es-NI", "es-PR" ], 11: ["fi"], 12: ["fr", "fr-FR", "fr-BE", "fr-CA", "fr-CH", "fr-LU", "fr-MC"], 13: ["he"], 14: ["hu"], 15: ["is"], 16: ["it", "it-IT", "it-CH"], 17: ["ja"], 18: ["ko"], 19: ["nl", "nl-NL", "nl-BE"], 20: ["no", "nb", "nn"], 21: ["pl"], 22: ["pt", "pt-BR", "pt-PT"], 23: ["rm"], 24: ["ro"], 25: ["ru"], 26: ["hr", null, "sr"], 27: ["sk"], 28: ["sq"], 29: ["sv", "sv-SE", "sv-FI"], 30: ["th"], 31: ["tr"], 32: ["ur"], 33: ["id"], 34: ["uk"], 35: ["be"], 36: ["sl"], 37: ["et"], 38: ["lv"], 39: ["lt"], 41: ["fa"], 42: ["vi"], 43: ["hy"], 44: ["az"], 45: ["eu"], 46: ["hsb"], 47: ["mk"], 48: ["st"], 49: ["ts"], 50: ["tn"], 52: ["xh"], 53: ["zu"], 54: ["af"], 55: ["ka"], 56: ["fo"], 57: ["hi"], 58: ["mt"], 59: ["se"], 62: ["ms"], 63: ["kk"], 65: ["sw"], 67: ["uz", null, "uz-UZ"], 68: ["tt"], 69: ["bn"], 70: ["pa"], 71: ["gu"], 72: ["or"], 73: ["ta"], 74: ["te"], 75: ["kn"], 76: ["ml"], 77: ["as"], 78: ["mr"], 79: ["sa"], 82: ["cy", "cy-GB"], 83: ["gl", "gl-ES"], 87: ["kok"], 97: ["ne"], 98: ["fy"] }; const concatTypedArray = (a, b) => { const result = new a.constructor(a.length + b.length); result.set(a); result.set(b, a.length); return result; }; const concatTypedArray3 = (a, b, c) => { const result = new a.constructor(a.length + b.length + c.length); result.set(a); result.set(b, a.length); result.set(c, a.length + b.length); return result; }; const decoder = new TextDecoder(); const getString = (buffer) => decoder.decode(buffer); const getUint = (buffer) => { if (!buffer) return; const l = buffer.byteLength; const func = l === 4 ? "getUint32" : l === 2 ? "getUint16" : "getUint8"; return new DataView(buffer)[func](0); }; const getStruct = (def, buffer) => Object.fromEntries(Array.from(Object.entries(def)).map(([key, [start, len, type]]) => [ key, (type === "string" ? getString : getUint)(buffer.slice(start, start + len)) ])); const getDecoder = (x) => new TextDecoder(MOBI_ENCODING[x]); const getVarLen = (byteArray, i = 0) => { let value = 0, length = 0; for (const byte of byteArray.subarray(i, i + 4)) { value = value << 7 | (byte & 127) >>> 0; length++; if (byte & 128) break; } return { value, length }; }; const getVarLenFromEnd = (byteArray) => { let value = 0; for (const byte of byteArray.subarray(-4)) { if (byte & 128) value = 0; value = value << 7 | byte & 127; } return value; }; const countBitsSet = (x) => { let count = 0; for (; x > 0; x = x >> 1) if ((x & 1) === 1) count++; return count; }; const countUnsetEnd = (x) => { let count = 0; while ((x & 1) === 0) x = x >> 1, count++; return count; }; const decompressPalmDOC = (array) => { let output = []; for (let i = 0; i < array.length; i++) { const byte = array[i]; if (byte === 0) output.push(0); else if (byte <= 8) for (const x of array.subarray(i + 1, (i += byte) + 1)) output.push(x); else if (byte <= 127) output.push(byte); else if (byte <= 191) { const bytes = byte << 8 | array[i++ + 1]; const distance = (bytes & 16383) >>> 3; const length = (bytes & 7) + 3; for (let j = 0; j < length; j++) output.push(output[output.length - distance]); } else output.push(32, byte ^ 128); } return Uint8Array.from(output); }; const read32Bits = (byteArray, from) => { const startByte = from >> 3; const end = from + 32; const endByte = end >> 3; let bits = 0n; for (let i = startByte; i <= endByte; i++) bits = bits << 8n | BigInt(byteArray[i] ?? 0); return bits >> 8n - BigInt(end & 7) & 0xffffffffn; }; const huffcdic = async (mobi, loadRecord) => { const huffRecord = await loadRecord(mobi.huffcdic); const { magic, offset1, offset2 } = getStruct(HUFF_HEADER, huffRecord); if (magic !== "HUFF") throw new Error("Invalid HUFF record"); const table1 = Array.from({ length: 256 }, (_, i) => offset1 + i * 4).map((offset) => getUint(huffRecord.slice(offset, offset + 4))).map((x) => [x & 128, x & 31, x >>> 8]); const table2 = [null].concat(Array.from({ length: 32 }, (_, i) => offset2 + i * 8).map((offset) => [ getUint(huffRecord.slice(offset, offset + 4)), getUint(huffRecord.slice(offset + 4, offset + 8)) ])); const dictionary = []; for (let i = 1; i < mobi.numHuffcdic; i++) { const record = await loadRecord(mobi.huffcdic + i); const cdic = getStruct(CDIC_HEADER, record); if (cdic.magic !== "CDIC") throw new Error("Invalid CDIC record"); const n = Math.min(1 << cdic.codeLength, cdic.numEntries - dictionary.length); const buffer = record.slice(cdic.length); for (let i2 = 0; i2 < n; i2++) { const offset = getUint(buffer.slice(i2 * 2, i2 * 2 + 2)); const x = getUint(buffer.slice(offset, offset + 2)); const length = x & 32767; const decompressed = x & 32768; const value = new Uint8Array( buffer.slice(offset + 2, offset + 2 + length) ); dictionary.push([value, decompressed]); } } const decompress = (byteArray) => { let output = new Uint8Array(); const bitLength = byteArray.byteLength * 8; for (let i = 0; i < bitLength; ) { const bits = Number(read32Bits(byteArray, i)); let [found, codeLength, value] = table1[bits >>> 24]; if (!found) { while (bits >>> 32 - codeLength < table2[codeLength][0]) codeLength += 1; value = table2[codeLength][1]; } if ((i += codeLength) > bitLength) break; const code = value - (bits >>> 32 - codeLength); let [result, decompressed] = dictionary[code]; if (!decompressed) { result = decompress(result); dictionary[code] = [result, true]; } output = concatTypedArray(output, result); } return output; }; return decompress; }; const getIndexData = async (indxIndex, loadRecord) => { const indxRecord = await loadRecord(indxIndex); const indx = getStruct(INDX_HEADER, indxRecord); if (indx.magic !== "INDX") throw new Error("Invalid INDX record"); const decoder2 = getDecoder(indx.encoding); const tagxBuffer = indxRecord.slice(indx.length); const tagx = getStruct(TAGX_HEADER, tagxBuffer); if (tagx.magic !== "TAGX") throw new Error("Invalid TAGX section"); const numTags = (tagx.length - 12) / 4; const tagTable = Array.from({ length: numTags }, (_, i) => new Uint8Array(tagxBuffer.slice(12 + i * 4, 12 + i * 4 + 4))); const cncx = {}; let cncxRecordOffset = 0; for (let i = 0; i < indx.numCncx; i++) { const record = await loadRecord(indxIndex + indx.numRecords + i + 1); const array = new Uint8Array(record); for (let pos = 0; pos < array.byteLength; ) { const index = pos; const { value, length } = getVarLen(array, pos); pos += length; const result = record.slice(pos, pos + value); pos += value; cncx[cncxRecordOffset + index] = decoder2.decode(result); } cncxRecordOffset += 65536; } const table = []; for (let i = 0; i < indx.numRecords; i++) { const record = await loadRecord(indxIndex + 1 + i); const array = new Uint8Array(record); const indx2 = getStruct(INDX_HEADER, record); if (indx2.magic !== "INDX") throw new Error("Invalid INDX record"); for (let j = 0; j < indx2.numRecords; j++) { const offsetOffset = indx2.idxt + 4 + 2 * j; const offset = getUint(record.slice(offsetOffset, offsetOffset + 2)); const length = getUint(record.slice(offset, offset + 1)); const name = getString(record.slice(offset + 1, offset + 1 + length)); const tags = []; const startPos = offset + 1 + length; let controlByteIndex = 0; let pos = startPos + tagx.numControlBytes; for (const [tag, numValues, mask, end] of tagTable) { if (end & 1) { controlByteIndex++; continue; } const offset2 = startPos + controlByteIndex; const value = getUint(record.slice(offset2, offset2 + 1)) & mask; if (value === mask) { if (countBitsSet(mask) > 1) { const { value: value2, length: length2 } = getVarLen(array, pos); tags.push([tag, null, value2, numValues]); pos += length2; } else tags.push([tag, 1, null, numValues]); } else tags.push([tag, value >> countUnsetEnd(mask), null, numValues]); } const tagMap = {}; for (const [tag, valueCount, valueBytes, numValues] of tags) { const values = []; if (valueCount != null) { for (let i2 = 0; i2 < valueCount * numValues; i2++) { const { value, length: length2 } = getVarLen(array, pos); values.push(value); pos += length2; } } else { let count = 0; while (count < valueBytes) { const { value, length: length2 } = getVarLen(array, pos); values.push(value); pos += length2; count += length2; } } tagMap[tag] = values; } table.push({ name, tagMap }); } } return { table, cncx }; }; const getNCX = async (indxIndex, loadRecord) => { const { table, cncx } = await getIndexData(indxIndex, loadRecord); const items = table.map(({ tagMap }, index) => { var _a, _b, _c, _d, _e, _f; return { index, offset: (_a = tagMap[1]) == null ? void 0 : _a[0], size: (_b = tagMap[2]) == null ? void 0 : _b[0], label: cncx[tagMap[3]] ?? "", headingLevel: (_c = tagMap[4]) == null ? void 0 : _c[0], pos: tagMap[6], parent: (_d = tagMap[21]) == null ? void 0 : _d[0], firstChild: (_e = tagMap[22]) == null ? void 0 : _e[0], lastChild: (_f = tagMap[23]) == null ? void 0 : _f[0] }; }); const getChildren = (item) => { if (item.firstChild == null) return item; item.children = items.filter((x) => x.parent === item.index).map(getChildren); return item; }; return items.filter((item) => item.headingLevel === 0).map(getChildren); }; const getEXTH = (buf, encoding) => { const { magic, count } = getStruct(EXTH_HEADER, buf); if (magic !== "EXTH") throw new Error("Invalid EXTH header"); const decoder2 = getDecoder(encoding); const results = {}; let offset = 12; for (let i = 0; i < count; i++) { const type = getUint(buf.slice(offset, offset + 4)); const length = getUint(buf.slice(offset + 4, offset + 8)); if (type in EXTH_RECORD_TYPE) { const [name, typ, many] = EXTH_RECORD_TYPE[type]; const data = buf.slice(offset + 8, offset + length); const value = typ === "uint" ? getUint(data) : decoder2.decode(data); if (many) { results[name] ??= []; results[name].push(value); } else results[name] = value; } offset += length; } return results; }; const getFont = async (buf, unzlib) => { const { flags, dataStart, keyLength, keyStart } = getStruct(FONT_HEADER, buf); const array = new Uint8Array(buf.slice(dataStart)); if (flags & 2) { const bytes = keyLength === 16 ? 1024 : 1040; const key = new Uint8Array(buf.slice(keyStart, keyStart + keyLength)); const length = Math.min(bytes, array.length); for (var i = 0; i < length; i++) array[i] = array[i] ^ key[i % key.length]; } if (flags & 1) try { return await unzlib(array); } catch (e) { console.warn(e); console.warn("Failed to decompress font"); } return array; }; const isMOBI = async (file) => { const magic = getString(await file.slice(60, 68).arrayBuffer()); return magic === "BOOKMOBI"; }; class PDB { #file; #offsets; pdb; async open(file) { this.#file = file; const pdb = getStruct(PDB_HEADER, await file.slice(0, 78).arrayBuffer()); this.pdb = pdb; const buffer = await file.slice(78, 78 + pdb.numRecords * 8).arrayBuffer(); this.#offsets = Array.from( { length: pdb.numRecords }, (_, i) => getUint(buffer.slice(i * 8, i * 8 + 4)) ).map((x, i, a) => [x, a[i + 1]]); } loadRecord(index) { const offsets = this.#offsets[index]; if (!offsets) throw new RangeError("Record index out of bounds"); return this.#file.slice(...offsets).arrayBuffer(); } async loadMagic(index) { const start = this.#offsets[index][0]; return getString(await this.#file.slice(start, start + 4).arrayBuffer()); } } class MOBI extends PDB { #start = 0; #resourceStart; #decoder; #encoder; #decompress; #removeTrailingEntries; constructor({ unzlib }) { super(); this.unzlib = unzlib; } async open(file) { var _a; await super.open(file); this.headers = this.#getHeaders(await super.loadRecord(0)); this.#resourceStart = this.headers.mobi.resourceStart; let isKF8 = this.headers.mobi.version >= 8; if (!isKF8) { const boundary = (_a = this.headers.exth) == null ? void 0 : _a.boundary; if (boundary < 4294967295) try { this.headers = this.#getHeaders(await super.loadRecord(boundary)); this.#start = boundary; isKF8 = true; } catch (e) { console.warn(e); console.warn("Failed to open KF8; falling back to MOBI"); } } await this.#setup(); return isKF8 ? new KF8(this).init() : new MOBI6(this).init(); } #getHeaders(buf) { const palmdoc = getStruct(PALMDOC_HEADER, buf); const mobi = getStruct(MOBI_HEADER, buf); if (mobi.magic !== "MOBI") throw new Error("Missing MOBI header"); const { titleOffset, titleLength, localeLanguage, localeRegion } = mobi; mobi.title = buf.slice(titleOffset, titleOffset + titleLength); const lang = MOBI_LANG[localeLanguage]; mobi.language = (lang == null ? void 0 : lang[localeRegion >> 2]) ?? (lang == null ? void 0 : lang[0]); const exth = mobi.exthFlag & 64 ? getEXTH(buf.slice(mobi.length + 16), mobi.encoding) : null; const kf8 = mobi.version >= 8 ? getStruct(KF8_HEADER, buf) : null; return { palmdoc, mobi, exth, kf8 }; } async #setup() { const { palmdoc, mobi } = this.headers; this.#decoder = getDecoder(mobi.encoding); this.#encoder = new TextEncoder(); const { compression } = palmdoc; this.#decompress = compression === 1 ? (f) => f : compression === 2 ? decompressPalmDOC : compression === 17480 ? await huffcdic(mobi, this.loadRecord.bind(this)) : null; if (!this.#decompress) throw new Error("Unknown compression type"); const { trailingFlags } = mobi; const multibyte = trailingFlags & 1; const numTrailingEntries = countBitsSet(trailingFlags >>> 1); this.#removeTrailingEntries = (array) => { for (let i = 0; i < numTrailingEntries; i++) { const length = getVarLenFromEnd(array); array = array.subarray(0, -length); } if (multibyte) { const length = (array[array.length - 1] & 3) + 1; array = array.subarray(0, -length); } return array; }; } decode(...args) { return this.#decoder.decode(...args); } encode(...args) { return this.#encoder.encode(...args); } loadRecord(index) { return super.loadRecord(this.#start + index); } loadMagic(index) { return super.loadMagic(this.#start + index); } loadText(index) { return this.loadRecord(index + 1).then((buf) => new Uint8Array(buf)).then(this.#removeTrailingEntries).then(this.#decompress); } async loadResource(index) { const buf = await super.loadRecord(this.#resourceStart + index); const magic = getString(buf.slice(0, 4)); if (magic === "FONT") return getFont(buf, this.unzlib); if (magic === "VIDE" || magic === "AUDI") return buf.slice(12); return buf; } getNCX() { const index = this.headers.mobi.indx; if (index < 4294967295) return getNCX(index, this.loadRecord.bind(this)); } getMetadata() { var _a, _b; const { mobi, exth } = this.headers; return { identifier: mobi.uid.toString(), title: unescapeHTML((exth == null ? void 0 : exth.title) || this.decode(mobi.title)), author: (_a = exth == null ? void 0 : exth.creator) == null ? void 0 : _a.map(unescapeHTML), publisher: unescapeHTML(exth == null ? void 0 : exth.publisher), language: (exth == null ? void 0 : exth.language) ?? mobi.language, published: exth == null ? void 0 : exth.date, description: unescapeHTML(exth == null ? void 0 : exth.description), subject: (_b = exth == null ? void 0 : exth.subject) == null ? void 0 : _b.map(unescapeHTML), rights: unescapeHTML(exth == null ? void 0 : exth.rights), contributor: exth == null ? void 0 : exth.contributor }; } async getCover() { const { exth } = this.headers; const offset = (exth == null ? void 0 : exth.coverOffset) < 4294967295 ? exth == null ? void 0 : exth.coverOffset : (exth == null ? void 0 : exth.thumbnailOffset) < 4294967295 ? exth == null ? void 0 : exth.thumbnailOffset : null; if (offset != null) { const buf = await this.loadResource(offset); return new Blob([buf]); } } } const mbpPagebreakRegex = /<\s*(?:mbp:)?pagebreak[^>]*>/gi; const fileposRegex = /<[^<>]+filepos=['"]{0,1}(\d+)[^<>]*>/gi; const getIndent = (el) => { let x = 0; while (el) { const parent = el.parentElement; if (parent) { const tag = parent.tagName.toLowerCase(); if (tag === "p") x += 1.5; else if (tag === "blockquote") x += 2; } el = parent; } return x; }; function rawBytesToString(uint8Array) { const chunkSize = 32768; let result = ""; for (let i = 0; i < uint8Array.length; i += chunkSize) { result += String.fromCharCode.apply(null, uint8Array.subarray(i, i + chunkSize)); } return result; } class MOBI6 { parser = new DOMParser(); serializer = new XMLSerializer(); #resourceCache = /* @__PURE__ */ new Map(); #textCache = /* @__PURE__ */ new Map(); #cache = /* @__PURE__ */ new Map(); #sections; #fileposList = []; #type = MIME.HTML; constructor(mobi) { this.mobi = mobi; } async init() { var _a; const recordBuffers = []; for (let i = 0; i < this.mobi.headers.palmdoc.numTextRecords; i++) { const buf = await this.mobi.loadText(i); recordBuffers.push(buf); } const totalLength = recordBuffers.reduce((sum, buf) => sum + buf.byteLength, 0); const array = new Uint8Array(totalLength); recordBuffers.reduce((offset, buf) => { array.set(new Uint8Array(buf), offset); return offset + buf.byteLength; }, 0); const str = rawBytesToString(array); this.#sections = [0].concat(Array.from(str.matchAll(mbpPagebreakRegex), (m) => m.index)).map((start, i, a) => { const end = a[i + 1] ?? array.length; return { book: this, raw: array.subarray(start, end) }; }).map((section, i, arr) => { var _a2; section.start = ((_a2 = arr[i - 1]) == null ? void 0 : _a2.end) ?? 0; section.end = section.start + section.raw.byteLength; return section; }); this.sections = this.#sections.map((section, index) => ({ id: index, load: () => this.loadSection(section), createDocument: () => this.createDocument(section), size: section.end - section.start })); try { this.landmarks = await this.getGuide(); const tocHref = (_a = this.landmarks.find(({ type }) => type == null ? void 0 : type.includes("toc"))) == null ? void 0 : _a.href; if (tocHref) { const { index } = this.resolveHref(tocHref); const doc = await this.sections[index].createDocument(); let lastItem; let lastLevel = 0; let lastIndent = 0; const lastLevelOfIndent = /* @__PURE__ */ new Map(); const lastParentOfLevel = /* @__PURE__ */ new Map(); this.toc = Array.from(doc.querySelectorAll("a[filepos]")).reduce((arr, a) => { var _a2; const indent = getIndent(a); const item = { label: ((_a2 = a.innerText) == null ? void 0 : _a2.trim()) ?? "", href: `filepos:${a.getAttribute("filepos")}` }; const level = indent > lastIndent ? lastLevel + 1 : indent === lastIndent ? lastLevel : lastLevelOfIndent.get(indent) ?? Math.max(0, lastLevel - 1); if (level > lastLevel) { if (lastItem) { lastItem.subitems ??= []; lastItem.subitems.push(item); lastParentOfLevel.set(level, lastItem); } else arr.push(item); } else { const parent = lastParentOfLevel.get(level); if (parent) parent.subitems.push(item); else arr.push(item); } lastItem = item; lastLevel = level; lastIndent = indent; lastLevelOfIndent.set(indent, level); return arr; }, []); } } catch (e) { console.warn(e); } this.#fileposList = [...new Set( Array.from(str.matchAll(fileposRegex), (m) => m[1]) )].map((filepos) => ({ filepos, number: Number(filepos) })).sort((a, b) => a.number - b.number); this.metadata = this.mobi.getMetadata(); this.getCover = this.mobi.getCover.bind(this.mobi); return this; } async getGuide() { const doc = await this.createDocument(this.#sections[0]); return Array.from(doc.getElementsByTagName("reference"), (ref) => { var _a; return { label: ref.getAttribute("title"), type: (_a = ref.getAttribute("type")) == null ? void 0 : _a.split(/\s/), href: `filepos:${ref.getAttribute("filepos")}` }; }); } async loadResource(index) { if (this.#resourceCache.has(index)) return this.#resourceCache.get(index); const raw = await this.mobi.loadResource(index); const url = URL.createObjectURL(new Blob([raw])); this.#resourceCache.set(index, url); return url; } async loadRecindex(recindex) { return this.loadResource(Number(recindex) - 1); } async replaceResources(doc) { for (const img of doc.querySelectorAll("img[recindex]")) { const recindex = img.getAttribute("recindex"); try { img.src = await this.loadRecindex(recindex); } catch { console.warn(`Failed to load image ${recindex}`); } } for (const media of doc.querySelectorAll("[mediarecindex]")) { const mediarecindex = media.getAttribute("mediarecindex"); const recindex = media.getAttribute("recindex"); try { media.src = await this.loadRecindex(mediarecindex); if (recindex) media.poster = await this.loadRecindex(recindex); } catch { console.warn(`Failed to load media ${mediarecindex}`); } } for (const a of doc.querySelectorAll("[filepos]")) { const filepos = a.getAttribute("filepos"); a.href = `filepos:${filepos}`; } } async loadText(section) { if (this.#textCache.has(section)) return this.#textCache.get(section); const { raw } = section; const fileposList = this.#fileposList.filter(({ number }) => number >= section.start && number < section.end).map((obj) => ({ ...obj, offset: obj.number - section.start })); let arr = raw; if (fileposList.length) { arr = raw.subarray(0, fileposList[0].offset); fileposList.forEach(({ filepos, offset }, i) => { const next = fileposList[i + 1]; const a = this.mobi.encode(`<a id="filepos${filepos}"></a>`); arr = concatTypedArray3(arr, a, raw.subarray(offset, next == null ? void 0 : next.offset)); }); } const str = this.mobi.decode(arr).replaceAll(mbpPagebreakRegex, ""); this.#textCache.set(section, str); return str; } async createDocument(section) { const str = await this.loadText(section); return this.parser.parseFromString(str, this.#type); } async loadSection(section) { if (this.#cache.has(section)) return this.#cache.get(section); const doc = await this.createDocument(section); const style = doc.createElement("style"); doc.head.append(style); style.append(doc.createTextNode(`blockquote { margin-block-start: 0; margin-block-end: 0; margin-inline-start: 1em; margin-inline-end: 0; }`)); await this.replaceResources(doc); const result = this.serializer.serializeToString(doc); const url = URL.createObjectURL(new Blob([result], { type: this.#type })); this.#cache.set(section, url); return url; } resolveHref(href) { const filepos = href.match(/filepos:(.*)/)[1]; const number = Number(filepos); const index = this.#sections.findIndex((section) => section.end > number); const anchor = (doc) => doc.getElementById(`filepos${filepos}`); return { index, anchor }; } splitTOCHref(href) { const filepos = href.match(/filepos:(.*)/)[1]; const number = Number(filepos); const index = this.#sections.findIndex((section) => section.end > number); return [index, `filepos${filepos}`]; } getTOCFragment(doc, id) { return doc.getElementById(id); } isExternal(uri) { return /^(?!blob|filepos)\w+:/i.test(uri); } destroy() { for (const url of this.#resourceCache.values()) URL.revokeObjectURL(url); for (const url of this.#cache.values()) URL.revokeObjectURL(url); } } const kindleResourceRegex = /kindle:(flow|embed):(\w+)(?:\?mime=(\w+\/[-+.\w]+))?/; const kindlePosRegex = /kindle:pos:fid:(\w+):off:(\w+)/; const parseResourceURI = (str) => { const [resourceType, id, type] = str.match(kindleResourceRegex).slice(1); return { resourceType, id: parseInt(id, 32), type }; }; const parsePosURI = (str) => { const [fid, off] = str.match(kindlePosRegex).slice(1); return { fid: parseInt(fid, 32), off: parseInt(off, 32) }; }; const makePosURI = (fid = 0, off = 0) => `kindle:pos:fid:${fid.toString(32).toUpperCase().padStart(4, "0")}:off:${off.toString(32).toUpperCase().padStart(10, "0")}`; const getFragmentSelector = (str) => { const match = str.match(/\s(id|name|aid)\s*=\s*['"]([^'"]*)['"]/i); if (!match) return; const [, attr, value] = match; return `[${attr}="${CSS.escape(value)}"]`; }; const replaceSeries = async (str, regex, f) => { const matches = []; str.replace(regex, (...args) => (matches.push(args), null)); const results = []; for (const args of matches) results.push(await f(...args)); return str.replace(regex, () => results.shift()); }; const getPageSpread = (properties) => { for (const p of properties) { if (p === "page-spread-left" || p === "rendition:page-spread-left") return "left"; if (p === "page-spread-right" || p === "rendition:page-spread-right") return "right"; if (p === "rendition:page-spread-center") return "center"; } }; class KF8 { parser = new DOMParser(); serializer = new XMLSerializer(); transformTarget = new EventTarget(); #cache = /* @__PURE__ */ new Map(); #fragmentOffsets = /* @__PURE__ */ new Map(); #fragmentSelectors = /* @__PURE__ */ new Map(); #tables = {}; #sections; #fullRawLength; #rawHead = new Uint8Array(); #rawTail = new Uint8Array(); #lastLoadedHead = -1; #lastLoadedTail = -1; #type = MIME.XHTML; #inlineMap = /* @__PURE__ */ new Map(); constructor(mobi) { this.mobi = mobi; } async init() { var _a, _b, _c, _d; const loadRecord = this.mobi.loadRecord.bind(this.mobi); const { kf8 } = this.mobi.headers; try { const fdstBuffer = await loadRecord(kf8.fdst); const fdst = getStruct(FDST_HEADER, fdstBuffer); if (fdst.magic !== "FDST") throw new Error("Missing FDST record"); const fdstTable = Array.from( { length: fdst.numEntries }, (_, i) => 12 + i * 8 ).map((offset) => [ getUint(fdstBuffer.slice(offset, offset + 4)), getUint(fdstBuffer.slice(offset + 4, offset + 8)) ]); this.#tables.fdstTable = fdstTable; this.#fullRawLength = fdstTable[fdstTable.length - 1][1]; } catch { } const skelTable = (await getIndexData(kf8.skel, loadRecord)).table.map(({ name, tagMap }, index) => ({ index, name, numFrag: tagMap[1][0], offset: tagMap[6][0], length: tagMap[6][1] })); const fragData = await getIndexData(kf8.frag, loadRecord); const fragTable = fragData.table.map(({ name, tagMap }) => ({ insertOffset: parseInt(name), selector: fragData.cncx[tagMap[2][0]], index: tagMap[4][0], offset: tagMap[6][0], length: tagMap[6][1] })); this.#tables.skelTable = skelTable; this.#tables.fragTable = fragTable; this.#sections = skelTable.reduce((arr, skel) => { const last = arr[arr.length - 1]; const fragStart = (last == null ? void 0 : last.fragEnd) ?? 0, fragEnd = fragStart + skel.numFrag; const frags = fragTable.slice(fragStart, fragEnd); const length = skel.length + frags.map((f) => f.length).reduce((a, b) => a + b); const totalLength = ((last == null ? void 0 : last.totalLength) ?? 0) + length; return arr.concat({ skel, frags, fragEnd, length, totalLength }); }, []); const resources = await this.getResourcesByMagic(["RESC", "PAGE"]); const pageSpreads = /* @__PURE__ */ new Map(); if (resources.RESC) { const buf = await this.mobi.loadRecord(resources.RESC); const str = this.mobi.decode(buf.slice(16)).replace(/\0/g, ""); const index = str.search(/\?>/); const xmlStr = `<package>${str.slice(index)}</package>`; const opf = this.parser.parseFromString(xmlStr, MIME.XML); for (const $itemref of opf.querySelectorAll("spine > itemref")) { const i = parseInt($itemref.getAttribute("skelid")); pageSpreads.set(i, getPageSpread( ((_a = $itemref.getAttribute("properties")) == null ? void 0 : _a.split(" ")) ?? [] )); } } this.sections = this.#sections.map((section, index) => section.frags.length ? { id: index, load: () => this.loadSection(section), createDocument: () => this.createDocument(section), size: section.length, pageSpread: pageSpreads.get(index) } : { linear: "no" }); try { const ncx = await this.mobi.getNCX(); const map = ({ label, pos, children }) => { const [fid, off] = pos; const href = makePosURI(fid, off); const arr = this.#fragmentOffsets.get(fid); if (arr) arr.push(off); else this.#fragmentOffsets.set(fid, [off]); return { label: unescapeHTML(label), href, subitems: children == null ? void 0 : children.map(map) }; }; this.toc = ncx == null ? void 0 : ncx.map(map); this.landmarks = await this.getGuide(); } catch (e) { console.warn(e); } const { exth } = this.mobi.headers; this.dir = exth.pageProgressionDirection; this.rendition = { layout: exth.fixedLayout === "true" ? "pre-paginated" : "reflowable", viewport: Object.fromEntries(((_d = (_c = (_b = exth.originalResolution) == null ? void 0 : _b.split("x")) == null ? void 0 : _c.slice(0, 2)) == null ? void 0 : _d.map((x, i) => [i ? "height" : "width", x])) ?? []) }; this.metadata = this.mobi.getMetadata(); this.getCover = this.mobi.getCover.bind(this.mobi); return this; } // is this really the only way of getting to RESC, PAGE, etc.? async getResourcesByMagic(keys) { const results = {}; const start = this.mobi.headers.kf8.resourceStart; const end = this.mobi.pdb.numRecords; for (let i = start; i < end; i++) { try { const magic = await this.mobi.loadMagic(i); const match = keys.find((key) => key === magic); if (match) results[match] = i; } catch { } } return results; } async getGuide() { const index = this.mobi.headers.kf8.guide; if (index < 4294967295) { const loadRecord = this.mobi.loadRecord.bind(this.mobi); const { table, cncx } = await getIndexData(index, loadRecord); return table.map(({ name, tagMap }) => { var _a, _b; return { label: cncx[tagMap[1][0]] ?? "", type: name == null ? void 0 : name.split(/\s/), href: makePosURI(((_a = tagMap[6]) == null ? void 0 : _a[0]) ?? ((_b = tagMap[3]) == null ? void 0 : _b[0])) }; }); } } async loadResourceBlob(str) { var _a; const { resourceType, id, type } = parseResourceURI(str); const raw = resourceType === "flow" ? await this.loadFlow(id) : await this.mobi.loadResource(id - 1); const result = [MIME.XHTML, MIME.HTML, MIME.CSS, MIME.SVG].includes(type) ? await this.replaceResources(this.mobi.decode(raw)) : raw; const detail = { data: result, type }; const event = new CustomEvent("data", { detail }); this.transformTarget.dispatchEvent(event); const newData = await event.detail.data; const newType = await event.detail.type; const doc = newType === MIME.SVG ? this.parser.parseFromString(newData, newType) : null; return [ new Blob([newData], { newType }), // SVG wrappers need to be inlined // as browsers don't allow external resources when loading SVG as an image ((_a = doc == null ? void 0 : doc.getElementsByTagNameNS("http://www.w3.org/2000/svg", "image")) == null ? void 0 : _a.length) ? doc.documentElement : null ]; } async loadResource(str) { if (this.#cache.has(str)) return this.#cache.get(str); const [blob, inline] = await this.loadResourceBlob(str); const url = inline ? str : URL.createObjectURL(blob); if (inline) this.#inlineMap.set(url, inline); this.#cache.set(str, url); return url; } replaceResources(str) { const regex = new RegExp(kindleResourceRegex, "g"); return replaceSeries(str, regex, this.loadResource.bind(this)); } // NOTE: there doesn't seem to be a way to access text randomly? // how to know the decompressed size of the records without decompressing? // 4096 is just the maximum size async loadRaw(start, end) { const distanceHead = end - this.#rawHead.length; const distanceEnd = this.#fullRawLength == null ? Infinity : this.#fullRawLength - this.#rawTail.length - start; if (distanceHead < 0 || distanceHead < distanceEnd) { while (this.#rawHead.length < end) { const index = ++this.#lastLoadedHead; const data = await this.mobi.loadText(index); this.#rawHead = concatTypedArray(this.#rawHead, data); } return this.#rawHead.slice(start, end); } while (this.#fullRawLength - this.#rawTail.length > start) { const index = this.mobi.headers.palmdoc.numTextRecords - 1 - ++this.#lastLoadedTail; const data = await this.mobi.loadText(index); this.#rawTail = concatTypedArray(data, this.#rawTail); } const rawTailStart = this.#fullRawLength - this.#rawTail.length; return this.#rawTail.slice(start - rawTailStart, end - rawTailStart); } loadFlow(index) { if (index < 4294967295) return this.loadRaw(...this.#tables.fdstTable[index]); } async loadText(section) { const { skel, frags, length } = section; const raw = await this.loadRaw(skel.offset, skel.offset + length); let skeleton = raw.slice(0, skel.length); for (const frag of frags) { const insertOffset = frag.insertOffset - skel.offset; const offset = skel.length + frag.offset; const fragRaw = raw.slice(offset, offset + frag.length); skeleton = concatTypedArray3( skeleton.slice(0, insertOffset), fragRaw, skeleton.slice(insertOffset) ); const offsets = this.#fragmentOffsets.get(frag.index); if (offsets) for (const offset2 of offsets) { const str = this.mobi.decode(fragRaw.slice(offset2)); const selector = getFragmentSelector(str); this.#setFragmentSelector(frag.index, offset2, selector); } } return this.mobi.decode(skeleton); } async createDocument(section) { const str = await this.loadText(section); return this.parser.parseFromString(str, this.#type); } async loadSection(section) { var _a; if (this.#cache.has(section)) return this.#cache.get(section); const str = await this.loadText(section); const replaced = await this.replaceResources(str); let doc = this.parser.parseFromString(replaced, this.#type); if (doc.querySelector("parsererror") || !((_a = doc.documentElement) == null ? void 0 : _a.namespaceURI)) { this.#type = MIME.HTML; doc = this.parser.parseFromString(replaced, this.#type); } for (const [url2, node] of this.#inlineMap) { for (const el of doc.querySelectorAll(`img[src="${url2}"]`)) el.replaceWith(node); } const url = URL.createObjectURL( new Blob([this.serializer.serializeToString(doc)], { type: this.#type }) ); this.#cache.set(section, url); return url; } getIndexByFID(fid) { return this.#sections.findIndex((section) => section.frags.some((frag) => frag.index === fid)); } #setFragmentSelector(id, offset, selector) { const map = this.#fragmentSelectors.get(id); if (map) map.set(offset, selector); else { const map2 = /* @__PURE__ */ new Map(); this.#fragmentSelectors.set(id, map2); map2.set(offset, selector); } } async resolveHref(href) { var _a; const { fid, off } = parsePosURI(href); const index = this.getIndexByFID(fid); if (index < 0) return; const saved = (_a = this.#fragmentSelectors.get(fid)) == null ? void 0 : _a.get(off); if (saved) return { index, anchor: (doc) => doc.querySelector(saved) }; const { skel, frags } = this.#sections[index]; const frag = frags.find((frag2) => frag2.index === fid); const offset = skel.offset + skel.length + frag.offset; const fragRaw = await this.loadRaw(offset, offset + frag.length); const str = this.mobi.decode(fragRaw).slice(off); const selector = getFragmentSelector(str); this.#setFragmentSelector(fid, off, selector); const anchor = (doc) => doc.querySelector(selector); return { index, anchor }; } splitTOCHref(href) { const pos = parsePosURI(href); const index = this.getIndexByFID(pos.fid); return [index, pos]; } getTOCFragment(doc, { fid, off }) { var _a; const selector = (_a = this.#fragmentSelectors.get(fid)) == null ? void 0 : _a.get(off); return doc.querySelector(selector); } isExternal(uri) { return /^(?!blob|kindle)\w+:/i.test(uri); } destroy() { for (const url of this.#cache.values()) URL.revokeObjectURL(url); } } export { MOBI, isMOBI };