bookish-press 0.9.2 → 0.9.3

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.
@@ -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
+ });
@@ -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 { chapterPath, document_, endnoteSection, escapeAttribute, escapeText, serializeChapter, serializeFormat, } from './xhtml';
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) => {
@@ -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. */
@@ -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, {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bookish-press",
3
3
  "private": false,
4
- "version": "0.9.2",
4
+ "version": "0.9.3",
5
5
  "type": "module",
6
6
  "description": "A platform for reading and writing readable, accessible, beautiful online books.",
7
7
  "license": "MIT",