Make PDF Contents entries clickable links to each step's page

Adds PdfBuilder.linkRect() for /Subtype /Link annotations with a
/Dest pointing at another page's destination. TOC entries record a
target placeholder that the per-step loop fills in once it knows
which page the step landed on, so clicking a Contents line jumps the
reader straight to that step.
This commit is contained in:
2026-06-15 13:22:51 -05:00
parent 7229b154c9
commit b51a4c46ce
3 changed files with 77 additions and 3 deletions
+17 -3
View File
@@ -252,13 +252,25 @@ function exportPdf(ast, outDir, template = {}) {
y = M;
}
// Filled in below as each step claims its page, so the Contents entries
// above (already laid out before pagination starts) can link to it.
const tocTargets = new Map(); // stepId -> { pageIndex }
if (tpl.includeToc && ast.steps.length > 1) {
writeLines('Contents', { size: 16, font: 'F2' });
y += 4;
const tocSize = 10.5;
const lineH = tocSize * 1.35;
for (const step of ast.steps) {
writeLines(`${step.number}. ${step.title || 'Untitled step'}`, {
size: 10.5, indent: 14 * step.depth,
});
const indent = 14 * step.depth;
const target = {};
tocTargets.set(step.stepId, target);
for (const line of pdf.wrapText(`${step.number}. ${step.title || 'Untitled step'}`, tocSize, usableW - indent, 'F1')) {
ensure(lineH);
pdf.text(line, M + indent, y, { size: tocSize });
pdf.linkRect(M + indent, y, usableW - indent, lineH, target);
y += lineH;
}
}
pdf.addPage();
y = M;
@@ -283,6 +295,8 @@ function exportPdf(ast, outDir, template = {}) {
// together — never split across a page boundary.
ensure(headHeight);
pdf.bookmark(`${step.number}. ${step.title || 'Untitled step'}`);
const tocTarget = tocTargets.get(step.stepId);
if (tocTarget) tocTarget.pageIndex = pdf.pages.length - 1;
const headSize = step.depth > 0 ? 12 : 14;
writeLines(`${step.number}. ${step.title || 'Untitled step'}${step.skipped ? ' (skipped)' : ''}`, { size: headSize, font: 'F2' });
pdf.rect(M, y, usableW, 0.8, { fill: [225, 228, 232] });