-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
295 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import * as fflate from 'fflate'; | ||
import { XMLBuilder } from 'fast-xml-parser'; | ||
|
||
import type { Epubook, PackageDocument } from '../epub'; | ||
import { BundleError } from '../error'; | ||
|
||
const MIMETYPE = 'application/epub+zip'; | ||
|
||
/** | ||
* Bundle epub to zip archive | ||
* | ||
* @param epub | ||
* @returns | ||
*/ | ||
export async function bundle(epub: Epubook): Promise<Uint8Array> { | ||
return new Promise((res, rej) => { | ||
const opfs = epub | ||
.packageDocuments() | ||
.map((opf) => [opf.filename(), fflate.strToU8(makePackageDocument(opf))] as const); | ||
|
||
const abstractContainer: fflate.AsyncZippable = { | ||
mimetype: fflate.strToU8(MIMETYPE), | ||
'META-INF': { | ||
'container.xml': fflate.strToU8(makeContainer(epub)) | ||
}, | ||
...Object.fromEntries(opfs) | ||
}; | ||
|
||
fflate.zip( | ||
abstractContainer, | ||
{ | ||
level: 0, | ||
mtime: new Date() | ||
}, | ||
(err, data) => { | ||
if (err) { | ||
rej(err); | ||
} else { | ||
res(data); | ||
} | ||
} | ||
); | ||
}); | ||
} | ||
|
||
/** | ||
* Generate META-INF/container.xml | ||
* | ||
* See: https://www.w3.org/TR/epub-33/#sec-container-metainf-container.xml | ||
* | ||
* Example: https://www.w3.org/TR/epub-33/#sec-container-container.xml-example | ||
* | ||
* @param epub | ||
* @returns xml string | ||
*/ | ||
export function makeContainer(epub: Epubook): string { | ||
const builder = new XMLBuilder({ | ||
format: true, | ||
ignoreAttributes: false, | ||
suppressUnpairedNode: false, | ||
unpairedTags: ['rootfile'] | ||
}); | ||
|
||
const rootfile = epub.packageDocuments().map((p) => ({ | ||
'@_full-path': p.filename(), | ||
'@_media-type': 'application/oebps-package+xml', | ||
'#text': '' | ||
})); | ||
|
||
return builder.build({ | ||
'?xml': { '#text': '', '@_version': '1.0', '@_encoding': 'UTF-8' }, | ||
container: { | ||
'@_version': '1.0', | ||
'@_xmlns': 'urn:oasis:names:tc:opendocument:xmlns:container', | ||
rootfiles: [ | ||
{ | ||
rootfile | ||
} | ||
] | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Generate package document | ||
* | ||
* @param opf | ||
* @returns | ||
*/ | ||
export function makePackageDocument(opf: PackageDocument): string { | ||
if (opf.version() !== '3.0') { | ||
throw new BundleError(`Unsupport EPUB spec ${opf.version()}`); | ||
} | ||
|
||
const builder = new XMLBuilder({ | ||
format: true, | ||
ignoreAttributes: false, | ||
suppressUnpairedNode: false, | ||
unpairedTags: ['rootfile'] | ||
}); | ||
|
||
const optionalMetadata: Record<string, any> = {}; | ||
const optionalList: Array<keyof ReturnType<typeof opf.metadata>> = [ | ||
'contributor', | ||
'coverage', | ||
'format', | ||
'publisher', | ||
'relation', | ||
'rights', | ||
'source', | ||
'subject', | ||
'type' | ||
]; | ||
for (const key of optionalList) { | ||
const m = opf.metadata(); | ||
if (!!m[key]) { | ||
optionalMetadata['dc:' + key] = m[key]; | ||
} | ||
} | ||
const metadata = { | ||
'dc:identifier': opf.identifier(), | ||
'dc:title': opf.title(), | ||
'dc:language': opf.language(), | ||
'dc:creator': { | ||
'@_id': 'creator', | ||
'@_opf:role': 'aut', | ||
'@_opf:file-as': opf.creator(), | ||
'#text': opf.creator() | ||
}, | ||
'dc:date': opf.metadata().date.toISOString(), | ||
'dc:description': opf.metadata().description, | ||
...optionalMetadata, | ||
meta: [ | ||
{ | ||
'@_property': 'dcterms:modified', | ||
'#text': opf.metadata().lastModified.toISOString() | ||
}, | ||
{ | ||
'@_refines': '#creator', | ||
'@_property': 'file-as', | ||
'#text': opf.creator() | ||
} | ||
] | ||
}; | ||
|
||
return builder.build({ | ||
'?xml': { '#text': '', '@_version': '1.0', '@_encoding': 'UTF-8' }, | ||
package: { | ||
'@_unique-identifier': opf.uniqueIdentifier(), | ||
'@_version': opf.version(), | ||
metadata, | ||
manifest: {}, | ||
spine: {} | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,119 @@ | ||
import { randomUUID } from 'node:crypto'; | ||
import { type PathLike, promises as fs } from 'node:fs'; | ||
|
||
import { createDefu } from 'defu'; | ||
|
||
export class Epubook { | ||
/** | ||
* See: https://www.w3.org/TR/epub-33/#sec-package-doc | ||
* | ||
* Now, it only supports single opf (OEBPS/content.opf) | ||
* | ||
* @returns list of package documents | ||
*/ | ||
private opfs: PackageDocument[] = [new PackageDocument('OEBPS/content.opf')]; | ||
|
||
constructor() {} | ||
|
||
public packageDocuments(): PackageDocument[] { | ||
return this.opfs; | ||
} | ||
|
||
public mainPackageDocument() { | ||
return this.opfs[0]; | ||
} | ||
|
||
async bundle() { | ||
const { bundle } = await import('./bundle'); | ||
return await bundle(this); | ||
} | ||
|
||
async writeFile(file: PathLike) { | ||
const buffer = await this.bundle(); | ||
await fs.writeFile(file, buffer); | ||
} | ||
} | ||
|
||
const defu = createDefu((obj: any, key, value: any) => { | ||
if (obj[key] instanceof Date && value instanceof Date) { | ||
obj[key] = value; | ||
return true; | ||
} | ||
}); | ||
|
||
export class PackageDocument { | ||
private readonly file: string; | ||
|
||
private readonly SpecVersion: '3.0' = '3.0'; | ||
|
||
private _uniqueIdentifier = 'uuid'; | ||
|
||
private _identifier = randomUUID(); | ||
|
||
private _metadata = { | ||
title: '', | ||
language: 'zh-CN', | ||
contributor: [] as string[], | ||
coverage: '', | ||
creator: 'unknown', | ||
date: new Date(), | ||
description: '', | ||
format: '', | ||
publisher: '', | ||
relation: '', | ||
rights: '', | ||
source: '', | ||
subject: '', | ||
type: '', | ||
lastModified: new Date() | ||
}; | ||
|
||
constructor(file: string) { | ||
this.file = file; | ||
} | ||
|
||
public filename() { | ||
return this.file; | ||
} | ||
|
||
public version() { | ||
return this.SpecVersion; | ||
} | ||
|
||
// --- metadata --- | ||
public update(info: Partial<typeof this._metadata>) { | ||
// TODO: valiate input data | ||
this._metadata = defu(info, this._metadata); | ||
return this; | ||
} | ||
|
||
public title() { | ||
return this._metadata.title; | ||
} | ||
|
||
public language() { | ||
return this._metadata.language; | ||
} | ||
|
||
public creator() { | ||
return this._metadata.creator; | ||
} | ||
|
||
public metadata() { | ||
return this._metadata; | ||
} | ||
|
||
// --- identifier --- | ||
public uniqueIdentifier() { | ||
return this._uniqueIdentifier; | ||
} | ||
|
||
public identifier() { | ||
return this._identifier; | ||
} | ||
|
||
public setIdentifier(identifier: string, uniqueIdentifier: string) { | ||
this._identifier = identifier; | ||
this._uniqueIdentifier = uniqueIdentifier; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class BundleError extends Error { | ||
constructor(msg: string) { | ||
super(msg); | ||
} | ||
} |
Oops, something went wrong.