mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-06 13:45:35 +02:00
3af86348f6
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>
124 lines
3.2 KiB
TypeScript
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";
|
|
}
|
|
}
|