Release EPUB Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import type {
|
||||
EpubMetadata,
|
||||
EpubPackage,
|
||||
ManifestItem,
|
||||
NavigationItem,
|
||||
SpineItem,
|
||||
} from "./types";
|
||||
import { directoryOf, resolvePackageHref, splitReference } from "./paths";
|
||||
import {
|
||||
directChildrenByLocalName,
|
||||
elementsByLocalName,
|
||||
parseXml,
|
||||
} from "./xml";
|
||||
|
||||
function text(element: Element | undefined): string {
|
||||
return element?.textContent?.trim() ?? "";
|
||||
}
|
||||
function firstDirect(parent: Element, name: string): Element | undefined {
|
||||
return directChildrenByLocalName(parent, name)[0];
|
||||
}
|
||||
|
||||
export function parsePackageDocument(
|
||||
packageXml: string,
|
||||
packagePath: string,
|
||||
): { package: EpubPackage; document: XMLDocument } {
|
||||
const document = parseXml(packageXml, packagePath);
|
||||
const root = document.documentElement;
|
||||
if (root.localName !== "package")
|
||||
throw new Error("The rootfile is not an EPUB package document.");
|
||||
const metadataElement = firstDirect(root, "metadata");
|
||||
const manifestElement = firstDirect(root, "manifest");
|
||||
const spineElement = firstDirect(root, "spine");
|
||||
if (!metadataElement || !manifestElement || !spineElement)
|
||||
throw new Error(
|
||||
"The package document must contain metadata, manifest, and spine elements.",
|
||||
);
|
||||
const metadata: EpubMetadata = {
|
||||
title: text(elementsByLocalName(metadataElement, "title")[0]),
|
||||
creators: elementsByLocalName(metadataElement, "creator")
|
||||
.map((item) => text(item))
|
||||
.filter(Boolean),
|
||||
language: text(elementsByLocalName(metadataElement, "language")[0]),
|
||||
identifier: text(elementsByLocalName(metadataElement, "identifier")[0]),
|
||||
publisher: text(elementsByLocalName(metadataElement, "publisher")[0]),
|
||||
date: text(elementsByLocalName(metadataElement, "date")[0]),
|
||||
description: text(elementsByLocalName(metadataElement, "description")[0]),
|
||||
rights: text(elementsByLocalName(metadataElement, "rights")[0]),
|
||||
subjects: elementsByLocalName(metadataElement, "subject")
|
||||
.map((item) => text(item))
|
||||
.filter(Boolean),
|
||||
};
|
||||
const packageDirectory = directoryOf(packagePath);
|
||||
const manifest: ManifestItem[] = directChildrenByLocalName(
|
||||
manifestElement,
|
||||
"item",
|
||||
).map((element) => {
|
||||
const href = element.getAttribute("href") ?? "";
|
||||
return {
|
||||
id: element.getAttribute("id") ?? "",
|
||||
href,
|
||||
path: resolvePackageHref(packagePath, href) ?? "",
|
||||
mediaType: element.getAttribute("media-type") ?? "",
|
||||
properties: (element.getAttribute("properties") ?? "")
|
||||
.split(/\s+/u)
|
||||
.filter(Boolean),
|
||||
fallback: element.getAttribute("fallback") ?? undefined,
|
||||
mediaOverlay: element.getAttribute("media-overlay") ?? undefined,
|
||||
};
|
||||
});
|
||||
const byId = new Map(manifest.map((item) => [item.id, item]));
|
||||
const spine: SpineItem[] = directChildrenByLocalName(
|
||||
spineElement,
|
||||
"itemref",
|
||||
).map((element) => {
|
||||
const idref = element.getAttribute("idref") ?? "";
|
||||
return {
|
||||
idref,
|
||||
linear: element.getAttribute("linear") !== "no",
|
||||
properties: (element.getAttribute("properties") ?? "")
|
||||
.split(/\s+/u)
|
||||
.filter(Boolean),
|
||||
item: byId.get(idref),
|
||||
};
|
||||
});
|
||||
const coverId = elementsByLocalName(metadataElement, "meta")
|
||||
.find((element) => element.getAttribute("name") === "cover")
|
||||
?.getAttribute("content");
|
||||
const cover =
|
||||
manifest.find((item) => item.properties.includes("cover-image")) ??
|
||||
(coverId ? byId.get(coverId) : undefined);
|
||||
const nav = manifest.find((item) => item.properties.includes("nav"));
|
||||
const ncxId = spineElement.getAttribute("toc") ?? undefined;
|
||||
const ncx =
|
||||
(ncxId ? byId.get(ncxId) : undefined) ??
|
||||
manifest.find((item) => item.mediaType === "application/x-dtbncx+xml");
|
||||
return {
|
||||
package: {
|
||||
version: root.getAttribute("version") ?? "",
|
||||
uniqueIdentifier: root.getAttribute("unique-identifier") ?? undefined,
|
||||
packagePath,
|
||||
packageDirectory,
|
||||
metadata,
|
||||
manifest,
|
||||
spine,
|
||||
navigation: [],
|
||||
cover,
|
||||
nav,
|
||||
ncx,
|
||||
},
|
||||
document,
|
||||
};
|
||||
}
|
||||
|
||||
function navigationChildren(list: Element, basePath: string): NavigationItem[] {
|
||||
return directChildrenByLocalName(list, "li").map((item) => {
|
||||
const anchor = directChildrenByLocalName(item, "a")[0];
|
||||
const labelNode = anchor ?? directChildrenByLocalName(item, "span")[0];
|
||||
const href = anchor?.getAttribute("href") ?? "";
|
||||
const nested = directChildrenByLocalName(item, "ol")[0];
|
||||
let path: string | undefined;
|
||||
try {
|
||||
path = href ? resolvePackageHref(basePath, href) : undefined;
|
||||
} catch {
|
||||
path = undefined;
|
||||
}
|
||||
return {
|
||||
label: text(labelNode) || "Untitled",
|
||||
href,
|
||||
path,
|
||||
fragment: splitReference(href).fragment || undefined,
|
||||
children: nested ? navigationChildren(nested, basePath) : [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function parseNavigationDocument(
|
||||
xml: string,
|
||||
path: string,
|
||||
): NavigationItem[] {
|
||||
const document = parseXml(xml, path);
|
||||
const navs = elementsByLocalName(document, "nav");
|
||||
const toc =
|
||||
navs.find((element) =>
|
||||
(
|
||||
element.getAttribute("epub:type") ??
|
||||
element.getAttributeNS("http://www.idpf.org/2007/ops", "type") ??
|
||||
""
|
||||
)
|
||||
.split(/\s+/u)
|
||||
.includes("toc"),
|
||||
) ?? navs[0];
|
||||
if (!toc) return [];
|
||||
const list = elementsByLocalName(toc, "ol")[0];
|
||||
return list ? navigationChildren(list, path) : [];
|
||||
}
|
||||
|
||||
export function parseNcxDocument(xml: string, path: string): NavigationItem[] {
|
||||
const document = parseXml(xml, path);
|
||||
const navMap = elementsByLocalName(document, "navMap")[0];
|
||||
if (!navMap) return [];
|
||||
const parsePoints = (parent: Element): NavigationItem[] =>
|
||||
directChildrenByLocalName(parent, "navPoint").map((point) => {
|
||||
const label = text(elementsByLocalName(point, "text")[0]) || "Untitled";
|
||||
const href =
|
||||
elementsByLocalName(point, "content")[0]?.getAttribute("src") ?? "";
|
||||
let target: string | undefined;
|
||||
try {
|
||||
target = href ? resolvePackageHref(path, href) : undefined;
|
||||
} catch {
|
||||
target = undefined;
|
||||
}
|
||||
return {
|
||||
label,
|
||||
href,
|
||||
path: target,
|
||||
fragment: splitReference(href).fragment || undefined,
|
||||
children: parsePoints(point),
|
||||
};
|
||||
});
|
||||
return parsePoints(navMap);
|
||||
}
|
||||
Reference in New Issue
Block a user