Files
gstack/make-pdf/src/types.ts
T
Garry Tan 3af86348f6 feat(make-pdf): new /make-pdf skill + orchestrator binary
Turn markdown into publication-quality PDFs. $P generate input.md out.pdf
produces a PDF with 1in margins, intelligent page breaks, page numbers,
running header, CONFIDENTIAL footer, and curly quotes/em dashes — all on
Helvetica so copy-paste extraction works ("S ai li ng" bug avoided).

Architecture (per Codex round 2):
  markdown → render.ts (marked + sanitize + smartypants) → orchestrator
    → $B newtab --json → $B load-html --tab-id → $B js (poll Paged.js)
    → $B pdf --tab-id → $B closetab

browseClient.ts shells out to the compiled browse CLI rather than
duplicating Playwright. --tab-id isolation per render means parallel
$P generate calls don't race on the active tab. try/finally tab cleanup
survives Paged.js timeouts, browser crashes, and output-path failures.

Features in v1:
  --cover              left-aligned cover page (eyebrow + title + hairline rule)
  --toc                clickable static TOC (Paged.js page numbers deferred)
  --watermark <text>   diagonal DRAFT/CONFIDENTIAL layer
  --no-chapter-breaks  opt out of H1-starts-new-page
  --page-numbers       "N of M" footer (default on)
  --tagged --outline   accessible PDF + bookmark outline (default on)
  --allow-network      opt in to external image loading (default off for privacy)
  --quiet --verbose    stderr control

Design decisions locked from the /plan-design-review pass:
  - Helvetica everywhere (Chromium emits single-word Tj operators for
    system fonts; bundled webfonts emit per-glyph and break extraction).
  - Left-aligned body, flush-left paragraphs, no text-indent, 12pt gap.
  - Cover shares 1in margins with body pages; no flexbox-center, no
    inset padding.
  - The reference HTMLs at .context/designs/*.html are the implementation
    source of truth for print-css.ts.

Tests (56 unit + 1 E2E combined-features gate):
  - smartypants: code/URL-safe, verified against 10 fixtures
  - sanitizer: strips <script>/<iframe>/on*/javascript: URLs
  - render: HTML assembly, CJK fallback, cover/TOC/chapter wrap
  - print-css: all @page rules, margin variants, watermark
  - pdftotext: normalize()+copyPasteGate() cross-OS tolerance
  - browseClient: binary resolution + typed error propagation
  - combined-features gate (P0): 2-chapter fixture with smartypants +
    hyphens + ligatures + bold/italic + inline code + lists + blockquote
    passes through PDF → pdftotext → expected.txt diff

Deferred to Phase 4 (future PR): Paged.js vendored for accurate TOC page
numbers, highlight.js for syntax highlighting, drop caps, pull quotes,
two-column, CMYK, watermark visual-diff acceptance.

Plan: .context/ceo-plans/2026-04-19-perfect-pdf-generator.md
References: .context/designs/make-pdf-*.html

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 05:34:05 +08:00

124 lines
3.2 KiB
TypeScript

/**
* make-pdf — shared types.
*
* No runtime code. Imports are safe from any module.
*/
export type PageSize = "letter" | "a4" | "legal" | "tabloid";
export type FontMode = "sans"; // v1: Helvetica only. Future: "serif" | "custom".
/**
* Options for `$P generate` — the public CLI contract.
* Matches the flag set documented in the CEO plan.
*/
export interface GenerateOptions {
input: string; // markdown input path
output?: string; // PDF output path (default: /tmp/<slug>.pdf)
// Page layout
margins?: string; // "1in" | "72pt" | "25mm" | "2.54cm"
marginTop?: string;
marginRight?: string;
marginBottom?: string;
marginLeft?: string;
pageSize?: PageSize; // default "letter"
// Document structure
cover?: boolean;
toc?: boolean;
noChapterBreaks?: boolean; // default: chapter breaks ON
// Branding
watermark?: string; // e.g. "DRAFT"
headerTemplate?: string; // raw HTML
footerTemplate?: string; // raw HTML, mutex with pageNumbers
confidential?: boolean; // default: true
// Output control
pageNumbers?: boolean; // default: true
tagged?: boolean; // default: true (accessible PDF)
outline?: boolean; // default: true (PDF bookmarks)
quiet?: boolean; // suppress progress on stderr
verbose?: boolean; // per-stage timings on stderr
// Network
allowNetwork?: boolean; // default: false
// Metadata
title?: string;
author?: string;
date?: string; // ISO-ish; default: today
}
/**
* Options for `$P preview`.
*/
export interface PreviewOptions {
input: string;
quiet?: boolean;
verbose?: boolean;
// Same render flags as generate so preview matches output
cover?: boolean;
toc?: boolean;
watermark?: string;
noChapterBreaks?: boolean;
confidential?: boolean;
allowNetwork?: boolean;
title?: string;
author?: string;
date?: string;
}
/**
* Parsed page.pdf() options passed to browse.
*/
export interface BrowsePdfOptions {
output: string;
tabId: number;
format?: PageSize;
width?: string;
height?: string;
margins?: {
top: string;
right: string;
bottom: string;
left: string;
};
headerTemplate?: string;
footerTemplate?: string;
pageNumbers?: boolean;
displayHeaderFooter?: boolean;
tagged?: boolean;
outline?: boolean;
printBackground?: boolean;
preferCSSPageSize?: boolean;
toc?: boolean; // signals browse to wait for Paged.js
}
/**
* Exit codes for $P generate.
* Mirror these in orchestrator error paths.
*/
export const ExitCode = {
Success: 0,
BadArgs: 1,
RenderError: 2,
PagedJsTimeout: 3,
BrowseUnavailable: 4,
} as const;
export type ExitCode = typeof ExitCode[keyof typeof ExitCode];
/**
* Structured error for browse CLI shell-out failures.
*/
export class BrowseClientError extends Error {
constructor(
public readonly exitCode: number,
public readonly command: string,
public readonly stderr: string,
) {
super(`browse ${command} exited ${exitCode}: ${stderr.trim()}`);
this.name = "BrowseClientError";
}
}