wp-epub-gen 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,144 +0,0 @@
1
- /**
2
- * generateTempFile
3
- * @author: oldj
4
- * @homepage: https://oldj.net
5
- */
6
-
7
- import * as ejs from 'ejs'
8
- import * as entities from 'entities'
9
- import * as fs from 'fs-extra'
10
- import * as path from 'path'
11
- import { IChapterData, IEpubData } from './types'
12
- import { readFile, simpleMinifier, writeFile } from './utils'
13
-
14
- export const generateTempFile = async (epubData: IEpubData) => {
15
- let { log } = epubData
16
- let oebps_dir = path.join(epubData.dir, 'OEBPS')
17
- await fs.ensureDir(oebps_dir)
18
-
19
- const templates_dir = path.join(epubData.baseDir, 'templates')
20
-
21
- epubData.css =
22
- epubData.css ||
23
- (await readFile(path.join(epubData.baseDir, 'templates', 'template.css'), 'utf-8'))
24
- await writeFile(path.join(oebps_dir, 'style.css'), epubData.css, 'utf-8')
25
-
26
- if (epubData.fonts?.length) {
27
- let fonts_dir = path.join(oebps_dir, 'fonts')
28
- await fs.ensureDir(fonts_dir)
29
- epubData.fonts = epubData.fonts.map((font) => {
30
- let filename = path.basename(font)
31
-
32
- if (!fs.existsSync(font)) {
33
- log(`Custom font not found at '${font}'.`)
34
- } else {
35
- fs.copySync(font, path.join(fonts_dir, filename))
36
- }
37
-
38
- return filename
39
- })
40
- }
41
-
42
- const isAppendTitle = (global_append?: boolean, local_append?: boolean): boolean => {
43
- if (typeof local_append === 'boolean') return local_append
44
- return !!global_append
45
- }
46
-
47
- const saveContentToFile = (content: IChapterData) => {
48
- let title = entities.encodeXML(content.title || '')
49
- let html = `${epubData.docHeader}
50
- <head>
51
- <meta charset="UTF-8" />
52
- <title>${title}</title>
53
- <link rel="stylesheet" type="text/css" href="style.css" />
54
- </head>
55
- <body>
56
- `
57
- if (content.title && isAppendTitle(epubData.appendChapterTitles, content.appendChapterTitle)) {
58
- html += `<h1>${title}</h1>`
59
- }
60
- html +=
61
- content.title && content.author && content.author?.length
62
- ? `<p class='epub-author'>${entities.encodeXML(content.author.join(', '))}</p>`
63
- : ''
64
- html +=
65
- content.title && content.url
66
- ? `<p class="epub-link"><a href="${content.url}">${content.url}</a></p>`
67
- : ''
68
- html += `${content.data}`
69
- html += '\n</body>\n</html>'
70
-
71
- fs.ensureDirSync(path.dirname(content.filePath))
72
- fs.writeFileSync(content.filePath, html, 'utf-8')
73
-
74
- if (Array.isArray(content.children)) {
75
- content.children.map(saveContentToFile)
76
- }
77
- }
78
-
79
- epubData.content.map(saveContentToFile)
80
-
81
- // write meta-inf/container.xml
82
- let metainf_dir = path.join(epubData.dir, 'META-INF')
83
- fs.ensureDirSync(metainf_dir)
84
- fs.writeFileSync(
85
- path.join(metainf_dir, 'container.xml'),
86
- `<?xml version="1.0" encoding="UTF-8" ?>
87
- <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
88
- <rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles>
89
- </container>`,
90
- 'utf-8',
91
- )
92
-
93
- if (epubData.version === 2) {
94
- let fn = path.join(metainf_dir, 'com.apple.ibooks.display-options.xml')
95
- fs.writeFileSync(
96
- fn,
97
- `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
98
- <display_options>
99
- <platform name="*">
100
- <option name="specified-fonts">true</option>
101
- </platform>
102
- </display_options>
103
- `,
104
- 'utf-8',
105
- )
106
- }
107
-
108
- let opfPath =
109
- epubData.customOpfTemplatePath ||
110
- path.join(templates_dir, `epub${epubData.version}`, 'content.opf.ejs')
111
- if (!fs.existsSync(opfPath)) {
112
- throw new Error('Custom file to OPF template not found.')
113
- }
114
-
115
- let ncxTocPath = epubData.customNcxTocTemplatePath || path.join(templates_dir, `toc.ncx.ejs`)
116
- if (!fs.existsSync(ncxTocPath)) {
117
- throw new Error('Custom file the NCX toc template not found.')
118
- }
119
-
120
- let htmlTocPath =
121
- epubData.customHtmlTocTemplatePath ||
122
- path.join(templates_dir, `epub${epubData.version}`, 'toc.xhtml.ejs')
123
- if (!fs.existsSync(htmlTocPath)) {
124
- throw new Error('Custom file to HTML toc template not found.')
125
- }
126
-
127
- let toc_depth = 1
128
- fs.writeFileSync(
129
- path.join(oebps_dir, 'content.opf'),
130
- await ejs.renderFile(opfPath, epubData),
131
- 'utf-8',
132
- )
133
- fs.writeFileSync(
134
- path.join(oebps_dir, 'toc.ncx'),
135
- await ejs.renderFile(ncxTocPath, { ...epubData, toc_depth }),
136
- 'utf-8',
137
- )
138
- // 说明:toc.xhtml 的内容在 macOS 自带的 Books 会被当作目录显示,如果空格太多,目录显示可能会不正常,因此这儿简单去掉了不必要的空格
139
- fs.writeFileSync(
140
- path.join(oebps_dir, 'toc.xhtml'),
141
- simpleMinifier(await ejs.renderFile(htmlTocPath, epubData)),
142
- 'utf-8',
143
- )
144
- }
package/src/index.ts DELETED
@@ -1,152 +0,0 @@
1
- /**
2
- * index.ts
3
- * @author: oldj
4
- * @homepage: https://oldj.net
5
- */
6
-
7
- import * as mime from 'mime'
8
- import * as os from 'os'
9
- import * as path from 'path'
10
- import { v4 as uuidv4 } from 'uuid'
11
- import { errors } from './errors'
12
- import parseContent from './parseContent'
13
- import { render } from './render'
14
- import { IEpubData, IEpubGenOptions, IOut } from './types'
15
-
16
- const baseDir = path.dirname(__dirname)
17
-
18
- function result(success: boolean, message?: string, options?: IEpubGenOptions): IOut {
19
- if (options && options.verbose) {
20
- if (!success) {
21
- console.error(new Error(message))
22
- }
23
- }
24
-
25
- let out: IOut = {
26
- success,
27
- }
28
-
29
- if (typeof message === 'string') {
30
- out.message = message
31
- }
32
-
33
- if (options) {
34
- out.options = options
35
- }
36
-
37
- return out
38
- }
39
-
40
- function check(options: IEpubGenOptions): IOut {
41
- if (!options.output) {
42
- return result(false, errors.no_output_path, options)
43
- }
44
-
45
- if (!options.title) {
46
- return result(false, errors.no_title, options)
47
- }
48
-
49
- if (!options.content) {
50
- return result(false, errors.no_content, options)
51
- }
52
-
53
- return result(true, undefined, options)
54
- }
55
-
56
- function parseOptions(options: IEpubGenOptions): IEpubData {
57
- let tmpDir = options.tmpDir || os.tmpdir()
58
- let id = uuidv4()
59
-
60
- let data: IEpubData = {
61
- description: options.title,
62
- publisher: 'anonymous',
63
- author: ['anonymous'],
64
- tocTitle: 'Table Of Contents',
65
- appendChapterTitles: true,
66
- date: new Date().toISOString(),
67
- lang: 'en',
68
- fonts: [],
69
- version: 3,
70
- verbose: true,
71
- timeoutSeconds: 15 * 60, // 15 min
72
- tocAutoNumber: false,
73
-
74
- ...options,
75
-
76
- id,
77
- tmpDir,
78
- dir: path.resolve(tmpDir, id),
79
- baseDir,
80
- docHeader: '',
81
- images: [],
82
- content: [],
83
- log: (msg) => options.verbose && console.log(msg),
84
- }
85
-
86
- if (data.version === 2) {
87
- data.docHeader = `<?xml version="1.0" encoding="UTF-8"?>
88
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
89
- <html xmlns="http://www.w3.org/1999/xhtml" lang="#{self.options.lang}">
90
- `
91
- } else {
92
- data.docHeader = `<?xml version="1.0" encoding="UTF-8"?>
93
- <!DOCTYPE html>
94
- <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="#{self.options.lang}">
95
- `
96
- }
97
-
98
- if (Array.isArray(data.author) && data.author.length === 0) {
99
- data.author = ['anonymous']
100
- }
101
- if (typeof data.author === 'string') {
102
- data.author = [data.author]
103
- }
104
-
105
- data.content = options.content.map((content, index) => parseContent(content, index, data))
106
-
107
- if (data.cover) {
108
- data._coverMediaType = mime.getType(data.cover) || ''
109
- data._coverExtension = mime.getExtension(data._coverMediaType) || ''
110
- }
111
-
112
- return data
113
- }
114
-
115
- export async function epubGen(options: IEpubGenOptions, output?: string): Promise<IOut> {
116
- options = { ...options }
117
- if (output) {
118
- options.output = output
119
- }
120
-
121
- let o = check(options)
122
- let verbose = options.verbose !== false
123
- if (!o.success) {
124
- if (verbose) console.error(o.message)
125
- return o
126
- }
127
-
128
- let t: any
129
- try {
130
- let data = parseOptions(options)
131
- let timeoutSeconds: number = data.timeoutSeconds || 0
132
-
133
- if (timeoutSeconds > 0) {
134
- if (verbose) console.log(`TIMEOUT: ${timeoutSeconds}s`)
135
- t = setTimeout(() => {
136
- throw new Error('timeout!')
137
- }, timeoutSeconds * 1000)
138
- } else {
139
- if (verbose) console.log(`TIMEOUT: N/A`)
140
- }
141
-
142
- await render(data)
143
- return result(true, undefined, data)
144
- } catch (e) {
145
- if (verbose) console.error(e)
146
- return result(false, e.message, options)
147
- } finally {
148
- clearTimeout(t)
149
- }
150
- }
151
-
152
- export const gen = epubGen
package/src/makeCover.ts DELETED
@@ -1,52 +0,0 @@
1
- /**
2
- * makeCover
3
- * @author: oldj
4
- * @homepage: https://oldj.net
5
- */
6
-
7
- import * as fs from 'fs-extra'
8
- import * as path from 'path'
9
- import * as request from 'superagent'
10
- import { IEpubData } from './types'
11
- import { USER_AGENT } from './utils'
12
-
13
- export default async function makeCover(data: IEpubData): Promise<void> {
14
- let { cover, _coverExtension, log } = data
15
- if (!cover) return
16
-
17
- let destPath = path.join(data.dir, 'OEBPS', `cover.${_coverExtension}`)
18
- let writeStream: any = null
19
-
20
- if (cover.startsWith('http')) {
21
- writeStream = request.get(cover).set({ 'User-Agent': USER_AGENT })
22
- writeStream.pipe(fs.createWriteStream(destPath))
23
- } else {
24
- if (!fs.existsSync(cover)) return
25
- log('local cover image: ' + cover)
26
-
27
- writeStream = fs.createReadStream(cover)
28
- writeStream.pipe(fs.createWriteStream(destPath))
29
- }
30
-
31
- return new Promise((resolve) => {
32
- writeStream.on('end', () => {
33
- log('[Success] cover image saved.')
34
- resolve()
35
- })
36
-
37
- writeStream.on('error', (e: any) => {
38
- log('[Error] cover image error: ' + e.message)
39
- log('destPath: ' + destPath)
40
- if (fs.existsSync(destPath)) {
41
- try {
42
- fs.unlinkSync(destPath)
43
- log('destPath removed.')
44
- } catch (e) {
45
- log('[Error] remove cover image error: ' + e.message)
46
- }
47
- }
48
- resolve(e)
49
- // throw new Error('[Fail] cover image save fail!')
50
- })
51
- })
52
- }
@@ -1,351 +0,0 @@
1
- /**
2
- * parseContent
3
- * @author: oldj
4
- * @homepage: https://oldj.net
5
- */
6
-
7
- import * as cheerio from 'cheerio'
8
- import { remove as removeDiacritics } from 'diacritics'
9
- import * as mime from 'mime'
10
- import * as path from 'path'
11
- import * as uslug from 'uslug'
12
- import { v4 as uuidv4 } from 'uuid'
13
- import { IChapter, IChapterData, IEpubData, IEpubImage } from './types'
14
-
15
- export default function parseContent(
16
- content: IChapter,
17
- index: number | string,
18
- epubConfigs: IEpubData,
19
- ): IChapterData {
20
- let chapter: IChapterData = { ...content } as IChapterData
21
-
22
- if (!chapter.filename) {
23
- let titleSlug = uslug(removeDiacritics(chapter.title || 'no title'))
24
- titleSlug = titleSlug.replace(/[\/\\]/g, '_')
25
- chapter.href = `${index}_${titleSlug}.xhtml`
26
- chapter.filePath = path.join(epubConfigs.dir, 'OEBPS', chapter.href)
27
- } else {
28
- let is_xhtml = chapter.filename.endsWith('.xhtml')
29
- chapter.href = is_xhtml ? chapter.filename : `${chapter.filename}.xhtml`
30
- if (is_xhtml) {
31
- chapter.filePath = path.join(epubConfigs.dir, 'OEBPS', chapter.filename)
32
- } else {
33
- chapter.filePath = path.join(epubConfigs.dir, 'OEBPS', `${chapter.filename}.xhtml`)
34
- }
35
- }
36
-
37
- chapter.id = `item_${index}`
38
- chapter.dir = path.dirname(chapter.filePath)
39
- chapter.excludeFromToc = chapter.excludeFromToc || false
40
- chapter.beforeToc = chapter.beforeToc || false
41
-
42
- // fix author array
43
- if (chapter.author && typeof chapter.author === 'string') {
44
- chapter.author = [chapter.author]
45
- } else if (!chapter.author || !Array.isArray(chapter.author)) {
46
- chapter.author = []
47
- }
48
-
49
- let allowedAttributes = [
50
- 'content',
51
- 'alt',
52
- 'id',
53
- 'title',
54
- 'src',
55
- 'href',
56
- 'about',
57
- 'accesskey',
58
- 'aria-activedescendant',
59
- 'aria-atomic',
60
- 'aria-autocomplete',
61
- 'aria-busy',
62
- 'aria-checked',
63
- 'aria-controls',
64
- 'aria-describedat',
65
- 'aria-describedby',
66
- 'aria-disabled',
67
- 'aria-dropeffect',
68
- 'aria-expanded',
69
- 'aria-flowto',
70
- 'aria-grabbed',
71
- 'aria-haspopup',
72
- 'aria-hidden',
73
- 'aria-invalid',
74
- 'aria-label',
75
- 'aria-labelledby',
76
- 'aria-level',
77
- 'aria-live',
78
- 'aria-multiline',
79
- 'aria-multiselectable',
80
- 'aria-orientation',
81
- 'aria-owns',
82
- 'aria-posinset',
83
- 'aria-pressed',
84
- 'aria-readonly',
85
- 'aria-relevant',
86
- 'aria-required',
87
- 'aria-selected',
88
- 'aria-setsize',
89
- 'aria-sort',
90
- 'aria-valuemax',
91
- 'aria-valuemin',
92
- 'aria-valuenow',
93
- 'aria-valuetext',
94
- 'class',
95
- 'content',
96
- 'contenteditable',
97
- 'contextmenu',
98
- 'datatype',
99
- 'dir',
100
- 'draggable',
101
- 'dropzone',
102
- 'hidden',
103
- 'hreflang',
104
- 'id',
105
- 'inlist',
106
- 'itemid',
107
- 'itemref',
108
- 'itemscope',
109
- 'itemtype',
110
- 'lang',
111
- 'media',
112
- 'ns1:type',
113
- 'ns2:alphabet',
114
- 'ns2:ph',
115
- 'onabort',
116
- 'onblur',
117
- 'oncanplay',
118
- 'oncanplaythrough',
119
- 'onchange',
120
- 'onclick',
121
- 'oncontextmenu',
122
- 'ondblclick',
123
- 'ondrag',
124
- 'ondragend',
125
- 'ondragenter',
126
- 'ondragleave',
127
- 'ondragover',
128
- 'ondragstart',
129
- 'ondrop',
130
- 'ondurationchange',
131
- 'onemptied',
132
- 'onended',
133
- 'onerror',
134
- 'onfocus',
135
- 'oninput',
136
- 'oninvalid',
137
- 'onkeydown',
138
- 'onkeypress',
139
- 'onkeyup',
140
- 'onload',
141
- 'onloadeddata',
142
- 'onloadedmetadata',
143
- 'onloadstart',
144
- 'onmousedown',
145
- 'onmousemove',
146
- 'onmouseout',
147
- 'onmouseover',
148
- 'onmouseup',
149
- 'onmousewheel',
150
- 'onpause',
151
- 'onplay',
152
- 'onplaying',
153
- 'onprogress',
154
- 'onratechange',
155
- 'onreadystatechange',
156
- 'onreset',
157
- 'onscroll',
158
- 'onseeked',
159
- 'onseeking',
160
- 'onselect',
161
- 'onshow',
162
- 'onstalled',
163
- 'onsubmit',
164
- 'onsuspend',
165
- 'ontimeupdate',
166
- 'onvolumechange',
167
- 'onwaiting',
168
- 'prefix',
169
- 'property',
170
- 'rel',
171
- 'resource',
172
- 'rev',
173
- 'role',
174
- 'spellcheck',
175
- 'style',
176
- 'tabindex',
177
- 'target',
178
- 'title',
179
- 'type',
180
- 'typeof',
181
- 'vocab',
182
- 'xml:base',
183
- 'xml:lang',
184
- 'xml:space',
185
- 'colspan',
186
- 'rowspan',
187
- 'epub:type',
188
- 'epub:prefix',
189
- ]
190
- let allowedXhtml11Tags = [
191
- 'div',
192
- 'p',
193
- 'h1',
194
- 'h2',
195
- 'h3',
196
- 'h4',
197
- 'h5',
198
- 'h6',
199
- 'ul',
200
- 'ol',
201
- 'li',
202
- 'dl',
203
- 'dt',
204
- 'dd',
205
- 'address',
206
- 'hr',
207
- 'pre',
208
- 'blockquote',
209
- 'center',
210
- 'ins',
211
- 'del',
212
- 'a',
213
- 'span',
214
- 'bdo',
215
- 'br',
216
- 'em',
217
- 'strong',
218
- 'dfn',
219
- 'code',
220
- 'samp',
221
- 'kbd',
222
- 'bar',
223
- 'cite',
224
- 'abbr',
225
- 'acronym',
226
- 'q',
227
- 'sub',
228
- 'sup',
229
- 'tt',
230
- 'i',
231
- 'b',
232
- 'big',
233
- 'small',
234
- 'u',
235
- 's',
236
- 'strike',
237
- 'basefont',
238
- 'font',
239
- 'object',
240
- 'param',
241
- 'img',
242
- 'table',
243
- 'caption',
244
- 'colgroup',
245
- 'col',
246
- 'thead',
247
- 'tfoot',
248
- 'tbody',
249
- 'tr',
250
- 'th',
251
- 'td',
252
- 'embed',
253
- 'applet',
254
- 'iframe',
255
- 'img',
256
- 'map',
257
- 'noscript',
258
- 'ns:svg',
259
- 'object',
260
- 'script',
261
- 'table',
262
- 'tt',
263
- 'var',
264
- ]
265
-
266
- let $ = cheerio.load(chapter.data, {
267
- lowerCaseTags: true,
268
- recognizeSelfClosing: true,
269
- })
270
-
271
- // only body innerHTML is allowed
272
- let body = $('body')
273
- if (body.length) {
274
- let html = body.html()
275
- if (html) {
276
- $ = cheerio.load(html, {
277
- lowerCaseTags: true,
278
- recognizeSelfClosing: true,
279
- })
280
- }
281
- }
282
-
283
- $($('*').get().reverse()).each(function (elemIndex, elem) {
284
- // @ts-ignore
285
- let attrs = elem.attribs
286
- // @ts-ignore
287
- let that: CheerioElement = this
288
- let tags = ['img', 'br', 'hr']
289
- if (tags.includes(that.name)) {
290
- if (that.name === 'img' && !$(that).attr('alt')) {
291
- $(that).attr('alt', 'image-placeholder')
292
- }
293
- }
294
-
295
- Object.entries(attrs).map(([k, v]) => {
296
- if (allowedAttributes.includes(k)) {
297
- if (k === 'type' && that.name !== 'script') {
298
- $(that).removeAttr(k)
299
- }
300
- } else {
301
- $(that).removeAttr(k)
302
- }
303
- })
304
-
305
- if (epubConfigs.version === 2) {
306
- if (!allowedXhtml11Tags.includes(that.name)) {
307
- if (epubConfigs.verbose) {
308
- console.log(
309
- 'Warning (content[' + index + ']):',
310
- that.name,
311
- "tag isn't allowed on EPUB 2/XHTML 1.1 DTD.",
312
- )
313
- }
314
- let child = $(that).html()
315
- $(that).replaceWith($('<div>' + child + '</div>'))
316
- }
317
- }
318
- })
319
-
320
- $('img').each((index, elem) => {
321
- let url = $(elem).attr('src') || ''
322
- let image = epubConfigs.images.find((el) => el.url === url)
323
- let id: string
324
- let extension: string
325
-
326
- if (image) {
327
- id = image.id
328
- extension = image.extension
329
- } else {
330
- id = uuidv4()
331
- let mediaType: string = mime.getType(url.replace(/\?.*/, '')) || ''
332
- extension = mime.getExtension(mediaType) || ''
333
- let dir = chapter.dir || ''
334
-
335
- let img: IEpubImage = { id, url, dir, mediaType, extension }
336
- epubConfigs.images.push(img)
337
- }
338
-
339
- $(elem).attr('src', `images/${id}.${extension}`)
340
- })
341
-
342
- chapter.data = $.xml()
343
-
344
- if (Array.isArray(chapter.children)) {
345
- chapter.children = chapter.children.map((content, idx) =>
346
- parseContent(content, `${index}_${idx}`, epubConfigs),
347
- )
348
- }
349
-
350
- return chapter
351
- }