bookish-press 0.9.2 → 0.9.4
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/README.md +16 -0
- package/dist/epub/build.test.js +42 -0
- package/dist/epub/package.js +17 -1
- package/dist/epub/xhtml.d.ts +6 -0
- package/dist/epub/xhtml.js +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,22 @@ The content lives in `scripts/demo-book.js` and the images are drawn by `scripts
|
|
|
50
50
|
|
|
51
51
|
It uses the offset emulator ports from `firebase.test.json`, so it doesn't collide with another project's emulators on the default ports, and it builds the app rather than running `vite dev` — the reader route's server render pulls the chapter model through a module cycle that Vite's dev SSR evaluator can't order, though the production build resolves it fine.
|
|
52
52
|
|
|
53
|
+
## Storage CORS
|
|
54
|
+
|
|
55
|
+
Book images live in Firebase Storage and are fetched by the browser when
|
|
56
|
+
building an EPUB, which requires the bucket to send CORS headers. It does not
|
|
57
|
+
by default, and without them every image is silently dropped from the export.
|
|
58
|
+
`cors.json` holds the policy; apply it to a project's bucket with:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
gcloud storage buckets update gs://<project>.appspot.com --cors-file=cors.json --project <project>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It is applied to both `bookish-prod` and `bookish-dev-21ac3`. The objects are
|
|
65
|
+
already world-readable — the thumbnail function calls `makePublic()` on upload —
|
|
66
|
+
so allowing `GET` from any origin grants nothing that a plain HTTP request
|
|
67
|
+
couldn't already do.
|
|
68
|
+
|
|
53
69
|
## Deployment notes
|
|
54
70
|
|
|
55
71
|
There are two major components to Bookish: the authoring platform and the reading front end. Changes to the authoring platform that do not affect reading can be verified and deployed to Firebase without any other coordination. However, changes to the reading experience have downstream dependencies that need to be managed. Here's the general deployment workflow, which I'm currently using as reminders for myself:
|
package/dist/epub/build.test.js
CHANGED
|
@@ -409,3 +409,45 @@ test('a reference whose fields are not strings still exports', async () => {
|
|
|
409
409
|
expect(references).toContain('-370');
|
|
410
410
|
expect(references).toContain('The Republic');
|
|
411
411
|
});
|
|
412
|
+
test("a chapter's header image is rendered, not just packaged", async () => {
|
|
413
|
+
// Edition.getEmbeds() collects chapter header images, so they were fetched
|
|
414
|
+
// and packaged, but chapterDocument never rendered one -- the reader saw
|
|
415
|
+
// the book cover and nothing else, and paid for the bytes anyway.
|
|
416
|
+
const edition = makeEdition({
|
|
417
|
+
chapters: [
|
|
418
|
+
chapter('one', 'One', 'Text with no images of its own.', {
|
|
419
|
+
image: '|header.jpg|A header image|A caption|A credit|',
|
|
420
|
+
}),
|
|
421
|
+
],
|
|
422
|
+
});
|
|
423
|
+
const { entries } = await build(edition);
|
|
424
|
+
const one = textOf(entries, 'OEBPS/one.xhtml');
|
|
425
|
+
expect(one).toContain('<img');
|
|
426
|
+
expect(one).toContain('A header image');
|
|
427
|
+
// Above the title, as the web reader places it.
|
|
428
|
+
expect(one.indexOf('<img')).toBeLessThan(one.indexOf('<h1>'));
|
|
429
|
+
});
|
|
430
|
+
test('every packaged image is referenced by a document', async () => {
|
|
431
|
+
// The general form of the bug above: an image that is fetched, shrunk and
|
|
432
|
+
// written into the package but never pointed at is pure weight.
|
|
433
|
+
const edition = makeEdition({
|
|
434
|
+
images: { cover: '|cover.jpg|The cover|||' },
|
|
435
|
+
chapters: [
|
|
436
|
+
chapter('one', 'One', 'A body image: |body.jpg|Alt|||', {
|
|
437
|
+
image: '|chapterhead.jpg|A chapter header|||',
|
|
438
|
+
}),
|
|
439
|
+
chapter('two', 'Two', 'No images here.', {
|
|
440
|
+
image: '|another.jpg|Another header|||',
|
|
441
|
+
}),
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
const { entries } = await build(edition);
|
|
445
|
+
const documents = [...entries.keys()].filter((path) => path.endsWith('.xhtml'));
|
|
446
|
+
const markup = documents.map((path) => textOf(entries, path)).join('\n');
|
|
447
|
+
const images = [...entries.keys()]
|
|
448
|
+
.filter((path) => path.startsWith('OEBPS/images/'))
|
|
449
|
+
.map((path) => path.replace('OEBPS/', ''));
|
|
450
|
+
expect(images.length).toBeGreaterThan(2);
|
|
451
|
+
const orphans = images.filter((image) => !markup.includes(`"${image}"`));
|
|
452
|
+
expect(orphans, `packaged but never referenced: ${orphans.join(', ')}`).toEqual([]);
|
|
453
|
+
});
|
package/dist/epub/package.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import Reference from '../models/book/Reference';
|
|
8
8
|
import FormatNode from '../models/chapter/FormatNode';
|
|
9
9
|
import Parser from '../models/chapter/Parser';
|
|
10
|
-
import
|
|
10
|
+
import EmbedNode from '../models/chapter/EmbedNode';
|
|
11
|
+
import { chapterPath, document_, endnoteSection, escapeAttribute, escapeText, serializeChapter, serializeEmbed, serializeFormat, } from './xhtml';
|
|
11
12
|
export const MIMETYPE = 'application/epub+zip';
|
|
12
13
|
/** Where the package's documents live inside the archive. */
|
|
13
14
|
export const CONTENT_DIRECTORY = 'OEBPS';
|
|
@@ -149,6 +150,18 @@ export function safeFilename(title) {
|
|
|
149
150
|
.toLowerCase();
|
|
150
151
|
return cleaned.length > 0 ? cleaned.slice(0, 80) : 'book';
|
|
151
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* A chapter or back-matter page's header image, rendered the way the web
|
|
155
|
+
* reader does: above the title, with its caption and credit. The image is
|
|
156
|
+
* stored as Bookdown and already packaged by Edition.getEmbeds(), so this only
|
|
157
|
+
* has to render it.
|
|
158
|
+
*/
|
|
159
|
+
function headerImage(edition, image, context) {
|
|
160
|
+
if (image === null || image.trim().length === 0)
|
|
161
|
+
return '';
|
|
162
|
+
const node = Parser.parseEmbed(edition, image);
|
|
163
|
+
return node instanceof EmbedNode ? serializeEmbed(node, context) : '';
|
|
164
|
+
}
|
|
152
165
|
/** The plain-text form of a Bookdown string, for metadata. */
|
|
153
166
|
function text(edition, bookdown) {
|
|
154
167
|
return Parser.parseFormat(edition, bookdown).toText().trim();
|
|
@@ -203,6 +216,7 @@ export function chapterDocument(edition, chapter, context) {
|
|
|
203
216
|
const section = chapter.getSection();
|
|
204
217
|
const authors = chapter.getAuthors();
|
|
205
218
|
const header = `<header>
|
|
219
|
+
${headerImage(edition, chapter.getImage(), context)}
|
|
206
220
|
${number !== undefined
|
|
207
221
|
? `<span class="chapter-number">Chapter ${number}</span>`
|
|
208
222
|
: ''}
|
|
@@ -242,6 +256,7 @@ export function referencesDocument(edition, context) {
|
|
|
242
256
|
return undefined;
|
|
243
257
|
const header = edition.getHeader('references');
|
|
244
258
|
const body = `<section epub:type="bibliography">
|
|
259
|
+
${headerImage(edition, edition.getImage('references'), context)}
|
|
245
260
|
<h1>${escapeText(header)}</h1>
|
|
246
261
|
${ids
|
|
247
262
|
.map((id) => `<p class="reference" id="ref-${escapeAttribute(id)}">${referenceContent(references[id], context)}</p>`)
|
|
@@ -267,6 +282,7 @@ export function glossaryDocument(edition, context) {
|
|
|
267
282
|
// content, so there is no way to put a block inside it that would survive.
|
|
268
283
|
// The semantics are carried by the classes instead.
|
|
269
284
|
const body = `<section epub:type="glossary">
|
|
285
|
+
${headerImage(edition, edition.getImage('glossary'), context)}
|
|
270
286
|
<h1>${escapeText(header)}</h1>
|
|
271
287
|
${ids
|
|
272
288
|
.map((id) => {
|
package/dist/epub/xhtml.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type Edition from '../models/book/Edition';
|
|
15
15
|
import type ChapterNode from '../models/chapter/ChapterNode';
|
|
16
|
+
import EmbedNode from '../models/chapter/EmbedNode';
|
|
16
17
|
import FormatNode from '../models/chapter/FormatNode';
|
|
17
18
|
/**
|
|
18
19
|
* Escape text content. Order matters: ampersands first, or we escape our own
|
|
@@ -56,6 +57,11 @@ export declare function serializeChapter(chapter: ChapterNode, context: Serializ
|
|
|
56
57
|
};
|
|
57
58
|
/** The document filename for a chapter. */
|
|
58
59
|
export declare function chapterPath(chapterID: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Serialize a standalone embed, for the header images that a chapter and the
|
|
62
|
+
* back matter pages carry. Those are stored as Bookdown, parsed by the caller.
|
|
63
|
+
*/
|
|
64
|
+
export declare function serializeEmbed(node: EmbedNode, context: SerializationContext): string;
|
|
59
65
|
/** Serialize a standalone format node, for titles, captions and definitions. */
|
|
60
66
|
export declare function serializeFormat(node: FormatNode, context: SerializationContext): string;
|
|
61
67
|
/** Render gathered footnotes as an endnote section with links back to the text. */
|
package/dist/epub/xhtml.js
CHANGED
|
@@ -326,6 +326,18 @@ function definition(node, local) {
|
|
|
326
326
|
? phrase
|
|
327
327
|
: `<a class="definition" href="glossary.xhtml#gloss-${escapeAttribute(id)}">${phrase}</a>`;
|
|
328
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Serialize a standalone embed, for the header images that a chapter and the
|
|
331
|
+
* back matter pages carry. Those are stored as Bookdown, parsed by the caller.
|
|
332
|
+
*/
|
|
333
|
+
export function serializeEmbed(node, context) {
|
|
334
|
+
return embed(node, {
|
|
335
|
+
...context,
|
|
336
|
+
footnotes: [],
|
|
337
|
+
headers: [],
|
|
338
|
+
endnotes: [],
|
|
339
|
+
});
|
|
340
|
+
}
|
|
329
341
|
/** Serialize a standalone format node, for titles, captions and definitions. */
|
|
330
342
|
export function serializeFormat(node, context) {
|
|
331
343
|
return inline(node, {
|