Add guide metadata, redesign PDF cover, paginate steps, and run exports in a helper process #19
+3
-2
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user