Skip to content

模板开发者指南

模板是编译后的可执行文件,通过 stdin/stdout 与 Presto 通信。 用户的 Markdown 内容从标准输入传入,模板将其转换为 Typst 源码后输出到标准输出, Presto 再调用 Typst 编译器生成最终的 PDF。

详细的通信规范见二进制协议, 核心术语见术语表


第一部分:5 分钟快速开始

脚手架方式(推荐)

bash
npx create-presto-template

交互式向导会询问 7 个问题:

  1. 模板名称(kebab-case,如 my-report
  2. 显示名称(如「我的报告模板」)
  3. 描述(一句话说明用途)
  4. 开发语言(Go 推荐 / Rust / TypeScript)
  5. 分类(不超过 20 个字符,如「公文」「简历」)
  6. GitHub 用户名
  7. 许可证(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 为例)

text
my-template/
  main.go           # 入口:协议实现 + Markdown → Typst 转换
  manifest.json     # 模板元数据
  example.md        # 示例文档
  template_head.typ # Typst 模板头(可选)
  Makefile          # 构建 / 测试 / 预览命令
  CLAUDE.md         # AI 开发配置

构建并预览

bash
make build    # 编译二进制
make preview  # 安装到 Presto 并预览

第二部分:二进制协议速查

完整规范见 binary-protocol.md,以下是实用摘要。

四种调用方式

命令stdinstdout用途
cat input.md | ./binaryMarkdownTypst 源码核心转换
./binary --manifestmanifest JSON元数据查询
./binary --example示例 Markdown示例文档
./binary --version版本号版本查询

不得添加协议外的 flag。

三语言实现对比

特性GoRustTypeScript
Markdown 解析库goldmarkpulldown-cmarkmarked
解析模式AST 遍历事件驱动token 遍历
嵌入方式//go:embedinclude_str!()import with { type: "text" }
构建命令go buildcargo build --releasebun build --compile

manifest.json 字段说明

字段必填说明
name模板唯一标识符,kebab-case
displayName用户可见的模板名称
version语义化版本号(semver)
description模板用途描述
author作者信息
license开源许可证
category分类标签,不超过 20 字符
keywords搜索关键词数组
minPrestoVersion最低兼容的 Presto 版本
requiredFonts依赖的字体列表(对象数组,含 namedisplayNameurl
frontmatterSchema用户可配置的 frontmatter 字段定义

frontmatterSchema 支持的类型:

type说明示例默认值
"string"文本"未命名论文"
"boolean"布尔值false
"number"数字1.5

可选的 format 字段用于提示 UI 渲染方式(如 "date" 显示日期选择器)。

示例 1:Starter 默认 manifest

json
{
  "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

json
{
  "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 源码, 以下是开发模板时最常用的语法。

页面设置

typst
#set page(paper: "a4", margin: (top: 2.54cm, bottom: 2.54cm, left: 2.58cm, right: 2.08cm))

字体与段落

typst
#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 转换对照

MarkdownTypst
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 级计数器:

typst
#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 页面设置:

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 中声明:

json
{
  "requiredFonts": [
    { "name": "FZXiaoBiaoSong-B05", "displayName": "方正小标宋", "url": "https://..." },
    { "name": "STFangsong", "displayName": "华文仿宋", "url": "https://..." }
  ]
}

Typst 中定义字体常量并使用:

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 代码):

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 —」。

typst
#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 代码中的处理逻辑:

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} 块语法包裹多个段落。


第五部分:开发工作流

本地开发循环

bash
make build    # 编译二进制
make test     # 运行测试(含安全检测)
make preview  # 安装到 Presto 并预览

测试内容

make test 会自动执行以下检查:

  1. manifest 是否为有效 JSON
  2. example round-trip:--example 输出能否被模板正确转换
  3. --version 是否正常输出
  4. category 字段校验(非空、不超过 20 字符、合法字符)
  5. 安全三层测试:
    • 静态分析:检查是否 import 了禁止的包
    • 网络隔离:在沙箱中运行模板,确认无网络访问
    • 输出验证:确认 stdout 只包含合法的 Typst 代码

AI 辅助开发

Starter 仓库已预配置 CLAUDE.md, 包含项目规则、技术栈约束和开发流程。 详细的 AI 工作流模式见 ai-workflow-patterns.md

推荐的四步法:

  1. 将参考文档(PDF/DOCX)提供给 AI,分析排版特征
  2. 确认排版参数和可配置项
  3. 分步生成代码(先 manifest,再转换逻辑,最后边界处理)
  4. make preview 目视检查 PDF 效果

安全要点

模板运行在严格的沙箱中(30 秒超时、无网络、无文件写入)。 开发时需注意 Typst 注入防护:

  • 用户输入嵌入 Typst 字符串("...")时,转义 \"#
  • 用户输入嵌入 Typst 内容块([...])时,转义 \]#

Go 的 internal/typst 包提供了两个转义函数:

go
typst.EscapeString(s)  // 用于 "..." 上下文
typst.EscapeContent(s) // 用于 [...] 上下文

禁止 import 的包:netnet/*os/execplugindebug/*


第六部分:发布检查清单

发布前检查

  • [ ] make test 全部通过(含安全测试)
  • [ ] manifest.jsonversion 字段已更新
  • [ ] example.md 能正确转换
  • [ ] 在 Presto 中预览效果正确
  • [ ] category 不超过 20 字符

发布步骤

bash
# 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 --tags

CI 会自动完成 6 平台构建(darwin/linux/windows x arm64/amd64)并创建 GitHub Release。 详细的发布流程和 CI 配置见 release-process.md

注册中心收录

  1. 在 GitHub 仓库设置中添加 presto-template topic
  2. 注册中心每日 UTC 08:00 自动扫描
  3. 自动获得 community 信任等级
  4. 如需 verified 等级,提交 PR 到 template-registry 仓库的 verified-templates.json

Presto — Markdown to PDF