二进制协议速查
简单理解
Presto 的模板不是一堆文件,而是一个可执行程序。你把 Markdown 文本喂给它,它吐出排版代码(Typst)。就这么简单。
想象一个黑盒子:左边塞进去 Markdown,右边出来 Typst。这个黑盒子还能回答几个问题——"你是谁?"(--manifest)、"给个示例?"(--example)、"什么版本?"(--version)。Presto 主应用通过这四种方式和模板交互,没有其他途径。
模板二进制运行在严格的沙箱中:30 秒超时、无网络、无文件写入、最小环境变量。这意味着模板代码不可能偷数据或搞破坏。
协议速查表
四种调用方式
| 调用方式 | 命令 | stdin | stdout | 用途 |
|---|---|---|---|---|
| 转换 | ./binary | Markdown 文本 | Typst 代码 | 核心功能:把 Markdown 变成排版代码 |
| 元数据 | ./binary --manifest | 无 | manifest JSON | 安装时提取模板信息 |
| 示例 | ./binary --example | 无 | Markdown 文本 | 展示模板效果 |
| 版本 | ./binary --version | 无 | 版本号字符串 | 版本检查与更新判断 |
安全限制
| 限制项 | 值 | 说明 |
|---|---|---|
| 执行超时 | 30 秒 | 单次调用最长时间 |
| 构建超时 | 300 秒(5 分钟) | 模板构建最长时间 |
| manifest 大小上限 | 1 MB | --manifest 输出上限 |
| 示例大小上限 | 1 MB | --example 输出上限 |
| Typst 输出上限 | 10 MB | 转换输出上限 |
| 构建产物上限 | 50 MB | 编译后二进制上限 |
| 环境变量 | 仅 PATH=/usr/local/bin:/usr/bin:/bin | 最小环境,无其他变量 |
| 网络访问 | 禁止 | macOS sandbox-exec / Linux unshare --net |
| 文件写入 | 禁止 | 只能通过 stdout 输出 |
| CLI flag | 仅上述四种 | 不得添加协议外的 flag |
三层安全防护
| 层级 | 机制 | 作用 |
|---|---|---|
| 静态分析 | 禁止 import 黑名单 | 编译前拦截危险依赖 |
| 运行时沙箱 | macOS sandbox-exec / Linux unshare --net | 隔离网络和文件系统 |
| 输出验证 | 格式检查 | 确保 stdout 只包含合法内容 |
交互时序图
详细解释
1. 转换:核心功能
这是模板存在的意义。Presto 把用户写的 Markdown 通过 stdin 传给模板二进制,二进制处理后通过 stdout 返回 Typst 源码。
内部处理流程:
splitFrontmatter()— 分离 YAML 头部(页面配置)和正文writePageSetup()— 根据 frontmatter 字段生成 Typst 页面设置代码renderBody()— 用 goldmark 解析 Markdown AST,逐节点转换为 Typst 语法
关键约束:
- stdin 只接受 Markdown 文本
- stdout 只能输出合法的 Typst 源码
- 不能读写任何文件,不能访问网络
- 超过 30 秒自动终止
2. 元数据:--manifest
安装模板时,Presto 第一件事就是调用 --manifest 拿到模板的自我描述。这个 JSON 告诉 Presto:
- 模板叫什么、谁写的、什么版本
- 需要哪些字体
- frontmatter 支持哪些字段(类型、默认值、格式)
manifest 数据在编译时通过 Go 的 //go:embed 嵌入二进制,不是运行时生成的。
3. 示例:--example
返回一段示例 Markdown,用于:
- 模板商店的预览展示
- 用户首次使用时的起步模板
- 自动化测试的输入数据
同样通过 //go:embed 嵌入。
4. 版本:--version
返回版本号字符串,从嵌入的 manifest.json 中读取 version 字段。用于:
- 检查模板是否有更新
- 兼容性判断(配合
minPrestoVersion)
5. 嵌入机制
模板是"二进制嵌入模型",不是"文件复制模型"。所有资源在编译时打包进可执行文件:
go
//go:embed manifest.json
var manifestData []byte
//go:embed example.md
var exampleData []byte这意味着:
- 分发时只有一个文件,不需要额外的资源目录
- 运行时不依赖外部文件,不会出现"找不到配置"的问题
- 沙箱限制更容易实施——二进制不需要读取任何外部文件
完整 manifest.json 示例
json
{
"name": "academic-paper",
"displayName": "学术论文",
"description": "适用于中文学术论文的排版模板,支持摘要、关键词、参考文献等常见结构",
"version": "1.2.0",
"author": "Zhang San <zhangsan@example.com>",
"license": "MIT",
"minPrestoVersion": "0.5.0",
"keywords": ["学术", "论文", "中文", "LaTeX 替代"],
"requiredFonts": [
{
"name": "NotoSerifCJKsc",
"displayName": "思源宋体",
"url": "https://github.com/notofonts/noto-cjk/releases"
},
{
"name": "NotoSansCJKsc",
"displayName": "思源黑体",
"url": "https://github.com/notofonts/noto-cjk/releases"
}
],
"frontmatterSchema": {
"title": {
"type": "string",
"default": "未命名论文"
},
"author": {
"type": "string",
"default": "佚名"
},
"date": {
"type": "string",
"format": "date"
},
"abstract": {
"type": "string",
"default": ""
},
"keywords": {
"type": "string",
"default": ""
},
"twocolumn": {
"type": "boolean",
"default": false
},
"fontsize": {
"type": "string",
"default": "12pt"
},
"papersize": {
"type": "string",
"default": "a4"
},
"margin": {
"type": "string",
"default": "2.5cm"
},
"linestretch": {
"type": "number",
"default": 1.5
}
}
}字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
name | 是 | 模板唯一标识符,小写字母 + 连字符 |
displayName | 是 | 用户可见的模板名称 |
description | 是 | 模板用途描述 |
version | 是 | 语义化版本号(semver) |
author | 是 | 作者信息 |
license | 是 | 开源许可证 |
minPrestoVersion | 是 | 最低兼容的 Presto 版本 |
keywords | 否 | 搜索关键词 |
requiredFonts | 否 | 依赖的字体列表 |
frontmatterSchema | 是 | 用户可配置的 frontmatter 字段定义 |
frontmatterSchema 支持的 type 值:
| type | 说明 | 示例默认值 |
|---|---|---|
"string" | 文本 | "未命名论文" |
"number" | 数字 | 1.5 |
"boolean" | 布尔值 | false |
可选的 format 字段用于提示 UI 渲染方式(如 "date" 会显示日期选择器)。
