From 9c7a6ddf3fa1bb1c85fe2e7f5d4cb54b5f15a07f Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 12:32:08 +0100 Subject: [PATCH 01/18] Add HTML template for cover images. --- src/index.ts | 11 +++++++++++ templates/cover.xhtml.ejs | 14 ++++++++++++++ templates/template.css | 20 ++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 templates/cover.xhtml.ejs diff --git a/src/index.ts b/src/index.ts index ef2d2e77..1ff8acb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -278,6 +278,7 @@ export interface EpubOptions { customOpfTemplatePath?: string; customNcxTocTemplatePath?: string; customHtmlTocTemplatePath?: string; + customHtmlCoverTemplatePath?: string; version?: number; userAgent?: string; verbose?: boolean; @@ -323,6 +324,7 @@ export class EPub { images: Array; customOpfTemplatePath: string | null; customNcxTocTemplatePath: string | null; + customHtmlCoverTemplatePath: string | null; customHtmlTocTemplatePath: string | null; version: number; userAgent: string; @@ -360,6 +362,7 @@ export class EPub { this.customOpfTemplatePath = options.customOpfTemplatePath ?? null; this.customNcxTocTemplatePath = options.customNcxTocTemplatePath ?? null; this.customHtmlTocTemplatePath = options.customHtmlTocTemplatePath ?? null; + this.customHtmlCoverTemplatePath = options.customHtmlCoverTemplatePath ?? null; this.version = options.version ?? 3; this.userAgent = options.userAgent ?? @@ -644,6 +647,14 @@ export class EPub { throw new Error("Custom file to HTML toc template not found."); } writeFileSync(resolve(this.tempEpubDir, "./OEBPS/toc.xhtml"), await renderFile(htmlTocPath, this)); + + if (this.cover) { + const htmlCoverPath = this.customHtmlCoverTemplatePath || resolve(__dirname, "../templates/cover.xhtml.ejs"); + if (!existsSync(htmlCoverPath)) { + throw new Error("Custom file to HTML cover template not found."); + } + writeFileSync(resolve(this.tempEpubDir, "./OEBPS/cover.xhtml"), await renderFile(htmlCoverPath, this)); + } } private async makeCover(): Promise { diff --git a/templates/cover.xhtml.ejs b/templates/cover.xhtml.ejs new file mode 100644 index 00000000..e667a860 --- /dev/null +++ b/templates/cover.xhtml.ejs @@ -0,0 +1,14 @@ + + + + + Book Cover + + + + +
+ cover image +
+ + \ No newline at end of file diff --git a/templates/template.css b/templates/template.css index 9abffa9f..a8f05cfe 100644 --- a/templates/template.css +++ b/templates/template.css @@ -27,3 +27,23 @@ hr { border-bottom: 1px solid #dedede; margin: 60px 10%; } + +body.cover { + padding: 0; + margin: 0; +} + +section.cover { + display: block; + text-align: center; + height: 95%; +} + +#image_cover { + height: 95%; +} + +/* ignored on older devices */ +#image_cover:only-of-type { + height: 95vh; +} \ No newline at end of file From 7b98f41f21e025d8886f3c1e9c62c51a81147a78 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 12:46:52 +0100 Subject: [PATCH 02/18] Attempt to insert cover page in the content. --- package.json | 3 ++- src/index.ts | 63 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 1ea4350c..75f1d0c1 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lint": "eslint . --fix --ext .js,.jsx,.ts,.tsx", "format": "prettier --write .", "build": "tsc", - "test": "NODE_OPTIONS='--loader=ts-node/esm' mocha" + "test": "cross-env NODE_OPTIONS='--loader=ts-node/esm' mocha" }, "dependencies": { "archiver": "^6.0.1", @@ -64,6 +64,7 @@ "@types/uslug": "^1.0.3", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", + "cross-env": "^7.0.3", "eslint": "^8.52.0", "eslint-config-prettier": "^9.0.0", "mocha": "^10.2.0", diff --git a/src/index.ts b/src/index.ts index 1ff8acb9..1ef98dde 100644 --- a/src/index.ts +++ b/src/index.ts @@ -388,9 +388,40 @@ export class EPub { this.coverExtension = null; } - // Parse contents & save images + const loadHtml = (content: string, plugins: Plugin[]) => + unified() + .use(rehypeParse, { fragment: true }) + .use(plugins) + // Voids: [] is required for epub generation, and causes little/no harm for non-epub usage + .use(rehypeStringify, { allowDangerousHtml: true, voids: [] }) + .processSync(content) + .toString(); + this.images = []; - this.content = options.content.map((content, index) => { + this.content = []; + + // Insert cover in content + if (this.cover) { + const filePath = resolve(this.tempEpubDir, `./OEBPS/cover.xhtml`); + + this.content.push({ + id: `item_${this.content.length}`, + href: 'cover.xhtml', + title: 'cover', + data: '', + url: null, + author: [], + filePath, + excludeFromToc: true, + beforeToc: true, + }); + } + + // Parse contents & save images + const contentOffset = this.content.length; + this.content.push(...options.content.map((content, i) => { + const index = contentOffset + i; + // Get the content URL & path let href, filePath; if (content.filename === undefined) { @@ -410,15 +441,6 @@ export class EPub { const id = `item_${index}`; const dir = dirname(filePath); - const loadHtml = (content: string, plugins: Plugin[]) => - unified() - .use(rehypeParse, { fragment: true }) - .use(plugins) - // Voids: [] is required for epub generation, and causes little/no harm for non-epub usage - .use(rehypeStringify, { allowDangerousHtml: true, voids: [] }) - .processSync(content) - .toString(); - // Parse the content const html = loadHtml(content.data, [ () => (tree) => { @@ -516,7 +538,7 @@ export class EPub { excludeFromToc: content.excludeFromToc === true, // Default to false beforeToc: content.beforeToc === true, // Default to false }; - }); + })); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -586,6 +608,15 @@ export class EPub { }); } + // Add cover page + if (this.cover) { + const htmlCoverPath = this.customHtmlCoverTemplatePath || resolve(__dirname, "../templates/cover.xhtml.ejs"); + if (!existsSync(htmlCoverPath)) { + throw new Error("Custom file to HTML cover template not found."); + } + writeFileSync(resolve(this.tempEpubDir, "./OEBPS/cover.xhtml"), await renderFile(htmlCoverPath, this)); + } + // Write content files contents.forEach((content) => { let data = `${docHeader} @@ -647,14 +678,6 @@ export class EPub { throw new Error("Custom file to HTML toc template not found."); } writeFileSync(resolve(this.tempEpubDir, "./OEBPS/toc.xhtml"), await renderFile(htmlTocPath, this)); - - if (this.cover) { - const htmlCoverPath = this.customHtmlCoverTemplatePath || resolve(__dirname, "../templates/cover.xhtml.ejs"); - if (!existsSync(htmlCoverPath)) { - throw new Error("Custom file to HTML cover template not found."); - } - writeFileSync(resolve(this.tempEpubDir, "./OEBPS/cover.xhtml"), await renderFile(htmlCoverPath, this)); - } } private async makeCover(): Promise { From 49a55a81cd6b61514b219ba6844101ca1b618d10 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 13:11:59 +0100 Subject: [PATCH 03/18] Write content files with a template. --- src/index.ts | 33 +++++++++++++++------------------ templates/content.xhtml.ejs | 18 ++++++++++++++++++ templates/cover.xhtml.ejs | 2 +- 3 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 templates/content.xhtml.ejs diff --git a/src/index.ts b/src/index.ts index 1ef98dde..d3cbf457 100644 --- a/src/index.ts +++ b/src/index.ts @@ -293,6 +293,7 @@ interface EpubContent { url: string | null; author: Array; filePath: string; + templatePath: string; excludeFromToc: boolean; beforeToc: boolean; } @@ -412,12 +413,14 @@ export class EPub { url: null, author: [], filePath, + templatePath: resolve(__dirname, "../templates/cover.xhtml.ejs"), excludeFromToc: true, beforeToc: true, }); } // Parse contents & save images + const contentTemplatePath = resolve(__dirname, "../templates/content.xhtml.ejs"); const contentOffset = this.content.length; this.content.push(...options.content.map((content, i) => { const index = contentOffset + i; @@ -535,6 +538,7 @@ export class EPub { url: content.url ?? null, author: content.author ? (typeof content.author === "string" ? [content.author] : content.author) : [], filePath: filePath, + templatePath: contentTemplatePath, excludeFromToc: content.excludeFromToc === true, // Default to false beforeToc: content.beforeToc === true, // Default to false }; @@ -618,24 +622,17 @@ export class EPub { } // Write content files - contents.forEach((content) => { - let data = `${docHeader} - - ${encodeXML(content.title || "")} - - - -`; - data += content.title && this.appendChapterTitles ? `

${encodeXML(content.title)}

` : ""; - data += - content.title && content.author && content.author.length - ? `

${encodeXML(content.author.join(", "))}

` - : ""; - data += - content.title && content.url ? `` : ""; - data += `${content.data}`; - writeFileSync(content.filePath, data); - }); + for (const content of contents) { + const result = await renderFile(content.templatePath, { + ...this, + ...content, + encodeXML, + docHeader, + }, { + escape: (markup) => markup, + }); + writeFileSync(content.filePath, result); + } // write meta-inf/container.xml mkdirSync(this.tempEpubDir + "/META-INF"); diff --git a/templates/content.xhtml.ejs b/templates/content.xhtml.ejs new file mode 100644 index 00000000..4fed8381 --- /dev/null +++ b/templates/content.xhtml.ejs @@ -0,0 +1,18 @@ +<%= docHeader %> + + <%= encodeXML(title || "") %> + + + + <% if (title && appendChapterTitles) { %> +

<%= encodeXML(title) %>

+ <% } %> + <% if (title && author && author.length) { %> +

<%= encodeXML(author.join(", ")) %>

+ <% } %> + <% if (title && url) { %> + + <% } %> + <%= data %> + + \ No newline at end of file diff --git a/templates/cover.xhtml.ejs b/templates/cover.xhtml.ejs index e667a860..6be106aa 100644 --- a/templates/cover.xhtml.ejs +++ b/templates/cover.xhtml.ejs @@ -8,7 +8,7 @@
- cover image + cover image
\ No newline at end of file From 404e5575444622956f15248cb92a210ee3cc8aba Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 13:13:20 +0100 Subject: [PATCH 04/18] Fix path to cover image. --- templates/cover.xhtml.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cover.xhtml.ejs b/templates/cover.xhtml.ejs index 6be106aa..ea6ad8db 100644 --- a/templates/cover.xhtml.ejs +++ b/templates/cover.xhtml.ejs @@ -8,7 +8,7 @@
- cover image + cover image
\ No newline at end of file From f2a3ca14dfec4ce480272a3639f94ca5c0936b34 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 13:25:49 +0100 Subject: [PATCH 05/18] Fix cover image template. --- src/index.ts | 13 +------------ templates/cover.xhtml.ejs | 9 +++------ templates/template.css | 2 +- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/index.ts b/src/index.ts index d3cbf457..6edf168e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -403,8 +403,6 @@ export class EPub { // Insert cover in content if (this.cover) { - const filePath = resolve(this.tempEpubDir, `./OEBPS/cover.xhtml`); - this.content.push({ id: `item_${this.content.length}`, href: 'cover.xhtml', @@ -412,7 +410,7 @@ export class EPub { data: '', url: null, author: [], - filePath, + filePath: resolve(this.tempEpubDir, `./OEBPS/cover.xhtml`), templatePath: resolve(__dirname, "../templates/cover.xhtml.ejs"), excludeFromToc: true, beforeToc: true, @@ -612,15 +610,6 @@ export class EPub { }); } - // Add cover page - if (this.cover) { - const htmlCoverPath = this.customHtmlCoverTemplatePath || resolve(__dirname, "../templates/cover.xhtml.ejs"); - if (!existsSync(htmlCoverPath)) { - throw new Error("Custom file to HTML cover template not found."); - } - writeFileSync(resolve(this.tempEpubDir, "./OEBPS/cover.xhtml"), await renderFile(htmlCoverPath, this)); - } - // Write content files for (const content of contents) { const result = await renderFile(content.templatePath, { diff --git a/templates/cover.xhtml.ejs b/templates/cover.xhtml.ejs index ea6ad8db..027b6106 100644 --- a/templates/cover.xhtml.ejs +++ b/templates/cover.xhtml.ejs @@ -1,14 +1,11 @@ - - - +<%= docHeader %> - Book Cover -
+
cover image -
+ \ No newline at end of file diff --git a/templates/template.css b/templates/template.css index a8f05cfe..c8a34838 100644 --- a/templates/template.css +++ b/templates/template.css @@ -33,7 +33,7 @@ body.cover { margin: 0; } -section.cover { +div.cover { display: block; text-align: center; height: 95%; From ea3b3aa08db590f5e89ecec3ba6c66ff791cc40c Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 13:25:57 +0100 Subject: [PATCH 06/18] Fix manifest link to cover image. --- templates/epub3/content.opf.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/epub3/content.opf.ejs b/templates/epub3/content.opf.ejs index a6d619c0..85f6d967 100644 --- a/templates/epub3/content.opf.ejs +++ b/templates/epub3/content.opf.ejs @@ -40,7 +40,7 @@ <% if(locals.cover) { %> - + <% } %> <% images.forEach(function(image, index){ %> From 6b58f6cd87c9d34bc5a920313505053f85126aba Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 13:38:32 +0100 Subject: [PATCH 07/18] Fix link to cover page. --- templates/epub2/content.opf.ejs | 3 ++- templates/epub3/content.opf.ejs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/epub2/content.opf.ejs b/templates/epub2/content.opf.ejs index e2c481ea..77411063 100644 --- a/templates/epub2/content.opf.ejs +++ b/templates/epub2/content.opf.ejs @@ -19,6 +19,7 @@ + <% if (locals.cover) { %><% } %> @@ -46,7 +47,7 @@ <% } %> <% }) %> - + <% if(locals.cover) { %><% } %> <% content.forEach(function(content, index){ %> <% if(!content.beforeToc && !content.excludeFromToc){ %> diff --git a/templates/epub3/content.opf.ejs b/templates/epub3/content.opf.ejs index 85f6d967..ddfa5321 100644 --- a/templates/epub3/content.opf.ejs +++ b/templates/epub3/content.opf.ejs @@ -35,6 +35,7 @@ + <% if (locals.cover) { %><% } %> @@ -62,6 +63,7 @@ <% } %> <% }) %> + <% if (locals.cover) { %><% } %> <% content.forEach(function(content, index){ %> <% if(!content.beforeToc && !content.excludeFromToc){ %> From e29b8201f58c42b13ae46f562e4d6657e175e972 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 13:43:03 +0100 Subject: [PATCH 08/18] Allow overriding of cover template path. --- src/index.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6edf168e..d4fd3912 100644 --- a/src/index.ts +++ b/src/index.ts @@ -403,6 +403,11 @@ export class EPub { // Insert cover in content if (this.cover) { + const templatePath = this.customHtmlCoverTemplatePath || resolve(__dirname, '../templates/cover.xhtml.ejs'); + if (!existsSync(templatePath)) { + throw new Error("Could not resolve path to cover template HTML."); + } + this.content.push({ id: `item_${this.content.length}`, href: 'cover.xhtml', @@ -411,7 +416,7 @@ export class EPub { url: null, author: [], filePath: resolve(this.tempEpubDir, `./OEBPS/cover.xhtml`), - templatePath: resolve(__dirname, "../templates/cover.xhtml.ejs"), + templatePath, excludeFromToc: true, beforeToc: true, }); @@ -529,13 +534,13 @@ export class EPub { // Return the EpubContent return { - id: id, - href: href, + id, + href, title: content.title, data: html, url: content.url ?? null, author: content.author ? (typeof content.author === "string" ? [content.author] : content.author) : [], - filePath: filePath, + filePath, templatePath: contentTemplatePath, excludeFromToc: content.excludeFromToc === true, // Default to false beforeToc: content.beforeToc === true, // Default to false From 67777d62ebe72c16bb466b62eff89d23b4079f30 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 13:59:09 +0100 Subject: [PATCH 09/18] Add reference to cover page. --- templates/epub2/content.opf.ejs | 1 + templates/epub3/content.opf.ejs | 1 + 2 files changed, 2 insertions(+) diff --git a/templates/epub2/content.opf.ejs b/templates/epub2/content.opf.ejs index 77411063..86da1357 100644 --- a/templates/epub2/content.opf.ejs +++ b/templates/epub2/content.opf.ejs @@ -55,6 +55,7 @@ <% }) %> + <% if (locals.cover) { %><% } %> \ No newline at end of file diff --git a/templates/epub3/content.opf.ejs b/templates/epub3/content.opf.ejs index ddfa5321..d18dde3d 100644 --- a/templates/epub3/content.opf.ejs +++ b/templates/epub3/content.opf.ejs @@ -72,6 +72,7 @@ <% }) %> + <% if (locals.cover) { %><% } %> \ No newline at end of file From ac9e0e9799f5031cd61273de47efeeb094e35276 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 14:00:41 +0100 Subject: [PATCH 10/18] Mark cover page as non-linear. --- templates/epub2/content.opf.ejs | 2 +- templates/epub3/content.opf.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/epub2/content.opf.ejs b/templates/epub2/content.opf.ejs index 86da1357..410c10a5 100644 --- a/templates/epub2/content.opf.ejs +++ b/templates/epub2/content.opf.ejs @@ -47,7 +47,7 @@ <% } %> <% }) %> - <% if(locals.cover) { %><% } %> + <% if(locals.cover) { %><% } %> <% content.forEach(function(content, index){ %> <% if(!content.beforeToc && !content.excludeFromToc){ %> diff --git a/templates/epub3/content.opf.ejs b/templates/epub3/content.opf.ejs index d18dde3d..e82217fd 100644 --- a/templates/epub3/content.opf.ejs +++ b/templates/epub3/content.opf.ejs @@ -63,7 +63,7 @@ <% } %> <% }) %> - <% if (locals.cover) { %><% } %> + <% if (locals.cover) { %><% } %> <% content.forEach(function(content, index){ %> <% if(!content.beforeToc && !content.excludeFromToc){ %> From d2f4f34ef0fbbfc5b8d7c10cd5f08c3bca80ed4d Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 14:15:32 +0100 Subject: [PATCH 11/18] Add book title to the cover page. --- src/index.ts | 1 + templates/cover.xhtml.ejs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index d4fd3912..49852ee4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -620,6 +620,7 @@ export class EPub { const result = await renderFile(content.templatePath, { ...this, ...content, + bookTitle: this.title, encodeXML, docHeader, }, { diff --git a/templates/cover.xhtml.ejs b/templates/cover.xhtml.ejs index 027b6106..1c1cf80a 100644 --- a/templates/cover.xhtml.ejs +++ b/templates/cover.xhtml.ejs @@ -1,6 +1,7 @@ <%= docHeader %> + <%= encodeXML(bookTitle || "") %> From 856c20eea2cf941dde9b495501742ea3b311dab8 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 15:08:03 +0100 Subject: [PATCH 12/18] Clean up cover page CSS. --- templates/template.css | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/templates/template.css b/templates/template.css index c8a34838..5de9f387 100644 --- a/templates/template.css +++ b/templates/template.css @@ -28,6 +28,8 @@ hr { margin: 60px 10%; } +/* Cover page */ + body.cover { padding: 0; margin: 0; @@ -36,14 +38,10 @@ body.cover { div.cover { display: block; text-align: center; - height: 95%; } #image_cover { - height: 95%; -} - -/* ignored on older devices */ -#image_cover:only-of-type { - height: 95vh; + max-width: 100%; + /* ignored on older devices */ + height: 100vh; } \ No newline at end of file From 9b5626716c298444dc12ded39b7e616b14770555 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 15:42:30 +0100 Subject: [PATCH 13/18] Retrieve image size. --- package.json | 1 + src/index.ts | 66 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 75f1d0c1..83774da4 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "ejs": "^3.1.9", "entities": "^4.5.0", "fs-extra": "^11.1.1", + "image-size": "^1.0.2", "mime": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", diff --git a/src/index.ts b/src/index.ts index 49852ee4..b8312a79 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { } from "fs"; import fsExtra from "fs-extra"; import { Element } from "hast"; +import { imageSize } from "image-size"; import mime from "mime"; import { basename, dirname, resolve } from "path"; import rehypeParse from "rehype-parse"; @@ -22,6 +23,7 @@ import { Plugin, unified } from "unified"; import { visit } from "unist-util-visit"; import { fileURLToPath } from "url"; import uslug from "uslug"; +import { promisify } from "util"; // Allowed HTML attributes & tags const allowedAttributes = [ @@ -313,6 +315,10 @@ export class EPub { cover: string | null; coverMediaType: string | null; coverExtension: string | null; + coverDimensions = { + width: 0, + height: 0 + }; publisher: string; author: Array; tocTitle: string; @@ -548,12 +554,13 @@ export class EPub { })); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async render(): Promise { - if (this.verbose) { - console.log("Generating Template Files....."); + async render(): Promise<{ result: string }> { + // Create directories + if (!existsSync(this.tempDir)) { + mkdirSync(this.tempDir); } - await this.generateTempFile(this.content); + mkdirSync(this.tempEpubDir); + mkdirSync(resolve(this.tempEpubDir, "./OEBPS")); if (this.verbose) { console.log("Downloading Images..."); @@ -565,6 +572,11 @@ export class EPub { } await this.makeCover(); + if (this.verbose) { + console.log("Generating Template Files....."); + } + await this.generateTempFile(this.content); + if (this.verbose) { console.log("Generating Epub Files..."); } @@ -589,13 +601,6 @@ export class EPub { `; - // Create directories - if (!existsSync(this.tempDir)) { - mkdirSync(this.tempDir); - } - mkdirSync(this.tempEpubDir); - mkdirSync(resolve(this.tempEpubDir, "./OEBPS")); - // Copy the CSS style if (!this.css) { this.css = readFileSync(resolve(__dirname, "../templates/template.css"), { encoding: "utf8" }); @@ -679,8 +684,7 @@ export class EPub { const destPath = resolve(this.tempEpubDir, `./OEBPS/cover.${this.coverExtension}`); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let writeStream: any; + let writeStream: fsExtra.ReadStream; if (this.cover.slice(0, 4) === "http" || this.cover.slice(0, 2) === "//") { try { const httpRequest = await axios.get(this.cover, { @@ -700,21 +704,35 @@ export class EPub { writeStream.pipe(createWriteStream(destPath)); } - return new Promise((resolve, reject) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - writeStream.on("error", (err: any) => { + const promiseStream = new Promise((resolve, reject) => { + writeStream.on("end", () => resolve()); + writeStream.on("error", (err: unknown) => { console.error("Error", err); unlinkSync(destPath); reject(err); }); - - writeStream.on("end", () => { - if (this.verbose) { - console.log("[Success] cover image downloaded successfully!"); - } - resolve(); - }); }); + + await promiseStream; + + if (this.verbose) { + console.log("[Success] cover image downloaded successfully!"); + } + + const sizeOf = promisify(imageSize); + + // Retrieve image dimensions + const result = await sizeOf(destPath); + if (!result || !result.width || !result.height) { + throw new Error(`Failed to retrieve cover image dimensions for "${destPath}"`); + } + + this.coverDimensions.width = result.width; + this.coverDimensions.height = result.height; + + if (this.verbose) { + console.log(`cover image dimensions: ${this.coverDimensions.width} x ${this.coverDimensions.height}`); + } } private async downloadImage(image: EpubImage): Promise { From 184cfb5c2dec1d97feed6d8637e8fb34956d189e Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 15:46:53 +0100 Subject: [PATCH 14/18] Remove linear attribute from the cover itemref. --- templates/epub2/content.opf.ejs | 2 +- templates/epub3/content.opf.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/epub2/content.opf.ejs b/templates/epub2/content.opf.ejs index 410c10a5..6c8f35fd 100644 --- a/templates/epub2/content.opf.ejs +++ b/templates/epub2/content.opf.ejs @@ -47,7 +47,7 @@ <% } %> <% }) %> - <% if(locals.cover) { %><% } %> + <% if(locals.cover) { %><% } %> <% content.forEach(function(content, index){ %> <% if(!content.beforeToc && !content.excludeFromToc){ %> diff --git a/templates/epub3/content.opf.ejs b/templates/epub3/content.opf.ejs index e82217fd..1d76842a 100644 --- a/templates/epub3/content.opf.ejs +++ b/templates/epub3/content.opf.ejs @@ -63,7 +63,7 @@ <% } %> <% }) %> - <% if (locals.cover) { %><% } %> + <% if (locals.cover) { %><% } %> <% content.forEach(function(content, index){ %> <% if(!content.beforeToc && !content.excludeFromToc){ %> From b04d4c9ea6b09de526c6f07aed605230bc0a041b Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 15:47:18 +0100 Subject: [PATCH 15/18] Render cover image as an SVG. --- templates/cover.xhtml.ejs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/templates/cover.xhtml.ejs b/templates/cover.xhtml.ejs index 1c1cf80a..a7002117 100644 --- a/templates/cover.xhtml.ejs +++ b/templates/cover.xhtml.ejs @@ -6,7 +6,21 @@
- cover image + + +
\ No newline at end of file From 76fd2e5ffd1715906e7ebab979f9dd2a453e19b0 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Sun, 12 Nov 2023 15:56:45 +0100 Subject: [PATCH 16/18] Add back item reference for the table of contents. --- templates/epub2/content.opf.ejs | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/epub2/content.opf.ejs b/templates/epub2/content.opf.ejs index 6c8f35fd..dac2e949 100644 --- a/templates/epub2/content.opf.ejs +++ b/templates/epub2/content.opf.ejs @@ -48,6 +48,7 @@ <% } %> <% }) %> <% if(locals.cover) { %><% } %> + <% content.forEach(function(content, index){ %> <% if(!content.beforeToc && !content.excludeFromToc){ %> From f0e3669a9dd6b3b98aa2c7300a49a8a35b44d141 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Fri, 24 Nov 2023 14:56:28 +0100 Subject: [PATCH 17/18] Run Prettier format. --- src/index.ts | 260 +++++++++++++++++++++-------------------- templates/template.css | 2 +- 2 files changed, 134 insertions(+), 128 deletions(-) diff --git a/src/index.ts b/src/index.ts index b8312a79..3c9660d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -317,7 +317,7 @@ export class EPub { coverExtension: string | null; coverDimensions = { width: 0, - height: 0 + height: 0, }; publisher: string; author: Array; @@ -396,29 +396,29 @@ export class EPub { } const loadHtml = (content: string, plugins: Plugin[]) => - unified() - .use(rehypeParse, { fragment: true }) - .use(plugins) - // Voids: [] is required for epub generation, and causes little/no harm for non-epub usage - .use(rehypeStringify, { allowDangerousHtml: true, voids: [] }) - .processSync(content) - .toString(); + unified() + .use(rehypeParse, { fragment: true }) + .use(plugins) + // Voids: [] is required for epub generation, and causes little/no harm for non-epub usage + .use(rehypeStringify, { allowDangerousHtml: true, voids: [] }) + .processSync(content) + .toString(); this.images = []; this.content = []; // Insert cover in content if (this.cover) { - const templatePath = this.customHtmlCoverTemplatePath || resolve(__dirname, '../templates/cover.xhtml.ejs'); + const templatePath = this.customHtmlCoverTemplatePath || resolve(__dirname, "../templates/cover.xhtml.ejs"); if (!existsSync(templatePath)) { throw new Error("Could not resolve path to cover template HTML."); } this.content.push({ id: `item_${this.content.length}`, - href: 'cover.xhtml', - title: 'cover', - data: '', + href: "cover.xhtml", + title: "cover", + data: "", url: null, author: [], filePath: resolve(this.tempEpubDir, `./OEBPS/cover.xhtml`), @@ -431,127 +431,129 @@ export class EPub { // Parse contents & save images const contentTemplatePath = resolve(__dirname, "../templates/content.xhtml.ejs"); const contentOffset = this.content.length; - this.content.push(...options.content.map((content, i) => { - const index = contentOffset + i; - - // Get the content URL & path - let href, filePath; - if (content.filename === undefined) { - const titleSlug = uslug(diacritics(content.title || "no title")); - href = `${index}_${titleSlug}.xhtml`; - filePath = resolve(this.tempEpubDir, `./OEBPS/${index}_${titleSlug}.xhtml`); - } else { - href = content.filename.match(/\.xhtml$/) ? content.filename : `${content.filename}.xhtml`; - if (content.filename.match(/\.xhtml$/)) { - filePath = resolve(this.tempEpubDir, `./OEBPS/${content.filename}`); + this.content.push( + ...options.content.map((content, i) => { + const index = contentOffset + i; + + // Get the content URL & path + let href, filePath; + if (content.filename === undefined) { + const titleSlug = uslug(diacritics(content.title || "no title")); + href = `${index}_${titleSlug}.xhtml`; + filePath = resolve(this.tempEpubDir, `./OEBPS/${index}_${titleSlug}.xhtml`); } else { - filePath = resolve(this.tempEpubDir, `./OEBPS/${content.filename}.xhtml`); + href = content.filename.match(/\.xhtml$/) ? content.filename : `${content.filename}.xhtml`; + if (content.filename.match(/\.xhtml$/)) { + filePath = resolve(this.tempEpubDir, `./OEBPS/${content.filename}`); + } else { + filePath = resolve(this.tempEpubDir, `./OEBPS/${content.filename}.xhtml`); + } } - } - // Content ID & directory - const id = `item_${index}`; - const dir = dirname(filePath); - - // Parse the content - const html = loadHtml(content.data, [ - () => (tree) => { - const validateElements = (node: Element) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const attrs = node.properties!; - if (["img", "br", "hr"].includes(node.tagName)) { - if (node.tagName === "img") { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - node.properties!.alt = node.properties?.alt || "image-placeholder"; + // Content ID & directory + const id = `item_${index}`; + const dir = dirname(filePath); + + // Parse the content + const html = loadHtml(content.data, [ + () => (tree) => { + const validateElements = (node: Element) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const attrs = node.properties!; + if (["img", "br", "hr"].includes(node.tagName)) { + if (node.tagName === "img") { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + node.properties!.alt = node.properties?.alt || "image-placeholder"; + } } - } - - for (const k of Object.keys(attrs)) { - if (allowedAttributes.includes(k)) { - if (k === "type") { - if (attrs[k] !== "script") { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - delete node.properties![k]; + + for (const k of Object.keys(attrs)) { + if (allowedAttributes.includes(k)) { + if (k === "type") { + if (attrs[k] !== "script") { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + delete node.properties![k]; + } } + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + delete node.properties![k]; } - } else { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - delete node.properties![k]; } - } - - if (this.version === 2) { - if (!allowedXhtml11Tags.includes(node.tagName)) { - if (this.verbose) { - console.log( - "Warning (content[" + index + "]):", - node.tagName, - "tag isn't allowed on EPUB 2/XHTML 1.1 DTD." - ); + + if (this.version === 2) { + if (!allowedXhtml11Tags.includes(node.tagName)) { + if (this.verbose) { + console.log( + "Warning (content[" + index + "]):", + node.tagName, + "tag isn't allowed on EPUB 2/XHTML 1.1 DTD." + ); + } + node.tagName = "div"; } - node.tagName = "div"; } - } - }; + }; - visit(tree, "element", validateElements); - }, - () => (tree) => { - const processImgTags = (node: Element) => { - if (!["img", "input"].includes(node.tagName)) { - return; - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const url = node.properties!.src as string | null | undefined; - if (url === undefined || url === null) { - return; - } - - let extension, id; - const image = this.images.find((element) => element.url === url); - if (image) { - id = image.id; - extension = image.extension; - } else { - id = uuid(); - const mediaType = mime.getType(url.replace(/\?.*/, "")); - if (mediaType === null) { - if (this.verbose) { - console.error("[Image Error]", `The image can't be processed : ${url}`); - } + visit(tree, "element", validateElements); + }, + () => (tree) => { + const processImgTags = (node: Element) => { + if (!["img", "input"].includes(node.tagName)) { return; } - extension = mime.getExtension(mediaType); - if (extension === null) { - if (this.verbose) { - console.error("[Image Error]", `The image can't be processed : ${url}`); - } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const url = node.properties!.src as string | null | undefined; + if (url === undefined || url === null) { return; } - this.images.push({ id, url, dir, mediaType, extension }); - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - node.properties!.src = `images/${id}.${extension}`; - }; - visit(tree, "element", processImgTags); - }, - ]); - - // Return the EpubContent - return { - id, - href, - title: content.title, - data: html, - url: content.url ?? null, - author: content.author ? (typeof content.author === "string" ? [content.author] : content.author) : [], - filePath, - templatePath: contentTemplatePath, - excludeFromToc: content.excludeFromToc === true, // Default to false - beforeToc: content.beforeToc === true, // Default to false - }; - })); + let extension, id; + const image = this.images.find((element) => element.url === url); + if (image) { + id = image.id; + extension = image.extension; + } else { + id = uuid(); + const mediaType = mime.getType(url.replace(/\?.*/, "")); + if (mediaType === null) { + if (this.verbose) { + console.error("[Image Error]", `The image can't be processed : ${url}`); + } + return; + } + extension = mime.getExtension(mediaType); + if (extension === null) { + if (this.verbose) { + console.error("[Image Error]", `The image can't be processed : ${url}`); + } + return; + } + this.images.push({ id, url, dir, mediaType, extension }); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + node.properties!.src = `images/${id}.${extension}`; + }; + + visit(tree, "element", processImgTags); + }, + ]); + + // Return the EpubContent + return { + id, + href, + title: content.title, + data: html, + url: content.url ?? null, + author: content.author ? (typeof content.author === "string" ? [content.author] : content.author) : [], + filePath, + templatePath: contentTemplatePath, + excludeFromToc: content.excludeFromToc === true, // Default to false + beforeToc: content.beforeToc === true, // Default to false + }; + }) + ); } async render(): Promise<{ result: string }> { @@ -622,15 +624,19 @@ export class EPub { // Write content files for (const content of contents) { - const result = await renderFile(content.templatePath, { - ...this, - ...content, - bookTitle: this.title, - encodeXML, - docHeader, - }, { - escape: (markup) => markup, - }); + const result = await renderFile( + content.templatePath, + { + ...this, + ...content, + bookTitle: this.title, + encodeXML, + docHeader, + }, + { + escape: (markup) => markup, + } + ); writeFileSync(content.filePath, result); } diff --git a/templates/template.css b/templates/template.css index 5de9f387..6f2dec4d 100644 --- a/templates/template.css +++ b/templates/template.css @@ -44,4 +44,4 @@ div.cover { max-width: 100%; /* ignored on older devices */ height: 100vh; -} \ No newline at end of file +} From d885d53e106aaaad5d161f71b37b0a55deba1bc7 Mon Sep 17 00:00:00 2001 From: "Mr. Hands" Date: Fri, 24 Nov 2023 14:58:20 +0100 Subject: [PATCH 18/18] Clean up CSS. --- templates/template.css | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/templates/template.css b/templates/template.css index 6f2dec4d..022a9323 100644 --- a/templates/template.css +++ b/templates/template.css @@ -38,10 +38,8 @@ body.cover { div.cover { display: block; text-align: center; -} - -#image_cover { max-width: 100%; + height: auto; /* ignored on older devices */ height: 100vh; }