skip to content
Joey 起居注
Table of Contents

最近这一个多月一直在优化typink AI,目标是通过AI让普通不懂代码的人也能使用typst排版精美的文档,但是难度很大,如果看了之前几篇博客就知道,为了让AI生成正确的typst代码,我试了:

  1. 完善提示词,加上各种常见错误例子
  2. 引入多智能体系统,critic agent 能调用typst编译器去验证primary agent生成代码的正确性
  3. 微调模型,让模型生成更准确的typst代码

我只能感叹,typst 这门语言还是太小众了,用户太少,而且版本才到 0.13,大家都不重视它,也包括 llm 开发公司,deepseek, claude, gemini 都不能生成正确的 typst 代码,在 typst 官方github 仓库的 issue 里面有人建议加个 llm.txt 文件,方便大模型厂商抓取和训练模型,但是这个不知道为啥也被官方否了。所以我估计短期内是很难让大模型生成正确的 typst 代码,我想让普通人0接触 typst 代码,依靠 AI 得到100%正确的代码的愿望是很难实现,如果要用 typink AI 还是得学点代码才行,因此写一下这篇博客,总结我的知识库,想学的人也可以看下快速入门 typst。

typst 基础知识

跟很多标记语言不一样,typst是一门编译语言,意思就是得先通过编译为目标文件才能渲染。目前typst编译器支持将 .typ 代码编译为:

  • html
  • svg, png
  • pdf

支持的最好的对象当然是 pdf

其它标记语言如:html 就没有编译一说,直接一个渲染器(一般是浏览器)就可以渲染 html 文件。另一个流行的标记语言 markdown 有编译器,就是能将 markdown 编译为 html 再渲染。不过 markdown 语法非常松散,不会出现报错,即使你语法错了,也会生成 html 也能预览。

typst, html, markdown 在排版方面的优缺点就不多说了,见仁见智,我觉得 typst 这门语言有前途,愿意花时间去学去投资,当然也可能我的判断是错的,也许过两年这个项目就黄了,谁也说不准,毕竟小众。

标记模式

typst 通过标记模式来写内容,就像 markdown 那样,通过各种标记来定义各级标题,段落,加粗,列表等,掌握以下这几个基本的标记,就能写出结构清晰内容丰富的文档了

段落

使用 =, ==, == ... 来表示各级标题,我们写文档一般都是采用总-分-总 的结构写,所以可以在最顶上写一段介绍性的文字,然后开始分段落写,每个段落都有个标题,最后总结一下,这样结构很清晰读者阅读不费劲。比如说

= 这是文章标题
这是文章总体介绍
== 第一章
第一章介绍
=== 第一节

常写 markdown 的人会很熟悉这种风格,不过要记住 markdown 使用的是 #,而 typst 用的是 =

列表

通过各级标题来分好段落确定好文档的结构后,就开始完善每段细节,一般来说我们不喜欢看长篇大论,这样让人找不到重点,所以适当插入一些列表可以帮助读者快速理解这一段的结构,列表分有序和无序列表,有序列表表示内容有前后优先级,无序列表表示内容都是平级的,只是列举在一起。

有序列表,使用 + 号或者数字开启,一般是推荐 + 号,这样在中间插入新内容不会改变序号

+ 项目1
+ 项目2

无序列表,使用 - 号开启

- 项目1
- 项目2

有序列表和无序列表都可以嵌套,但是一般不会嵌套太多,如果嵌套太多提出来作为一个新的章节段落更合适。

换行

跟 html 和 markdown 类似,typst 不是用回车来换行的,如果要手动换行要插入一个反斜杠 \ 再回车

注释

typst 是一种代码,所以写的时候可以加一些注释给自己看,这些注释编译器会直接删掉,不会影响文档生成

使用 // 开启一行注释

脚本模式

学了上面的标记模式,学会了几种标记的用法,其实就能上手编写一些文档了,不过这时候排版还比较难看,因为字体、行间距、颜色 这些都还没调整。要调整这些排版得依赖脚本代码。顺便说一句,可以将 css 当作是 html 的脚本代码。

脚本模式可以说是 typst 最强大也是最核心的功能,进入脚本模式之后,可以调用typst内置的函数或者是第三方库的函数,例如插入图片、改颜色、改对齐都是通过进入脚本模式来做的。

以插入一个图片为例:

之前的内容...
#image('a.png')
之后的内容...

这个例子中,在内容中间插入了一张图片,编译器一个字符一个字符的读入,在检测到这个 # 号时就知道要进入脚本模式,这时候就不会将 #image('a.png') 当作文本,而是会当作函数,调用函数,读取到文件系统里的这张图,插入到段落中。

所以我们只要在文档中,看到 # 井号开头的内容,就知道要进入脚本模式了,等脚本执行完自动退回到标记模式。

typst 脚本

在上面一章中,我们知道了 typst 有标记模式和脚本模式2个模式,并且知道了通过 # 进入脚本模式,在脚本模式里可以执行代码。本章我们将深入学习一下 typst 脚本,了解一下语法,学完这章我希望大家看到 typst 脚本后,就能知道哪里犯了语法错误,这样遇到一些小问题自己就能改了。

