@leopiccionia/epub-builder 0.0.1
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.
- package/LICENSE +28 -0
- package/README.md +67 -0
- package/dist/index.d.mts +309 -0
- package/dist/index.d.ts +309 -0
- package/dist/index.mjs +458 -0
- package/dist/locales/en.json +6 -0
- package/dist/locales/pt.json +6 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Leonardo Piccioni de Almeida
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Epub Builder
|
|
2
|
+
|
|
3
|
+
A non-opinionated EPUB builder for reflowable content
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @leopiccionia/epub-builder
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
Generates EPUBs that are fully compatible with [EPUB 3.3](https://www.w3.org/TR/epub-33/) standard, with some additional compatibility with EPUB 2.
|
|
14
|
+
|
|
15
|
+
It provides a declarative API, for easy configuration, and writes for you:
|
|
16
|
+
|
|
17
|
+
- a machine-readable table of contents
|
|
18
|
+
- accessibility landmarks
|
|
19
|
+
- all the boring EPUB ceremony
|
|
20
|
+
|
|
21
|
+
## Usage example
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { EpubBuilder } from '@leopiccionia/epub-builder'
|
|
25
|
+
|
|
26
|
+
const ebook = await EpubBuilder.init({
|
|
27
|
+
// Some book metadata
|
|
28
|
+
meta: {
|
|
29
|
+
title: 'An example book',
|
|
30
|
+
creators: [
|
|
31
|
+
{ name: 'John Doe' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
// The reading order
|
|
35
|
+
spine: [
|
|
36
|
+
'cover.xhtml',
|
|
37
|
+
'toc.xhtml',
|
|
38
|
+
'chapter-1.xhtml',
|
|
39
|
+
],
|
|
40
|
+
// The table of contents
|
|
41
|
+
toc: [
|
|
42
|
+
{ href: 'toc.xhtml', text: 'Table of contents' },
|
|
43
|
+
{ href: 'chapter-1.xhtml', text: 'Chapter 1', children: [
|
|
44
|
+
{ href: 'chapter-1.xhtml#introduction', text: 'Introduction' },
|
|
45
|
+
{ href: 'chapter-1.xhtml#conclusion', text: 'Conclusion' },
|
|
46
|
+
] },
|
|
47
|
+
],
|
|
48
|
+
// Some accessibility landmarks
|
|
49
|
+
landmarks: {
|
|
50
|
+
toc: 'toc.xhtml', // The table of contents
|
|
51
|
+
bodymatter: 'chapter-1.xhtml', // The start of content
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Add the ebook files
|
|
56
|
+
await ebook.addTextFile('toc.xhtml', '...')
|
|
57
|
+
await ebook.addTextFile('chapter-1.xhtml', '...')
|
|
58
|
+
await ebook.addTextFile('cover.xhtml', '...')
|
|
59
|
+
await ebook.copyFile('cover.png', 'PATH TO THE IMAGE')
|
|
60
|
+
|
|
61
|
+
// Returns the ZIP file
|
|
62
|
+
const blob = await ebook.seal()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Related projects
|
|
66
|
+
|
|
67
|
+
- [Pubmark](https://github.com/leopiccionia/pubmark) - A Markdown to EPUB converter powered by this package
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An EpubBuilder locale
|
|
3
|
+
*/
|
|
4
|
+
interface Locale {
|
|
5
|
+
/**
|
|
6
|
+
* Translation of "Start of content"
|
|
7
|
+
*/
|
|
8
|
+
bodymatter: string;
|
|
9
|
+
/**
|
|
10
|
+
* Translation of "Guide"
|
|
11
|
+
*/
|
|
12
|
+
landmarks: string;
|
|
13
|
+
/**
|
|
14
|
+
* Translation of "List of images"
|
|
15
|
+
*/
|
|
16
|
+
loi: string;
|
|
17
|
+
/**
|
|
18
|
+
* Translation of "Table of contents"
|
|
19
|
+
*/
|
|
20
|
+
toc: string;
|
|
21
|
+
/**
|
|
22
|
+
* Translation of the name of another landmark
|
|
23
|
+
*/
|
|
24
|
+
[landmark: string]: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An ebook set of metadata
|
|
29
|
+
*/
|
|
30
|
+
interface EbookMeta {
|
|
31
|
+
/**
|
|
32
|
+
* The book title
|
|
33
|
+
*/
|
|
34
|
+
title: string;
|
|
35
|
+
/**
|
|
36
|
+
* The book subtitle
|
|
37
|
+
*/
|
|
38
|
+
subtitle: string;
|
|
39
|
+
/**
|
|
40
|
+
* The book description
|
|
41
|
+
*/
|
|
42
|
+
description: string;
|
|
43
|
+
/**
|
|
44
|
+
* The reading direction (left-to-right, right-to-left, or auto)
|
|
45
|
+
*/
|
|
46
|
+
direction: 'ltr' | 'rtl' | 'auto';
|
|
47
|
+
/**
|
|
48
|
+
* The language tag
|
|
49
|
+
* @see https://www.w3.org/International/articles/language-tags/
|
|
50
|
+
*/
|
|
51
|
+
language: string;
|
|
52
|
+
/**
|
|
53
|
+
* The book publishing date, in `YYYY-MM-DD` format
|
|
54
|
+
*/
|
|
55
|
+
date: string;
|
|
56
|
+
/**
|
|
57
|
+
* The book publisher
|
|
58
|
+
*/
|
|
59
|
+
publisher: {
|
|
60
|
+
/**
|
|
61
|
+
* The publisher type
|
|
62
|
+
*/
|
|
63
|
+
type?: 'Organization' | 'Person';
|
|
64
|
+
/**
|
|
65
|
+
* The publisher name
|
|
66
|
+
*/
|
|
67
|
+
name: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* A list of contributors to the book
|
|
71
|
+
*/
|
|
72
|
+
creators: Array<{
|
|
73
|
+
/**
|
|
74
|
+
* The contributor's name
|
|
75
|
+
*/
|
|
76
|
+
name: string;
|
|
77
|
+
/**
|
|
78
|
+
* The contributor's role MARC code
|
|
79
|
+
* @see https://www.loc.gov/marc/relators/relaterm.html
|
|
80
|
+
*/
|
|
81
|
+
role: string;
|
|
82
|
+
/**
|
|
83
|
+
* The contributor's type
|
|
84
|
+
*/
|
|
85
|
+
type?: 'Organization' | 'Person';
|
|
86
|
+
/**
|
|
87
|
+
* Normalized form of contributor's name
|
|
88
|
+
*/
|
|
89
|
+
'file as'?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Contributor's name in alternative scripts or languages
|
|
92
|
+
*/
|
|
93
|
+
alternate?: {
|
|
94
|
+
[language: string]: string;
|
|
95
|
+
};
|
|
96
|
+
}>;
|
|
97
|
+
/**
|
|
98
|
+
* The book subjects
|
|
99
|
+
*/
|
|
100
|
+
subjects: Array<{
|
|
101
|
+
/**
|
|
102
|
+
* The human-readable subject description
|
|
103
|
+
*/
|
|
104
|
+
label: string;
|
|
105
|
+
/**
|
|
106
|
+
* The authority that issued the subject term
|
|
107
|
+
*/
|
|
108
|
+
authority: string;
|
|
109
|
+
/**
|
|
110
|
+
* The machine-readable subject term
|
|
111
|
+
*/
|
|
112
|
+
term: number | string;
|
|
113
|
+
}>;
|
|
114
|
+
/**
|
|
115
|
+
* Unique identifiers
|
|
116
|
+
*/
|
|
117
|
+
ids: {
|
|
118
|
+
/**
|
|
119
|
+
* An ISBN (International Standard Book Number) identifier
|
|
120
|
+
*/
|
|
121
|
+
isbn?: string;
|
|
122
|
+
/**
|
|
123
|
+
* A DOI (Digital Object Identifier) identifier
|
|
124
|
+
*/
|
|
125
|
+
doi?: string;
|
|
126
|
+
/**
|
|
127
|
+
* An UUID (Universally Unique Identifier) v4-compatible identifier
|
|
128
|
+
*/
|
|
129
|
+
uuid?: string;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* An EPUB landmarks definition
|
|
134
|
+
*/
|
|
135
|
+
interface Landmarks {
|
|
136
|
+
/**
|
|
137
|
+
* The start of content
|
|
138
|
+
*/
|
|
139
|
+
bodymatter: string;
|
|
140
|
+
/**
|
|
141
|
+
* The table of contents
|
|
142
|
+
*/
|
|
143
|
+
toc: string;
|
|
144
|
+
/**
|
|
145
|
+
* The list of images
|
|
146
|
+
*/
|
|
147
|
+
loi?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Other landmarks (may not be supported by all readers)
|
|
150
|
+
*/
|
|
151
|
+
[landmark: string]: string | undefined;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A link in the table of contents
|
|
155
|
+
*/
|
|
156
|
+
type TocEntry = {
|
|
157
|
+
text: string;
|
|
158
|
+
href: string;
|
|
159
|
+
children?: TocEntry[];
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* An EPUB builder config
|
|
163
|
+
*/
|
|
164
|
+
interface EpubBuilderConfig {
|
|
165
|
+
/**
|
|
166
|
+
* The ebook landmarks
|
|
167
|
+
*/
|
|
168
|
+
landmarks: Landmarks;
|
|
169
|
+
/**
|
|
170
|
+
* The builder locale
|
|
171
|
+
*/
|
|
172
|
+
locale: Locale;
|
|
173
|
+
/**
|
|
174
|
+
* The ebook metadata
|
|
175
|
+
*/
|
|
176
|
+
meta: EbookMeta;
|
|
177
|
+
/**
|
|
178
|
+
* The ebook spine
|
|
179
|
+
*/
|
|
180
|
+
spine: string[];
|
|
181
|
+
/**
|
|
182
|
+
* The table of contents
|
|
183
|
+
*/
|
|
184
|
+
toc: TocEntry[];
|
|
185
|
+
}
|
|
186
|
+
interface EpubBuilderPartialConfig {
|
|
187
|
+
/**
|
|
188
|
+
* The ebook landmarks
|
|
189
|
+
*/
|
|
190
|
+
landmarks: Landmarks;
|
|
191
|
+
/**
|
|
192
|
+
* The builder locale
|
|
193
|
+
*/
|
|
194
|
+
locale?: Locale;
|
|
195
|
+
/**
|
|
196
|
+
* The ebook metadata
|
|
197
|
+
*/
|
|
198
|
+
meta?: Partial<EbookMeta>;
|
|
199
|
+
/**
|
|
200
|
+
* The ebook spine
|
|
201
|
+
*/
|
|
202
|
+
spine: string[];
|
|
203
|
+
/**
|
|
204
|
+
* The table of contents
|
|
205
|
+
*/
|
|
206
|
+
toc: TocEntry[];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Fills the user-provided config with default values
|
|
210
|
+
* @param partialConfig The partial config
|
|
211
|
+
* @returns The completely-filled config
|
|
212
|
+
*/
|
|
213
|
+
declare function populateConfig(partialConfig: EpubBuilderPartialConfig): EpubBuilderConfig;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A resource manifest property
|
|
217
|
+
* @see https://www.w3.org/TR/epub-33/#app-item-properties-vocab
|
|
218
|
+
*/
|
|
219
|
+
type ResourceProperty = 'cover-image' | 'mathml' | 'nav' | 'remote-resources' | 'scripted' | 'svg' | 'switch';
|
|
220
|
+
/**
|
|
221
|
+
* A resource representing a file asset
|
|
222
|
+
*/
|
|
223
|
+
interface Resource {
|
|
224
|
+
/**
|
|
225
|
+
* The resource's path
|
|
226
|
+
*/
|
|
227
|
+
href: string;
|
|
228
|
+
/**
|
|
229
|
+
* The resource's MIME type
|
|
230
|
+
*/
|
|
231
|
+
mime: string;
|
|
232
|
+
/**
|
|
233
|
+
* The resource's manifest properties
|
|
234
|
+
*/
|
|
235
|
+
properties: ResourceProperty[];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A non-opinionated EPUB builder
|
|
240
|
+
*/
|
|
241
|
+
declare class EpubBuilder {
|
|
242
|
+
#private;
|
|
243
|
+
/**
|
|
244
|
+
* The builder config
|
|
245
|
+
*/
|
|
246
|
+
config: EpubBuilderConfig;
|
|
247
|
+
/**
|
|
248
|
+
* The list of registered EPUB resources
|
|
249
|
+
*/
|
|
250
|
+
readonly resources: Resource[];
|
|
251
|
+
/**
|
|
252
|
+
* The EPUB ZIP container
|
|
253
|
+
*/
|
|
254
|
+
private zip;
|
|
255
|
+
/**
|
|
256
|
+
* The private constructor
|
|
257
|
+
* @param config The builder config
|
|
258
|
+
*/
|
|
259
|
+
private constructor();
|
|
260
|
+
/**
|
|
261
|
+
* Registers a binary-encoded file
|
|
262
|
+
* @param href The resource's path inside the EPUB container
|
|
263
|
+
* @param content The resource's binary-encoded content
|
|
264
|
+
* @param properties The resource's manifest properties
|
|
265
|
+
* @returns The registered resource
|
|
266
|
+
*/
|
|
267
|
+
addBinaryFile(href: string, content: Blob, properties?: ResourceProperty[]): Promise<Resource>;
|
|
268
|
+
/**
|
|
269
|
+
* Registers a text-encoded file
|
|
270
|
+
* @param href The resource's path inside the EPUB container
|
|
271
|
+
* @param content The resource's text-encoded content
|
|
272
|
+
* @param properties The resource's manifest properties
|
|
273
|
+
* @returns The registered resource
|
|
274
|
+
*/
|
|
275
|
+
addTextFile(href: string, content: string, properties?: ResourceProperty[]): Promise<Resource>;
|
|
276
|
+
/**
|
|
277
|
+
* Reads a file and registers it as a resource
|
|
278
|
+
* @param href The resource's path inside the EPUB container
|
|
279
|
+
* @param path The file's physical path
|
|
280
|
+
* @param properties The resource's manifest properties
|
|
281
|
+
* @returns The registered resource
|
|
282
|
+
*/
|
|
283
|
+
copyFile(href: string, path: string, properties?: ResourceProperty[]): Promise<Resource>;
|
|
284
|
+
/**
|
|
285
|
+
* Returns an empty `EpubBuilder`
|
|
286
|
+
* @param partialConfig The builder config
|
|
287
|
+
*/
|
|
288
|
+
static init(partialConfig: EpubBuilderPartialConfig): Promise<EpubBuilder>;
|
|
289
|
+
/**
|
|
290
|
+
* Closes the ZIP container, returning its content
|
|
291
|
+
* @returns The EPUB binary data
|
|
292
|
+
*/
|
|
293
|
+
seal(): Promise<Blob>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Detects the MIME type of a certain file
|
|
298
|
+
* @param path The file path
|
|
299
|
+
* @returns The MIME type of the file, or `undefined` if not recognized
|
|
300
|
+
*/
|
|
301
|
+
declare function getMimeType(path: string): string | undefined;
|
|
302
|
+
/**
|
|
303
|
+
* Detects if the MIME type is an EPUB core media type
|
|
304
|
+
* @param mimeType The MIME type
|
|
305
|
+
* @returns Whether the media type is recognized and a core media type
|
|
306
|
+
*/
|
|
307
|
+
declare function isCoreMediaType(mimeType: string | undefined): boolean;
|
|
308
|
+
|
|
309
|
+
export { type EbookMeta, EpubBuilder, type EpubBuilderConfig, type EpubBuilderPartialConfig, type Landmarks, type Locale, type Resource, type ResourceProperty, type TocEntry, getMimeType, isCoreMediaType, populateConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An EpubBuilder locale
|
|
3
|
+
*/
|
|
4
|
+
interface Locale {
|
|
5
|
+
/**
|
|
6
|
+
* Translation of "Start of content"
|
|
7
|
+
*/
|
|
8
|
+
bodymatter: string;
|
|
9
|
+
/**
|
|
10
|
+
* Translation of "Guide"
|
|
11
|
+
*/
|
|
12
|
+
landmarks: string;
|
|
13
|
+
/**
|
|
14
|
+
* Translation of "List of images"
|
|
15
|
+
*/
|
|
16
|
+
loi: string;
|
|
17
|
+
/**
|
|
18
|
+
* Translation of "Table of contents"
|
|
19
|
+
*/
|
|
20
|
+
toc: string;
|
|
21
|
+
/**
|
|
22
|
+
* Translation of the name of another landmark
|
|
23
|
+
*/
|
|
24
|
+
[landmark: string]: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An ebook set of metadata
|
|
29
|
+
*/
|
|
30
|
+
interface EbookMeta {
|
|
31
|
+
/**
|
|
32
|
+
* The book title
|
|
33
|
+
*/
|
|
34
|
+
title: string;
|
|
35
|
+
/**
|
|
36
|
+
* The book subtitle
|
|
37
|
+
*/
|
|
38
|
+
subtitle: string;
|
|
39
|
+
/**
|
|
40
|
+
* The book description
|
|
41
|
+
*/
|
|
42
|
+
description: string;
|
|
43
|
+
/**
|
|
44
|
+
* The reading direction (left-to-right, right-to-left, or auto)
|
|
45
|
+
*/
|
|
46
|
+
direction: 'ltr' | 'rtl' | 'auto';
|
|
47
|
+
/**
|
|
48
|
+
* The language tag
|
|
49
|
+
* @see https://www.w3.org/International/articles/language-tags/
|
|
50
|
+
*/
|
|
51
|
+
language: string;
|
|
52
|
+
/**
|
|
53
|
+
* The book publishing date, in `YYYY-MM-DD` format
|
|
54
|
+
*/
|
|
55
|
+
date: string;
|
|
56
|
+
/**
|
|
57
|
+
* The book publisher
|
|
58
|
+
*/
|
|
59
|
+
publisher: {
|
|
60
|
+
/**
|
|
61
|
+
* The publisher type
|
|
62
|
+
*/
|
|
63
|
+
type?: 'Organization' | 'Person';
|
|
64
|
+
/**
|
|
65
|
+
* The publisher name
|
|
66
|
+
*/
|
|
67
|
+
name: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* A list of contributors to the book
|
|
71
|
+
*/
|
|
72
|
+
creators: Array<{
|
|
73
|
+
/**
|
|
74
|
+
* The contributor's name
|
|
75
|
+
*/
|
|
76
|
+
name: string;
|
|
77
|
+
/**
|
|
78
|
+
* The contributor's role MARC code
|
|
79
|
+
* @see https://www.loc.gov/marc/relators/relaterm.html
|
|
80
|
+
*/
|
|
81
|
+
role: string;
|
|
82
|
+
/**
|
|
83
|
+
* The contributor's type
|
|
84
|
+
*/
|
|
85
|
+
type?: 'Organization' | 'Person';
|
|
86
|
+
/**
|
|
87
|
+
* Normalized form of contributor's name
|
|
88
|
+
*/
|
|
89
|
+
'file as'?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Contributor's name in alternative scripts or languages
|
|
92
|
+
*/
|
|
93
|
+
alternate?: {
|
|
94
|
+
[language: string]: string;
|
|
95
|
+
};
|
|
96
|
+
}>;
|
|
97
|
+
/**
|
|
98
|
+
* The book subjects
|
|
99
|
+
*/
|
|
100
|
+
subjects: Array<{
|
|
101
|
+
/**
|
|
102
|
+
* The human-readable subject description
|
|
103
|
+
*/
|
|
104
|
+
label: string;
|
|
105
|
+
/**
|
|
106
|
+
* The authority that issued the subject term
|
|
107
|
+
*/
|
|
108
|
+
authority: string;
|
|
109
|
+
/**
|
|
110
|
+
* The machine-readable subject term
|
|
111
|
+
*/
|
|
112
|
+
term: number | string;
|
|
113
|
+
}>;
|
|
114
|
+
/**
|
|
115
|
+
* Unique identifiers
|
|
116
|
+
*/
|
|
117
|
+
ids: {
|
|
118
|
+
/**
|
|
119
|
+
* An ISBN (International Standard Book Number) identifier
|
|
120
|
+
*/
|
|
121
|
+
isbn?: string;
|
|
122
|
+
/**
|
|
123
|
+
* A DOI (Digital Object Identifier) identifier
|
|
124
|
+
*/
|
|
125
|
+
doi?: string;
|
|
126
|
+
/**
|
|
127
|
+
* An UUID (Universally Unique Identifier) v4-compatible identifier
|
|
128
|
+
*/
|
|
129
|
+
uuid?: string;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* An EPUB landmarks definition
|
|
134
|
+
*/
|
|
135
|
+
interface Landmarks {
|
|
136
|
+
/**
|
|
137
|
+
* The start of content
|
|
138
|
+
*/
|
|
139
|
+
bodymatter: string;
|
|
140
|
+
/**
|
|
141
|
+
* The table of contents
|
|
142
|
+
*/
|
|
143
|
+
toc: string;
|
|
144
|
+
/**
|
|
145
|
+
* The list of images
|
|
146
|
+
*/
|
|
147
|
+
loi?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Other landmarks (may not be supported by all readers)
|
|
150
|
+
*/
|
|
151
|
+
[landmark: string]: string | undefined;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A link in the table of contents
|
|
155
|
+
*/
|
|
156
|
+
type TocEntry = {
|
|
157
|
+
text: string;
|
|
158
|
+
href: string;
|
|
159
|
+
children?: TocEntry[];
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* An EPUB builder config
|
|
163
|
+
*/
|
|
164
|
+
interface EpubBuilderConfig {
|
|
165
|
+
/**
|
|
166
|
+
* The ebook landmarks
|
|
167
|
+
*/
|
|
168
|
+
landmarks: Landmarks;
|
|
169
|
+
/**
|
|
170
|
+
* The builder locale
|
|
171
|
+
*/
|
|
172
|
+
locale: Locale;
|
|
173
|
+
/**
|
|
174
|
+
* The ebook metadata
|
|
175
|
+
*/
|
|
176
|
+
meta: EbookMeta;
|
|
177
|
+
/**
|
|
178
|
+
* The ebook spine
|
|
179
|
+
*/
|
|
180
|
+
spine: string[];
|
|
181
|
+
/**
|
|
182
|
+
* The table of contents
|
|
183
|
+
*/
|
|
184
|
+
toc: TocEntry[];
|
|
185
|
+
}
|
|
186
|
+
interface EpubBuilderPartialConfig {
|
|
187
|
+
/**
|
|
188
|
+
* The ebook landmarks
|
|
189
|
+
*/
|
|
190
|
+
landmarks: Landmarks;
|
|
191
|
+
/**
|
|
192
|
+
* The builder locale
|
|
193
|
+
*/
|
|
194
|
+
locale?: Locale;
|
|
195
|
+
/**
|
|
196
|
+
* The ebook metadata
|
|
197
|
+
*/
|
|
198
|
+
meta?: Partial<EbookMeta>;
|
|
199
|
+
/**
|
|
200
|
+
* The ebook spine
|
|
201
|
+
*/
|
|
202
|
+
spine: string[];
|
|
203
|
+
/**
|
|
204
|
+
* The table of contents
|
|
205
|
+
*/
|
|
206
|
+
toc: TocEntry[];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Fills the user-provided config with default values
|
|
210
|
+
* @param partialConfig The partial config
|
|
211
|
+
* @returns The completely-filled config
|
|
212
|
+
*/
|
|
213
|
+
declare function populateConfig(partialConfig: EpubBuilderPartialConfig): EpubBuilderConfig;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A resource manifest property
|
|
217
|
+
* @see https://www.w3.org/TR/epub-33/#app-item-properties-vocab
|
|
218
|
+
*/
|
|
219
|
+
type ResourceProperty = 'cover-image' | 'mathml' | 'nav' | 'remote-resources' | 'scripted' | 'svg' | 'switch';
|
|
220
|
+
/**
|
|
221
|
+
* A resource representing a file asset
|
|
222
|
+
*/
|
|
223
|
+
interface Resource {
|
|
224
|
+
/**
|
|
225
|
+
* The resource's path
|
|
226
|
+
*/
|
|
227
|
+
href: string;
|
|
228
|
+
/**
|
|
229
|
+
* The resource's MIME type
|
|
230
|
+
*/
|
|
231
|
+
mime: string;
|
|
232
|
+
/**
|
|
233
|
+
* The resource's manifest properties
|
|
234
|
+
*/
|
|
235
|
+
properties: ResourceProperty[];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A non-opinionated EPUB builder
|
|
240
|
+
*/
|
|
241
|
+
declare class EpubBuilder {
|
|
242
|
+
#private;
|
|
243
|
+
/**
|
|
244
|
+
* The builder config
|
|
245
|
+
*/
|
|
246
|
+
config: EpubBuilderConfig;
|
|
247
|
+
/**
|
|
248
|
+
* The list of registered EPUB resources
|
|
249
|
+
*/
|
|
250
|
+
readonly resources: Resource[];
|
|
251
|
+
/**
|
|
252
|
+
* The EPUB ZIP container
|
|
253
|
+
*/
|
|
254
|
+
private zip;
|
|
255
|
+
/**
|
|
256
|
+
* The private constructor
|
|
257
|
+
* @param config The builder config
|
|
258
|
+
*/
|
|
259
|
+
private constructor();
|
|
260
|
+
/**
|
|
261
|
+
* Registers a binary-encoded file
|
|
262
|
+
* @param href The resource's path inside the EPUB container
|
|
263
|
+
* @param content The resource's binary-encoded content
|
|
264
|
+
* @param properties The resource's manifest properties
|
|
265
|
+
* @returns The registered resource
|
|
266
|
+
*/
|
|
267
|
+
addBinaryFile(href: string, content: Blob, properties?: ResourceProperty[]): Promise<Resource>;
|
|
268
|
+
/**
|
|
269
|
+
* Registers a text-encoded file
|
|
270
|
+
* @param href The resource's path inside the EPUB container
|
|
271
|
+
* @param content The resource's text-encoded content
|
|
272
|
+
* @param properties The resource's manifest properties
|
|
273
|
+
* @returns The registered resource
|
|
274
|
+
*/
|
|
275
|
+
addTextFile(href: string, content: string, properties?: ResourceProperty[]): Promise<Resource>;
|
|
276
|
+
/**
|
|
277
|
+
* Reads a file and registers it as a resource
|
|
278
|
+
* @param href The resource's path inside the EPUB container
|
|
279
|
+
* @param path The file's physical path
|
|
280
|
+
* @param properties The resource's manifest properties
|
|
281
|
+
* @returns The registered resource
|
|
282
|
+
*/
|
|
283
|
+
copyFile(href: string, path: string, properties?: ResourceProperty[]): Promise<Resource>;
|
|
284
|
+
/**
|
|
285
|
+
* Returns an empty `EpubBuilder`
|
|
286
|
+
* @param partialConfig The builder config
|
|
287
|
+
*/
|
|
288
|
+
static init(partialConfig: EpubBuilderPartialConfig): Promise<EpubBuilder>;
|
|
289
|
+
/**
|
|
290
|
+
* Closes the ZIP container, returning its content
|
|
291
|
+
* @returns The EPUB binary data
|
|
292
|
+
*/
|
|
293
|
+
seal(): Promise<Blob>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Detects the MIME type of a certain file
|
|
298
|
+
* @param path The file path
|
|
299
|
+
* @returns The MIME type of the file, or `undefined` if not recognized
|
|
300
|
+
*/
|
|
301
|
+
declare function getMimeType(path: string): string | undefined;
|
|
302
|
+
/**
|
|
303
|
+
* Detects if the MIME type is an EPUB core media type
|
|
304
|
+
* @param mimeType The MIME type
|
|
305
|
+
* @returns Whether the media type is recognized and a core media type
|
|
306
|
+
*/
|
|
307
|
+
declare function isCoreMediaType(mimeType: string | undefined): boolean;
|
|
308
|
+
|
|
309
|
+
export { type EbookMeta, EpubBuilder, type EpubBuilderConfig, type EpubBuilderPartialConfig, type Landmarks, type Locale, type Resource, type ResourceProperty, type TocEntry, getMimeType, isCoreMediaType, populateConfig };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { u } from 'unist-builder';
|
|
2
|
+
import { x } from 'xastscript';
|
|
3
|
+
import { toXml } from 'xast-util-to-xml';
|
|
4
|
+
import { v4 } from '@lukeed/uuid/secure';
|
|
5
|
+
import { defu } from 'defu';
|
|
6
|
+
import mime from 'mime';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { BlobWriter, ZipWriter, BlobReader, TextReader } from '@zip.js/zip.js';
|
|
9
|
+
|
|
10
|
+
function stringifyXml(tree) {
|
|
11
|
+
return toXml(tree, {
|
|
12
|
+
closeEmptyElements: true
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function generateAppleDisplayOptionsXml(resources) {
|
|
17
|
+
const hasEmbeddedFonts = resources.some((resource) => {
|
|
18
|
+
return resource.mime.startsWith("font/");
|
|
19
|
+
});
|
|
20
|
+
const hasInteractiveContent = resources.some((resource) => {
|
|
21
|
+
return resource.mime === "application/javascript" || resource.properties.includes("scripted");
|
|
22
|
+
});
|
|
23
|
+
const tree = x(null, [
|
|
24
|
+
u("instruction", { name: "xml" }, 'version="1.0" encoding="utf-8"'),
|
|
25
|
+
x("display_options", [
|
|
26
|
+
x("platform", { name: "*" }, [
|
|
27
|
+
x("option", { name: "specified-fonts" }, String(hasEmbeddedFonts)),
|
|
28
|
+
x("option", { name: "interactive" }, String(hasInteractiveContent))
|
|
29
|
+
])
|
|
30
|
+
])
|
|
31
|
+
]);
|
|
32
|
+
return stringifyXml(tree);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const bodymatter = "Start of content";
|
|
36
|
+
const landmarks = "Guide";
|
|
37
|
+
const loi = "List of images";
|
|
38
|
+
const toc = "Table of Contents";
|
|
39
|
+
const EN = {
|
|
40
|
+
bodymatter: bodymatter,
|
|
41
|
+
landmarks: landmarks,
|
|
42
|
+
loi: loi,
|
|
43
|
+
toc: toc
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function populateConfig(partialConfig) {
|
|
47
|
+
return defu(partialConfig, {
|
|
48
|
+
locale: EN,
|
|
49
|
+
meta: {
|
|
50
|
+
title: "EPUB",
|
|
51
|
+
subtitle: "",
|
|
52
|
+
description: "",
|
|
53
|
+
direction: "ltr",
|
|
54
|
+
language: "en",
|
|
55
|
+
date: "",
|
|
56
|
+
publisher: {
|
|
57
|
+
type: "Organization",
|
|
58
|
+
name: ""
|
|
59
|
+
},
|
|
60
|
+
creators: [],
|
|
61
|
+
subjects: [],
|
|
62
|
+
ids: {
|
|
63
|
+
uuid: v4()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function generateContainerXml() {
|
|
70
|
+
const tree = x(null, [
|
|
71
|
+
u("instruction", { name: "xml" }, 'version="1.0" encoding="utf-8"'),
|
|
72
|
+
x("container", { version: "1.0", xmlns: "urn:oasis:names:tc:opendocument:xmlns:container" }, [
|
|
73
|
+
x("rootfiles", [
|
|
74
|
+
x("rootfile", { "full-path": "OEBPS/content.opf", "media-type": "application/oebps-package+xml" })
|
|
75
|
+
])
|
|
76
|
+
])
|
|
77
|
+
]);
|
|
78
|
+
return stringifyXml(tree);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function generateGuide(landmarks, locale) {
|
|
82
|
+
return x("guide", [
|
|
83
|
+
x("reference", { href: landmarks.toc, title: locale.toc, type: "toc" })
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function generateItemId(path) {
|
|
88
|
+
return path.replaceAll(/\W/g, "-");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function generateManifest(resources) {
|
|
92
|
+
return x("manifest", resources.map(({ href, mime, properties }) => x("item", {
|
|
93
|
+
href,
|
|
94
|
+
id: generateItemId(href),
|
|
95
|
+
"media-type": mime,
|
|
96
|
+
properties: properties.length > 0 ? properties.join(" ") : undefined
|
|
97
|
+
})));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const PUB_ID = "pub-id";
|
|
101
|
+
function generateCreators(meta) {
|
|
102
|
+
return meta.creators.flatMap((creator, index) => {
|
|
103
|
+
const id = `creators-${index + 1}`;
|
|
104
|
+
const dcTag = ["aut", "dub"].includes(creator.role) ? "dc:creator" : "dc:contributor";
|
|
105
|
+
const creatorMeta = [
|
|
106
|
+
x(dcTag, { id }, creator.name),
|
|
107
|
+
x("meta", { refines: `#${id}`, property: "role", scheme: "marc:relators" }, creator.role)
|
|
108
|
+
];
|
|
109
|
+
if (creator["file as"]) {
|
|
110
|
+
creatorMeta.push(
|
|
111
|
+
x("meta", { refines: `#${id}`, property: "file-as" }, creator["file as"])
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (creator.alternate) {
|
|
115
|
+
for (const [lang, alias] of Object.entries(creator.alternate)) {
|
|
116
|
+
x("meta", { refines: `#${id}`, property: "alternate-script", "xml:lang": lang }, alias);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return creatorMeta;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function generateSubjects(meta) {
|
|
123
|
+
return meta.subjects.flatMap((subject, index) => {
|
|
124
|
+
const id = `subject-${index + 1}`;
|
|
125
|
+
return [
|
|
126
|
+
x("dc:subject", { id }, subject.label),
|
|
127
|
+
x("meta", { refines: `#${id}`, property: "authority" }, subject.authority),
|
|
128
|
+
x("meta", { refines: `#${id}`, property: "term" }, subject.term)
|
|
129
|
+
];
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function generateMetadata(meta, cover) {
|
|
133
|
+
const { id: pubId, onix } = getUniqueIdentifier(meta);
|
|
134
|
+
return x("metadata", { "xmlns:dc": "http://purl.org/dc/elements/1.1/", "xmlns:opf": "http://www.idpf.org/2007/opf" }, [
|
|
135
|
+
x("dc:identifier", { id: PUB_ID }, pubId),
|
|
136
|
+
x("dc:title", { id: "title" }, meta.title),
|
|
137
|
+
meta.subtitle ? x(null, [
|
|
138
|
+
x("dc:title", { id: "subtitle" }, meta.subtitle),
|
|
139
|
+
x("meta", { refines: "#subtitle", property: "title-type" }, "subtitle")
|
|
140
|
+
]) : null,
|
|
141
|
+
meta.description ? x("dc:description", meta.description) : null,
|
|
142
|
+
meta.publisher.name ? x("dc:publisher", meta.publisher.name) : null,
|
|
143
|
+
meta.date ? x("dc:date", meta.date) : null,
|
|
144
|
+
x("dc:language", meta.language),
|
|
145
|
+
x("meta", { property: "dcterms:modified" }, getTimestamp()),
|
|
146
|
+
x("meta", { refines: `#${PUB_ID}`, property: "identifier-type", scheme: "onix:codelist5" }, onix),
|
|
147
|
+
cover && x("meta", { name: "cover", content: cover.href }),
|
|
148
|
+
...generateCreators(meta),
|
|
149
|
+
...generateSubjects(meta)
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
function getTimestamp() {
|
|
153
|
+
const isoString = (/* @__PURE__ */ new Date()).toISOString();
|
|
154
|
+
return isoString.slice(0, 19) + "Z";
|
|
155
|
+
}
|
|
156
|
+
function getUniqueIdentifier(meta) {
|
|
157
|
+
const { doi, isbn, uuid } = meta.ids;
|
|
158
|
+
if (isbn) {
|
|
159
|
+
const onix = isbn.length > 10 ? "15" : "02";
|
|
160
|
+
return { id: `urn:isbn:${isbn}`, onix };
|
|
161
|
+
} else if (doi) {
|
|
162
|
+
return { id: `urn:doi:${doi}`, onix: "06" };
|
|
163
|
+
} else {
|
|
164
|
+
return { id: `urn:uuid:${uuid}`, onix: "01" };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function generateSpine(meta, spine) {
|
|
169
|
+
return x(
|
|
170
|
+
"spine",
|
|
171
|
+
{ "page-progression-direction": meta.direction, toc: "toc-ncx" },
|
|
172
|
+
spine.map((href) => x("itemref", {
|
|
173
|
+
idref: generateItemId(href),
|
|
174
|
+
linear: "yes"
|
|
175
|
+
}))
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function generateContentOpf(config, resources) {
|
|
180
|
+
const { landmarks, locale, meta, spine } = config;
|
|
181
|
+
const cover = resources.find((resource) => resource.properties.includes("cover-image"));
|
|
182
|
+
const tree = x(null, [
|
|
183
|
+
u("instruction", { name: "xml" }, 'version="1.0" encoding="utf-8"'),
|
|
184
|
+
x("package", {
|
|
185
|
+
dir: meta.direction,
|
|
186
|
+
"unique-identifier": PUB_ID,
|
|
187
|
+
version: "3.0",
|
|
188
|
+
"xml:lang": meta.language,
|
|
189
|
+
xmlns: "http://www.idpf.org/2007/opf"
|
|
190
|
+
}, [
|
|
191
|
+
generateMetadata(meta, cover),
|
|
192
|
+
generateManifest(resources),
|
|
193
|
+
generateSpine(meta, spine),
|
|
194
|
+
generateGuide(landmarks, locale)
|
|
195
|
+
])
|
|
196
|
+
]);
|
|
197
|
+
return stringifyXml(tree);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function generateLandmarks(landmarks, locale) {
|
|
201
|
+
return x(
|
|
202
|
+
"ol",
|
|
203
|
+
Object.entries(landmarks).map(([landmark, href]) => x("li", [
|
|
204
|
+
x("a", { "epub:type": landmark, "href": href }, locale[landmark] ?? landmark)
|
|
205
|
+
]))
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
function generateTocList(entries) {
|
|
209
|
+
return x(
|
|
210
|
+
"ol",
|
|
211
|
+
entries.map(({ children, href, text }) => x("li", [
|
|
212
|
+
x("a", { href }, text),
|
|
213
|
+
children && children.length > 0 ? generateTocList(children) : null
|
|
214
|
+
]))
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
function generateNavXhtml(config) {
|
|
218
|
+
const { landmarks, locale, meta, toc } = config;
|
|
219
|
+
const tree = x("html", {
|
|
220
|
+
dir: meta.direction,
|
|
221
|
+
lang: meta.language,
|
|
222
|
+
"xml:lang": meta.language,
|
|
223
|
+
xmlns: "http://www.w3.org/1999/xhtml",
|
|
224
|
+
"xmlns:epub": "http://www.idpf.org/2007/ops"
|
|
225
|
+
}, [
|
|
226
|
+
x("head", [
|
|
227
|
+
x("meta", { charset: "UTF-8" }),
|
|
228
|
+
x("title", meta.title)
|
|
229
|
+
]),
|
|
230
|
+
x("body", [
|
|
231
|
+
x("h1", meta.title),
|
|
232
|
+
x("nav", { "epub:type": "toc" }, [
|
|
233
|
+
x("h2", locale.toc),
|
|
234
|
+
generateTocList(toc)
|
|
235
|
+
]),
|
|
236
|
+
x("nav", { "epub:type": "landmarks" }, [
|
|
237
|
+
x("h2", locale.landmarks),
|
|
238
|
+
generateLandmarks(landmarks, locale)
|
|
239
|
+
])
|
|
240
|
+
])
|
|
241
|
+
]);
|
|
242
|
+
return stringifyXml(tree);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function generateList(entries, prefix = "ncx") {
|
|
246
|
+
return entries.map((entry, index) => {
|
|
247
|
+
const entryId = `${prefix}-${index}`;
|
|
248
|
+
return x("navPoint", { id: entryId }, [
|
|
249
|
+
x("navLabel", [
|
|
250
|
+
x("text", entry.text)
|
|
251
|
+
]),
|
|
252
|
+
x("content", { src: entry.href }),
|
|
253
|
+
...entry.children && entry.children.length > 0 ? generateList(entry.children, entryId) : []
|
|
254
|
+
]);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function generateNcx(config) {
|
|
258
|
+
const { meta, toc } = config;
|
|
259
|
+
const tree = x("ncx", { version: "2005-1", "xml:lang": meta.language, xmlns: "http://www.daisy.org/z3986/2005/ncx/" }, [
|
|
260
|
+
x("head", [
|
|
261
|
+
x("meta", { content: getUniqueIdentifier(meta).id, name: "dtb:uid" })
|
|
262
|
+
]),
|
|
263
|
+
x("docTitle", [
|
|
264
|
+
x("text", meta.title)
|
|
265
|
+
]),
|
|
266
|
+
x("navMap", generateList(toc))
|
|
267
|
+
]);
|
|
268
|
+
return stringifyXml(tree);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const ALLOWED_MEDIA_TYPES = [
|
|
272
|
+
// Images
|
|
273
|
+
"image/gif",
|
|
274
|
+
"image/jpeg",
|
|
275
|
+
"image/png",
|
|
276
|
+
"image/svg+xml",
|
|
277
|
+
"image/webp",
|
|
278
|
+
// Audio
|
|
279
|
+
"audio/mpeg",
|
|
280
|
+
"audio/mp4",
|
|
281
|
+
"audio/ogg",
|
|
282
|
+
// Style
|
|
283
|
+
"text/css",
|
|
284
|
+
// Fonts
|
|
285
|
+
"font/ttf",
|
|
286
|
+
"font/otf",
|
|
287
|
+
"font/woff",
|
|
288
|
+
"font/woff2",
|
|
289
|
+
// Other
|
|
290
|
+
"application/xhtml+xml",
|
|
291
|
+
"application/javascript",
|
|
292
|
+
"application/x-dtbncx+xml",
|
|
293
|
+
"application/smil+xml"
|
|
294
|
+
];
|
|
295
|
+
function getMimeType(path) {
|
|
296
|
+
const mimeType = mime.getType(path) ?? undefined;
|
|
297
|
+
if (mimeType === "video/mp4") {
|
|
298
|
+
return "audio/mp4";
|
|
299
|
+
} else if (mimeType === "text/javascript") {
|
|
300
|
+
return "application/javascript";
|
|
301
|
+
}
|
|
302
|
+
return mimeType;
|
|
303
|
+
}
|
|
304
|
+
function isCoreMediaType(mimeType) {
|
|
305
|
+
if (!mimeType) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return ALLOWED_MEDIA_TYPES.includes(mimeType);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function createResource(href, properties = []) {
|
|
312
|
+
return {
|
|
313
|
+
href,
|
|
314
|
+
mime: getMimeType(href),
|
|
315
|
+
properties
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function readBinaryFile(path) {
|
|
320
|
+
const buffer = await readFile(path);
|
|
321
|
+
return new Blob([buffer]);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
class ZipContainer {
|
|
325
|
+
#zip;
|
|
326
|
+
/**
|
|
327
|
+
* The private constructor
|
|
328
|
+
*/
|
|
329
|
+
constructor() {
|
|
330
|
+
const writer = new BlobWriter("application/epub+zip");
|
|
331
|
+
this.#zip = new ZipWriter(writer);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Adds a binary file to the container
|
|
335
|
+
* @param path The path to the file inside the container
|
|
336
|
+
* @param blob The binary data
|
|
337
|
+
* @param options Options passed to `zip.js`
|
|
338
|
+
*/
|
|
339
|
+
async addBinaryFile(path, blob, options = {}) {
|
|
340
|
+
const reader = new BlobReader(blob);
|
|
341
|
+
await this.#zip.add(path, reader, options);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Adds a text file to the container
|
|
345
|
+
* @param path The path to the file inside the container
|
|
346
|
+
* @param text The textual data
|
|
347
|
+
* @param options Options passed to `zip.js`
|
|
348
|
+
*/
|
|
349
|
+
async addTextFile(path, text, options = {}) {
|
|
350
|
+
const reader = new TextReader(text);
|
|
351
|
+
await this.#zip.add(path, reader, options);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Returns an empty `ZipContainer`
|
|
355
|
+
*/
|
|
356
|
+
static async init() {
|
|
357
|
+
const container = new ZipContainer();
|
|
358
|
+
await container.addTextFile("mimetype", "application/epub+zip", { compressionMethod: 0, extendedTimestamp: false });
|
|
359
|
+
return container;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Closes the container, returning its content
|
|
363
|
+
* @returns The EPUB binary data
|
|
364
|
+
*/
|
|
365
|
+
async seal() {
|
|
366
|
+
return this.#zip.close();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
class EpubBuilder {
|
|
371
|
+
/**
|
|
372
|
+
* The builder config
|
|
373
|
+
*/
|
|
374
|
+
config;
|
|
375
|
+
/**
|
|
376
|
+
* The list of registered EPUB resources
|
|
377
|
+
*/
|
|
378
|
+
resources;
|
|
379
|
+
/**
|
|
380
|
+
* The EPUB ZIP container
|
|
381
|
+
*/
|
|
382
|
+
zip;
|
|
383
|
+
/**
|
|
384
|
+
* The private constructor
|
|
385
|
+
* @param config The builder config
|
|
386
|
+
*/
|
|
387
|
+
constructor(config) {
|
|
388
|
+
this.config = config;
|
|
389
|
+
this.resources = [];
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Registers a binary-encoded file
|
|
393
|
+
* @param href The resource's path inside the EPUB container
|
|
394
|
+
* @param content The resource's binary-encoded content
|
|
395
|
+
* @param properties The resource's manifest properties
|
|
396
|
+
* @returns The registered resource
|
|
397
|
+
*/
|
|
398
|
+
async addBinaryFile(href, content, properties = []) {
|
|
399
|
+
const resource = createResource(href, properties);
|
|
400
|
+
this.resources.push(resource);
|
|
401
|
+
await this.zip.addBinaryFile(`OEBPS/${href}`, content);
|
|
402
|
+
return resource;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Registers a text-encoded file
|
|
406
|
+
* @param href The resource's path inside the EPUB container
|
|
407
|
+
* @param content The resource's text-encoded content
|
|
408
|
+
* @param properties The resource's manifest properties
|
|
409
|
+
* @returns The registered resource
|
|
410
|
+
*/
|
|
411
|
+
async addTextFile(href, content, properties = []) {
|
|
412
|
+
const resource = createResource(href, properties);
|
|
413
|
+
this.resources.push(resource);
|
|
414
|
+
await this.zip.addTextFile(`OEBPS/${href}`, content);
|
|
415
|
+
return resource;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Reads a file and registers it as a resource
|
|
419
|
+
* @param href The resource's path inside the EPUB container
|
|
420
|
+
* @param path The file's physical path
|
|
421
|
+
* @param properties The resource's manifest properties
|
|
422
|
+
* @returns The registered resource
|
|
423
|
+
*/
|
|
424
|
+
async copyFile(href, path, properties = []) {
|
|
425
|
+
const blob = await readBinaryFile(path);
|
|
426
|
+
return this.addBinaryFile(href, blob, properties);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Generate metadata files for EPUB compliance
|
|
430
|
+
*/
|
|
431
|
+
async #generateFiles() {
|
|
432
|
+
await this.addTextFile("nav.xhtml", generateNavXhtml(this.config), ["nav"]);
|
|
433
|
+
await this.addTextFile("toc.ncx", generateNcx(this.config));
|
|
434
|
+
await this.zip.addTextFile("OEBPS/content.opf", generateContentOpf(this.config, this.resources));
|
|
435
|
+
await this.zip.addTextFile("META-INF/com.apple.ibooks.display-options.xml", generateAppleDisplayOptionsXml(this.resources));
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Returns an empty `EpubBuilder`
|
|
439
|
+
* @param partialConfig The builder config
|
|
440
|
+
*/
|
|
441
|
+
static async init(partialConfig) {
|
|
442
|
+
const config = populateConfig(partialConfig);
|
|
443
|
+
const builder = new EpubBuilder(config);
|
|
444
|
+
builder.zip = await ZipContainer.init();
|
|
445
|
+
await builder.zip.addTextFile("META-INF/container.xml", generateContainerXml());
|
|
446
|
+
return builder;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Closes the ZIP container, returning its content
|
|
450
|
+
* @returns The EPUB binary data
|
|
451
|
+
*/
|
|
452
|
+
async seal() {
|
|
453
|
+
await this.#generateFiles();
|
|
454
|
+
return this.zip.seal();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export { EpubBuilder, getMimeType, isCoreMediaType, populateConfig };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@leopiccionia/epub-builder",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A non-opinionated EPUB builder for reflowable content",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ebook",
|
|
8
|
+
"epub"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/leopiccionia/epub-builder",
|
|
11
|
+
"author": {
|
|
12
|
+
"email": "leopiccionia@gmail.com",
|
|
13
|
+
"name": "Leonardo Piccioni de Almeida",
|
|
14
|
+
"url": "https://leopiccionia.github.io"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/leopiccionia/epub-builder.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/leopiccionia/epub-builder/issues",
|
|
22
|
+
"email": "leopiccionia@gmail.com"
|
|
23
|
+
},
|
|
24
|
+
"license": "BSD-3-Clause",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./locales/*": "./dist/locales/*"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "unbuild",
|
|
37
|
+
"test": "vitest"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.13.0",
|
|
41
|
+
"@types/xast": "^2.0.4",
|
|
42
|
+
"unbuild": "^3.3.1",
|
|
43
|
+
"vitest": "^3.0.4"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@lukeed/uuid": "^2.0.1",
|
|
47
|
+
"@zip.js/zip.js": "^2.7.57",
|
|
48
|
+
"defu": "^6.1.4",
|
|
49
|
+
"mime": "^4.0.6",
|
|
50
|
+
"unist-builder": "^4.0.0",
|
|
51
|
+
"xast-util-to-xml": "^4.0.0",
|
|
52
|
+
"xastscript": "^4.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|