tosijs-ui 1.16.37 → 1.16.38
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/CHANGELOG.md +18 -0
- package/dist/doc-system/site/epub-cli.js +13 -2
- package/dist/doc-system/site/epub-volumes.d.ts +6 -0
- package/dist/doc-system/site/epub-volumes.js +6 -0
- package/dist/doc-system/site/epub.d.ts +26 -0
- package/dist/doc-system/site/epub.js +179 -7
- package/dist/doc-system/site/index.d.ts +1 -1
- package/dist/doc-system/site/index.js +1 -1
- package/dist/doc-system/site/orchestrator.js +6 -1
- package/dist/iife.js +2 -2
- package/dist/iife.js.map +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.38
|
|
4
|
+
|
|
5
|
+
- **The ePub carries its images (#3519).** Every local image went into the book as a site path
|
|
6
|
+
that does not exist inside the zip, and no image file was copied, so every figure was a broken
|
|
7
|
+
image in every reader. It never showed in a library's book, whose images are remote README
|
|
8
|
+
badges; it made an illustrated book impossible. A local image is now copied into the ePub and
|
|
9
|
+
listed in its manifest once, however many chapters use it. A `/images/fig.jpg` path (with or
|
|
10
|
+
without `basePath`) is found in the built site, where your `staticDirs` are; a relative path is
|
|
11
|
+
resolved against the page's URL, as on the site. That covers `<img src>` and `srcset` in
|
|
12
|
+
markdown and raw HTML; `<picture><source>` and SVG `<image>` are not bound yet. Remote and
|
|
13
|
+
`data:` images are unchanged. One that is missing, outside the site, or not
|
|
14
|
+
JPEG/PNG/GIF/SVG/WebP is listed in a warning, and fails a `strict` build, including an image
|
|
15
|
+
only your host serves (a CDN rewrite, a proxied path): the page shows it, the book cannot.
|
|
16
|
+
- **A book with images is bigger, and so is its rebuild.** The ePub now carries every figure,
|
|
17
|
+
and it is rebuilt on every dev-server rebuild; already-compressed images are stored, not
|
|
18
|
+
deflated again, so the cost is copying them. If you commit your output folder, the ePub in it
|
|
19
|
+
grows by the size of your images.
|
|
20
|
+
|
|
3
21
|
## 1.16.37
|
|
4
22
|
|
|
5
23
|
Doc-site test running, as a developer meets it. On a deployed site, tests still start off; the
|
|
@@ -12,7 +12,7 @@ the Bun.build note in orchestrator.ts.
|
|
|
12
12
|
|
|
13
13
|
Build-time only. Never import this from browser code.
|
|
14
14
|
*/
|
|
15
|
-
import { buildEpub } from './epub.js';
|
|
15
|
+
import { buildEpub, EpubImageError, EPUB_IMAGE_EXIT } from './epub.js';
|
|
16
16
|
import { BUILT_IN_DIALECTS, normalizeDialectNames, registerLiveLanguage, } from '../example-policy.js';
|
|
17
17
|
const payloadPath = process.argv[2];
|
|
18
18
|
if (!payloadPath) {
|
|
@@ -26,4 +26,15 @@ for (const name of normalizeDialectNames(config.dialects)) {
|
|
|
26
26
|
if (!BUILT_IN_DIALECTS.has(name))
|
|
27
27
|
registerLiveLanguage(name);
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
try {
|
|
30
|
+
await buildEpub(config, opts ?? {});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// A distinct exit code, so the parent fails a `strict` build on THIS and still only warns
|
|
34
|
+
// about the ePub's other failures (it is otherwise a side artifact).
|
|
35
|
+
if (error instanceof EpubImageError) {
|
|
36
|
+
console.error(error.message);
|
|
37
|
+
process.exit(EPUB_IMAGE_EXIT);
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
@@ -79,3 +79,9 @@ export declare function renderEpubDownloads(text: string, volumes: EpubVolume[])
|
|
|
79
79
|
* `undefined` when it should not: no ePub is built, or `epub.printCover` is `false`.
|
|
80
80
|
*/
|
|
81
81
|
export declare function printCoverManifestUrl(config: VolumeNamingConfig): string | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* epub-cli's exit code when a local image cannot be bound into the book. The build fails on it
|
|
84
|
+
* under `strict` and only warns otherwise. Here, not in epub.ts, so the orchestrator can read it
|
|
85
|
+
* without importing the ePub builder it runs in a child.
|
|
86
|
+
*/
|
|
87
|
+
export declare const EPUB_IMAGE_EXIT = 3;
|
|
@@ -139,3 +139,9 @@ export function printCoverManifestUrl(config) {
|
|
|
139
139
|
return undefined;
|
|
140
140
|
return `${(config.basePath ?? '/').replace(/\/+$/, '')}/${VOLUME_MANIFEST}`;
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* epub-cli's exit code when a local image cannot be bound into the book. The build fails on it
|
|
144
|
+
* under `strict` and only warns otherwise. Here, not in epub.ts, so the orchestrator can read it
|
|
145
|
+
* without importing the ePub builder it runs in a child.
|
|
146
|
+
*/
|
|
147
|
+
export const EPUB_IMAGE_EXIT = 3;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EPUB_IMAGE_EXIT } from './epub-volumes.js';
|
|
1
2
|
import type { SiteConfig } from './site-config.js';
|
|
2
3
|
import { DEFAULT_BOOK_CSS, stripDocMeta } from '../book-html.js';
|
|
3
4
|
export { DEFAULT_BOOK_CSS, stripDocMeta };
|
|
@@ -75,8 +76,33 @@ export declare function toXhtml(html: string): string;
|
|
|
75
76
|
* untouched. `bookFiles` maps in-book slug → chapter filename.
|
|
76
77
|
*/
|
|
77
78
|
export declare function rewriteInBookLinks(html: string, bookFiles: Map<string, string>, slugMap: Record<string, string>, basePath?: string): string;
|
|
79
|
+
export { EPUB_IMAGE_EXIT };
|
|
80
|
+
/** Thrown by buildEpub under `strict` when a local image cannot be bound into the book. */
|
|
81
|
+
export declare class EpubImageError extends Error {
|
|
82
|
+
name: string;
|
|
83
|
+
}
|
|
84
|
+
/** An image bound into the book: where it lives in OEBPS, and where it came from. */
|
|
85
|
+
export interface BookImage {
|
|
86
|
+
/** path inside OEBPS, e.g. `images/figs/a.jpg` */
|
|
87
|
+
file: string;
|
|
88
|
+
mediaType: string;
|
|
89
|
+
/** absolute path of the source file */
|
|
90
|
+
source: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Rewrite the local images in `html` (`<img src>` and each `srcset` candidate) to book-local
|
|
94
|
+
* paths, registering each file in `images` (keyed by absolute source path, so a figure used
|
|
95
|
+
* twice is bound once). Root-absolute srcs have `basePath` stripped; relative ones resolve
|
|
96
|
+
* against `pagePath`, the page's own site path. Local srcs that cannot be bound are added to
|
|
97
|
+
* `unbound` and left untouched. Not handled: `<picture><source>` and SVG `<image>`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function embedLocalImages(html: string, webRoot: string, basePath: string | undefined, images: Map<string, BookImage>, unbound?: Set<string>, pagePath?: string): string;
|
|
78
100
|
/**
|
|
79
101
|
* Build an EPUB 3 book from the extracted corpus. Returns the output path.
|
|
80
102
|
*/
|
|
81
103
|
export declare function versionAnchoredDate(version: string, override?: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* Build the ePub for `config`'s corpus and return its path. Under `config.strict`, throws
|
|
106
|
+
* {@link EpubImageError} when a local image cannot be bound into the book.
|
|
107
|
+
*/
|
|
82
108
|
export declare function buildEpub(config: SiteConfig, opts?: BuildEpubOptions): Promise<string>;
|
|
@@ -21,7 +21,7 @@ import { highlightHtml } from '../highlight.js';
|
|
|
21
21
|
import { buildSlugMap, pathForSlug, resolveParent, slugForPath, withBase, wikilinkTargets, } from '../routing.js';
|
|
22
22
|
import { buildNavTree } from '../nav-tree.js';
|
|
23
23
|
import { partitionByBook, placeInBook, bookTextOf, DEFAULT_BOOK, } from '../book-target.js';
|
|
24
|
-
import { COVER_MEDIA_TYPES, coverStem, epubVolumeIdentity, } from './epub-volumes.js';
|
|
24
|
+
import { COVER_MEDIA_TYPES, EPUB_IMAGE_EXIT, coverStem, epubVolumeIdentity, } from './epub-volumes.js';
|
|
25
25
|
import { isLiveFence, languageOfClass } from '../example-policy.js';
|
|
26
26
|
import { DEFAULT_BOOK_CSS, stripDocMeta, flatten, slugify, } from '../book-html.js';
|
|
27
27
|
import { selectBookDocs } from '../book-manifest.js';
|
|
@@ -216,6 +216,145 @@ export function rewriteInBookLinks(html, bookFiles, slugMap, basePath) {
|
|
|
216
216
|
return file ? `href="${file}${hash}"` : match;
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
|
+
/*
|
|
220
|
+
Images bound INTO the book.
|
|
221
|
+
|
|
222
|
+
A chapter's `<img src="/images/fig.jpg">` used to ship as-is, pointing at a site path that
|
|
223
|
+
does not exist inside the zip: the book carried no image files at all, so every local
|
|
224
|
+
figure was a broken image in every reader. Fine for a library whose only images are remote
|
|
225
|
+
README badges; fatal for a manual whose content IS its figures.
|
|
226
|
+
|
|
227
|
+
A root-absolute src (`/images/fig.jpg`) is resolved against the built web root — the output
|
|
228
|
+
dir, which already holds the copied `staticDirs` — so the book embeds exactly the file the page
|
|
229
|
+
shows. basePath is stripped first, as for links. A relative src (`img/fig.jpg`) is first
|
|
230
|
+
resolved against the page's own path (`/slug/`), as a browser on the site resolves it, so it
|
|
231
|
+
binds the same file the page shows. Remote and `data:` srcs are left alone. A local image that
|
|
232
|
+
is missing, escapes the web root, or is not an EPUB core image type is left as-is and reported,
|
|
233
|
+
because a figure that silently vanishes from a book is worse than one that visibly fails; under
|
|
234
|
+
`strict` it fails the build.
|
|
235
|
+
*/
|
|
236
|
+
export { EPUB_IMAGE_EXIT };
|
|
237
|
+
/** Thrown by buildEpub under `strict` when a local image cannot be bound into the book. */
|
|
238
|
+
export class EpubImageError extends Error {
|
|
239
|
+
name = 'EpubImageError';
|
|
240
|
+
}
|
|
241
|
+
/** EPUB 3 core media types for images, by extension. */
|
|
242
|
+
const IMAGE_MEDIA_TYPES = {
|
|
243
|
+
'.jpg': 'image/jpeg',
|
|
244
|
+
'.jpeg': 'image/jpeg',
|
|
245
|
+
'.png': 'image/png',
|
|
246
|
+
'.gif': 'image/gif',
|
|
247
|
+
'.svg': 'image/svg+xml',
|
|
248
|
+
'.webp': 'image/webp',
|
|
249
|
+
};
|
|
250
|
+
/** The five entities an attribute value can carry, and numeric references. */
|
|
251
|
+
function decodeAttr(value) {
|
|
252
|
+
return value.replace(/&(?:#(\d+)|#x([\da-f]+)|(amp|lt|gt|quot|apos));/gi, (match, dec, hex, name) => dec
|
|
253
|
+
? String.fromCodePoint(Number(dec))
|
|
254
|
+
: hex
|
|
255
|
+
? String.fromCodePoint(parseInt(hex, 16))
|
|
256
|
+
: { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" }[name.toLowerCase()] ?? match);
|
|
257
|
+
}
|
|
258
|
+
// An `<img>` tag, with `>` allowed inside quoted attribute values (`alt="a > b"`).
|
|
259
|
+
const IMG_TAG = /<img\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
|
|
260
|
+
// One attribute of a tag: name, then a double-, single- or un-quoted value.
|
|
261
|
+
const ATTRIBUTE = /(\s)([^\s=>/"']+)(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g;
|
|
262
|
+
/**
|
|
263
|
+
* Rewrite the local images in `html` (`<img src>` and each `srcset` candidate) to book-local
|
|
264
|
+
* paths, registering each file in `images` (keyed by absolute source path, so a figure used
|
|
265
|
+
* twice is bound once). Root-absolute srcs have `basePath` stripped; relative ones resolve
|
|
266
|
+
* against `pagePath`, the page's own site path. Local srcs that cannot be bound are added to
|
|
267
|
+
* `unbound` and left untouched. Not handled: `<picture><source>` and SVG `<image>`.
|
|
268
|
+
*/
|
|
269
|
+
export function embedLocalImages(html, webRoot, basePath, images, unbound = new Set(),
|
|
270
|
+
// the page's own site path, for relative srcs; '/' for the root page
|
|
271
|
+
pagePath = '/') {
|
|
272
|
+
const root = path.resolve(webRoot);
|
|
273
|
+
const bp = basePath && basePath !== '/' ? basePath.replace(/\/$/, '') : '';
|
|
274
|
+
// the book-local path for one src, or null to leave it as it is
|
|
275
|
+
const bind = (src) => {
|
|
276
|
+
// remote (any scheme, or protocol-relative) and data: stay as they are
|
|
277
|
+
if (!src || /^[a-z][a-z\d+.-]*:/i.test(src) || src.startsWith('//'))
|
|
278
|
+
return null;
|
|
279
|
+
const fragment = /#.*$/.exec(src)?.[0] ?? '';
|
|
280
|
+
let p = src.replace(/[?#].*$/, '');
|
|
281
|
+
if (!p)
|
|
282
|
+
return null;
|
|
283
|
+
if (p.startsWith('/')) {
|
|
284
|
+
if (bp && (p === bp || p.startsWith(bp + '/')))
|
|
285
|
+
p = p.slice(bp.length);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
// keep the raw percent-encoding: decoded once below, like a root-absolute src
|
|
289
|
+
p = new URL(p, `https://book.invalid${pagePath}`).pathname;
|
|
290
|
+
}
|
|
291
|
+
let decoded;
|
|
292
|
+
try {
|
|
293
|
+
decoded = decodeURIComponent(p);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
decoded = p;
|
|
297
|
+
}
|
|
298
|
+
const source = path.join(root, decoded);
|
|
299
|
+
const mediaType = IMAGE_MEDIA_TYPES[path.extname(source).toLowerCase()];
|
|
300
|
+
if (!source.startsWith(root + path.sep) ||
|
|
301
|
+
!mediaType ||
|
|
302
|
+
!fs.existsSync(source) ||
|
|
303
|
+
!fs.statSync(source).isFile()) {
|
|
304
|
+
unbound.add(src);
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
let image = images.get(source);
|
|
308
|
+
if (!image) {
|
|
309
|
+
const rel = path
|
|
310
|
+
.relative(root, source)
|
|
311
|
+
.split(path.sep)
|
|
312
|
+
.map((seg) => seg.replace(/[^\w.-]/g, '_'))
|
|
313
|
+
.join('/');
|
|
314
|
+
// Sanitizing can make two sources collide (`a b.jpg`, `a_b.jpg`); keep both.
|
|
315
|
+
const taken = new Set([...images.values()].map((i) => i.file));
|
|
316
|
+
let file = `images/${rel}`;
|
|
317
|
+
const ext = path.extname(file);
|
|
318
|
+
for (let n = 2; taken.has(file); n += 1) {
|
|
319
|
+
file = `images/${rel.slice(0, rel.length - ext.length)}-${n}${ext}`;
|
|
320
|
+
}
|
|
321
|
+
image = { file, mediaType, source };
|
|
322
|
+
images.set(source, image);
|
|
323
|
+
}
|
|
324
|
+
// A fragment names a part of the file (an SVG sprite), so it travels with it.
|
|
325
|
+
return image.file + fragment;
|
|
326
|
+
};
|
|
327
|
+
return html.replace(IMG_TAG, (tag) => tag.replace(ATTRIBUTE, (match, space, name, eq, dq, sq, bare) => {
|
|
328
|
+
const attr = name.toLowerCase();
|
|
329
|
+
if (attr !== 'src' && attr !== 'srcset')
|
|
330
|
+
return match;
|
|
331
|
+
const raw = dq ?? sq ?? bare ?? '';
|
|
332
|
+
const quote = dq !== undefined ? '"' : sq !== undefined ? "'" : '"';
|
|
333
|
+
const value = decodeAttr(raw);
|
|
334
|
+
let rewritten;
|
|
335
|
+
if (attr === 'src') {
|
|
336
|
+
rewritten = bind(value);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
// `url 2x, url 640w`: bind each candidate, keep its descriptor
|
|
340
|
+
let changed = false;
|
|
341
|
+
const candidates = value.split(',').map((candidate) => {
|
|
342
|
+
const [url, ...descriptor] = candidate.trim().split(/\s+/);
|
|
343
|
+
const bound = bind(url);
|
|
344
|
+
if (bound === null)
|
|
345
|
+
return candidate.trim();
|
|
346
|
+
changed = true;
|
|
347
|
+
return [bound, ...descriptor].join(' ');
|
|
348
|
+
});
|
|
349
|
+
rewritten = changed ? candidates.join(', ') : null;
|
|
350
|
+
}
|
|
351
|
+
// decoded above, so escaped again: a fragment or an untouched srcset candidate may
|
|
352
|
+
// carry `&` or a quote
|
|
353
|
+
return rewritten === null
|
|
354
|
+
? match
|
|
355
|
+
: `${space}${name}${eq}${quote}${escapeXml(rewritten)}${quote}`;
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
219
358
|
// `pageUrl` is the live-site URL of THIS doc (already including baseUrl, basePath
|
|
220
359
|
// and the correct root for README → '/'); each example link just appends `#id`.
|
|
221
360
|
function injectExampleLinks(doc, pageUrl, policy = 'auto') {
|
|
@@ -267,7 +406,7 @@ function containerXml() {
|
|
|
267
406
|
</container>
|
|
268
407
|
`;
|
|
269
408
|
}
|
|
270
|
-
function packageOpf(meta, chapters, cover) {
|
|
409
|
+
function packageOpf(meta, chapters, cover, images = []) {
|
|
271
410
|
const manifestItems = [
|
|
272
411
|
`<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>`,
|
|
273
412
|
`<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>`,
|
|
@@ -280,6 +419,7 @@ function packageOpf(meta, chapters, cover) {
|
|
|
280
419
|
]
|
|
281
420
|
: []),
|
|
282
421
|
...chapters.map((c) => `<item id="${c.id}" href="${c.href}" media-type="application/xhtml+xml"/>`),
|
|
422
|
+
...images.map((img, i) => `<item id="img-${i + 1}" href="${escapeXml(img.file)}" media-type="${img.mediaType}"/>`),
|
|
283
423
|
];
|
|
284
424
|
const spine = [
|
|
285
425
|
...(cover ? [`<itemref idref="cover-page"/>`] : []),
|
|
@@ -415,8 +555,10 @@ async function zipEpub(buildDir, outputAbs) {
|
|
|
415
555
|
await $ `find . -exec touch -t 202001010000 {} +`.cwd(buildDir).quiet();
|
|
416
556
|
// 1) mimetype first, stored (-0), no extra fields (-X)
|
|
417
557
|
await $ `zip -X0 ${outputAbs} mimetype`.cwd(buildDir).quiet();
|
|
418
|
-
// 2) everything else, deflated (-9), recursive (-r), no extra fields, no dir entries (-D)
|
|
419
|
-
|
|
558
|
+
// 2) everything else, deflated (-9), recursive (-r), no extra fields, no dir entries (-D).
|
|
559
|
+
// Images that are already compressed are stored (-n): deflating a book's worth of JPEGs on
|
|
560
|
+
// every rebuild costs time and saves nothing.
|
|
561
|
+
await $ `zip -Xr9D -n .jpg:.jpeg:.png:.gif:.webp ${outputAbs} META-INF OEBPS -x mimetype`
|
|
420
562
|
.cwd(buildDir)
|
|
421
563
|
.quiet();
|
|
422
564
|
}
|
|
@@ -601,6 +743,10 @@ export function versionAnchoredDate(version, override) {
|
|
|
601
743
|
.toISOString()
|
|
602
744
|
.replace(/\.\d+Z$/, 'Z');
|
|
603
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* Build the ePub for `config`'s corpus and return its path. Under `config.strict`, throws
|
|
748
|
+
* {@link EpubImageError} when a local image cannot be bound into the book.
|
|
749
|
+
*/
|
|
604
750
|
export async function buildEpub(config, opts = {}) {
|
|
605
751
|
const projectVersion = await Bun.file(`${process.cwd()}/package.json`)
|
|
606
752
|
.json()
|
|
@@ -731,6 +877,8 @@ export async function buildEpub(config, opts = {}) {
|
|
|
731
877
|
// example links back to its anchor on the live site.
|
|
732
878
|
const baseUrl = config.baseUrl?.replace(/\/+$/, '');
|
|
733
879
|
const chapters = [];
|
|
880
|
+
const images = new Map();
|
|
881
|
+
const unboundImages = new Set();
|
|
734
882
|
for (const node of flatten(roots)) {
|
|
735
883
|
const doc = node.doc;
|
|
736
884
|
// The doc's live-site URL, matching generate-site's canonical link: README
|
|
@@ -743,7 +891,7 @@ export async function buildEpub(config, opts = {}) {
|
|
|
743
891
|
is the same `highlightHtml` the static pages use, so a code block looks the same on the
|
|
744
892
|
site, in the book and in print.
|
|
745
893
|
*/
|
|
746
|
-
const
|
|
894
|
+
const linked = rewriteInBookLinks(
|
|
747
895
|
/*
|
|
748
896
|
`'none'` — an ePub has no live examples, so the live-fence skip that protects the web
|
|
749
897
|
page buys nothing here and cost the reader 237 of 284 code blocks (js, html, css and
|
|
@@ -752,6 +900,7 @@ export async function buildEpub(config, opts = {}) {
|
|
|
752
900
|
highlighting cannot break the "Run this example live" links.
|
|
753
901
|
*/
|
|
754
902
|
await highlightHtml(renderDocMarkdown(stripDocMeta(bookTextOf(doc)), { wikilinks }), 'none'), bookFiles, slugMap, config.basePath);
|
|
903
|
+
const html = embedLocalImages(linked, outDir, config.basePath, images, unboundImages, pathForSlug(slugMap[doc.filename] ?? ''));
|
|
755
904
|
// happy-dom occasionally throws on exotic content (e.g. an internal selector
|
|
756
905
|
// bug); fall back to the regex pass for that doc rather than aborting.
|
|
757
906
|
let bodyHtml;
|
|
@@ -772,13 +921,34 @@ export async function buildEpub(config, opts = {}) {
|
|
|
772
921
|
title: doc.title,
|
|
773
922
|
});
|
|
774
923
|
}
|
|
924
|
+
for (const img of images.values()) {
|
|
925
|
+
const dest = path.join(buildDir, 'OEBPS', img.file);
|
|
926
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
927
|
+
fs.copyFileSync(img.source, dest);
|
|
928
|
+
}
|
|
929
|
+
if (unboundImages.size) {
|
|
930
|
+
const list = [...unboundImages];
|
|
931
|
+
const report = `epub: ${list.length} local image(s) not bound into the book (missing, outside ` +
|
|
932
|
+
`the site, or not jpg/png/gif/svg/webp):\n` +
|
|
933
|
+
list
|
|
934
|
+
.slice(0, 10)
|
|
935
|
+
.map((s) => ` ${s}`)
|
|
936
|
+
.join('\n') +
|
|
937
|
+
(list.length > 10 ? `\n … and ${list.length - 10} more` : '');
|
|
938
|
+
if (config.strict) {
|
|
939
|
+
// nothing of this book is kept: a stray build folder would land in the site output
|
|
940
|
+
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
941
|
+
throw new EpubImageError(report);
|
|
942
|
+
}
|
|
943
|
+
console.warn(report);
|
|
944
|
+
}
|
|
775
945
|
// Cover (explicit image, or generated from the title + favicon).
|
|
776
946
|
const cover = await makeCover(config, opts, meta);
|
|
777
947
|
if (cover) {
|
|
778
948
|
fs.writeFileSync(path.join(buildDir, 'OEBPS', cover.file), cover.data);
|
|
779
949
|
fs.writeFileSync(path.join(buildDir, 'OEBPS', 'cover.xhtml'), coverPageXhtml(meta.title, cover.file));
|
|
780
950
|
}
|
|
781
|
-
fs.writeFileSync(path.join(buildDir, 'OEBPS', 'package.opf'), packageOpf(meta, chapters, cover));
|
|
951
|
+
fs.writeFileSync(path.join(buildDir, 'OEBPS', 'package.opf'), packageOpf(meta, chapters, cover, [...images.values()]));
|
|
782
952
|
fs.writeFileSync(path.join(buildDir, 'OEBPS', 'contents.xhtml'), tocPageXhtml(roots, fileFor));
|
|
783
953
|
fs.writeFileSync(path.join(buildDir, 'OEBPS', 'nav.xhtml'), navXhtml(meta, roots, fileFor));
|
|
784
954
|
fs.writeFileSync(path.join(buildDir, 'OEBPS', 'toc.ncx'), tocNcx(meta, roots, fileFor));
|
|
@@ -804,7 +974,9 @@ export async function buildEpub(config, opts = {}) {
|
|
|
804
974
|
// is still retained. buildEpub now runs in a child process (see orchestrator),
|
|
805
975
|
// which is what actually bounds it.
|
|
806
976
|
win?.close?.();
|
|
807
|
-
console.log(`epub: ${output} (${chapters.length} chapters
|
|
977
|
+
console.log(`epub: ${output} (${chapters.length} chapters` +
|
|
978
|
+
(images.size ? `, ${images.size} images` : '') +
|
|
979
|
+
')' +
|
|
808
980
|
(coverPath ? `, cover ${path.basename(coverPath)}` : ''));
|
|
809
981
|
return output;
|
|
810
982
|
}
|
|
@@ -18,7 +18,7 @@ export type { Doc } from './docs.js';
|
|
|
18
18
|
export type { LibraryBundle } from './library-bundle.js';
|
|
19
19
|
export { generateSite } from './generate-site.js';
|
|
20
20
|
export type { GenerateSiteConfig } from './generate-site.js';
|
|
21
|
-
export { buildEpub, DEFAULT_BOOK_CSS } from './epub.js';
|
|
21
|
+
export { buildEpub, DEFAULT_BOOK_CSS, EpubImageError } from './epub.js';
|
|
22
22
|
export type { BuildEpubOptions, BookMeta } from './epub.js';
|
|
23
23
|
export { selectBookDocs } from '../book-manifest.js';
|
|
24
24
|
export type { BookManifest } from '../book-manifest.js';
|
|
@@ -20,7 +20,7 @@ export { auditDependencies, reportAudit, resolveAuditMode, } from './audit-guard
|
|
|
20
20
|
export { openDevBrowser, buildOpenPlan } from './open-browser.js';
|
|
21
21
|
export { extractDocs, saveDocsJSON } from './docs.js';
|
|
22
22
|
export { generateSite } from './generate-site.js';
|
|
23
|
-
export { buildEpub, DEFAULT_BOOK_CSS } from './epub.js';
|
|
23
|
+
export { buildEpub, DEFAULT_BOOK_CSS, EpubImageError } from './epub.js';
|
|
24
24
|
export { selectBookDocs } from '../book-manifest.js';
|
|
25
25
|
export { placeInBook, bookTextOf } from '../book-target.js';
|
|
26
26
|
// Insets and conditional text (#217): `extractDocs` applies these; exported for a consumer
|
|
@@ -29,7 +29,7 @@ export function registerSiteDialects(config) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
import { namedBooks, partitionByBook, DEFAULT_BOOK } from '../book-target.js';
|
|
32
|
-
import { attachCovers, listEpubVolumes, printCoverManifestUrl, VOLUME_MANIFEST, renderEpubDownloads, } from './epub-volumes.js';
|
|
32
|
+
import { EPUB_IMAGE_EXIT, attachCovers, listEpubVolumes, printCoverManifestUrl, VOLUME_MANIFEST, renderEpubDownloads, } from './epub-volumes.js';
|
|
33
33
|
import { buildSlugMap } from '../routing.js';
|
|
34
34
|
import { assetStampInputs, computeAssetStamp, missingStampInputWarning, } from './asset-stamp.js';
|
|
35
35
|
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, } from 'fs';
|
|
@@ -88,6 +88,8 @@ async function buildEpubInChild(config, opts) {
|
|
|
88
88
|
// the ePub's "run this example live" links count examples the way the page does, so it
|
|
89
89
|
// must know the site's dialects too (1.16.3 review F3)
|
|
90
90
|
dialects: config.dialects,
|
|
91
|
+
// an image the book cannot bind fails a strict build (EPUB_IMAGE_EXIT)
|
|
92
|
+
strict: config.strict,
|
|
91
93
|
},
|
|
92
94
|
opts,
|
|
93
95
|
};
|
|
@@ -103,6 +105,9 @@ async function buildEpubInChild(config, opts) {
|
|
|
103
105
|
}, EPUB_TIMEOUT_MS);
|
|
104
106
|
try {
|
|
105
107
|
const code = await child.exited;
|
|
108
|
+
if (code === EPUB_IMAGE_EXIT && config.strict) {
|
|
109
|
+
misconfigured(true, '⚠️ epub: a local image could not be bound into the book (listed above).');
|
|
110
|
+
}
|
|
106
111
|
if (code !== 0) {
|
|
107
112
|
console.warn(`⚠️ epub build failed (exit ${code}). The site is fine; the .epub may be stale.`);
|
|
108
113
|
}
|