就像学习通用编程语言如:python,javascript 那样,我们学习 typst 脚本也是从 变量、函数、判断条件、循环、导入库 等结构入手。

变量

对于有一些编程基础的人来说,理解变量很容易,就是先定义一个盒子,然后往盒子里可以放任意的东西。对于不同语言有不同的定义变量的语法,对于 typst 来说,是通过 #let 来定义变量的,举一个很常见的例子,我们写文档时经常需要插入页眉页脚,对于很懂 word 的人来说就是点两下鼠标的事,对于不太熟悉word的人,可能就会手动在每一页上复制粘贴那几句话。对于 typst 来说可以通过变量来定义,就像这样:

#let pageFooter = 'xxx公司内部资料'
#pageFooter

定义了一个叫作 pageFooter 的变量,赋予了一个值之后,在需要使用的地方通过 #pageFooter 来调用,这样哪天需要改页脚的文案,只需要找到变量定义的地方修改这一处就行。

字面量

对于有编程基础的人来说,理解字面量也不难,就是内容本身,如数字等,还是举个简单的例子,我如果要排版 “一天有86400秒” 这句话,我可以写为:

一天有86400秒

但是我一般不会这么写,因为这个 86400 让人比较费解,所以如果写成下面这种形式,例如:

一天有#(60*60*24)秒

这2种方式最终渲染为pdf上面的内容都是一样的,不过下面这种写法比较好理解,让我们看到了86400是怎么得来的,在这里面 #() 里面的数字都是字面量

函数

函数是将一个变量映射为另一个变量,常常用来封装一段业务逻辑,typst 本身内置了很多常用的函数,有需要拿来直接用就行,有时候需要封装一些自己的函数,可以这样写:

#let uline(x) = [
#underline(stroke: 1pt+red)[#(x * 3)]
]
// or
#let uline(x) = {
underline(stroke: 1pt+red)[#(x * 3)]
}

在这里我定义了一个叫 uline 的函数,它能接受一个变量x,然后会返回一个带下划线的值,比如说我如果调用

#uline([cao])
// 或者
#uline[cao]
// 或者
#uline("cao")

就会输出 caocaocao 并且加上一条红色的下划线

判断语句

使用 #if 来判断一个表达式

像这个例子:

#let a = 5
#let b = 3
#(a) #if a > b { [>]} else if a == b {[=]} else { [<]} #(b)

定义了2个变量,如果修改变量值要自动变方向,就得使用 if 语句

循环

用 for 或者 while 来开启一个循环

#for i in range(3) {
[cao]
}

上面这个例子会输出:caocaocao

typst 布局

对于有点前端基础的人来说会比较好理解布局的意思,就像 css 布局有 float —> flex —> grid 那样,typst 也有自己的布局语言,当然都得基于对长度的定义而来。

绝对长度

typst 有以下方式定义绝对长度:

  • 厘米:1cm
  • 毫米:1mm
  • 英寸:1in
  • 点位:1pt

跟 css 里的 px 这些有点像

应用场景如:定义下划线的时候规定长度

相对长度

相对长度是基于父容器来说的,有2种定义相对长度的方式:

  1. 基于百分比
  2. 基于分数比

举例:

#let p(w, f, ..args) = box(
width: w, height: 10pt, fill: f, ..args
)
4比6:#p(100pt, blue, p(40%, red))
4比6:#p(100pt, blue, p(4fr, red) + p(6fr, blue))

定义这么一个函数,它会调用系统的 box 函数画一个方形,高度固定10点,长度和填充的颜色可根据传入参数变,

第一种调用是通过百分比,将这个box前40%的内容填充为红色 第二种调用是通过分数比,规定了前4份是红色,后6份是蓝色

上下文相关长度

跟 css 里的 em 类似,基于当前字体大小来定义的

布局引擎

typst 的布局引擎我感觉有点类似 css 的 flex,也就是说内容默认是往下挤的,这样的好处是可以避免内容往右流动导致超出页面范围。如果想要左右结构就得手动通过 grid 来定义列数

#let r = rect(fill: blue, width: 30pt, height: 10pt)
#grid(columns: 3, gutter: 5pt, r, r, r, r, r, r, r)

这里定义了一个3列的grid,但是似乎跟 css 的效果不太一样,如果是 css 的话,grid宽度是父容器的宽度,也就是会占据页宽,因为每个方块是固定长度的,所以应该会留下很多空白才对,不知道是bug还是设计如此。

样式

在网页排版中是通过 css 来修改某个元素的样式的,比如说:

p: {
color: red
}

定义这个样式就会将所有段落的文字都改为红色,在 typst 里也有类似的机制,叫作 #set

通过set可以改变某些元素的样式,如:

#set text(fill: red)

会将set的作用域里的所有文本都改为红色,css 也有作用域,也是按照最近法则来优先应用的