gstack make-pdf 实战指南:把 Markdown 变成出版级 PDF 的完整管线
【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack
gstack 的make-pdf技能将任意 Markdown 文件渲染为出版级 PDF:1 英寸页边距、智能分页、页码、封面、running header、弯引号与破折号、可点击目录、斜向 DRAFT 水印。本文基于 make-pdf/SKILL.md 的完整功能说明,结合make-pdf/src/下的源码实现,讲清楚每条命令的用法、每个参数的语义、渲染管线的分层结构,以及排障时该看哪里。读完后你能在自己的 gstack 环境中直接调用$P generate产出成品文档,并能读懂其底层实现。
一、这是什么:gstack 的 make-pdf 技能
make-pdf是 gstack 技能集合中的文档生成技能(frontmatter 中name: make-pdf、preamble-tier: 1,见 SKILL.md 文件头)。它产出的 PDF 被定位为"finished artifact"而非草稿:正文左对齐、全程 Helvetica、弯引号(curly quotes)与 em dash 排版、复制粘贴出的是干净单词(而不是S a i l i n g这种按字形切散的结果)。
触发意图很宽:用户说"make a PDF"、"export to PDF"、"turn this markdown into a PDF"、"generate a document",甚至语音转写别名("make this a pdf"、"pdf this markdown" 等)都应该调用该技能。
技能前置检查:$P 是什么
SKILL.md 中所有命令都通过$P变量引用二进制,它在技能启动时按以下优先级解析(来自 SKILL.md 的 "MAKE-PDF SETUP" 检查块):
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) P="" # 1. 显式环境变量 [ -n "$MAKE_PDF_BIN" ] && [ -x "$MAKE_PDF_BIN" ] && P="$MAKE_PDF_BIN" # 2. 仓库内 vendored 安装 [ -z "$P" ] && [ -n "$_ROOT" ] && [ -x "$_ROOT/.claude/skills/gstack/make-pdf/dist/pdf" ] && P="$_ROOT/.claude/skills/gstack/make-pdf/dist/pdf" # 3. 用户目录安装(默认) [ -z "$P" ] && P="$HOME/.claude/skills/gstack/make-pdf/dist/pdf"- 打印
MAKE_PDF_READY: $P后,后续所有命令都写$P(而不是硬编码路径),保证可移植性; - 打印
MAKE_PDF_NOT_AVAILABLE时,说明二进制还没构建——需在 gstack 仓库执行./setup构建后重试。
平台字体前提
PDF 的观感高度依赖字体,SKILL.md 给出了两个平台级前提:
- Linux 需安装
fonts-liberation。Helvetica 和 Arial 在 Linux 上默认不存在,Liberation Sans 是标准的度量兼容(metric-compatible)回退字体。CI 与 Docker 构建通过 Dockerfile.ci 自动安装。 - Emoji 需要彩色 emoji 字体。macOS(Apple Color Emoji)与 Windows(Segoe UI Emoji)自带;大多数 Linux 发行版和容器不带,emoji 会渲染成空心方块(▯)。
./setup会在 Linux 上自动安装fonts-noto-color-emoji(apt/dnf/pacman/apk,尽力而为),打印 CSS 的字体栈依次回退到 Apple / Segoe / Noto emoji 族。CI 无 sudo、托管或离线机器可设GSTACK_SKIP_FONTS=1跳过安装。
字体策略在源码 make-pdf/src/print-css.ts 中有单一事实来源(single source of truth):
// Metric-compatible sans stack: Helvetica (macOS), Liberation Sans (Linux), Arial (Windows) const SANS_STACK = `Helvetica, "Liberation Sans", Arial`; // CJK fallback families,追加到正文字体栈末尾 const CJK_STACK = `"PingFang SC", "Heiti SC", "Noto Sans CJK SC", ...`; // 彩色 emoji 族:Apple / Segoe / Noto,放在通用 sans-serif 之前 const EMOJI_FAMILIES = `"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji"`;注释还解释了为什么"不打包 webfont":规避 Chromium 逐字形Tj的 bug——那会破坏复制粘贴时的文本提取。CJK 文档(苹方、黑体、Noto Sans CJK 等)与 emoji 都在字体栈中有显式位置。
二、命令速览与输出契约
命令注册表在 make-pdf/src/commands.ts 中集中定义,是 CLI 分发、SKILL.md 文档生成和测试共用的单一事实来源。四个命令:
| 命令 | 作用 |
|---|---|
$P generate <input.md> [output.pdf] | Markdown 渲染为 PDF(80% 的使用场景) |
$P generate --cover --toc essay.md out.pdf | 完整出版版式(封面 + 可点击目录) |
$P generate --watermark DRAFT memo.md draft.pdf | 斜向 DRAFT 水印 |
$P preview <input.md> | 渲染 HTML 并在浏览器打开(快速迭代) |
$P setup | 校验 browse + Chromium + pdftotext 并跑冒烟测试 |
$P --help | 完整 flag 参考 |
输出契约(SKILL.md 明确约定,也是 make-pdf/src/cli.ts 文件头注释的 DX 规格):
stdout: 成功时只有输出路径,一行,别无其他 stderr: 进度输出(Rendering HTML... Generating PDF...),除非 --quiet exit code: 0 成功 / 1 参数错误 / 2 渲染错误 / 3 Paged.js 超时 / 4 browse 不可用捕获输出路径的标准写法:PDF=$($P generate letter.md),之后直接使用$PDF。
退出码常量定义在 make-pdf/src/types.ts:
export const ExitCode = { Success: 0, BadArgs: 1, RenderError: 2, PagedJsTimeout: 3, BrowseUnavailable: 4, } as const;cli.ts的main()按错误类型映射到对应退出码:BrowseClientError(browse CLI 外壳调用失败)→ 4;PagedJsTimeout→ 3;ENOENT(文件不存在)→ 1;其余 → 2。这让 CI 脚本可以精确区分"参数写错了"、"渲染炸了"、"Paged.js 卡死"和"浏览器没起来"四类故障。
布尔 flag 的解析细节(一个曾经踩过的坑)
generate的参数解析在 make-pdf/src/cli.ts 的parseArgs()中。源码注释记录了一个真实回归(#2514):旧解析器会把任何紧跟在 flag 后的非 flag 词都当作 flag 的值,导致技能自己文档里的用法$P generate --cover --toc essay.md essay.pdf只是"靠 flag 相邻的运气"才工作,而$P generate --toc essay.md会把essay.md吃掉当作--toc的值并报 "missing input"。现在的实现用一个布尔 flag 白名单来区分:
export const BOOLEAN_FLAGS = new Set([ "cover", "toc", "no-chapter-breaks", "confidential", "no-confidential", "page-numbers", "no-page-numbers", "tagged", "no-tagged", "outline", "no-outline", "quiet", "verbose", "allow-network", "strict", ]);只有不在该集合中的 flag(如--margins、--watermark、--title)才会消费下一个词作为值。这一细节意味着布尔开关可以与文件参数任意相邻排列,不会互相吞噬。
三、核心使用模式
1. 备忘录/信函模式(80% 场景)
一条命令、无 flag,得到干净 PDF:默认带 running header、页码,以及右下角的 CONFIDENTIAL 页脚。
$P generate letter.md # 写到 /tmp/letter.pdf $P generate letter.md letter.pdf # 显式输出路径默认输出路径规则(见 make-pdf/src/orchestrator.ts):省略输出参数时写/tmp/<slug>.pdf,其中 slug 由输入文件名派生(去扩展名、非法字符替换为-、截断 64 字符)。
2. 出版模式:封面 + 目录 + 章节分页
$P generate --cover --toc --author "Garry Tan" --title "On Horizons" \ essay.md essay.pdf行为要点:
- 每个顶层 H1 起始一个新页面(chapter break)。对"恰好有多个 H1 的备忘录"可用
--no-chapter-breaks关闭; --title缺省时取第一个 H1;--date缺省为今天;--author用于封面和 PDF 元数据(见 make-pdf/src/types.ts 的GenerateOptions注释);- 封面版式在 make-pdf/src/print-css.ts 中锁定:56pt 标题、13pt 元信息、
padding-top 1.4in的"海报式"上三分之一落位(v1.58.0.0 的改版决定——早期 32pt 封面在印刷中显得太小);不用 flexbox、不做垂直居中; - 目录可点击:make-pdf/src/render.ts 的
addHeadingIds()按目录扫描顺序给每个 H1–H3 分配id="toc-N",已带 id 的标题保留原 id,保证目录链接在 PDF(Chromium outline 书签)和--to html输出中都能解析到真实锚点。
3. 草稿阶段水印
$P generate --watermark DRAFT memo.md draft.pdf每页斜向 10% 透明度的 DRAFT 水印;定稿时去掉 flag 重新生成即可。水印实现为正文前的<div class="watermark">(见 render.ts 组装逻辑),打印调用相应开启printBackground(orchestrator.ts 中printBackground: !!opts.watermark)。
4. preview:快速迭代
$P preview essay.md用同一份打印 CSS 渲染 HTML 并在浏览器打开(xdg-open/open/start),编辑 markdown 后刷新即可,跳过 PDF 往返。源码上有一个重要的诚实差异(make-pdf/src/orchestrator.ts 的preview()):preview 刻意跳过图表/图片预处理(不经过 browse daemon 往返),因此图表 fence 在 preview 里只显示为代码块、本地图片可能解析不到——它会显式打印一条preview note提醒你"generate 才会完整渲染这些内容",防止你在缺了内容的预览上签字。
5. 无品牌(去掉 CONFIDENTIAL 页脚)
$P generate --no-confidential memo.md memo.pdfCONFIDENTIAL 页脚默认开启(confidential默认true),通过@page { @bottom-right { content: "CONFIDENTIAL"; ... } }注入(print-css.ts)。
四、图表与图片:离线矢量渲染与尺寸策略
mermaid / excalidraw fence 渲染为图片
markdown 中位于第 0 列的excalidrawfence 会渲染为清晰的矢量图,完全离线(vendored bundle,不走 CDN)。缩进的 fence(列表内)按设计保留为普通代码块。坏掉的 fence 产出可见的红色诊断块并附带解析错误——绝不静默降级为原始代码。
fence 的 info-string 选项:
mermaid render=false ← 保留为代码块 mermaid page=portrait ← 否决该图的自动横排实现上这是一个"预扫描"阶段(make-pdf/src/diagram-prepass.ts,由 orchestrator 在 Stage 1.5 调用):先extractDiagramFences把 fence 抽走换成占位 token;HTML 渲染完成后,在专用的渲染 tab(bundle tab)里逐张渲染再substituteSlots回填。如果渲染 tab 不可用,每个占位符会被替换为 "Diagram not rendered — diagram-render bundle unavailable" 的可见诊断块,而不是留裸 token。
SKILL.md 还划清了分工:从英文描述创作新图是/diagram技能的职责——它产出可编辑的三元组(source、.excalidraw、SVG/PNG);与 make-pdf 配对的正确姿势是把.mmd源码嵌进 markdown,而不是嵌 PNG。
图片:缩放正确,永不截断
本地图片自动内联(相对路径相对 markdown 文件解析),规则是"每张图片都被内容盒封顶——零截断";过大的照片会缩到印刷分辨率(300dpi),体积小而肉眼无损。
远程图片默认拦截:http/https 图片默认渲染为可见的拦截占位符(离线姿态,避免打印时拉取 tracking pixel);显式传--allow-network才允许抓取。解析到 markdown 目录之外的图片(即使通过符号链接)仍会内联但大声警告;--strict则让它直接失败。超过 64MB 的文件或非普通文件(fifo、设备文件)降级为占位符,而不是挂死整次运行。
逐图指令,写在图片标记后面:
chart{width=full} ← 拉伸到内容盒宽度 chart{width=50%} ← 百分比或 3in/8cm/200px wide{page=landscape} ← 独占横向页 wide{page=portrait} ← 否决自动横排指令的解析在 make-pdf/src/image-policy.ts:applyImageDirectives()在 marked 之后、净化器之前运行,把alt{width=50%}翻译成data-gstack-*属性(净化器保留>const MIN_ASPECT = 1.8; // 宽高比下限 const SHRINK_LIMIT = 2.5; // 内宽超过内容盒 2.5 倍才提升(≈1600px @ 6.5in letter 盒) const ALT_HINT_TOKENS = ["diagram", "architecture", "flowchart", "chart", "graph"];
即:宽高比 ≥ 1.8且内宽超过内容盒约 2.5 倍(缩到原尺寸 ~40% 以下就不可读了)且有图表来源(渲染出的 fence)或 alt 文本含图表类词。假阴性代价低、假阳性代价高,所以刻意保守;提升后的页面垂直居中。启发式猜错时用{page=portrait}否决,漏判时用{page=landscape}手动提升。
横向页的落地依赖 Chromium 的 CSS 命名页:print-css.ts 生成@page wide { size: <size> landscape; }与.page-wide { page: wide; },而 orchestrator仅在确实存在提升块时才给打印调用传preferCSSPageSize: true(hasLandscape ? true : undefined),对其它所有文档保持最小行为变更。垂直居中不用 CSS flex/min-height——源码注释记录了实测:flex 的.page-wide在 Chromium 里会碎片化成幽灵横向空白页(landscape-gate 测试数出 3 个提升却 5 页);改为由 image-policy 依据每个块的宽高比计算内联margin-top来实现。
e2e 门禁测试在 make-pdf/test/e2e/:landscape-gate.test.ts(横排提升)、diagram-gate.test.ts(图表渲染)、emoji-gate.test.ts、format-gate.test.ts、combined-gate.test.ts,配套 fixture 在 make-pdf/test/fixtures/。
五、其他输出格式:单文件 HTML 与 Word
$P generate readme.md out.html --to html # 单一自包含文件:内联 SVG 图、 #>$P generate docs.md --strict缺失图片、远程图片、树外图片、超大图片、非普通文件——默认行为是"警告 + 占位符"继续跑;--strict下这些全部以非零退出码失败(CI mode,为需要确定性的文档流水线设计)。types.ts 中的注释把它定位为 "eng-review D6.1"。
七、常用参数完整参考
以下参数表继承自 SKILL.md,默认值与 make-pdf/src/types.ts 的GenerateOptions一致:
Page layout: --margins <dim> 1in(默认)| 72pt | 2.54cm | 25mm --page-size letter|a4|legal (别名 --format;还支持 tabloid,见 PageSize 类型) Structure: --cover 封面页(标题、作者、日期、细线分隔) --toc 带页码的可点击目录 --no-chapter-breaks 不在每个 H1 处另起一页 Branding: --watermark <text> 斜向水印("DRAFT"、"CONFIDENTIAL") --header-template <html> 自定义 running header --footer-template <html> 自定义页脚(与 --page-numbers 互斥) --no-confidential 关闭右下角 CONFIDENTIAL 页脚 Output: --to pdf|html|docx 输出格式(默认 pdf)。html = 单文件自包含; docx = 内容保真 --strict 缺失/远程/树外/超大/非普通文件图片直接失败(CI 模式) --page-numbers "N of M" 页脚(默认开) --tagged 无障碍 PDF(默认开) --outline 由标题生成的 PDF 书签(默认开) --quiet 抑制 stderr 进度 --verbose 每阶段耗时 Network: --allow-network 抓取外部图片。默认关闭:远程图片渲染为 可见的拦截占位符 Metadata: --title "..." 文档标题(默认取第一个 H1) --author "..." 封面 + PDF 元数据的作者 --date "..." 封面日期(默认今天)页边距还支持单侧覆盖:--margin-top/--margin-right/--margin-bottom/--margin-left(commands.ts 的 flag 列表),单侧值优先于--margins。一个容易被忽略的实现细节:页码在 CSS 层是唯一事实来源——orchestrator 总是关闭 Chromium 原生页码(pageNumbers: false),@page { @bottom-center { content: counter(page) " of " counter(pages); } }负责显示;设置--footer-template时会同时压掉 CSS 页码(render.ts:showPageNumbers = pageNumbers !== false && !footerTemplate),避免双页脚。
八、渲染管线源码剖析
把 make-pdf/src/orchestrator.ts 的generate()按进度阶段串起来,完整管线是:
- Reading markdown— 读文件(不存在则抛错);
- Stage 1.5 图表预扫描—
extractDiagramFences抽出 mermaid/excalidraw fence,替换为占位 token; - Rendering HTML— 纯函数
render()(make-pdf/src/render.ts):stripFrontmatter去掉 YAML frontmatter(marked 不认 frontmatter,会把它渲染成一页正文段落);marked解析为 HTML;applyImageDirectives消费{...}图片指令;- 净化器:剥离
<script>、<iframe>、<object>、<embed>、<link>、<meta>、<base>、<form>、全部on*事件处理器与javascript:URL(注释:不可信 markdown 可以内嵌原始 HTML); - 解码 marked 产出的
"/'等实体(不解码,smartypants 的正则永远匹配不到); - smartypants排版变换;
- 元数据推导、打印 CSS 生成、cover/TOC/章节分块、水印块,组装成完整 HTML 文档。
- 图表渲染 + 图片内联— 专用 bundle tab 渲染 fence、回填占位;
inlineLocalImages内联本地图片(过大时缩到 300dpi,此时才懒开渲染 tab); - image policy— 宽度规则 + 自动横排判定;docx 路径在此补一步图表光栅化;
--to html到此直接写盘返回;- Opening tab → Loading HTML into Chromium— 每次 generate 开专用 tab(
$B newtab --json),所有 load-html/js/pdf 都带--tab-id,并在try/finally中关闭——并行的$P generate调用永远不会竞争活动 tab; - Generating PDF— browse 的
pdf调用;--toc时内部等待 Paged.js 分页完成。
每个阶段的进度由ProgressReporter输出到 stderr:--quiet时静默(但失败信息永远输出——那是错误路径);--verbose时给出每阶段毫秒耗时与总耗时(Done in 1.5s. 43 words · 22KB · /tmp/letter.pdf)。
smartypants:只改正文,不碰代码、标签和 URL
排版变换实现于 make-pdf/src/smartypants.ts:"quoted"→ 弯引号、don't→ 右单引号、--→ em dash、...→ 省略号。关键是"保护区"机制:先用占位符 token 挖出<pre>/<code>/<script>/<style>块、所有 HTML 标签、所有http(s)://URL,只对剩余纯文本做变换,再拼回去。源码里两个细节值得注意:
- em dash 规则要求两侧有单词/空格边界,避免把散文里的
--verbose这类 flag 弄坏; - 占位符以
\u0000包裹,且 URL 正则显式排除\u0000——否则紧贴标签的 URL(<a href="...">https://ex.com</a>)会让\S+吞掉占位符,导致标签永久丢失(单次 restore 无法恢复)。
setup:五步冒烟测试
$P setup(make-pdf/src/setup.ts)按 CEO 计划的 CLI UX 规格执行五步:
- 校验 browse 二进制存在且可响应(失败 → 退出码 4);
- 启动 Chromium(
$B newtab about:blank开一个专用 tab 冒烟,失败会提示cd ~/.claude/skills/gstack && ./setup); - 校验
pdftotext(可选,只警告不失败——它是 CI 复制粘贴门禁用的;macOS 装brew install poppler,Ubuntu 装sudo apt-get install poppler-utils); - 用内联两段式 fixture 生成一份冒烟 PDF——内容特意包含弯引号、em dash、省略号以验证排版变换,失败 → 退出码 2;
- 打印三条命令速查表(default memo / full publication / diagonal watermark)。
九、调试与故障排查
SKILL.md 的 Debugging 一节,结合源码定位:
| 症状 | 原因与处理 |
|---|---|
| 输出空白/空页 | 检查 browse daemon 是否在运行:$B status |
| 复制粘贴出碎片化文本 | highlight.js 输出(Phase 4 计划项)。目前临时方案:删掉围栏代码块重新生成 |
| Paged.js 超时(退出码 3) | 通常因为 markdown 里没有标题,去掉--toc |
| 输出里出现 "[remote image blocked]" 占位符 | 加--allow-network——你是在授权该 markdown 文件按其中图片 URL 抓取资源 |
| PDF 太高/太宽 | --page-size a4或--margins 0.75in |
十、技能调用时机与运行约定
SKILL.md 还定义了 Claude 何时应运行它:任何 "make this markdown a PDF" / "Export it as a PDF" / "Turn this letter into a PDF" / "I need a PDF of the essay" / "Print this as a PDF for me" 意图都直接跑$P generate;用户打开着.md文件说"make it look nice"时,主动提议$P generate --cover --toc并在运行前确认。
两个运行层面的约定来自技能文档:
- Plan Mode 安全操作:在 plan 模式下,浏览/查看类命令、写
~/.gstack/、写计划文件、open生成的产物均被允许(它们为计划提供信息); - 技能 preamble:技能启动时先执行一段通用 preamble(更新检查、会话状态、遥测开关、路由规则检查等,见 SKILL.md 的 "Preamble" 与 "Artifacts Sync" 部分),与 make-pdf 具体功能解耦;
PROACTIVE=false时不主动建议技能,UPDATE_CHECK=false时跳过升级检查输出。
小结
make-pdf的设计可以概括为三条主线:输出契约严格到机器可依赖(stdout 只有一行路径、五档退出码、--quiet/--verbose分离进度与错误);默认姿态是离线与安全(远程图片拦截、HTML 净化、fence 坏损可见化、64MB 与非普通文件防挂死);排版是刻意的工程决策(Helvetica/Liberation Sans 度量兼容栈、CSS 为页码唯一事实来源、命名页实现单页横排、smartypants 的保护区机制)。所有行为在 make-pdf/test/ 下有对应单测与 e2e 门禁(如cli-args.test.ts、render-offline-sanitize.test.ts、image-policy.test.ts与test/e2e/的各 gate),使这条"markdown → 出版级 PDF"管线的每个承诺都可验证。
【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考