模板开发者指南
模板是编译后的可执行文件,通过 stdin/stdout 与 Presto 通信。 用户的 Markdown 内容从标准输入传入,模板将其转换为 Typst 源码后输出到标准输出, Presto 再调用 Typst 编译器生成最终的 PDF。
第一部分:5 分钟快速开始
脚手架方式(推荐)
npx create-presto-template交互式向导会询问 7 个问题:
- 模板名称(kebab-case,如
my-report) - 显示名称(如「我的报告模板」)
- 描述(一句话说明用途)
- 开发语言(Go 推荐 / Rust / TypeScript)
- 分类(不超过 20 个字符,如「公文」「简历」)
- GitHub 用户名
- 许可证(MIT / Apache-2.0 / GPL-3.0)
手动方式
Fork 对应语言的 Starter 仓库:
- Go:
presto-template-starter-go - Rust:
presto-template-starter-rust - TypeScript:
presto-template-starter-typescript
项目结构(Go 为例)
my-template/
main.go # 入口:协议实现 + Markdown → Typst 转换
manifest.json # 模板元数据
example.md # 示例文档
template_head.typ # Typst 模板头(可选)
Makefile # 构建 / 测试 / 预览命令
CLAUDE.md # AI 开发配置构建并预览
make build # 编译二进制
make preview # 安装到 Presto 并预览第二部分:二进制协议速查
完整规范见 binary-protocol.md,以下是实用摘要。
四种调用方式
| 命令 | stdin | stdout | 用途 |
|---|---|---|---|
cat input.md | ./binary | Markdown | Typst 源码 | 核心转换 |
./binary --manifest | 无 | manifest JSON | 元数据查询 |
./binary --example | 无 | 示例 Markdown | 示例文档 |
./binary --version | 无 | 版本号 | 版本查询 |
不得添加协议外的 flag。
三语言实现对比
| 特性 | Go | Rust | TypeScript |
|---|---|---|---|
| Markdown 解析库 | goldmark | pulldown-cmark | marked |
| 解析模式 | AST 遍历 | 事件驱动 | token 遍历 |
| 嵌入方式 | //go:embed | include_str!() | import with { type: "text" } |
| 构建命令 | go build | cargo build --release | bun build --compile |
manifest.json 字段说明
| 字段 | 必填 | 说明 |
|---|---|---|
name | 是 | 模板唯一标识符,kebab-case |
displayName | 是 | 用户可见的模板名称 |
version | 是 | 语义化版本号(semver) |
description | 否 | 模板用途描述 |
author | 否 | 作者信息 |
license | 否 | 开源许可证 |
category | 否 | 分类标签,不超过 20 字符 |
keywords | 否 | 搜索关键词数组 |
minPrestoVersion | 否 | 最低兼容的 Presto 版本 |
requiredFonts | 否 | 依赖的字体列表(对象数组,含 name、displayName、url) |
frontmatterSchema | 否 | 用户可配置的 frontmatter 字段定义 |
frontmatterSchema 支持的类型:
| type | 说明 | 示例默认值 |
|---|---|---|
"string" | 文本 | "未命名论文" |
"boolean" | 布尔值 | false |
"number" | 数字 | 1.5 |
可选的 format 字段用于提示 UI 渲染方式(如 "date" 显示日期选择器)。
示例 1:Starter 默认 manifest
{
"name": "my-template",
"displayName": "我的模板",
"description": "请替换为模板描述",
"version": "0.1.0",
"author": "your-github-username",
"license": "MIT",
"category": "通用",
"keywords": ["模板"],
"minPrestoVersion": "0.1.0",
"requiredFonts": [],
"frontmatterSchema": {
"title": { "type": "string", "default": "请输入标题" }
}
}示例 2:gongwen 官方模板 manifest
{
"name": "gongwen",
"displayName": "类公文模板",
"description": "符合 GB/T 9704-2012 标准的类公文排版",
"version": "1.0.0",
"author": "Presto-io",
"license": "MIT",
"category": "公文",
"keywords": ["公文", "通知", "报告", "政府", "GB/T 9704"],
"minPrestoVersion": "0.1.0",
"requiredFonts": [
{ "name": "FZXiaoBiaoSong-B05", "displayName": "方正小标宋", "url": "https://www.foundertype.com/..." },
{ "name": "STHeiti", "displayName": "华文黑体", "url": "https://www.foundertype.com/..." },
{ "name": "STFangsong", "displayName": "华文仿宋", "url": "https://www.foundertype.com/..." },
{ "name": "STKaiti", "displayName": "华文楷体", "url": "https://www.foundertype.com/..." },
{ "name": "STSong", "displayName": "华文宋体", "url": "https://www.foundertype.com/..." }
],
"frontmatterSchema": {
"title": { "type": "string", "default": "请输入文字" },
"author": { "type": "string", "default": "请输入文字" },
"date": { "type": "string", "format": "YYYY-MM-DD" },
"signature": { "type": "boolean", "default": false }
}
}第三部分:Typst 速查
Typst 是 Presto 的底层排版引擎。模板的输出就是 Typst 源码, 以下是开发模板时最常用的语法。
页面设置
#set page(paper: "a4", margin: (top: 2.54cm, bottom: 2.54cm, left: 2.58cm, right: 2.08cm))字体与段落
#set text(font: "SimSun", size: 12pt, lang: "zh")
#set par(leading: 1.5em, first-line-indent: 2em)常用函数
| 函数 | 用途 | 示例 |
|---|---|---|
page | 页面设置 | #set page(paper: "a4") |
text | 字体样式 | #set text(font: "SimSun", size: 12pt) |
par | 段落样式 | #set par(leading: 1.5em) |
heading | 标题 | #heading(level: 1)[标题] |
table | 表格 | #table(columns: 3, [...], [...]) |
image | 图片 | #image("photo.png", width: 80%) |
v | 垂直间距 | #v(1em) |
h | 水平间距 | #h(2em) |
align | 对齐 | #align(center)[内容] |
box | 行内容器 | #box(width: 5cm)[...] |
block | 块级容器 | #block(fill: luma(230))[...] |
grid | 网格布局 | #grid(columns: 2, gutter: 1em, [...], [...]) |
pagebreak | 分页 | #pagebreak() |
line | 线条 | #line(length: 100%) |
中文字体映射
| Typst 字体名 | 中文名称 |
|---|---|
| SimSun | 宋体 |
| SimHei | 黑体 |
| FangSong | 仿宋 |
| KaiTi | 楷体 |
| Microsoft YaHei | 微软雅黑 |
| FZXiaoBiaoSong-B05 | 方正小标宋 |
| FZFangSong-Z02 | 方正仿宋 |
Markdown 到 Typst 转换对照
| Markdown | Typst |
|---|---|
| frontmatter 字段 | #let variable = "value" |
# heading | #heading(level: 1)[...] |
**bold** | #strong[...] |
*italic* | #emph[...] |
`code` | #raw("...") |
- item | - item |
--- | #line(length: 100%) |
第四部分:模式食谱
以下 6 个模式均提取自官方模板的真实代码。
食谱 1:自定义标题编号
需求:公文标题使用「一、」「(一)」「1.」「(1)」四级编号。
gongwen 模板在 template_head.typ 中定义了 5 级计数器:
#let h2-counter = counter("h2")
#let h3-counter = counter("h3")
#let h4-counter = counter("h4")
#let h5-counter = counter("h5")
#let custom-heading(level, body, numbering: auto) = {
if level == 2 {
h2-counter.step()
h3-counter.update(0)
text(font: FONT_HEI, size: zh(3))[#context h2-counter.display("一、")#body]
} else if level == 3 {
h3-counter.step()
text(font: FONT_KAI, size: zh(3))[#context h3-counter.display("(一)")#body]
} else if level == 4 {
h4-counter.step()
let number = h4-counter.get().first()
text(size: zh(3))[#number. #body]
} else if level == 5 {
h5-counter.step()
let number = h5-counter.get().first()
text(size: zh(3))[(#number)#body]
}
}
#show heading: it => {
custom-heading(it.level, it.body, numbering: it.numbering)
}模板中的 Go 代码将 Markdown 标题转换为 Typst 标题语法, #show heading 规则自动应用编号格式。
食谱 2:表格布局
需求:jiaoan-shicao 模板使用横向 A4 + 固定列宽 + rowspan 合并。
Typst 页面设置:
#set page(paper: "a4", flipped: true, margin: (top: 2.54cm, bottom: 2.54cm, left: 2.58cm, right: 2.08cm))
#table(
columns: (2.3cm, 4.2cm, auto, auto, 2.2cm, 1.1cm),
stroke: 0.5pt,
align: center + horizon,
// rowspan 合并
table.cell(rowspan: 3)[教学活动名称],
[内容1], [内容2], [内容3], [方法], [课时],
)Go 代码中通过检测「同上」标记实现 rowspan 合并: 当某个单元格内容为「同上」时,自动与上方单元格合并, 输出 table.cell(rowspan: N)[...]。
食谱 3:字体声明
需求:在 manifest 中声明依赖字体,在 Typst 中使用。
manifest.json 中声明:
{
"requiredFonts": [
{ "name": "FZXiaoBiaoSong-B05", "displayName": "方正小标宋", "url": "https://..." },
{ "name": "STFangsong", "displayName": "华文仿宋", "url": "https://..." }
]
}Typst 中定义字体常量并使用:
#let FONT_XBS = "FZXiaoBiaoSong-B05"
#let FONT_FS = "STFangsong"
#set text(font: FONT_FS, size: zh(3))
// 标题使用小标宋
#text(font: FONT_XBS, size: zh(2), weight: "bold")[标题内容]Presto 安装模板时会读取 requiredFonts,提示用户安装缺失字体。
食谱 4:图片缩放
需求:gongwen 模板的图片智能缩放(最大 13.4cm),多图使用 grid 布局。
单图缩放(Go 生成的 Typst 代码):
#figure(
context {
let img = image("photo.png")
let img-size = measure(img)
let x = img-size.width
let y = img-size.height
let max-size = 13.4cm
let new-x = x
let new-y = y
if x > max-size {
let scale = max-size / x
new-x = max-size
new-y = y * scale
}
if new-y > max-size {
let scale = max-size / new-y
new-x = new-x * scale
new-y = max-size
}
image("photo.png", width: new-x, height: new-y)
},
caption: [photo],
)多图时使用 grid 自动排列,根据图片宽高比计算每行容纳数量。
食谱 5:奇偶页页脚
需求:gongwen 模板的奇数页页码居右、偶数页居左,格式为「— N —」。
#set page(
footer-descent: 7mm,
footer: context {
let page-num = here().page()
let is-even = calc.even(page-num)
let num = str(page-num)
let pm = text(font: FONT_SONG, size: zh(4))[— #num —]
if is-even {
align(left, [#h(1em) #pm])
} else {
align(right, [#pm #h(1em)])
}
},
)关键点:使用 context 获取当前页码,calc.even() 判断奇偶。
食谱 6:自定义标记
需求:在 Markdown 中使用特殊标记控制排版。
gongwen 模板支持三种自定义标记:
| Markdown 标记 | 效果 | 生成的 Typst |
|---|---|---|
{v} 或 {v:N} | 插入 N 个空行 | #linebreak(justify: false) |
{pagebreak} | 强制分页 | #pagebreak() |
{.noindent} | 取消段落首行缩进 | #set par(first-line-indent: 0pt) |
Go 代码中的处理逻辑:
// processMarker 检查文本是否为独立标记
func processMarker(text string) (string, bool) {
text = strings.TrimSpace(text)
if m := vMarkerRe.FindStringSubmatch(text); m != nil {
count := 1
if m[1] != "" {
count, _ = strconv.Atoi(m[1])
}
var lines []string
for i := 0; i < count; i++ {
lines = append(lines, "#linebreak(justify: false)")
}
return strings.Join(lines, "\n") + "\n", true
}
if text == "{pagebreak}" {
return "#pagebreak()\n", true
}
return "", false
}{.noindent} 可以作为段落尾部标记使用,也可以用 Pandoc 风格的 ::: {.noindent} 块语法包裹多个段落。
第五部分:开发工作流
本地开发循环
make build # 编译二进制
make test # 运行测试(含安全检测)
make preview # 安装到 Presto 并预览测试内容
make test 会自动执行以下检查:
- manifest 是否为有效 JSON
- example round-trip:
--example输出能否被模板正确转换 --version是否正常输出- category 字段校验(非空、不超过 20 字符、合法字符)
- 安全三层测试:
- 静态分析:检查是否 import 了禁止的包
- 网络隔离:在沙箱中运行模板,确认无网络访问
- 输出验证:确认 stdout 只包含合法的 Typst 代码
AI 辅助开发
Starter 仓库已预配置 CLAUDE.md, 包含项目规则、技术栈约束和开发流程。 详细的 AI 工作流模式见 ai-workflow-patterns.md。
推荐的四步法:
- 将参考文档(PDF/DOCX)提供给 AI,分析排版特征
- 确认排版参数和可配置项
- 分步生成代码(先 manifest,再转换逻辑,最后边界处理)
make preview目视检查 PDF 效果
安全要点
模板运行在严格的沙箱中(30 秒超时、无网络、无文件写入)。 开发时需注意 Typst 注入防护:
- 用户输入嵌入 Typst 字符串(
"...")时,转义\、"、# - 用户输入嵌入 Typst 内容块(
[...])时,转义\、]、#
Go 的 internal/typst 包提供了两个转义函数:
typst.EscapeString(s) // 用于 "..." 上下文
typst.EscapeContent(s) // 用于 [...] 上下文禁止 import 的包:net、net/*、os/exec、plugin、debug/*。
第六部分:发布检查清单
发布前检查
- [ ]
make test全部通过(含安全测试) - [ ]
manifest.json的version字段已更新 - [ ]
example.md能正确转换 - [ ] 在 Presto 中预览效果正确
- [ ]
category不超过 20 字符
发布步骤
# 1. 更新 manifest.json 中的 version
# 2. 提交变更
git add manifest.json
git commit -m "chore: bump version to 1.1.0"
# 3. 打 tag 并推送
git tag v1.1.0
git push origin main --tagsCI 会自动完成 6 平台构建(darwin/linux/windows x arm64/amd64)并创建 GitHub Release。 详细的发布流程和 CI 配置见 release-process.md。
