mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
e23ff280a1
* fix(make-pdf): single-source page numbers via CSS, honor --no-page-numbers end-to-end
Two page-number sources were stacking in every PDF: Chromium's native footer
and our @page @bottom-center CSS. The CLI flag --page-numbers/--no-page-numbers
also never reached the CSS layer, because RenderOptions didn't carry it.
Passing --footer-template likewise dropped the "custom footer replaces stock
footer" semantic.
- orchestrator.ts: browseClient.pdf() gets pageNumbers:false unconditionally.
CSS is the single source of truth. Chromium native numbering always off.
- render.ts: RenderOptions gains pageNumbers + footerTemplate. render() computes
showPageNumbers = pageNumbers !== false && !footerTemplate and passes to
printCss(), preserving the prior footerTemplate-suppresses-stock semantic.
- print-css.ts: PrintCssOptions.pageNumbers wraps @bottom-center in a conditional
matching the existing showConfidential pattern.
- types.ts: PreviewOptions.pageNumbers so preview path compiles and matches CLI.
- render.test.ts: 7 regression tests covering printCss({pageNumbers}) in
isolation AND the full render() data flow incl. footerTemplate path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(make-pdf): decode HTML entities in titles and TOC to prevent double-escape
A markdown title like "# Herbert & Garry" rendered as "Herbert &amp; Garry"
in <title>, cover block, and TOC entries. marked emits "&" (correct HTML),
but extractFirstHeading and extractHeadings only stripTags — leaving the entity
intact. That string then flows through escapeHtml, producing the double-encode.
- render.ts: new decodeTextEntities helper, distinct from decodeTypographicEntities
(which runs on in-pipeline HTML and intentionally preserves &). Covers
named entities (lt/gt/quot/apos/39/x27/amp) AND numeric (decimal + hex) so
inputs like "©" or "—" don't create the same partial-fix bug.
Amp-last ordering prevents double-decode on "&lt;" et al.
- Apply in both extractFirstHeading and extractHeadings. extractHeadings feeds
buildTocBlock → escapeHtml, so the TOC site had the same bug.
- render.test.ts: 8 tests covering the contract — parameterized across &, <, >,
©, — chars; single-escape in <title>/cover; TOC double-escape check; numeric
entity decode; smartypants-interacts-with-quotes contract (no raw equality).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(make-pdf): Liberation Sans font fallback for Linux rendering
On Linux (Docker, CI, servers), neither Helvetica nor Arial exist. Our CSS
stacks were falling through to DejaVu Sans — wider letterforms that look like
Verdana, not the intended Helvetica/Faber look. Liberation Sans is the standard
metric-compatible Arial clone (SIL OFL 1.1, apt package fonts-liberation).
- print-css.ts: all four font stacks (body + @top-center + @bottom-center +
@bottom-right CONFIDENTIAL) gain "Liberation Sans" between Helvetica and
Arial. File-header docblock updated to reflect the new stack.
- .github/docker/Dockerfile.ci: explicit apt-get install fonts-liberation +
fontconfig with retry, fc-cache -f, and a verify step that fails the build
loud if the font disappears. Playwright's install-deps happens to pull this
in today but the dep is implicit and could silently regress.
- SKILL.md.tmpl: one-sentence note pointing Linux users at fonts-liberation.
- SKILL.md: regenerated via bun run gen:skill-docs --host all (only make-pdf's
generated file changed — verified clean diff scope).
- render.test.ts: 2 assertions — Liberation Sans in body stack AND in at least
one @page margin-box rule (proves all four intended stacks got touched, not
just one).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v1.4.1.0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: anonymize test fixtures, drop VC-partner framing
- CHANGELOG + render.test.ts fixtures use "Faber & Faber" instead of a
personal name. Same regression coverage (ampersand in <title>, cover,
TOC, body), neutral subject.
- make-pdf/SKILL.md.tmpl description drops the "send to a VC partner, a
book agent, a judge, or Rick Rubin's team" line. "Not a draft artifact
— a finished artifact" stands on its own without the audience posturing.
- SKILL.md regenerated.
No functional changes. All 58 make-pdf tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
3.2 KiB
TypeScript
125 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;
|
|
pageNumbers?: 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";
|
|
}
|
|
}
|