book-cover-generator 1.6.0 → 1.7.0

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 CHANGED
@@ -7,7 +7,7 @@
7
7
  ![Types](https://img.shields.io/badge/types-included-blue)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
9
9
 
10
- Generate book covers from a **title** and an **author**. 34 styles, 28 palettes or your own colours, a seeded randomizer, any size, and export to **SVG, PNG, JPEG or WebP**. The library has no dependencies and ships TypeScript types. It's written as plain ES modules, so it runs in the browser and in Node.
10
+ Generate book covers from a **title** and an **author**. 39 styles, 28 palettes or your own colours, a seeded randomizer, any size, and export to **SVG, PNG, JPEG or WebP**. The library has no dependencies and ships TypeScript types. It's written as plain ES modules, so it runs in the browser and in Node.
11
11
 
12
12
  **[▶ Open the generator](https://covers.otsirh.dev)**
13
13
 
@@ -15,9 +15,9 @@ Generate book covers from a **title** and an **author**. 34 styles, 28 palettes
15
15
 
16
16
  ## Features
17
17
 
18
- - **34 styles in six families.**
18
+ - **39 styles in six families.**
19
19
  - Typographic: Big Type, Minimal, Big Letter, Swiss.
20
- - Genre: Thriller, Botanical, Noir, Blueprint.
20
+ - Genre: Thriller, Botanical, Noir, Blueprint, Hollow (horror), Ribbon (romance), Crest (fantasy), Storybook (children's), Big Idea (non-fiction).
21
21
  - Vintage: Art Deco, Classic Paperback, Engraved, Sunburst, Typewriter.
22
22
  - Scenic: Landscape, Retro Sun, Silhouette, Constellation, Orbit, Waves, Aurora, Bookshelf.
23
23
  - Geometric: Bauhaus, Split, Blocks, Concentric, Stripes.
@@ -33,13 +33,13 @@ Generate book covers from a **title** and an **author**. 34 styles, 28 palettes
33
33
 
34
34
  On [covers.otsirh.dev](https://covers.otsirh.dev) you type a title and author (press Enter in the title to break its line where you want), and if you like a subtitle, a series line ("Book Two of the Ember Cycle"), a credit ("Translated by…") and a badge ("Winner of the Hugo Award", set as a seal in a corner clear of the text). Then you choose:
35
35
 
36
- - **a style**, from a rail that previews your book in all 34 styles. Eleven styles let you pin a choice the variation otherwise makes, such as Silhouette's city or forest, or where Aurora puts the title;
36
+ - **a style**, from a rail that previews your book in all 39 styles (its Genre menu shows only those that suit a genre, and Randomize then stays in it). Fifteen styles let you pin a choice the variation otherwise makes, such as Silhouette's city or forest, or where Aurora puts the title;
37
37
  - **title type**: any of the nine fonts instead of the style's, and the title in capitals or as typed. It stays when you change style, so the rail shows every style in your type;
38
38
  - **a palette**, from swatches that show the background, text colour and accents, or **Custom** for your own five colours (it starts from the palette on the cover and shows the text contrast);
39
39
  - **a variation**, the seed behind the artwork: the strip under the preview shows six others to click;
40
40
  - **a format**: paperback, eBook, square, or **Custom** for any size in pixels, typed in or picked from common sizes (Kindle, print trim sizes at 300 dpi, audiobook, social posts).
41
41
 
42
- **Store size** shrinks the preview to the size a store lists covers at, to check the title still reads. **Randomize** reshuffles everything, except what you've locked. Undo and redo step back and forth through your designs, and so do the browser's Back and Forward buttons. **Copy link** shares the exact design, because the URL holds every setting. The export panel tells you which size suits Kindle, print or audiobook covers.
42
+ **Store size** shrinks the preview to the size a store lists covers at, to check the title still reads. **Randomize** reshuffles everything, except what you've locked. Undo and redo step back and forth through your designs, and so do the browser's Back and Forward buttons. **Copy link** shares the exact design, because the URL holds every setting, and **Copy image link** gives the [HTTP API](#http-api) URL that draws it, to put in any page. The export panel tells you which size suits Kindle, print or audiobook covers.
43
43
 
44
44
  **Mockup** shows and downloads the cover as a 3D book, a flat shot, on an e-reader, or on a social card (a 1200 × 630 link preview, a square post or a story, with a line such as "Out now").
45
45
 
@@ -58,6 +58,30 @@ Keyboard shortcuts:
58
58
 
59
59
  The single-key shortcuts can be turned off under the controls (for example with speech input).
60
60
 
61
+ ## HTTP API
62
+
63
+ The site draws covers from a URL too, so a page can show one with nothing installed:
64
+
65
+ ```html
66
+ <img src="https://covers.otsirh.dev/api/cover.svg?title=Dune&author=Frank+Herbert&style=retro" alt="Dune" />
67
+ ```
68
+
69
+ - `GET /api/cover.svg` answers with the cover as SVG.
70
+ - `GET /api/mockup.svg` answers with a [mockup](#mockups) of it. Add `kind` (`book`, `flat`, `ereader`, `card`, `square`, `story`), `text` for the line on a card, square or story, and `background`.
71
+
72
+ The query takes the options' own names (`title`, `author`, `subtitle`, `series`, `credit`, `badge`, `style`, `palette`, `seed`, `genre`, `font`, `lang`, `width`, `height`) and the app's link parameters, so the part of an app link after `#` draws the same design (the server measures text with the widths of the fonts as macOS draws them, so on another system a line can break a little differently from the app's preview):
73
+
74
+ | parameter | value |
75
+ | --------------------- | ---------------------------------------------------------------------------------------------- |
76
+ | `format` | `paperback`, `ebook`, `square`, or a size in px such as `1600x2560` |
77
+ | `case` | the title `upper`, or `normal` (as typed) |
78
+ | `texture` | `1` (on) or `0` (off) |
79
+ | `variant` | pinned choices as `name.value`, joined by `-`: `field.pale-emblem.sun` |
80
+ | `palette` | a palette id, or your own colours as `custom-0b132b-f8f9fa-ff6b6b` (background, text, accents) |
81
+ | `bg`, `ink`, `accent` | your own colours, as hex (the `#` is optional; accents separated by commas) |
82
+
83
+ A title breaks its line at `%0A`. Anyone may embed the answers (CORS and `Cross-Origin-Resource-Policy` allow it), and the headers `X-Cover-Style`, `X-Cover-Palette` and `X-Cover-Version` say what was drawn. The same URL always draws the same cover in a version of the library, so answers are cached for a day. Options the library rejects get a `400` with the reason as plain text.
84
+
61
85
  ## Library usage
62
86
 
63
87
  ### Browser (no install)
@@ -116,7 +140,7 @@ npx book-cover-generator --batch books.csv --style minimal --seed series --out-d
116
140
  npx book-cover-generator "Salt and Static" --count 12 -o ideas/cover.svg
117
141
  ```
118
142
 
119
- Options: `-s/--style`, `-p/--palette`, `--bg`, `--ink`, `--accent`, `--subtitle`, `--texture`, `--seed`, `--format` (`paperback`, `ebook` or `square`), `--width`, `--height`, `--size`, `--lang`, `--series`, `--credit`, `--badge`, `--font`, `--case upper|normal`, `--variant name=value` (`--list` shows each style's), `-o/--out`, `--count`, `--batch <file|->`, `--out-dir`, `--json`, `--list`, `--help`.
143
+ Options: `-s/--style`, `-p/--palette`, `--bg`, `--ink`, `--accent`, `--subtitle`, `--texture`, `--seed`, `--format` (`paperback`, `ebook` or `square`), `--width`, `--height`, `--size`, `--genre`, `--lang`, `--series`, `--credit`, `--badge`, `--font`, `--case upper|normal`, `--variant name=value` (`--list` shows each style's), `-o/--out`, `--count`, `--batch <file|->`, `--out-dir`, `--json`, `--list`, `--help`.
120
144
 
121
145
  ## API
122
146
 
@@ -144,9 +168,12 @@ Same as `generateCover`, but also tells you which style and palette were chosen.
144
168
  | `height` | `number` | `width × 1.5` | px, 50–10000 |
145
169
  | `lang` | `string` | inferred | language of the text, such as `'ja'` |
146
170
  | `variants` | `object` | `{}` | pin a style's choices: `{ scene: 'forest' }` |
171
+ | `genre` | `string` | | with a random style, one that suits this genre |
147
172
  | `font` | `string` | the style's | the title's font, one of `FONTS` |
148
173
  | `titleCase` | `string` | the style's | `'upper'` (capitals) or `'normal'` (as typed) |
149
174
 
175
+ Styles, palettes, genres and fonts may be given by id or by name, in any case: `'artdeco'`, `'Art Deco'` and `'ART DECO'` are the same style, and a book list or a link can name them as the app's menus do.
176
+
150
177
  `lang` is set on the cover so Chinese characters get the right glyphs for the language. Without it, text with kana is marked Japanese and text with Hangul Korean; pass `'zh-Hans'`, `'zh-Hant'` or `'ja'` for text in Chinese characters only.
151
178
 
152
179
  Titles and subtitles longer than 300 characters (authors: 200) are cut. With `'random'`, the style and palette are picked from the seed, so you get the same result every time for the same seed. Some styles prefer dark or light palettes when they pick one themselves. Invalid options throw an error that says what's wrong.
@@ -154,13 +181,16 @@ Titles and subtitles longer than 300 characters (authors: 200) are cut. With `'r
154
181
  ### Other exports
155
182
 
156
183
  - `styles`: array of style ids
157
- - `listStyles()`: `[{ id, name, description, group, variants }]`. `variants` lists the choices a style lets you pin, as `[{ name, label, values: [{ value, label }] }]`; pin them with `createCover({ variants: { scene: 'forest' } })`, and `createCover` reports the ones it used in `variants`. Names a style lacks are ignored; a value it lacks throws
184
+ - `genres`: `[{ id, name }]`, the genres styles suit: `literary`, `thriller`, `horror`, `romance`, `fantasy`, `children`, `nonfiction`
185
+ - `listStyles()`: `[{ id, name, description, group, variants, genres }]`. `variants` lists the choices a style lets you pin, as `[{ name, label, values: [{ value, label }] }]`; pin them with `createCover({ variants: { scene: 'forest' } })`, and `createCover` reports the ones it used in `variants`. Names a style lacks are ignored. A value no style offers throws; with a style you name, so does a value that style lacks (a style picked at random takes only the pins it can)
158
186
  - `palettes`: `[{ id, name, bg, ink, accents, dark? }]`
159
187
  - `randomSeed()`: a short random seed string
160
188
  - `pickStyle(options)`: the style id `createCover` would use for these options (resolving `'random'` from the seed), without drawing the cover
161
189
  - `registerStyle({ id, name, description, group, prefer, render(ctx) })`: add your own style (see below). Replacing a built-in style takes `registerStyle(style, { replace: true })`; `unregisterStyle(id)` removes a style or brings the built-in back
162
190
  - `registerPalette({ id, name, bg, ink, accents })`: add a palette to use by id, as `palette: 'brand'` (`{ replace: true }` for a built-in id); `unregisterPalette(id)` removes it. Registered styles and palettes can also be picked by `'random'`, so registering one changes what a seed gives
163
191
  - `parseBookList(text)`: a list of books (CSV with commas, semicolons or tabs and a header row naming a `title` column, JSON lines, or one "Title | Author | Subtitle" per line) as cover options, one per book
192
+ - `styleIdFor(name)`: the id of the style a name or id names, in any case (`'Art Deco'` → `'artdeco'`), or `undefined`
193
+ - `optionsForBook(book, defaults)`: the options for one of those books over the options every book shares (its own values win; a genre the shared style doesn't suit picks a style that does)
164
194
  - `noteText(x0, y0, x1, y1)` and `recordText(draw)`: note where a custom style writes its own text (so a badge stays clear of it), and collect what `draw` notes
165
195
  - `version`: the library's version (artwork for a seed can change between minor versions)
166
196
  - `sizeLimits` (`{ min: 50, max: 10000, ratio: 3 }`) and `formats` (`paperback`, `ebook` and `square` as `[width, height]` at 600 px wide): the sizes covers can have, and the app's shapes
@@ -256,7 +286,7 @@ registerStyle({
256
286
 
257
287
  ## Notes
258
288
 
259
- - **Fonts.** Covers only use system font stacks (Georgia, Helvetica, Futura, Didot, Impact, Courier and others), each with close matches on Windows, Android and Linux. This is because an SVG drawn into a canvas for PNG/JPEG export can't load web fonts. In the browser, text is measured with a canvas, so it fits exactly. In Node, widths are estimated; plug in a real font engine with `setTextMeasurer` if you need exact fits there.
289
+ - **Fonts.** Covers only use system font stacks (Georgia, Helvetica, Futura, Didot, Impact, Courier and others), each with close matches on Windows, Android and Linux. This is because an SVG drawn into a canvas for PNG/JPEG export can't load web fonts. In the browser, text is measured with a canvas, so it fits exactly. In Node (and the CLI and HTTP API), the built-in fonts are measured from their characters' widths as macOS draws them, so text fits much as it does in the app; fonts of your own are estimated. Plug in a real font engine with `setTextMeasurer` if you need exact fits there.
260
290
  - **Deterministic.** The same options always give the same artwork. In Node the SVG is identical every time. In the browser, text is measured with the fonts installed on that computer, so line breaks can differ slightly between machines. Registering a custom style changes which style `style: 'random'` picks for a given seed.
261
291
  - **Versioning.** Artwork for a given seed can change in minor releases (a style gets better). Pin an exact version if you need byte-identical covers forever.
262
292
  - **Browser support.** Current Chrome, Edge, Firefox and Safari. Every release is tested in Chromium, Firefox and WebKit, including PNG and JPEG export.
@@ -285,7 +315,7 @@ Open `examples/contact-sheet.html?palettes=noir,mint&seeds=a,b` on the dev serve
285
315
 
286
316
  ```
287
317
  src/ the library (plain ES modules + .d.ts types)
288
- styles/ the 34 styles, one file per family
318
+ styles/ the 39 styles, in a file per family
289
319
  bin/ the CLI
290
320
  site/ the web app (index.html is at the root)
291
321
  public/ files served at the site root: headers, 404, icons, manifest, service worker
@@ -293,9 +323,10 @@ test/ unit tests (node --test)
293
323
  test-d/ type tests (tsc)
294
324
  e2e/ browser tests (Playwright)
295
325
  scripts/ build, dev server and asset generation
326
+ worker/ the HTTP API (the site's Worker)
296
327
  ```
297
328
 
298
- The site is a static [Cloudflare Workers](https://developers.cloudflare.com/workers/static-assets/) deployment configured in `wrangler.jsonc`, with security headers in `public/_headers`.
329
+ The site is a [Cloudflare Workers](https://developers.cloudflare.com/workers/static-assets/) deployment configured in `wrangler.jsonc`: static assets, with security headers in `public/_headers`, and a Worker (`worker/index.js`) that answers `/api/*`. The dev server answers `/api/*` the same way.
299
330
 
300
331
  ## Contributing
301
332
 
package/bin/book-cover.js CHANGED
@@ -1,14 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { dirname, join } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
- import { createCover, listStyles, palettes, formats, parseBookList } from '../src/index.js';
5
+ import { createCover, listStyles, palettes, formats, parseBookList, optionsForBook, genres } from '../src/index.js';
6
+ import { hex, slug } from '../src/shared.js';
6
7
 
7
8
  const HELP = `Usage: book-cover "<title>" ["<author>"] [options]
8
9
 
9
10
  Options:
10
11
  -s, --style <id> style id or "random" (default: random from seed)
11
12
  -p, --palette <id> palette id or "random" (default: random from seed)
13
+ --genre <id> with a random style, one that suits this genre:
14
+ literary, thriller, horror, romance, fantasy, children
15
+ or nonfiction
12
16
  --bg <hex> your own colours instead of a palette: background,
13
17
  --ink <hex> text,
14
18
  --accent <hex> and accents (repeat it, or separate them with commas)
@@ -30,7 +34,7 @@ Options:
30
34
  --lang <tag> language of the text, e.g. ja or zh-Hant (default: inferred)
31
35
  --batch <file> a cover for each book in a list (- reads stdin): CSV or
32
36
  TSV with a header row (title, author, subtitle, series,
33
- credit, badge, style, palette, seed, font, case, lang,
37
+ credit, badge, style, genre, palette, seed, font, case, lang,
34
38
  width, height, texture), JSON lines, or one book per
35
39
  line as "Title | Author | Subtitle"; the other options
36
40
  apply to every book the list doesn't say otherwise for
@@ -57,6 +61,7 @@ try {
57
61
  options: {
58
62
  style: { type: 'string', short: 's' },
59
63
  palette: { type: 'string', short: 'p' },
64
+ genre: { type: 'string' },
60
65
  bg: { type: 'string' },
61
66
  ink: { type: 'string' },
62
67
  accent: { type: 'string', multiple: true },
@@ -103,10 +108,13 @@ function preflight() {
103
108
  console.log('Styles:');
104
109
  for (const s of listStyles()) {
105
110
  console.log(` ${s.id.padEnd(14)} ${s.description}`);
111
+ if (s.genres.length) console.log(` ${''.padEnd(14)} genres: ${s.genres.join(', ')}`);
106
112
  for (const v of s.variants) {
107
113
  console.log(` ${''.padEnd(14)} --variant ${v.name}=${v.values.map((x) => x.value).join('|')}`);
108
114
  }
109
115
  }
116
+ console.log('\nGenres (--genre):');
117
+ for (const g of genres) console.log(` ${g.id.padEnd(14)} ${g.name}`);
110
118
  console.log('\nPalettes:');
111
119
  for (const p of palettes) console.log(` ${p.id.padEnd(14)} ${p.name}${p.dark ? ' (dark)' : ''}`);
112
120
  return false;
@@ -166,9 +174,6 @@ function dimensions() {
166
174
  return { width: W, height: Math.round((W * h) / w) };
167
175
  }
168
176
 
169
- /** A colour as typed, with the # added (it starts a comment in most shells). @param {string} c */
170
- const hex = (c) => (c.trim().startsWith('#') ? c.trim() : `#${c.trim()}`);
171
-
172
177
  /** The palette: an id, or your own colours from --bg, --ink and --accent. */
173
178
  function palette() {
174
179
  const custom = values.bg !== undefined || values.ink !== undefined || values.accent !== undefined;
@@ -231,18 +236,6 @@ function fileFor(out, n, total, seed) {
231
236
  */
232
237
  const safeName = (seed) => seed.replace(/[^\p{L}\p{N}._-]+/gu, '-').replace(/^[.-]+/, '') || 'cover';
233
238
 
234
- /** A title as part of a file name: lower case, accents off Latin letters, dashes between words. @param {string} title */
235
- function slug(title) {
236
- const words = title
237
- .normalize('NFKD')
238
- .replace(/(\p{Script=Latin})[\u0300-\u036f]+/gu, '$1')
239
- .normalize('NFC')
240
- .toLowerCase()
241
- .replace(/[^\p{L}\p{M}\p{N}]+/gu, '-')
242
- .replace(/^-+/, '');
243
- return Array.from(words).slice(0, 60).join('').replace(/-+$/, '') || 'cover';
244
- }
245
-
246
239
  /**
247
240
  * Write a cover for each book in the --batch list into --out-dir; the command line's options
248
241
  * apply to every book the list doesn't say otherwise for.
@@ -266,14 +259,9 @@ function batch(defaults) {
266
259
  books.forEach((book, i) => {
267
260
  const n = i + 1;
268
261
  try {
269
- // A size in the list replaces the command line's.
270
- const sized = book.width !== undefined || book.height !== undefined;
271
- const cover = createCover({
272
- ...defaults,
273
- ...(sized ? { width: undefined, height: undefined } : {}),
274
- ...book,
275
- seed: book.seed ?? values.seed,
276
- });
262
+ // (The book's own values win, a size in the list replaces the command line's, and a genre
263
+ // --style doesn't suit picks a style that does: see optionsForBook.)
264
+ const cover = createCover({ ...optionsForBook(book, defaults), seed: book.seed ?? values.seed });
277
265
  const file = join(dir, `${String(n).padStart(digits, '0')}-${slug(cover.title)}.svg`);
278
266
  writeFileSync(file, cover.svg);
279
267
  const { style, palette: paletteId, seed, width, height } = cover;
@@ -298,6 +286,7 @@ function run() {
298
286
  badge: values.badge,
299
287
  texture: values.texture,
300
288
  style: values.style,
289
+ genre: values.genre,
301
290
  palette: palette(),
302
291
  lang: values.lang,
303
292
  variants: variants(),
@@ -317,6 +306,7 @@ function run() {
317
306
  badge: values.badge,
318
307
  texture: values.texture,
319
308
  style: values.style,
309
+ genre: values.genre,
320
310
  palette: palette(),
321
311
  lang: values.lang,
322
312
  variants: variants(),
@@ -332,6 +322,7 @@ function run() {
332
322
  const choices = { style, palette: paletteId, seed: cover.seed, width, height, lang, variants: cover.variants };
333
323
  if (values.out) {
334
324
  const file = fileFor(values.out, n, total, cover.seed);
325
+ mkdirSync(dirname(file), { recursive: true });
335
326
  writeFileSync(file, cover.svg);
336
327
  if (values.json) process.stdout.write(JSON.stringify({ file, ...choices }) + '\n');
337
328
  else console.error(`Wrote ${file} (style: ${style}, palette: ${paletteId}, ${width}x${height})`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "book-cover-generator",
3
- "version": "1.6.0",
4
- "description": "Generate SVG book covers from a title and an author. 34 styles, 28 palettes, seeded randomizer, zero dependencies.",
3
+ "version": "1.7.0",
4
+ "description": "Generate SVG book covers from a title and an author. 39 styles, 28 palettes, seeded randomizer, zero dependencies.",
5
5
  "keywords": [
6
6
  "book",
7
7
  "cover",
@@ -60,6 +60,7 @@
60
60
  "test:e2e": "playwright test",
61
61
  "check": "npm run lint && npm run format:check && npm run typecheck && npm run lint:package && npm test",
62
62
  "assets": "node scripts/generate-assets.js",
63
+ "metrics": "node scripts/generate-metrics.js",
63
64
  "deploy": "npm run check && npm run test:e2e && wrangler deploy"
64
65
  },
65
66
  "devDependencies": {
package/src/books.js CHANGED
@@ -10,6 +10,7 @@ const COLUMNS = /** @type {const} */ ({
10
10
  credit: 'credit',
11
11
  badge: 'badge',
12
12
  style: 'style',
13
+ genre: 'genre',
13
14
  palette: 'palette',
14
15
  seed: 'seed',
15
16
  font: 'font',
@@ -21,8 +22,12 @@ const COLUMNS = /** @type {const} */ ({
21
22
  texture: 'texture',
22
23
  });
23
24
 
24
- /** Separators of a plain line: a tab, or a bar or dash with spaces around it. */
25
- const LINE_SEPARATOR = /\t| \| | — | – /;
25
+ /**
26
+ * Separators of a plain line, by precedence: a tab, a bar, or a dash, with spaces around it. A line
27
+ * is split at the first kind it uses only, so "Harry Potter – The Stone | J. K. Rowling" keeps
28
+ * its dash in the title.
29
+ */
30
+ const LINE_SEPARATORS = ['\t', ' | ', ' — ', ' – '];
26
31
 
27
32
  /**
28
33
  * Split CSV (or TSV) text into rows of fields: quoted fields may hold the delimiter, quotes ("")
@@ -84,7 +89,8 @@ function optionsFrom(named) {
84
89
  // (Numbers stay numbers for sizes and the seed; any other column is text, a title of 1984 too.)
85
90
  const value = typeof raw === 'number' && option !== 'title' ? raw : String(raw).trim();
86
91
  if (value === '') continue;
87
- if (option === 'width' || option === 'height') out[option] = Number(value);
92
+ // (A size that isn't a number is kept as written, so the error says what the list says.)
93
+ if (option === 'width' || option === 'height') out[option] = Number.isFinite(Number(value)) ? Number(value) : value;
88
94
  else out[option] = option === 'seed' ? value : String(value);
89
95
  }
90
96
  return /** @type {import('./index.js').CoverOptions} */ (out);
@@ -94,8 +100,8 @@ function optionsFrom(named) {
94
100
  * Read a list of books into cover options, one per book (rows without a title are left out):
95
101
  *
96
102
  * - CSV or TSV whose first row names its columns (`title`, `author`, `subtitle`, `series`,
97
- * `credit`, `badge`, `style`, `palette`, `seed`, `font`, `case`, `lang`, `width`, `height`,
98
- * `texture`; others are ignored);
103
+ * `credit`, `badge`, `style`, `genre`, `palette`, `seed`, `font`, `case`, `lang`, `width`,
104
+ * `height`, `texture`; others are ignored);
99
105
  * - JSON lines, one object per book, with the same names;
100
106
  * - else one book per line: "Title | Author | Subtitle" (a tab, or " — " or " – ", also separates).
101
107
  *
@@ -134,7 +140,8 @@ export function parseBookList(text) {
134
140
  books = rows.map((row) => optionsFrom(Object.fromEntries(names.map((name, i) => [name, row[i] ?? '']))));
135
141
  } else {
136
142
  books = lines.map(({ line }) => {
137
- const [title, author, subtitle] = line.split(LINE_SEPARATOR);
143
+ const separator = LINE_SEPARATORS.find((sep) => line.includes(sep));
144
+ const [title, author, subtitle] = separator ? line.split(separator) : [line];
138
145
  return optionsFrom({ title, author, subtitle });
139
146
  });
140
147
  }
package/src/index.d.ts CHANGED
@@ -37,10 +37,20 @@ export interface MockupOptions {
37
37
  }
38
38
  /**
39
39
  * Read a list of books into cover options, one per book with a title: CSV or TSV whose first row
40
- * names its columns (title, author, subtitle, series, credit, badge, style, palette, seed, font,
41
- * case, lang, width, height, texture), JSON lines, or one book per line ("Title | Author | Subtitle").
40
+ * names its columns (title, author, subtitle, series, credit, badge, style, genre, palette, seed,
41
+ * font, case, lang, width, height, texture), JSON lines, or one book per line ("Title | Author | Subtitle").
42
42
  */
43
43
  export declare function parseBookList(text: string): CoverOptions[];
44
+ /**
45
+ * Cover options for a book from a list over `defaults`, the options every book shares: the book's
46
+ * own values win, a size it gives replaces both sides of the defaults', a book naming another
47
+ * style than the defaults' drops their pinned variants (with no shared style, it keeps the ones
48
+ * its own style offers), and one naming a genre the defaults' style doesn't suit gets a style that
49
+ * suits it. A shared style that doesn't exist throws.
50
+ */
51
+ export declare function optionsForBook(book: CoverOptions, defaults?: CoverOptions): CoverOptions;
52
+ /** The id of the style that `name` names, by id or name in any case ('Art Deco', 'artdeco'), or undefined. */
53
+ export declare function styleIdFor(name: string): string | undefined;
44
54
  /** The cover as a product shot (flat, book, e-reader) or on a social card, as SVG. */
45
55
  export declare function createMockup(
46
56
  cover: Cover,
@@ -92,7 +102,12 @@ export type StyleId =
92
102
  | 'engraved'
93
103
  | 'papercut'
94
104
  | 'silhouette'
95
- | 'blueprint';
105
+ | 'blueprint'
106
+ | 'hollow'
107
+ | 'ribbon'
108
+ | 'crest'
109
+ | 'storybook'
110
+ | 'idea';
96
111
 
97
112
  export type StyleGroup = 'Typographic' | 'Geometric' | 'Pattern' | 'Scenic' | 'Vintage' | 'Genre';
98
113
 
@@ -123,6 +138,8 @@ export interface CoverOptions {
123
138
  texture?: boolean;
124
139
  /** A style id, a registered custom style, or `'random'` (default; picked from the seed). */
125
140
  style?: StyleId | 'random' | (string & {});
141
+ /** With a random style, pick one that suits this genre (see `genres`). */
142
+ genre?: GenreId | (string & {});
126
143
  /** A palette id, a custom palette, or `'random'` / `'auto'` (default; picked from the seed). */
127
144
  palette?: PaletteId | 'random' | 'auto' | CustomPalette | (string & {});
128
145
  /** Controls every random choice. Defaults to a hash of title + author. */
@@ -138,8 +155,9 @@ export interface CoverOptions {
138
155
  lang?: string;
139
156
  /**
140
157
  * Choices to pin instead of leaving them to the seed, such as `{ scene: 'forest' }` for
141
- * Silhouette (see each style's `variants` in `listStyles()`). Names the style doesn't have
142
- * are ignored; a value it doesn't offer throws.
158
+ * Silhouette (see each style's `variants` in `listStyles()`), in any case. Names the style
159
+ * doesn't have are ignored. A value no style offers throws; with a style you name, so does a
160
+ * value that style lacks (a style picked at random takes only the pins it can).
143
161
  */
144
162
  variants?: Readonly<Record<string, string>>;
145
163
  /**
@@ -188,8 +206,14 @@ export interface StyleInfo {
188
206
  group: StyleGroup | (string & {});
189
207
  /** The choices you can pin with `createCover({ variants })`; empty for styles without any. */
190
208
  variants: StyleVariant[];
209
+ /** The genres the style suits (ids from `genres`). */
210
+ genres: string[];
191
211
  }
192
212
 
213
+ export type GenreId = 'literary' | 'thriller' | 'horror' | 'romance' | 'fantasy' | 'children' | 'nonfiction';
214
+ /** The genres styles can suit, as `createCover({ genre })` and `listStyles()` name them. */
215
+ export declare const genres: readonly { readonly id: GenreId; readonly name: string }[];
216
+
193
217
  /** A palette as seen by styles, with derived light/dark colours. */
194
218
  export interface ResolvedPalette extends Palette {
195
219
  readonly accents: readonly string[];
@@ -241,6 +265,11 @@ export interface CoverStyle {
241
265
  group?: StyleGroup | (string & {});
242
266
  /** Background preference when the palette is picked automatically. */
243
267
  prefer?: 'light' | 'dark';
268
+ /**
269
+ * The genres the style suits (ids or names from `genres`), so `createCover({ genre })` can pick it. A
270
+ * replacement for a built-in style suits the built-in's genres unless it names its own.
271
+ */
272
+ genres?: readonly string[];
244
273
  /**
245
274
  * The choices `render` makes with `ctx.choose(name, values)` that people can pin, with labels:
246
275
  * `{ scene: { label: 'Scene', values: { city: 'City', forest: 'Forest' } } }`.
@@ -274,7 +303,9 @@ export declare function unregisterPalette(id: string): boolean;
274
303
  /** A short random seed, e.g. for a "randomize" button. */
275
304
  export declare function randomSeed(): string;
276
305
  /** The style id `createCover` would use for these options (resolving `'random'` from the seed), without drawing. */
277
- export declare function pickStyle(options?: Pick<CoverOptions, 'title' | 'author' | 'seed' | 'style'>): string;
306
+ export declare function pickStyle(
307
+ options?: Pick<CoverOptions, 'title' | 'author' | 'seed' | 'style' | 'genre'>,
308
+ ): string;
278
309
  /** Generate a cover and report the style and palette that were chosen. */
279
310
  export declare function createCover(options?: CoverOptions): Cover;
280
311
  /** Generate a cover and return the SVG markup. */