1037b84eaa
scripts/cover.js computes the exact cover canvas dimensions from the page count in book-meta.json (written by build.js), using the KDP Premium Color spine formula (0.002347 in/page), and renders a Nunjucks template to a single PDF containing back cover, spine, and front cover with bleed (0.125 in) and safe-zone overlay guides. - `pnpm cover` — generate output/cover.pdf - `pnpm all` — build interior + both PDFs in one command - Cover artwork slots: images/cover/front.png, images/cover/back.png Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78 lines
2.4 KiB
JavaScript
78 lines
2.4 KiB
JavaScript
import { readdir, readFile, writeFile, access } from 'fs/promises';
|
||
import { join, resolve, dirname } from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
import matter from 'gray-matter';
|
||
import { marked } from 'marked';
|
||
import nunjucks from 'nunjucks';
|
||
|
||
const __dir = dirname(fileURLToPath(import.meta.url));
|
||
const root = resolve(__dir, '..');
|
||
|
||
// Nunjucks: load templates relative to project root
|
||
const env = nunjucks.configure(join(root, 'templates'), { autoescape: true });
|
||
|
||
async function fileExists(path) {
|
||
try { await access(path); return true; } catch { return false; }
|
||
}
|
||
|
||
async function loadStories() {
|
||
const contentDir = join(root, 'content');
|
||
const files = (await readdir(contentDir)).filter(f => f.endsWith('.md')).sort();
|
||
|
||
const stories = [];
|
||
let finale = null;
|
||
|
||
for (const file of files) {
|
||
const raw = await readFile(join(contentDir, file), 'utf-8');
|
||
const { data, content } = matter(raw);
|
||
|
||
if (data.type === 'front-matter') continue;
|
||
|
||
if (data.type === 'finale') {
|
||
finale = data;
|
||
continue;
|
||
}
|
||
|
||
// Split body by --- into individual scene texts
|
||
const sceneTexts = content.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
|
||
|
||
const scenes = await Promise.all(
|
||
(data.scenes || []).map(async (scene, i) => {
|
||
const imagePath = join(root, scene.image);
|
||
const imageExists = await fileExists(imagePath);
|
||
return {
|
||
...scene,
|
||
imageExists,
|
||
// Relative path from output/book.html back to project root
|
||
image: `../${scene.image}`,
|
||
html: marked.parse(sceneTexts[i] || ''),
|
||
};
|
||
})
|
||
);
|
||
|
||
stories.push({ ...data, scenes });
|
||
}
|
||
|
||
return { stories, finale: finale || {} };
|
||
}
|
||
|
||
async function build() {
|
||
const { stories, finale } = await loadStories();
|
||
|
||
const html = env.render('book.html', { stories, finale });
|
||
|
||
const outPath = join(root, 'output', 'book.html');
|
||
await writeFile(outPath, html, 'utf-8');
|
||
|
||
// title page + copyright + TOC + (4 scenes × 2 pages per story) + finale
|
||
const pageCount = 3 + stories.reduce((acc, s) => acc + s.scenes.length * 2, 0) + 1;
|
||
await writeFile(
|
||
join(root, 'output', 'book-meta.json'),
|
||
JSON.stringify({ pageCount, storyCount: stories.length, builtAt: new Date().toISOString() }, null, 2),
|
||
'utf-8'
|
||
);
|
||
console.log(`Built output/book.html — ${stories.length} stories, ${pageCount} pages`);
|
||
}
|
||
|
||
build().catch(err => { console.error(err); process.exit(1); });
|