UNPKG

@friedrich.fschr/epub-parser

Version:

An epub parser which can extract chapter contents from an epub file

356 lines (304 loc) 9.69 kB
'use strict'; var JSZip = require('jszip'); var shared = require('@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 shared.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 = shared.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 shared.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 = shared.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 = shared.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; } exports.EPUB_HREF_PREFIX = EPUB_HREF_PREFIX; exports.HREF_PREFIX = HREF_PREFIX; exports.createFileFromData = createFileFromData; exports.getResourceData = getResourceData; exports.initEpubFile = initEpubFile; exports.loadEpubFromFS = loadEpubFromFS;