@friedrich.fschr/epub-parser
Version:
An epub parser which can extract chapter contents from an epub file
349 lines (298 loc) • 9.5 kB
JavaScript
import JSZip from 'jszip';
import { parsexml, path } from '@friedrich.fschr/shared';
// JavaScript entry point for epub-parser React Native build
// Constants
const HREF_PREFIX = 'epub:';
const EPUB_HREF_PREFIX = HREF_PREFIX;
// In-memory storage for resources
const imageRecord = {};
const resourceDataCache = new Map();
const resourceUrlCache = new Map();
function existsSync(file) {
return imageRecord[file] !== undefined;
}
function mkdirSync(dir) {
imageRecord[dir] = new Uint8Array();
}
function unlink(path) {
delete imageRecord[path];
}
// Utility functions
function createFileFromData(name, data, type = 'application/epub+zip') {
return {
name,
type,
arrayBuffer: async () => data.buffer,
size: data.length,
lastModified: Date.now()
};
}
async function loadEpubFromFS(filePath, readFunction) {
const data = await readFunction(filePath);
const fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
return createFileFromData(fileName, data);
}
function getResourceData(url) {
if (url.startsWith('resource://')) {
const id = url.replace('resource://', '');
return resourceDataCache.get(id);
}
return undefined;
}
// ZipFile class
class ZipFile {
constructor(filePath) {
this.filePath = filePath;
this.jsZip = null;
this.names = null;
}
async loadZip() {
this.jsZip = await this.readZip(this.filePath);
this.names = new Map(Object.keys(this.jsZip.files).map(name => [name.toLowerCase(), name]));
}
getNames() {
return [...this.names.values()];
}
async readZip(file) {
return new Promise((resolve, reject) => {
file.arrayBuffer()
.then(buffer => {
new JSZip()
.loadAsync(buffer)
.then(zipFile => resolve(zipFile))
.catch(reject);
})
.catch(reject);
});
}
hasFile(name) {
return this.names.has(name.toLowerCase());
}
getFileName(name) {
return this.names.get(name.toLowerCase());
}
async readFile(name) {
if (!this.hasFile(name)) {
throw new Error(name + ' file was not found in ' + this.filePath);
}
const fileName = this.getFileName(name);
const file = await this.jsZip.file(fileName).async('string');
return file;
}
async readResource(name) {
if (!this.hasFile(name)) {
throw new Error(name + ' file was not found in ' + this.filePath);
}
const fileName = this.getFileName(name);
const file = await this.jsZip.file(fileName).async('uint8array');
return file;
}
}
async function createZipFile(filePath) {
const zip = new ZipFile(filePath);
await zip.loadZip();
return zip;
}
// Main EpubFile class
class EpubFile {
constructor(epub, resourceSaveDir = './images') {
this.epub = epub;
this.fileName = epub.name;
this.resourceSaveDir = resourceSaveDir;
this.mimeType = '';
this.metadata = null;
this.manifest = {};
this.spine = [];
this.guide = [];
this.collections = [];
this.navMap = [];
this.pageList = { pageTarget: [] };
this.navList = { navLabel: '', navTarget: [] };
this.zip = null;
this.opfPath = '';
this.opfDir = '';
this.savedResourcePath = [];
this.hrefToIdMap = {};
this.chapterCache = new Map();
if (!existsSync(resourceSaveDir)) {
mkdirSync(resourceSaveDir);
}
}
async loadEpub() {
this.zip = await createZipFile(this.epub);
}
async parse() {
// Simplified version of the parsing logic
try {
// Parse mimetype
const mimetype = await this.zip.readFile('mimetype');
this.mimeType = mimetype.trim();
// Parse container.xml to find OPF file
const containerXml = await this.zip.readFile('meta-inf/container.xml');
const containerAST = await parsexml(containerXml);
if (containerAST && containerAST.container &&
containerAST.container.rootfiles &&
containerAST.container.rootfiles[0].rootfile) {
// Get the OPF path
const rootfile = containerAST.container.rootfiles[0].rootfile[0];
this.opfPath = rootfile['@'] && rootfile['@']['full-path'] || '';
this.opfDir = path.dirname(this.opfPath);
// Parse the OPF file
await this.parseRootFile();
}
} catch (error) {
console.error('Error parsing EPUB:', error);
throw error;
}
}
async parseRootFile() {
// Simplified version that just sets basic metadata
try {
const rootFileOPF = await this.zip.readFile(this.opfPath);
const xml = await parsexml(rootFileOPF);
if (xml && xml.package) {
const rootFile = xml.package;
// Basic metadata
if (rootFile.metadata && rootFile.metadata[0]) {
const metadata = rootFile.metadata[0];
this.metadata = {
title: this.extractFirstValue(metadata, 'dc:title'),
creator: this.extractFirstValue(metadata, 'dc:creator'),
identifier: this.extractFirstValue(metadata, 'dc:identifier'),
language: this.extractFirstValue(metadata, 'dc:language'),
publisher: this.extractFirstValue(metadata, 'dc:publisher'),
description: this.extractFirstValue(metadata, 'dc:description'),
subject: this.extractFirstValue(metadata, 'dc:subject'),
date: this.extractFirstValue(metadata, 'dc:date'),
rights: this.extractFirstValue(metadata, 'dc:rights'),
meta: []
};
}
// Basic manifest parsing
if (rootFile.manifest && rootFile.manifest[0] && rootFile.manifest[0].item) {
const items = rootFile.manifest[0].item;
for (const item of items) {
if (item['@']) {
const id = item['@'].id;
const href = item['@'].href;
const mediaType = item['@']['media-type'];
if (id && href) {
const fullHref = path.joinPosix(this.opfDir, href);
this.manifest[id] = { id, href: fullHref, mediaType };
this.hrefToIdMap[fullHref] = id;
}
}
}
}
// Basic spine parsing
if (rootFile.spine && rootFile.spine[0] && rootFile.spine[0].itemref) {
const itemrefs = rootFile.spine[0].itemref;
for (const itemref of itemrefs) {
if (itemref['@']) {
const id = itemref['@'].idref;
const linear = itemref['@'].linear !== 'no';
this.spine.push({ id, linear });
}
}
}
}
} catch (error) {
console.error('Error parsing OPF file:', error);
throw error;
}
}
extractFirstValue(obj, key) {
if (obj && obj[key] && obj[key][0] && typeof obj[key][0] === 'object') {
return obj[key][0]['#'] || obj[key][0];
} else if (obj && obj[key] && obj[key][0]) {
return obj[key][0];
}
return '';
}
async loadChapter(id) {
if (this.chapterCache.has(id)) {
return this.chapterCache.get(id);
}
const item = this.manifest[id];
if (!item) {
throw new Error('Chapter with id ' + id + ' not found');
}
try {
const xmlHref = item.href;
const htmlDir = path.dirname(xmlHref);
const html = await this.zip.readFile(xmlHref);
// Basic HTML processing
const result = {
html: html,
css: []
};
// Cache and return
this.chapterCache.set(id, result);
return result;
} catch (error) {
console.error('Error loading chapter ' + id + ':', error);
throw error;
}
}
getFileInfo() {
return {
fileName: this.fileName,
mimetype: this.mimeType
};
}
getMetadata() {
return this.metadata || {};
}
getManifest() {
return this.manifest;
}
getSpine() {
return this.spine;
}
getGuide() {
return this.guide;
}
getCollection() {
return this.collections;
}
getToc() {
return this.navMap;
}
getPageList() {
return this.pageList;
}
getNavList() {
return this.navList;
}
resolveHref(href) {
if (!href.startsWith(HREF_PREFIX)) {
return undefined;
}
href = href.slice(5).trim();
const [urlPath, hrefId] = href.split('#');
if (this.hrefToIdMap[urlPath]) {
const id = this.hrefToIdMap[urlPath]; let selector = '';
if (hrefId) {
selector = '[id="' + hrefId + '"]';
}
return { id, selector };
}
return undefined;
}
destroy() {
for (const filePath of this.savedResourcePath) {
unlink(filePath);
}
this.savedResourcePath.length = 0;
resourceUrlCache.clear();
resourceDataCache.clear();
}
}
// Main entry point
async function initEpubFile(epubPath, resourceSaveDir = './images') {
const epub = new EpubFile(epubPath, resourceSaveDir);
await epub.loadEpub();
await epub.parse();
return epub;
}
export { EPUB_HREF_PREFIX, HREF_PREFIX, createFileFromData, getResourceData, initEpubFile, loadEpubFromFS };