Add guide metadata, redesign PDF cover, paginate steps, and run exports in a helper process #19

Merged
Tyler merged 16 commits from export_changes into main 2026-06-15 22:10:32 +00:00
2 changed files with 8 additions and 2 deletions
Showing only changes of commit ab280daf63 - Show all commits
+3 -2
View File
@@ -232,7 +232,8 @@ function exportPdf(ast, outDir, template = {}) {
pdf.addPage(); pdf.addPage();
if (tpl.includeCover) { if (tpl.includeCover) {
y = M + usableH * 0.18; // Keep the cover title near the top edge instead of vertically centering it.
y = M;
writeLines(ast.guide.title, { size: 28, font: 'F2' }); writeLines(ast.guide.title, { size: 28, font: 'F2' });
y += 10; y += 10;
pdf.rect(M, y, usableW, 3, { fill: tpl.accentColor }); pdf.rect(M, y, usableW, 3, { fill: tpl.accentColor });
@@ -267,7 +268,7 @@ function exportPdf(ast, outDir, template = {}) {
tocTargets.set(step.stepId, target); tocTargets.set(step.stepId, target);
for (const line of pdf.wrapText(`${step.number}. ${step.title || 'Untitled step'}`, tocSize, usableW - indent, 'F1')) { for (const line of pdf.wrapText(`${step.number}. ${step.title || 'Untitled step'}`, tocSize, usableW - indent, 'F1')) {
ensure(lineH); ensure(lineH);
pdf.text(line, M + indent, y, { size: tocSize }); pdf.text(line, M + indent, y, { size: tocSize, color: tpl.accentColor });
pdf.linkRect(M + indent, y, usableW - indent, lineH, target); pdf.linkRect(M + indent, y, usableW - indent, lineH, target);
y += lineH; y += lineH;
} }
+5
View File
@@ -127,8 +127,12 @@ test('PDF Contents: each entry is a clickable link to its step\'s page', (t) =>
const bookmarks = bookmarkPages(buf); // one per step, in document order const bookmarks = bookmarkPages(buf); // one per step, in document order
const tocTargets = tocLinkTargets(buf, 1); // page 0 = cover, page 1 = Contents const tocTargets = tocLinkTargets(buf, 1); // page 0 = cover, page 1 = Contents
const tocPage = pageContents(buf)[1];
const blueTocLines = [...tocPage.matchAll(/BT \/F1 10\.5 Tf 0\.145 0\.388 0\.922 rg 1 0 0 1 [\d.]+ [\d.]+ Tm \(([^)]*)\) Tj ET/g)]
.map((m) => m[1]);
assert.deepEqual(tocTargets, bookmarks.map((b) => b.pageIndex)); assert.deepEqual(tocTargets, bookmarks.map((b) => b.pageIndex));
assert.deepEqual(blueTocLines, ast.steps.map((step) => `${step.number}. ${step.title || 'Untitled step'}`));
}); });
test('PDF renders under Ghostscript end-to-end', { skip: !hasTool('gs') }, (t) => { test('PDF renders under Ghostscript end-to-end', { skip: !hasTool('gs') }, (t) => {
@@ -169,6 +173,7 @@ test('PDF cover: title in big text above the accent rule, guide metadata below i
const titleY = Number(/\/F2 28 Tf [\d.]+ [\d.]+ [\d.]+ rg 1 0 0 1 [\d.]+ ([\d.]+) Tm \(Configure AcmeSync backups\) Tj/.exec(cover)[1]); const titleY = Number(/\/F2 28 Tf [\d.]+ [\d.]+ [\d.]+ rg 1 0 0 1 [\d.]+ ([\d.]+) Tm \(Configure AcmeSync backups\) Tj/.exec(cover)[1]);
const ruleY = Number(/[\d.]+ [\d.]+ [\d.]+ rg ([\d.]+) ([\d.]+) [\d.]+ 3\.00 re f/.exec(cover)[2]); const ruleY = Number(/[\d.]+ [\d.]+ [\d.]+ rg ([\d.]+) ([\d.]+) [\d.]+ 3\.00 re f/.exec(cover)[2]);
const authorY = Number(/\/F1 11 Tf [\d.]+ [\d.]+ [\d.]+ rg 1 0 0 1 [\d.]+ ([\d.]+) Tm \(Author: Jane Doe\) Tj/.exec(cover)[1]); const authorY = Number(/\/F1 11 Tf [\d.]+ [\d.]+ [\d.]+ rg 1 0 0 1 [\d.]+ ([\d.]+) Tm \(Author: Jane Doe\) Tj/.exec(cover)[1]);
assert.ok(titleY > 740, 'title starts near the top edge');
assert.ok(titleY > ruleY, 'title sits above the accent rule'); assert.ok(titleY > ruleY, 'title sits above the accent rule');
assert.ok(ruleY > authorY, 'metadata sits below the accent rule'); assert.ok(ruleY > authorY, 'metadata sits below the accent rule');
}); });