epub-maker2
Version:
Easily create epub files with javascript
658 lines (530 loc) • 13.3 kB
text/typescript
import _slugify from 'slugify';
import { templateManagers } from './template';
import { trimFilename } from 'fs-iconv';
import { parseFileSetting } from './epubtpl-lib/zip';
import { createUUID } from './lib/uuid';
import {
EnumEpubConfigVertical,
EpubConfig,
ICollection,
ICover,
IEpubConfig,
IRightsConfig,
IStylesheet,
} from './config';
import { BPromise, hashSum, moment, shortid } from './lib/util';
import { EnumEpubType, EnumEpubTypeName } from './epub-types';
import { EnumSectionCollectType } from './var';
export { EnumEpubType, EnumEpubTypeName };
export { EnumSectionCollectType };
import * as libEpubtypes from './epub-types';
import JSZip from 'jszip';
import fixedJSZipDate from 'jszip-fixed-date';
import Bluebird from 'bluebird';
import { createJSZipGeneratorOptions } from '@node-novel/epub-util/lib/const';
export { shortid, hashSum }
export function slugify(input: string, ...argv): string
{
let fn = EpubMaker.libSlugify ||
// @ts-ignore
_slugify as ISlugify
;
return fn(input || '', ...argv).trim();
}
export function slugifyWithFallback(input: string, ...argv): string
{
let ret = slugify(input, ...argv);
return ret || hashSum(input);
}
export class EpubMaker
{
public epubConfig: EpubConfig;
constructor(options = {}, config?: IEpubConfig)
{
this.epubConfig = new EpubConfig(config, options);
}
static create(options?, ...argv)
{
return new this(options, ...argv);
}
withUuid(uuid: string)
{
this.epubConfig.uuid = uuid;
return this;
}
withTemplate(templateName)
{
this.epubConfig.templateName = templateName;
return this;
}
slugify(input: string, ...argv): string
{
let fn = this.epubConfig.options.libSlugify || slugify;
return fn(input || '', ...argv).trim();
}
slugifyWithFallback(input: string, ...argv): string
{
let ret = this.slugify(input, ...argv);
return ret || hashSum(input);
}
withTitle(title: string, title_short?: string)
{
this.epubConfig.title = title;
// @ts-ignore
this.epubConfig.slug = this.slugifyWithFallback(title || title_short);
if (title_short)
{
this.epubConfig.title_short = title_short;
}
return this;
}
addTitles(titles: string[])
{
this.epubConfig.titles = titles || [];
return this;
}
withLanguage(lang: string)
{
this.epubConfig.lang = lang;
return this;
}
get lang()
{
return this.epubConfig.lang;
}
withAuthor(fullName: string, url?: string)
{
this.epubConfig.author = fullName;
this.epubConfig.authorUrl = url;
return this;
}
addAuthor(fullName: string, url?: string): this
addAuthor(fullName: string[]): this
addAuthor(fullName: { [key: string]: string; }): this
addAuthor(fullName: string
| string[]
| { [key: string]: string; }
, url?: string
): this
{
let self = this;
if (Array.isArray(fullName))
{
fullName.forEach(name => {
name && self.epubConfig.addAuthor(name);
});
}
else if (typeof fullName == 'object')
{
for (let name in fullName)
{
name && self.epubConfig.addAuthor(name, fullName[name]);
}
}
else if (fullName)
{
self.epubConfig.addAuthor(fullName as string, url);
}
return this;
}
withPublisher(publisher: string)
{
this.epubConfig.publisher = publisher;
return this;
}
withCollection(data: ICollection)
{
this.epubConfig.collection = Object.assign(this.epubConfig.collection || {}, data);
//console.log(this.epubConfig.collection);
return this;
}
withSeries(name: string, position = 1)
{
if (name)
{
this.withCollection({
name,
position,
type: 'series',
})
}
}
withModificationDate(modificationDate, ...argv)
{
let data = moment(modificationDate, ...argv).local();
this.epubConfig.modification = data;
this.epubConfig.modificationDate = data.format(EpubMaker.dateFormat);
this.epubConfig.modificationDateYMD = data.format('YYYY-MM-DD');
return this;
}
withRights(rightsConfig: IRightsConfig)
{
this.epubConfig.rights = rightsConfig;
return this;
}
withCover(coverUrl: string | ICover, rightsConfig?: IRightsConfig)
{
let cover = parseFileSetting(coverUrl, this.epubConfig) as ICover;
if (cover && rightsConfig)
{
cover.rights = rightsConfig;
}
if (!cover)
{
throw new ReferenceError();
}
this.epubConfig.cover = Object.assign(this.epubConfig.cover || {}, cover);
//this.epubConfig.coverUrl = coverUrl;
//this.epubConfig.coverRights = rightsConfig;
return this;
}
withAttributionUrl(attributionUrl)
{
this.epubConfig.attributionUrl = attributionUrl;
return this;
}
withStylesheetUrl(stylesheetUrl, replaceOriginal?: boolean)
{
let data = parseFileSetting(stylesheetUrl, this.epubConfig) as IStylesheet;
this.epubConfig.stylesheet = Object.assign(this.epubConfig.stylesheet, data, {
replaceOriginal: replaceOriginal,
});
return this;
}
/**
* 用來檢查 Section 是否已經加入
*/
hasSection(section: EpubMaker.Section): boolean
{
return this.epubConfig.sections.includes(section);
}
withSection(section: EpubMaker.Section)
{
section.parentEpubMaker = this;
this.epubConfig.sections.push(section);
Array.prototype.push.apply(this.epubConfig.toc, section.collectToc());
Array.prototype.push.apply(this.epubConfig.landmarks, section.collectLandmarks());
return this;
}
withAdditionalFile(fileUrl, folder, filename)
{
let _file = parseFileSetting(fileUrl, this.epubConfig) as ICover;
_file = Object.assign({}, _file, {
folder: folder,
name: filename
});
this.epubConfig.additionalFiles.push(_file);
return this;
}
withOption(key: string, value)
{
this.epubConfig.options[key] = value;
return this;
}
withInfoPreface(str: string)
{
if (str)
{
this.epubConfig.infoPreface = str.toString();
}
return this;
}
addIdentifier(type: string, id?: string)
{
this.epubConfig.addIdentifier(type, id);
return this;
}
addLinks(links, rel?: string)
{
const self = this;
links = Array.isArray(links) ? links.slice() : [links];
links.forEach(function (url)
{
if (url)
{
self.epubConfig.addLink(url, rel);
}
});
return this;
}
addTag(tag)
{
tag = (Array.isArray(tag) ? tag : [tag]).reduce(function (a, b)
{
if (Array.isArray(b))
{
return a.concat(b);
}
else
{
a.push(b);
}
return a;
}, []);
this.epubConfig.tags = (this.epubConfig.tags || []).concat(tag);
return this;
}
setPublicationDate(new_data?)
{
let data = moment(new_data);
this.epubConfig.publication = data;
this.epubConfig.publicationDate = data.format(EpubMaker.dateFormat);
this.epubConfig.publicationDateYMD = data.format('YYYY-MM-DD');
return this;
}
setVertical(vertical?: boolean | EnumEpubConfigVertical)
{
this.epubConfig.vertical = vertical;
return this;
}
getFilename(useTitle?: boolean, noExt?: boolean): string
{
let ext = this.epubConfig.options.ext || EpubMaker.defaultExt;
let filename;
if (this.epubConfig.filename)
{
filename = this.epubConfig.filename;
}
else if (useTitle && this.epubConfig.title_short)
{
filename = this.epubConfig.title_short;
}
else if (useTitle && this.epubConfig.title)
{
filename = this.epubConfig.title;
}
else if (!this.epubConfig.slug)
{
// @ts-ignore
this.epubConfig.slug = shortid();
filename = this.epubConfig.slug;
}
else
{
filename = this.epubConfig.slug;
}
return trimFilename(filename) + (noExt ? '' : ext);
}
vaild()
{
let ret: string[] = [];
if (!this.epubConfig.title || !this.epubConfig.slug)
{
ret.push('title, slug');
}
if (ret.length)
{
return ret;
}
return null;
}
withContextDate(epubContextDate: moment.MomentInput | Date | moment.Moment)
{
this.epubConfig.epubContextDate = moment(epubContextDate).local();
return this
}
build(options?): Bluebird<JSZip>
{
let self = this;
if (!this.epubConfig.publication)
{
this.setPublicationDate();
}
if (!this.epubConfig.uuid)
{
this.withUuid(createUUID(this.epubConfig));
}
this.epubConfig.$auto();
let chk = this.vaild();
if (chk)
{
throw chk;
}
[]
.concat(this.epubConfig.sections, this.epubConfig.toc, this.epubConfig.landmarks)
.forEach(function (section: EpubMaker.Section, index)
{
section._EpubMaker_ = self;
})
;
return templateManagers
.exec(this.epubConfig.templateName, this, options)
.tap((epubZip: JSZip) => {
if (this.epubConfig.epubContextDate)
{
let date = (this.epubConfig.epubContextDate as moment.Moment).toDate();
return fixedJSZipDate(epubZip, date)
}
})
;
}
/**
* for node.js
*
* @param options
* @returns {Bluebird<T>}
*/
makeEpub<T = Buffer | Blob>(options?): BPromise<T | any | Buffer | Blob>
{
let self = this;
// @ts-ignore
return this.build(options).then(async function (epubZip)
{
let generateOptions = Object.assign(createJSZipGeneratorOptions(), self.epubConfig.options.generateOptions, options);
console.info('generating epub for: ' + self.epubConfig.title);
return epubZip.generateAsync(generateOptions);
});
}
}
export interface ISectionConfig
{
lang?: string;
}
export interface ISectionContent
{
title?: string;
content?: string;
renderTitle?: boolean;
cover?: {
name?: string,
url?: string,
}
}
export interface ISlugify
{
(input: string, ...argv): string
}
export namespace EpubMaker
{
export let defaultExt = '.epub';
export let dateFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
// epubtypes and descriptions, useful for vendors implementing a GUI
// @ts-ignore
export const epubtypes = libEpubtypes;
// @ts-ignore
export let libSlugify = _slugify as ISlugify;
/**
* @epubType Optional. Allows you to add specific epub type content such as [epub:type="titlepage"]
* @id Optional, but required if section should be included in toc and / or landmarks
* @content Optional. Should not be empty if there will be no subsections added to this section. Format: { title, content, renderTitle }
*/
export class Section
{
public _EpubMaker_: EpubMaker;
public epubType: string | EnumEpubTypeName;
public id;
public content: ISectionContent;
public includeInToc: boolean;
public includeInLandmarks: boolean;
public subSections: Section[] = [];
public sectionConfig: ISectionConfig = {};
public parentSection: Section;
public parentEpubMaker: EpubMaker;
constructor(epubType: string | EnumEpubTypeName, id: string, content?: string | ISectionContent, includeInToc?: boolean, includeInLandmarks?: boolean, ...argv)
{
this.epubType = epubType;
this.id = id;
this.includeInToc = includeInToc;
this.includeInLandmarks = includeInLandmarks;
this.subSections = [];
/*
this.content = content;
if (content)
{
content.renderTitle = content.renderTitle !== false; // 'undefined' should default to true
}
*/
this.setContent(content, true);
}
/**
*
* @param {ISectionContent|string} content
* @param {boolean} allow_null
* @returns {this}
*/
setContent(content: string | ISectionContent, allow_null?: boolean)
{
let o = {} as ISectionContent;
if (typeof content == 'string')
{
o.content = content;
}
else if (content && (content.title || content.content || content.renderTitle || content.cover))
{
o = content;
}
if (Object.keys(o).length)
{
if (this.content)
{
this.content = Object.assign(this.content, o);
}
else
{
this.content = o;
}
this.content.renderTitle = this.content.renderTitle !== false;
} else if (content)
{
this.content = content as ISectionContent;
}
else if (!allow_null)
{
throw new ReferenceError();
}
return this;
}
get epubTypeGroup(): string | EnumEpubType
{
return epubtypes.getGroup(this.epubType);
}
get lang()
{
return this.sectionConfig.lang || (this._EpubMaker_ ? this._EpubMaker_.lang : null) || null;
}
get langMain()
{
return (this._EpubMaker_ ? this._EpubMaker_.lang : null) || null;
}
static create(epubType: string | EnumEpubTypeName, id, content, includeInToc: boolean, includeInLandmarks: boolean, ...argv)
{
return new this(epubType, id, content, includeInToc, includeInLandmarks, ...argv);
}
/**
* 用來檢查 Section 是否已經加入
*/
hasSubSection(subsection: Section): boolean
{
return this.subSections.includes(subsection);
}
withSubSection(subsection: Section)
{
subsection.parentSection = this;
this.subSections.push(subsection);
return this;
};
collectToc()
{
return this.collectSections(this, EnumSectionCollectType.INCLUDE_IN_TOC);
}
collectLandmarks()
{
return this.collectSections(this, EnumSectionCollectType.INCLUDE_IN_LANDMARKS);
}
collectSections(section: Section, prop: string | EnumSectionCollectType): Section[]
{
let sections = section[prop] ? [section] : [];
for (let i = 0; i < section.subSections.length; i++)
{
Array.prototype.push.apply(sections, this.collectSections(section.subSections[i], prop));
}
return sections;
}
}
}
export default EpubMaker;
if (typeof window !== 'undefined')
{
// @ts-ignore
window.EpubMaker = EpubMaker;
}