学习 typst 语言
/ 15 min read
Table of Contents
最近这一个多月一直在优化typink AI,目标是通过AI让普通不懂代码的人也能使用typst排版精美的文档,但是难度很大,如果看了之前几篇博客就知道,为了让AI生成正确的typst代码,我试了:
- 完善提示词,加上各种常见错误例子
- 引入多智能体系统,critic agent 能调用typst编译器去验证primary agent生成代码的正确性
- 微调模型,让模型生成更准确的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
其它标记语言如: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种定义相对长度的方式:
- 基于百分比
- 基于分数比
举例:
#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 也有作用域,也是按照最近法则来优先应用的