From 6cde61ca3ded43ded47125f71b037f8fe0cc959d Mon Sep 17 00:00:00 2001 From: XLor Date: Sun, 26 Feb 2023 22:55:03 +0800 Subject: [PATCH] feat: generate manifest and spine --- packages/core/src/bundle/constant.ts | 15 +++ packages/core/src/bundle/index.ts | 34 ++++- packages/core/src/epub.ts | 119 ---------------- packages/core/src/epub/epub.ts | 34 +++++ packages/core/src/epub/index.ts | 3 + packages/core/src/epub/opf.ts | 194 +++++++++++++++++++++++++++ packages/core/test/bundle.test.ts | 33 ++++- 7 files changed, 306 insertions(+), 126 deletions(-) create mode 100644 packages/core/src/bundle/constant.ts delete mode 100644 packages/core/src/epub.ts create mode 100644 packages/core/src/epub/epub.ts create mode 100644 packages/core/src/epub/index.ts create mode 100644 packages/core/src/epub/opf.ts diff --git a/packages/core/src/bundle/constant.ts b/packages/core/src/bundle/constant.ts new file mode 100644 index 0000000..d062b24 --- /dev/null +++ b/packages/core/src/bundle/constant.ts @@ -0,0 +1,15 @@ +export const MIMETYPE = 'application/epub+zip'; + +export const ImageGif = 'image/gif'; +export const ImageJpeg = 'image/jpeg'; +export const ImagePng = 'image/png'; +export const ImageSvg = 'image/svg+xml'; +export const ImageWebp = 'image/webp'; + +export type ImageMediaType = typeof ImageGif | typeof ImageJpeg | typeof ImagePng; + +export const TextCSS = 'text/css'; + +export const XHTML = 'application/xhtml+xml'; + +export type MediaType = ImageMediaType | typeof TextCSS | typeof XHTML; diff --git a/packages/core/src/bundle/index.ts b/packages/core/src/bundle/index.ts index 7ff3cdb..fdede21 100644 --- a/packages/core/src/bundle/index.ts +++ b/packages/core/src/bundle/index.ts @@ -1,10 +1,13 @@ import * as fflate from 'fflate'; import { XMLBuilder } from 'fast-xml-parser'; -import type { Epubook, PackageDocument } from '../epub'; +import type { Epubook, ManifestItem, ManifestItemRef, PackageDocument } from '../epub'; + import { BundleError } from '../error'; -const MIMETYPE = 'application/epub+zip'; +import { MIMETYPE } from './constant'; + +export * from './constant'; /** * Bundle epub to zip archive @@ -96,7 +99,7 @@ export function makePackageDocument(opf: PackageDocument): string { format: true, ignoreAttributes: false, suppressUnpairedNode: false, - unpairedTags: ['rootfile'] + unpairedTags: ['item', 'itemref'] }); const optionalMetadata: Record = {}; @@ -143,14 +146,35 @@ export function makePackageDocument(opf: PackageDocument): string { ] }; + function makeManifestItem(item: ManifestItem) { + return { + '@_fallback': item.fallback(), + '@_href': item.href(), + '@_id': item.id(), + '@_media-overlay': item.mediaOverlay(), + '@_media-type': item.mediaType(), + '@_properties': item.properties() + }; + } + + function makeManifestItemRef(item: ManifestItemRef) { + return { + '@_idref': item.idref() + }; + } + return builder.build({ '?xml': { '#text': '', '@_version': '1.0', '@_encoding': 'UTF-8' }, package: { '@_unique-identifier': opf.uniqueIdentifier(), '@_version': opf.version(), metadata, - manifest: {}, - spine: {} + manifest: { + item: opf.manifest().map((i) => makeManifestItem(i)) + }, + spine: { + itemref: opf.spine().map((ir) => makeManifestItemRef(ir)) + } } }); } diff --git a/packages/core/src/epub.ts b/packages/core/src/epub.ts deleted file mode 100644 index 1655b64..0000000 --- a/packages/core/src/epub.ts +++ /dev/null @@ -1,119 +0,0 @@ -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) { - // 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; - } -} diff --git a/packages/core/src/epub/epub.ts b/packages/core/src/epub/epub.ts new file mode 100644 index 0000000..6aacbb0 --- /dev/null +++ b/packages/core/src/epub/epub.ts @@ -0,0 +1,34 @@ +import { type PathLike, promises as fs } from 'node:fs'; + +import { PackageDocument } from './opf'; + +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); + } +} diff --git a/packages/core/src/epub/index.ts b/packages/core/src/epub/index.ts new file mode 100644 index 0000000..598ca7a --- /dev/null +++ b/packages/core/src/epub/index.ts @@ -0,0 +1,3 @@ +export * from './epub'; + +export * from './opf'; diff --git a/packages/core/src/epub/opf.ts b/packages/core/src/epub/opf.ts new file mode 100644 index 0000000..2faf44d --- /dev/null +++ b/packages/core/src/epub/opf.ts @@ -0,0 +1,194 @@ +import { randomUUID } from 'node:crypto'; + +import { createDefu } from 'defu'; + +import type { MediaType } from '../bundle/constant'; + +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() + }; + + private _manifest: ManifestItem[] = []; + + private _spine: ManifestItemRef[] = []; + + constructor(file: string) { + this.file = file; + } + + public filename() { + return this.file; + } + + public version() { + return this.SpecVersion; + } + + // --- metadata --- + public update(info: Partial) { + // 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; + } + + // --- manifest --- + public manifest() { + return this._manifest; + } + + public spine() { + return this._spine; + } + + // --- identifier --- + public uniqueIdentifier() { + return this._uniqueIdentifier; + } + + public identifier() { + return this._identifier; + } + + public setIdentifier(identifier: string, uniqueIdentifier: string) { + this._identifier = identifier; + this._uniqueIdentifier = uniqueIdentifier; + } +} + +export class ManifestItem { + private _href: string; + + private _id: string; + + private optional: { + fallback?: string; + mediaOverlay?: string; + mediaType?: MediaType; + properties?: string; + } = {}; + + constructor(href: string, id: string) { + this._href = href; + this._id = id; + } + + public update(info: typeof this.optional) { + for (const [key, value] of Object.entries(info)) { + if (!!value) { + this.optional[key as keyof typeof this.optional] = value as any; + } + } + return this; + } + + public href() { + return this._href; + } + + public id() { + return this._id; + } + + public fallback() { + return this.optional.fallback; + } + + public mediaOverlay() { + return this.optional.mediaOverlay; + } + + public mediaType() { + return this.optional.mediaType; + } + + public properties() { + return this.optional.properties; + } +} + +export class ManifestItemRef { + private _idref: string; + + private optional: { + id?: string; + + linear?: string; + + properties?: string; + } = {}; + + constructor(idref: string) { + this._idref = idref; + } + + public update(info: typeof this.optional) { + for (const [key, value] of Object.entries(info)) { + if (!!value) { + this.optional[key as keyof typeof this.optional] = value as any; + } + } + return this; + } + + public idref() { + return this._idref; + } + + public id() { + return this.optional.id; + } + + public linear() { + return this.optional.linear; + } + + public properties() { + return this.optional.properties; + } +} diff --git a/packages/core/test/bundle.test.ts b/packages/core/test/bundle.test.ts index 9264da3..51ac00b 100644 --- a/packages/core/test/bundle.test.ts +++ b/packages/core/test/bundle.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { Epubook } from '../src'; +import { Epubook, ManifestItem, ManifestItemRef } from '../src'; import { makeContainer, makePackageDocument } from '../src/bundle'; describe('Bundle Epub', () => { @@ -31,7 +31,36 @@ describe('Bundle Epub', () => { source: 'imagine' }); + opf + .manifest() + .push( + new ManifestItem('Text/cover.xhtml', 'cover.xhtml').update({ properties: 'cover-image' }) + ); + opf.spine().push(new ManifestItemRef('cover.xhtml')); + const res = makePackageDocument(opf); - console.log(res); + expect(res).toMatchInlineSnapshot(` + " + + + test-book-id + Test Book + zh-CN + XLor + 2023-02-01T11:00:00.000Z + for test usage + imagine + 2023-02-26T11:00:00.000Z + XLor + + + + + + + + + " + `); }); });