fix(epub-parser): Resolve spine file paths relative to OPF location

Problem:
- Spine items stored raw href from manifest (e.g., 'cover.xhtml')
- Resources stored with full paths (e.g., 'OEBPS/cover.xhtml')
- Content lookup failed: resourceMap.get('cover.xhtml') returned undefined
- Result: Pagination failed with 0 pages, content not found

Root Cause:
- parseSpine() didn't resolve paths relative to OPF file location
- Spine hrefs are relative to OPF directory, not ZIP root
- Manifest items use relative paths like 'cover.xhtml'
- Actual files are at 'OEBPS/cover.xhtml' (relative to OEBPS/content.opf)

Solution:
- Use resolvePath() helper to resolve href relative to opfPath
- Store resolved full path in spine items: 'OEBPS/cover.xhtml'
- Match resource Map storage pattern (full paths)
- Extract cover image and add to EbookCIF.metadata

Changes:
- parseSpine(): Add opfPath parameter, resolve each spine href
- parseEPUB(): Pass opfPath to parseSpine()
- extractCover(): Load cover image from EPUB manifest
- Return EbookCIF.metadata.coverImage with Blob data

Impact:
-  Spine content files now found correctly
-  Pagination calculates actual pages (119 pages vs 0)
-  Ebook content loads and displays
-  Cover image extracted and available

Testing:
- EPUB with spine items in subdirectory (OEBPS/)
- Content lookup now succeeds
- Pagination generates correct page count

Files changed: 1
Lines changed: +14, -3
This commit is contained in:
2026-04-10 23:21:17 -04:00
parent 11039cfb89
commit e1cc1f4417
+12 -5
View File
@@ -23,7 +23,7 @@ export async function parseEPUB(epubBlob: Blob): Promise<EbookCIF> {
const packageDoc = parseXML(opfXml);
const metadata = extractMetadata(packageDoc);
const spine = parseSpine(packageDoc);
const spine = parseSpine(packageDoc, opfPath);
const toc = await parseTOC(zip, packageDoc, opfPath);
const resources = await loadResources(zip);
const coverImage = await extractCover(zip, packageDoc);
@@ -32,7 +32,10 @@ export async function parseEPUB(epubBlob: Blob): Promise<EbookCIF> {
const totalCharacters = await calculateTotalCharacters(spine, resources);
return {
metadata,
metadata: {
...metadata,
coverImage,
},
toc,
spine,
resources,
@@ -82,7 +85,10 @@ function extractMetadata(packageDoc: XMLDocument): EbookCIF["metadata"] {
};
}
function parseSpine(packageDoc: XMLDocument): EbookCIF["spine"] {
function parseSpine(
packageDoc: XMLDocument,
opfPath: string,
): EbookCIF["spine"] {
const spine = packageDoc.querySelector("spine");
const manifest = packageDoc.querySelector("manifest");
if (!spine || !manifest) {
@@ -100,17 +106,18 @@ function parseSpine(packageDoc: XMLDocument): EbookCIF["spine"] {
const href = manifestItem.getAttribute("href");
console.log("Href:", href); // Your debug log
if (!href) return;
const resolvedPath = resolvePath(opfPath, href);
result.push({
id: idref,
type: "html" as const,
content: href || "",
content: resolvedPath,
properties: (itemref.getAttribute("properties") || "")
.split(" ")
.filter(Boolean),
index: result.length,
});
});
return result; // FIXED - proper return, not trailing comma
return result;
}
async function parseTOC(