Typst 快速入门手册

· · 科技·工程

阅读 PDF 以获得更好的阅读体验:PDF

本文将介绍基础 Typst 用法。

在后文的例子和演示中,本文将尽量使用有实际意义的句子,以更贴合实际使用情况。由于 Typst 在未进行设置时,对中文的支持不尽如人意(比如默认字体不是很好),所以后文基本上将使用英语作为示例。所以示例均在初始 Typst 下编写,没有任何预设置和模板(文本中提到或代码中包含除外)。

本文用于快速入门 Typst,但不意味着你读完本文就能完全掌握 Typst 排版技术。但至少读完本文,你可以直接去读参考文档(References)而不需要再苦苦寻找教程。

Typst 基础信息

Typst 是一款现代化的开源的排版工具,是专门为排版而设计的编程语言。

Typst 基础语法基于 Markdown,但又有可比拟 LaTeX 的强大排版能力(如公式、参考文献、脚注、交叉引用等),可视作在 Markdown 与 LaTeX 之间的产物。

Typst 使用编译器将源代码编译为 PDF 文件(除此之外,还可以编译为各种格式的图片,甚至是 HTML 文件)。其编译器是用 Rust 写的增量编译器。该编译器仅检查代码哪些部分改动并针对性编译,因此编译速度极快,可以做到几乎实时预览,彻底解决了 LaTeX 在大型文档上的编译缓慢的痛点(一本书可以编译十几分钟)。Rust 语言也为 Typst 编译器提供了高性能与高安全的特性。

:::info[关于增量编译器与文档文件拆分]{open} 先说结论:在文档比较长的情况下,建议将文档拆分成多个文件(代码模式部分末尾有讲如何导入和包含文件),而不是直接使用单个文件。

Typst 编译器是增量编译器,会缓存抽象语法树(AST)、语义分析结果等编译信息。当源代码内容更改时,整个文档结构都可能因此变化,因此 Typst 需要将整个源代码文件重新编译并缓存编译信息。如果是单文件并且文档很长的话,有可能导致占用非常大的内存。如 60 页的包含多个图片的文档,在单文件源代码长度达将近 3000 行时,内存峰值可达 5 GB。然而,如果拆分为多个文件,由于 Typst 对于每个文件是单独编译(甚至有多线程)的,它只需要重建你所修改的那一个文件的缓存,内存占用会明显下降。还是之前那个例子,将文档拆分为 8 到 9 个文件后,内存峰值仅有 2 GB。

Typst 源代码使用 .typ 后缀。

Typst 编译器的源代码仓库:https://github.com/typst/typst

名字来源

“Typst” 是一个合成词,来源于“typesetting”(排版)和“Rust”。其读音为 /taɪpst/。

发明者

Typst 目前由 Typst GmbH 公司负责开发、维护等。该公司由两位德国程序员成立,这两位也是 Typst 最初的开发者。

Martin Ernst Haug,Typst GmbH 公司的创始人、CEO(首席执行官):负责项目的业务与设计方向,是一位全栈 Web 开发者和设计师,掌舵 Typst 的商业版图。

Laurenz Mädje,Typst GmbH 公司的创始人、CTO(首席技术官):Typst 的主要设计者,也是编译器绝大部分的代码作者,把手项目的技术关口,确保所有改动都符合一致的设计哲学。

背景故事

2019 年,Martin 和 Laurenz 都还是柏林工业大学(TU Berlin)的学生。他们撰写学术论文时,频繁使用 LaTeX,对其运行迟缓、语法繁冗、配置复杂的特性感到相当不满。他们希望有一种工具,既能拥有 LaTeX 那样强大的排版能力,有能具备现代工具该有的易用性和交互体验。于是,他们怀着“解决自己的痛点”的想法,启动了一个雄心勃勃的“业余爱好项目”——创造一个新的排版语言。

这个项目最初很小,但随着开发深入,他们意识到这是一个需要长期投入的庞大工程。

这个项目很快就被赋予了正式的学术身份。2019 年左右,Martin 和 Laurenz 不约而同地决定将这个后来成为 Typst 的项目作为他们的硕士论文课题。Martin 的论文 Fast Typesetting with Incremental Compilation 聚焦项目的性能架构;Laurenz 的论文 Typst: A Programmable Markup Language for Typesetting 则规定了其语言语义。

大约在 4 年后,2023 年 3 月 21 日,正式发布了 Typst 的第一个发行版本(v0.1.0)。

项目在开源社区爆火,两人看到其发展潜力,与 2023 年在柏林成立了 Typst GmbH 公司,将开源项目商业化(放心,还是开源免费的),旨在为 Typst 的长期发展提供稳固的财务支持。

至此,Typst 已从一个简单的“业余爱好项目”,成为被《财富》世界 500 强公司和全球 3500 多所学术机构使用的排版系统,并在 GitHub 上拥有超过 50000 颗 star,时排版领域极具影响力的新兴力量,挑战 LaTeX 的新一代选手。

Linux、Rust、Markdown、Typst、C,以及更多的伟大的发明,都来源于对现状的不满、对改变的渴望;都来源于一个微不足道的“业余项目”。但它们最终都成为参天大树。

通过 Web APP 使用 Typst

你可以通过 Typst 官方提供的 Web APP 来使用 Typst。地址为:https://typst.app/。

Typst Web APP 是使用 Typst 进行文档编写的最常见方式。

优势与劣势

使用 Typst Web APP 的好处有:

使用 Typst Web APP 的不足有:

使用方式

你可以通过邮箱免费注册 Typst。

下面是 Typst Web APP 的主页:

右上角可以注册(“Sign up”)或登录(“Sign in”)。当然,你也可以点击中间的“Try it yourself!”。

你也可以往下翻一翻主页,看看 Typst 究竟有多强大。

进入之后,你会看到一个 Dashboard(仪表盘)。其中,你可以管理你的项目、查看账户信息、了解 Typst,等等。

这里是作者的 Dashboard。其中已经包含了两个测试项目。

然后就可以创建项目,编写 Typst 代码了!

进入项目之后,你可以看到这样的界面:

左边就是 Typst 代码,右边就是 Typst 编译器实时编译之后的预览 PDF。

更多有关 Typst Web APP 的用法,可以自行阅读其官方文档。

关于“Typst Pro”

你也许会看到“Upgrade to Typst Pro”的字样。不用担心,Typst 是完全开源免费的排版编程语言,其编译器是完全开源的(还有 GitHub 仓库)。这里的“Typst Pro”指的是 Typst Web APP 的 Pro 版:更多的存储空间、更方便的功能、更好的多人协作,仅此而已。

Typst 编译器是完全开源的!

本地安装

你也可以通过安装 Typst 编译器到本地来在你的计算机上使用 Typst。

优势与劣势

使用 Typst 本地编译器的好处有:

使用 Typst 本地编译器的不足有:

代码编辑器

强烈推荐使用 VS Code 作为 Typst 源代码的编辑器。推荐使用 Tinymist Typst、Typst Sync、Typst Math 三个插件。这三个插件可以实现代码高亮、实时预览、实时差错、代码建议补全、源代码与预览的协同定位,等等。

通过 GitHub Release 安装预编译版本

你可以在官方 GitHub 仓库的 Release 页面(https://github.com/typst/typst/releases)安装各个平台的预编译版本。该页面下收录了 Typst 最新版的已经构建好的编译器的压缩包。你可以下载与自己平台和系统对应的压缩包,解压到系统对应的程序目录,并手动设置环境变量。

后续如果需要更新 Typst,则在 Release 页面找到更新的发布,下载压缩包、解压,然后用同样了流程安装即可。通常来说这时不需要再设置环境变量了(除非你更改了安装位置)。建议安装后将原版本删除或备份到别处,以防止版本冲突。

从源代码编译

如果你的计算机中有 Rust 工具链,那么直接编译源代码将是最方便也是最推荐的安装方法。利用 Rust 强大的 Cargo 包管理器,可以将这一流程大大简化。在有 Rust 工具链的情况下,我个人建议将这种方法的优先级放在使用其他方法安装之前。

使用下面的命令进行 Typst 编译器的安装:

cargo install --locked typst-cli

上面一条命令就可以完成全部的源码下载、依赖检测、依赖下载、编译链接、环境设置等所有的软件编译安装流程。你不需要专门准备存放源代码用的目录,在任意位置执行即可,Cargo 会将源代码与编译后的程序放在正确的位置。

注意,非常建议添加 --locked 选项。该选项可以保证 Cargo 根据项目源代码中的 Cargo.lock 来解析依赖版本,避免因为版本问题导致编译失败。

编译过程中可能会出现缺失 OpenSSL 的情况。不用惊慌,Cargo 在编译失败后会自行执行清理工作。清理工作完成后,将 OpenSSL 安装好即可。以 Ubuntu 举例,你可以这样安装:

sudo apt install libssl-dev pkg-config  # pkg-config 帮助 OpenSSL 找到正确版本的编译命令

然后重新执行 Cargo 的安装命令即可。

后续可以使用同样的命令更新 Typst。Cargo 会下载最新的 Typst 源代码并执行完整的构建流程。

Linux 下安装

通过软件包管理器安装

可以使用你的 Linux 发行版对应的软件包管理器进行安装。具体命令请参考你的 Linux 发行版的软件包文档。

值得注意的是,不一定所有标准软件包都有 Typst 的软件源。例如,在 Ubuntu 使用的 apt 软件包列表中,就没有 Typst 的软件源。

sudo apt install typst  # 提示找不到软件包

执行软件包管理器的列表命令,结合文本搜索,可以查看你的软件包管理器列表中是否包含 Typst 的软件源。例如,以 Ubuntu 的 apt 软件包管理器为例,你可以使用下面的命令来检查:

sudo apt list | grep "typst"

在作者的 Ubuntu 26.04 (Resolute Raccoon) 上,这条命令的输出是:

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

libghc-skylighting-format-typst-dev/resolute 0.1-1build1 amd64
libghc-skylighting-format-typst-doc/resolute 0.1-1build1 all
libghc-skylighting-format-typst-prof/resolute 0.1-1build1 amd64
libghc-typst-dev/resolute 0.8.0.2-1build1 amd64
libghc-typst-doc/resolute 0.8.0.2-1build1 all
libghc-typst-prof/resolute 0.8.0.2-1build1 amd64
libghc-typst-symbols-dev/resolute 0.1.8.1-1 amd64
libghc-typst-symbols-doc/resolute 0.1.8.1-1 all
libghc-typst-symbols-prof/resolute 0.1.8.1-1 amd64

可以看到是并没有 Typst 的软件源的。

如果你的 Linux 发行版的软件包管理器的软件源列表中有 Typst,那么恭喜你,你可以通过这个方法安装 Typst。否则,你应该尝试其他的方法。

如果你使用软件包管理器安装了 Typst,那么后续更新就可以直接使用软件包管理器。

通过 Snap 安装

Snap 几乎适用于所有的 Linux 发行版。当你的系统中安装有 Snap 软件包管理器和 snapd 服务时,可以使用下面的命令安装 Typst:

sudo snap install typst

Snap 会给软件创造沙盒环境,这在提高安全性的同时,也保证了所安装的软件包是尽可能新的版本,而不受系统限制。(系统自己的软件包管理器可能会有版本锁定,目的是让系统核心代码保持兼容。例如,每一个 Ubuntu 版本都会锁定 Python 的版本,保证自己的系统工具能够正常运行。)

使用 Snap 会带来一些轻微的缺陷,主要是性能上和占用空间上的。Snap 的沙盒环境会稍微拖累软件的性能,同时也会增大安装的软件包的空间。(这也是为什么很多开发者在能用标准软件包管理器的时候不会选择 Snap,除非有很高的安全要求。)

后续可以使用这个命令更新 Typst:

sudo snap refresh typst

Docker 容器

你还可以使用 Docker 容器安装 Typst。这将在一个完全隔离的虚拟环境中安装 Typst。

优先安装预编译版本或手动编译源代码。通过 Docker 安装的方法的优先级应该是最低的。

安装好 Docker 之后,使用以下命令安装 Typst 的 Docker 容器:

docker run -it --rm ghcr.io/typst/typst:latest --help

后续可以使用这个命令更新 Typst:

docker pull ghcr.io/typst/typst:latest

macOS 下安装

通过 Homebrew 安装

Homebrew 是 macOS 系统下安装 Typst 编译器的最简单便捷的方式。

首先你需要安装 Homebrew。安装好 Homebrew 之后,可以使用下面的命令安装 Typst 编译器:

brew install typst

你应该会在稍后被要求进行身份验证。输入密码或进行 ID 验证即可。

后续可以使用这个命令更新 Typst:

brew upgrade typst

该命令可能也需要进行身份验证。

通过 MacPorts 安装

如果你习惯使用 MacPorts 进行安装,也可以使用这个方法。

首先你需要安装 MacPorts。安装好 MacPorts 之后,可以使用下面的命令安装 Typst 编译器:

sudo port install typst

注意:如遇到安装失败的情况,你可能需要备好 Rust 工具链并将其更新到最新版本。

后续可以使用下面两条命令更新 Typst:

sudo port selfupdate  # MacPorts 自我更新
sudo port upgrade outdated  # 自动更新过期软件包

Windows 下安装

以下是在 Windows 系统中常见的安装 Typst 编译器的方法。

通过 winget 安装

winget 是由微软官方维护的 Windows 包管理器,很多现代 Windows 系统已经内置。

打开 PowerShell 或者“命令提示符”(当然如果有 Windows Terminal 则直接打开终端就好),输入以下命令:

winget install --id Typst.Typst

如果需要指定安装位置,则可以使用下面的命令:

winget install --location="安装目录" --id Typst.Typst

后续可以使用这个命令更新 Typst:

winget upgrade Typst.Typst

通过 Scoop 安装

Scoop 是一个社区维护的可在 Windows 下使用的命令行包管理器。

安装 Scoop 之后,可以使用以下命令安装 Typst 编译器:

scoop install typst

后续可以使用这个命令更新 Typst:

scoop update typst

通过 Chocolatey 安装

Chocolatey 是另一个在 Windows 下流行的包管理器。

安装 Chocolatey 之后,可以使用以下命令安装 Typst 编译器:

choco install typst

后续可以使用这个命令更新 Typst:

choco upgrade typst

Typst CLI 工具使用

接下来介绍在 CLI(命令行接口)中使用 Typst 的基本方法。

查看欢迎与指导(检查安装)

使用下面的命令查看 Typst 的欢迎信息和初步指导:

typst

如果正常输出,它也能代表你已经成功安装了 Typst CLI 工具。

在我的计算机上,其输出是:

Welcome to Typst, we are glad to have you here! ❤️

If you are new to Typst, start with the tutorial at https://typst.app/docs/tutorial/.
To get a quick start with your first project, choose a template on https://typst.app/universe/.

Here are the most important commands you will be using:

- Compile a file once: typst compile file.typ
- Compile a file on every change: typst watch file.typ
- Set up a project from a template: typst init @preview/<TEMPLATE>

Learn more about these commands by running typst help.

If you have a question, we and our community would be glad to help you out on
the Typst Forum at https://forum.typst.app/.

Happy Typsting!

查看版本

使用下面的命令检查 Typst 的安装情况和版本:

typst --version

你应该可以看到 Typst 的版本号。

编译

使用下面的命令将 main.typ 编译为 PDF 文档:

typst compile main.typ

其中,compile 可以被简写为 c:

typst c main.typ

监视

使用下面的命令监视 main.typ 文件的更改。当文件更改时,自动进行增量编译,以做到类似实时预览的效果。

:::info[监视与实时阅读]{open} 启用监视后,当你的 PDF 阅读器有实时加载更改的功能时(如 Sumatra PDF),你每次更改源文件都能立即(或很快)看到 PDF 更改;当你的 PDF 阅读器没有此功能时(如浏览器),你每次更改完源文件需要在阅读器中刷新才能看到更改。 :::

typst watch main.typ

其中,watch 可以被简写为 w:

typst w main.typ

查看可用字体

使用下面的命令查看 Typst 可以使用的字体(一般和系统已安装字体无异):

typst fonts

更新 Typst CLI 工具

使用下面的命令让 Typst CLI 工具进行自我更新(如果可以的话):

typst update

Typst 文本模式

接下来介绍一些 Typst 的文本模式中基本的排版功能。这部分介绍的是 Typst 在文本模式下的标记语言的语法,也难免会接触到一些代码模式下的语法。

需要时刻谨记,Typst 是图灵完备的编程语言,不是简单的标记语言。

此部分默认你已经熟悉了至少一门标记语言(最好是 Markdown。详见:Markdown 快速入门手册),所以会稍显简洁。如果你未曾使用过标记语言,先去学习 Markdown。

文本

直接在 Typst 源代码中写入文字,即可被编译为 PDF 中的文字。

例子:

This is a piece of text.

分段

使用空行分段。

例子:

In this article, we're going to talk about Typst.

Typst is a wonderful typesetting tool...

源代码中断行

在源代码中断行,编译后显示在同一行,且添加空格。这一点与 Markdown 一致。

例子:

This sentence is too long so that we write it
in two lines in the source code.

But it's still in one line after compiled.

段落中换行

在源代码某一行后面添加反斜杠(\)即可在同一个段落中换行。反斜杠后可以有空格。

例子:

A piece of lyrics from a famous French song _Dehors_:

"Et quand tu briseras ta cage \
On ira à la foire \
On tournera la page et tu serreras mon corps \
On partira à la nage \
On aura la mer à boire \
Tu manques pas de courage \
Alors viens jouer dehors"

注释

使用 // 或 /* ... */ 来标记注释。

例子:

Let's use Typst for writing now!  // in English

文章标题

Typst 是用于编写各类文章的编程语言,自然就需要给文章设置一个标题。

在代码模式中,使用 title() 函数即可设置文章标题。

例子:

#title[Use Typst Anywhere]

You can use Typst anywhere you want. You can use Typst to write passages, articles, papers, letters, and so on.

标题

Typst 中,标题格式与 Markdown 类似,但使用等号(=)而不是井号(#)。同样地,连续 1 到 6 个等号分别代表 H1 到 H6。Typst 的标题等级实际上是不限的(有多少个等号就是几级标题),但是我们一般只用到 H3 或 H4,最坏情况下也只用到 H6。连续的等号后与内容间需要一个空格。

例子:

This is a piece of text.

= H1 Heading

== H2 Heading

=== H3 Heading

==== H4 Heading

===== H5 Heading

====== H6 Heading

该语法本质上相当于在代码模式中使用 heading() 函数并设置等级。

例子:

#heading(level: 3)[
  H3 Heading
]

#heading(level: 1)[
  H1 Heading
]

文本格式

加粗

使用成对的一个星号(*)对文本进行加粗。

例子:

When you program in *Rust*, always pay attention to the *ownerships*.

该语法本质上相当于在代码模式中使用 strong() 函数。

例子:

When you program in #strong[Rust], always pay attention to the #strong[ownerships].

编译结果是一样的。

斜体

使用成对的下划线(_)对文本进行斜体处理。

例子:

To be or no to be, _that_ is a question.

该语法本质上相当于在代码模式中使用 emph() 函数。

例子:

To be or no to be, #emph[that] is a question.

编译结果是一样的。

删除线

在代码模式中使用 strike() 函数可以为文本添加删除线。

例子:

We're having #strike[diner] (Oops! That is a typo.) dinner.

可以给 strike() 函数添加 stroke 参数来控制删除线的颜色、粗细等。使用加号 + 组合颜色值(类型:颜色)和粗细值(类型:长度)。

:::info[数据类型、函数、参数]{open} 关于数据类型、函数与参数,后面的代码模式部分会详细讲解。 :::

例子:

We're having #strike(stroke: red + 2pt)[diner] (Oops! That is a typo.) dinner.

下划线

在代码模式中使用 underline() 函数可以为文本添加下划线。

例子:

The underlined word "#underline[present]" means "gift" in this context.

同样可以使用 stroke 参数来设置颜色和粗细。

例子:

The underlined word "#underline(stroke: blue + 1.5pt)[present]" means "gift" in this context.

同时,underline() 函数还接收一个布尔类型的 evade 参数。有时,文字可能会有一部分超出下划线(如占下格的英文字母 g、p、q 等),若 evade 参数是 true(默认值),则会在这些笔画断开,以增加可读性;若是 false,则不会断开,画出一整条完整的线。

例子:

What does "#underline(evade: true)[query]" mean here?

What does "#underline(evade: false)[query]" mean here?

高亮

在代码模式中使用 highlight() 函数可以高亮一段文字。

例子:

Let's use #highlight[Typst] to write papers!

可以设置 fill 参数为某个颜色,来控制高亮的背景颜色。

例子:

#highlight(fill: green)[Green] is the color of nature.

可以设置 radius 参数为某个长度,来控制高亮背景的圆角半径。

例子:

As we can see in this picture, the earth is like a huge ball with #highlight(fill: aqua, radius: 3pt)[blue] water on it.

小型大写

这个格式在标题中非常常用,是一种常见的文字艺术形式。其将文本中的小写替换为正常小写高度的大写。

在代码模式中使用 smallcaps() 函数可以使用小型大写格式。

例子:

= #smallcaps[Introduction to Typst]

Typst is a #smallcaps[Wonderful Typesetting Tool] for writing documents.

上标

在代码模式中使用 super() 函数可以书写上标文本。

例子:

The 1#super[st] person who complete the task will win a prize.

下标

在代码模式中使用 sub() 函数可以书写下标文本。

例子:

In chemistry, we write "H#sub[2]O" instead of "water."

文本样式设置

文本样式设置主要通过 text() 函数实现,每一种样式设置通常对应该函数的一个参数。在后面我们会学习到,text() 除了是个函数之外,还是 Typst 中的基本元素(element)之一。

字号

使用 text() 函数的 size 参数来设置字号。参数数据类型是长度。

例子:

Let's see a #text(size: 20pt)[BIG] word.

字体颜色

使用 text() 函数的 fill 参数来设置字体颜色。参数数据类型是颜色。

例子:

The additive primary colors of light are #text(fill: red)[red], #text(fill: green)[green] and #text(fill: blue)[blue].

字重

使用 text() 函数的 weight 参数来设置字重。参数数据类型是字符串,且内容有要求。

例子:

We can write #text(weight: "semibold")[bold] words. We can make them even #text(weight: "bold")[bolder].

如果你仔细看,可以发现后者比前者要更粗。

下面是常用的 weight 参数的值和对应含义。

从细到粗的排名 名称 数值
1 thin 100
2 extralight 200
3 light 300
4 regular 400
5 medium 500
6 semibold 600
7 bold 700
8 extrabold 800
9 black 900

默认值是 regular。

注意:使用对应的字重,需要你的字体支持相应字重。如果不支持,Typst 会选择一个在视觉上最接近的可用的字重。

字体

使用 text() 函数的 font 参数可以设置字体。参数数据类型是字符串或数组。

例子:

#text(font: "Times New Roman")[
  Times New Roman is a traditional font which is used widely in various fields.
]

当 font 参数是数组时,则会设置字体链:当某个字符不存在于第一个字体中时,则回退并尝试第二个字体,随后是第三个字体……依此类推。

这样我们就可以设置中英文混排的字体了:英文字体在前、中文字体在后。遇到英文字符时,先尝试第一个字体:是英文字体,所以使用这个字体;遇到中文字符时,先尝试第一个字体:是英文字体,不包含中文字符,于是尝试第二个字体。

例子:

#text(font: ("Times New Roman", "SimSun"))[
  在英语中,you 这个词既可以指“你”,也可以指“你们”。并且 you 的这两个意思都应该使用 be 动词的 are 的形式。
]

注意数组使用小括号。我们在后面代码模式的部分会详解。

智能符号

智能引号

在书面英语中,引号一般使用弯引号(与中文习惯相同)。而我们键盘上输入的一般是直引号。因此,Typst 可以自动识别我们在源代码中输入的直引号的位置,并智能替换为正确的弯引号。

例子:

He said, "I'm going to say 'Welcome to our club!' to her."

在设置了语言的情况下,Typst 还可以将源代码中的直引号替换为该语言中标准书面语的引号。

下面是一个例子。注意到,在这个例子中,我们使用了 set 语法来设置后面的代码中 text() 函数的 lang 参数的默认值。lang 参数表示文本的语言,为字符串类型,使用 ISO 639-1 中规定的两个字母作为语言的代号。我们在后面的代码模式部分会详细讲解 set 语法。另外,这个例子中还使用了 // 标记的注释。

#set text(lang: "en")  // English

English: He said, "I'm going to say 'Welcome to our club!' to her."

#set text(lang: "fr")  // French

Français : Il a dit : "Je vais lui dire 'Bienvenue dans notre club !'"

#set text(lang: "de")  // German

Deutsch: Er sagte: "Ich werde zu ihr sagen: 'Willkommen in unserem Club!'"

#set text(lang: "es")  // Spain

Español: Él dijo: "Voy a decirle '¡Bienvenida a nuestro club!' a ella."

#set text(lang: "eo")  // Esperanto

Esperanto: Li diris, "Mi diros al ŝi 'Bonvenon al nia klubo!'"

你可以通过设置 smartquote() 函数的 enable 参数来启用或禁用智能引号。该参数是布尔值。这里不再演示。

如果你需要保留源代码中的直引号,可以使用转义符 \"。

例子:

English: He said, \"I'm going to say 'Welcome to our club!' to her.\"

智能破折号

下面以英文为例。

英文书面语中共有三种横杠:连字符(-)、en-dash(–)和 em-dash(—)。连字符的用法大家都清楚。en-dash(宽度近似于字母 N)一般用于表示数字范围;em-dash(宽度近似于字母 M)与中文中破折号的用法类似,起解释说明的作用。

en-dash 和 em-dash 在普通键盘和输入法中难以键入,因此 Typst 也提供了替代方案。在 Typst 源代码中,一个连字符(减号,-)会被原样编译为连字符,两个(--)会被编译为 en-dash,而三个(---)会被编译为 em-dash。

例子:

The range of ages of the participants is very wide---it's about 20--60.

智能省略号

使用三个连点(...)可以构成省略号。这个效果不是特别明显,并且通常我们也是这样打省略号的,因此这里不再给出例子。

分隔线

使用 line() 函数即可生成分隔线。

默认情况下,line() 函数生成的线是 30pt 宽的,你可以设置 length 参数(长度或比例、分数类型)来规定其长度。设置为 100% 即可生成全宽的分隔线。而使用 stroke 参数(stroke 类型)可以规定其线宽、线条样式、颜色等。

例子:

#line()

#line(length: 50%)

#line(length: 100%)

#line(length: 100%, stroke: red)

#line(length: 100%, stroke: 3pt)

#line(length: 100%, stroke: (dash: "dashed"))

#line(length: 100%, stroke: (dash: "dotted"))

#line(length: 100%, stroke: 3pt + blue)

#line(length: 100%, stroke: (dash: "dashed", paint: green))

列表

无序列表

在 Typst 中,使用减号(-)来创建无序列表。注意减号与内容之间需要一个空格。

与 Markdown 类似,你可以选择使用行距或段距。

例子(注意这里用了转义符 \#):

These are the supported programming languages:

- C
- C\#
- C++
- Java
- Python
- Rust
- Swift

另一个例子:

These are the reasons why we should use Typst instead of LaTeX:

- Typst is quite easy to master, while LaTeX is confusing for almost everyone.

- Typst is a turing-complet programming language, while LaTeX is just a markup language with macros.

- Typst source code can be compiled even faster than you imagine, while LaTeX can compile a document in several minutes!

其中第一个例子使用行距,第二个例子使用段距。

无序列表相当于在代码模式中使用 list() 函数并传入多个内容类型的参数。关于内容数据类型,我们会在后面的代码模式部分详细讲解。

例子:

Factors that may cause global warming:

#list([The produce of carbon dioxide.], [Cutting down trees.], [The use of fossil fuels.])

Ways to prevent global warming:

#list[Plant more trees.][Use electric energy instead of fossil fuels.][Reduce waste.]

使用 list() 函数时,可以使用 marker 参数来规定列表标记符号。该参数是字符串类型或内容类型的。

例子:

#list(marker: [--])[Chinese][Math][English][Physics][Chemistry][Biology][History][Politics][Geography]

有序列表

在 Typst 中,使用加号(+)来创建有序列表。注意加号与内容之间需要一个空格。

与 Markdown 类似,你可以选择使用行距或段距。

例子:

Steps to install VS Code on Windows:

+ Visit the official website of VS Code.
+ Find the installation package and download it.
+ Run the installation package and just wait.

有序列表相当于在代码模式中使用 enum() 函数并传入多个内容类型的参数。

例子:

Translate these sentences into French:

#enum(
  [Hello!],
  [Good morning!],
  [How are you today?],
  [I live in Paris.],
  [Do you have books?]
)

Answers:

#enum[
  Bonjour !
][
  Bonjour !
][
  Comment ça va aujourd'hui ?
][
  J'habit à Paris.
][
  As-tu des livres ?
]

当使用 enum() 函数时,可以通过 numbering 参数设置序号样式。该参数是字符串类型或函数类型的。

例子:

Which of the statements is true?

#enum(numbering: "A.")[
  The computers use binary numbers to store data.
][
  The maximum number which an unsigned 8-bit integer type can store is 256.
][
  Floating-point numbers are stored according to the IEEE 154 standard.
]

Answer: A.

Explanations:

#enum(numbering: "A)")[
  That is true.
][
  It should be 255 instead of 256.
][
  The standard is IEEE 754.
]

缩进

与 Markdown 类似,通过缩进可以将几乎任何内容包含在一项列表项中。我们一般采用 2 空格缩进。

例子:

Let's look through the passage, and then we can find:

- The beginning of the passage is:

  What is computer science? How does it help in our daily life? ...

- In turn, the ending of the passage is:

  That is the basic information of computer science. Hopefully, all of you can make the most of the power of computer science.

术语列表

这个术语列表是 Typst 中一个很独特的功能了。

术语列表的格式类似于下面这样:

/ Term 1: Discription 1
/ Term 2: Discription 2
/ Term 3: Discription 3

当然,术语列表也可以选择使用行距或者段距。

例子:

/ Linear Function: A function of the form $y = a x + b$, where $a != 0$.

/ Quadratic Function: A function of the form $y = a x^2 + b x + c$, where $a != 0$.

/ Cubic Function: A function of the form $y = a x^3 + b x^2 + c x + d$, where $a != 0$.

这个例子中还使用了数学公式。Typst 的数学公式与 Markdown、LaTeX 不一样,做了很多简化。

题外话:如果你觉得上面的字体不统一而觉得不好看,则可以将数学字体设置为 Libertinus Math。这与 Typst 的默认字体 Libertinus Serif 是统一的。下面是效果图:

当然,数学公式我们要在后面的部分讲到。

代码块

行间代码块

与 Markdown 的语法几乎一致。使用成对的三个反引号(```)加上可选的语言名称即可。Typst 默认会对代码块进行高亮。

例子:

Below is a simple program written in Rust that says hello to the world.

```rust
fn main() {
    println!("Hello, World!");
}
```

当代码块中需要包含三个反引号时,可能会与标记代码块边界的三个反引号冲突。此时我们可以增加反引号的个数,使用四个反引号标记代码块边界。

例子:

Markdown is a powerful markup language used widely in many fields, especially writing. Below is an example of Markdown code:

// 4 backquotes
````markdown
Below is a **simple** program written in _Rust_ that says hello to the world.

```rust
fn main() {
    println!("Hello, World!");
}
```
````

可以看到,Typst 对代码高亮的支持还是很好的。

行间代码块相当于使用 raw() 函数并设置 block 参数为 true。使用 lang 参数(字符串类型)可以设置语言名称,而 text 参数(字符串类型)(这是一个位置参数,后面会介绍,目前可理解为不需要也不能指定名称的参数)就是其内容。

小提示:Typst 中的字符串是可以跨行的。

例子:

#raw(
  "#include <iostream>

int main() {
    std::cout << \"Hello, World!\" << std::endl;

    return 0;
}",
  block: true,
  lang: "cpp",
)

事实上,如果你觉得这种代码块不好看,你可以自己设置一些格式。具体如何设置会在后面代码模式的部分讲。现在先看一个例子:这是使用作者自己编写的 article-template 模板的 0.1.1 版本之后,代码块的效果。

#import "@local/article-template:0.1.1": *

#show: set-style

Below is a simple program written in Rust that says hello to the world.

```rust
fn main() {
    println!("Hello, World!");
}
```

普通行内代码块

该语法与 Markdown 高度相似。使用成对的单个反引号(`)即可标记普通行内代码块。

例子:

We use the type `int` to store integers in C/C++ and Java.

行内代码块会默认使用等宽字体。

高亮行内代码块

使用成对的三个反引号(```)加上语言,但不换行,可以标记有高亮的行内代码块。

例子:

We use the type ```rust i32``` to store integers in Rust.

这样行内代码块也有高亮了。

脚注

使用 footnote() 函数可以添加脚注。

例子:

If we don't stop pollution#footnote[Such as air pollution, water pollution, noise pollution, light pollution, etc.], more and more animals will go extinction.

(为了避免图片过长,这里分成两次截图。)

表格

:::info[代码模式]{open} Typst 中的表格需要你掌握一些基本的代码模式的技能,所以建议你先阅读完代码模式的部分,再回过头来学习表格。这一节放在这里是为了文章结构的清晰。 :::

table() 函数

使用 table() 函数即可在 Typst 中创建表格。其关键参数有:

当然还有一些其他的参数,你可以自行查阅文档。

随后跟随几个内容类型的参数,表示表格的内容。Typst 会根据列数自动换列。

下面是一个例子。在这个例子中,我们创建了一个有三列的表格,使用水平和垂直居中,并设置每一列的宽度为自动(即根据内容调整列宽)。

#table(
  align: center + horizon,
  columns: (auto,) * 3,
  [*English*], [*Français*], [*Esperanto*],
  [Hello!], [Bonjour !], [Saluton!],
  [I], [je], [mi],
  [I love my parents.], [J'aime mes parents.], [Mi amas miajn gepatrojn.],
  [He is eating an apple.], [Il mange une pomme.], [Li manĝas pomon.],
  [If you are interested in a language, you should take courses to learn the language.], [Si vous êtes intéressé par une langue, vous devriez suivre des cours pour apprendre la langue.], [Se vi interesiĝas pri lingvo, vi devus preni kursojn por lerni la lingvon.],
)

当然,columns 参数设置为分数的元组时,可以填满整页,而非根据内容调整宽度。下面是一个例子:

#table(
  align: center + horizon,
  columns: (1fr,) * 4,
  [*English*], [*Français*], [*Esperanto*], [*Deutsch*],
  [file], [fichier], [dosiero], [Datei],
  [computer], [ordinateur], [komputilo], [Computer],
  [science], [science], [scienco], [Wissenschaft],
  [run], [courir], [kuri], [laufen],
  [eat], [manger], [manĝi], [essen],
  [write], [écrire], [skribi], [schreiben],
  [China], [Chine], [Ĉinio], [China],
  [The United Nations], [Les Nations Unies], [Unuiĝintaj Nacioj], [Die Vereinten Nationen],
  [The United Kingdom], [Le Royaume-Uni], [Unuiĝinta Reĝlando], [Vereinigtes Königreich],
  [The United States], [Les États-Unis], [Usono], [Die Vereinigten Staaten],
)

合并单元格

可以使用 table.cell() 函数来进行单元格合并,其中 colspan 参数(整数类型)表示列合并,rawspan 参数(整数类型)表示行合并。

例子:

#table(
  columns: (auto, auto, 3em, 3em, 3em),
  align: center + horizon,
  table.cell(colspan: 2)[*Catalyst*],
  table.cell(colspan: 3, rowspan: 2)[*Reaction Time (seconds)*],
  [*Type*], [*Code*],
  table.cell(rowspan: 2)[*Transition Metal*], [A], [45], [62], [78],
  [B], [50], [68], [85],
  [*Noble Metal*], [C], [88], [92], [95],
)

图像

使用 image() 函数可以插入图像。image() 函数的第一个参数表示图片的路径(绝对路径或相对路径;相对路径相对于源代码文件位置),是字符串类型的。width 参数(长度或分数、比例类型)控制图片显示宽度,而 height 参数(长度或分数、比例类型)控制图片显示高度。该函数支持绝大多数的图片格式,包括 PNG、JPG、SVG 等。

例子(假设当前目录中有名为 typst-example.png 的图片文件):

#image("typst-example.png")

#image("typst-example.png", width: 10em)

#image("typst-example.png", width: 50%)

figure() 函数

figure() 函数用于创建可浮动的图片、表格、代码等,并且可以自动编号,还可以使用后面将介绍的标签与交叉引用。

figure() 函数的第一个参数 body 为内容参数,表示你要插入的东西。caption 参数(内容类型)表示你对这个 Figure 的标题。Typst 会自动选择名称前缀和编号。(不使用 caption 参数将没有名称前缀和编号。如果需要名称前缀和编号但不需要标题,则可以将 caption 设置为空内容。)名称前缀是跟随你设置的语言而变化的。

例子:

// Set fonts for western languages and Chinese.
#set text(
  font: ("Libertinus Serif", "Noto Serif CJK SC"),
)

// English

#figure(
  image("typst-title.png"),
  caption: [The Title Image of Typst],
)

#set text(lang: "fr")  // French

#figure(
  image("typst-title.png"),
  caption: [L'image de titre de Typst],
)

#set text(lang: "de")  // Deutsch

#figure(
  image("typst-title.png"),
  caption: [Das Titelbild von Typst],
)

#set text(lang: "eo")  // Esperanto

#figure(
  image("typst-title.png"),
  caption: [La Titola Bildo de Typst],
)

#set text(lang: "ru")  // Russian

#figure(
  image("typst-title.png"),
  caption: [Заглавное изображение Typst],
)

#set text(lang: "el")  // Greek

#figure(
  image("typst-title.png"),
  caption: [Η Εικόνα Τίτλου του Typst],
)

#set text(lang: "zh")  // Chinese

#figure(
  image("typst-title.png"),
  caption: [Typst 的标题图],
)

figure() 函数的名称前缀是根据内容所决定的。下面是一个例子:

#figure(
  image("typst-title.png"),
  caption: [The Title Image of Typst],
)

#figure(
  table(
    columns: 2,
    align: center + horizon,
    [*Typst*], [*LaTeX*],
    [Easy], [Difficult],
    [Fast], [Slow],
    [Programming Language], [Markup Language with Macros],
  ),
  caption: [The Differences Between Typst and LaTeX]
)

#figure(
  [
    ```typst
    Let's use *Typst* together!

    This is an exmaple code of Typst. It shows the basic syntax of Typst.

], caption: [An Example Code of Typst] )


![编译后的结果](https://cdn.luogu.com.cn/upload/image_hosting/n55jf1k5.png "编译后的结果")

## 标签和交叉引用

在 Typst 中,可以使用标签来标记一个内容,并使用交叉引用来在文章中引用这些内容。

标签跟随在元素的后面,使用一对尖括号(`<>`)包裹。其中可以包含 Unicode 非空白非标点字符(大小写西文字母、CJK 字符等)以及冒号(`:`)、下划线(`_`)、连字符(`-`)、点号(`.`)。

一般来说,建议标签遵循以下命名习惯:

1. 使用英文。
2. 在冒号前写上标签所标记的内容类型(如小节标题、图片、表格、代码、公式等)。
3. 在冒号后写上简洁的可以唯一表示内容的名称。
4. 使用连字符代替空格(与 Typst 代码命名习惯相符合)。

当然,你也可以使用自己的命名习惯。建议始终将内容类型写在名称前面。

以下是几个标签的例子:

- `section:introduction`
- `figure:typst-title`
- `table:experiment-result`
- `listing:impl-of-problem-a`
- `equation:area-of-rect`

注意,尽量保证标签在整个源代码中的唯一性。

标记标签后,可以在文本模式中使用 @ 符号加标签名称来引用该标签所标记的内容。标签是可以标记几乎任何内容的,但交叉引用能够引用的标签对内容类型有所限制。一般来说,常见的能够被引用的标签的内容有以下几种:

1. 包裹在 `figure` 内的图像。
2. 包裹在 `figure` 内的表格。
3. 包裹在 `figure` 内的代码。
4. 启用了序号的标题。
5. 启用了序号的行间公式。

我们来看一个比较长的例子:

```typst line-numbers
#set heading(numbering: "1.")  // Numbering for headings.
#set math.equation(numbering: "1")  // Numbering for equations.

#title[
  Converting Fahrenheit to Celsius
]

= Mathematical Analysis <section:math-analysis>

We need to convert the given Fahrenheit temperature $F$ to Celsius $C$. We can use the following formula for the conversion:

$
  C = (F - 32) times 5/9
$ <equation:conversion>

= Implementation in Rust

We can use a computer program to perform the calculation. According to @equation:conversion given in @section:math-analysis, we can write the code shown in @listing:implementation.

#figure(
  [
    ```rust
    use std::io;

    fn convert(temp_f: f64) -> f64 {
        (temp_f - 32.0) * 5.0 / 9.0
    }

    fn main() {
        let input = io::stdin();

        println!("Input the Fahrenheit temperature:");

        let mut line = String::new();
        input.read_line(&mut line).unwrap();

        let temp_f: f64 = line.trim().parse().unwrap();

        let temp_c = convert(temp_f);
        println!("The Celsius temperature is {temp_c:.2}");
    }

], caption: [Implementation of @equation:conversion in Rust] ) <listing:implementation>

= Running the Program

After writing the code, we can run this program to convert Fahrenheit to Celsius.

We can use Cargo to manage our project. Use bash cargo run to run the program.

@figure:example is an image showing an example of the program in operation.

figure(

image("example.png", width: 80%), caption: [An Example of the Program in Operation] ) <figure:example>

= Organizing the Data

We can run the program shown in @listing:implementation several times to obtain the Celsius degrees corresponding to different Fahrenheit degrees.

@table:data lists several Fahrenheit degrees and their corresponding Celsius degrees, with the relevant data obtained by running the program.

figure(

table( columns: 2, align: center + horizon, [Fahrenheit Degrees], [Celsius Degrees], [-100.00], [-73.33], [-50.00], [-45.56], [0.00], [-17.78], [0.55], [-17.47], [12.34], [-10.92], [20.00], [-6.67], [56.78], [13.77], [100.00], [37.78], [12345.67], [6840.93], ), caption: [Some Data] ) <table:data>


![编译后的结果](https://cdn.luogu.com.cn/upload/image_hosting/l9s11wza.png "编译后的结果")

是不是已经有种在写论文的感觉了?你已经可以用 Typst 编写简单的文章了!

使用标签和交叉引用的最大的好处,就是在文章布局变动的时候,不用自己手动编号。Typst 会在编号变动的时候,通过交叉引用自动生成对应的文本。

## 假文

有的时候,为了测试我们的排版效果,我们需要生成一些文字。但是我们可以偷个懒:与其手动思考并编写测试用的文字,不如直接用看起来像真实文本但实际上无意义的文本,排版中我们称之为“假文”。

使用 `lorem()` 函数可以生成假文。函数接收一个整型参数,表示要生成的单词数。

例子:

```typst line-numbers
#set text(font: "Libertinus Serif")

= #lorem(3)

#lorem(100)

#set text(font: "Times New Roman")

= #lorem(4)

#lorem(120)

#set text(font: "Arial")

= #lorem(4)

#lorem(110)

#set text(font: "Libertinus Sans")

= #lorem(5)

#lorem(130)

Typst 数学排版

Typst 作为强大的排版编程语言,自然少不了数学公式的支持。而 Typst 既然已经将文档排版简化到了这种地步,也应该简化一下数学公式排版吧?还真是。Typst 摈弃了 LaTeX 那一眼望去全是花括号和反斜杠的繁杂的语法,采用更简单的语法来编写数学公式。有多简单呢?我们接下来就来看看吧。

:::info[数学字体]{open} 在后文中,为了美观,我们将使用 Libertinus Math 作为数学字体,使用 Libertinus Serif(Typst 的默认字体)作为正文字体(保证正文字体与数学字体统一)。如果你没有这个字体、想用默认字体或其他数学字体,可以删除或修改字体设置的代码。 :::

基本数学语法

行内数学公式

与 Markdown 和 LaTeX 类似,在段落内使用一对美元符号($)来创建行内公式。

需要注意的是,美元符号之间不能有空白符。这个我们后面会说明原因。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

We can use the formula $A = w l$ to calculate the area of a rectangle, where $w$ is the width of the rectangle and $l$ is its length.

行间数学公式

行间数学公式使用一对美元符号($),并且美元符号之间需要使用空白符。

虽然说是使用任意空白符,但是我们一般采用换行符或空格。

现在你应该知道为什么在行内公式中不能在美元符号之间加空格了吧。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

We can use the following formula to calculate the volume of a sphere (where $r$ represents the radius of the sphere):
$ V_"sphere" = 4/3 pi r^3 $

We can also use the following formula to calculate the volume of a cone (where $r$ represents the radius of the base and $h$ represents the height):
$
  V_"cone" = 1/3 pi r^2 h
$

仔细阅读数学公式代码,你应该已经对 Typst 数学语法的简洁性有一些印象了。接下来我们详细讲解。

字母、符号、括号与文本

在 Typst 数学公式中,如果要连续使用多个字母,则应该要在其中打空格。因为 Typst 会将两个及以上的字母所构成的单词视作数学变量或数学函数。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

The formula for the volume of a cylinder (where $r$ represents the radius of the base and $h$ represents the height):
$
  V = S h = pi r^2 h
$

你可以双写一个大写字母来使用黑板粗体(也就是我们表示常见集合的字体)。例如:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

The set of natural numbers $NN$ is contained in the set of integers $ZZ$. That is:
$
  NN subset.eq ZZ
$

你可以书写希腊字母单词来表示希腊字母。若单词首字母大写,则表示大写希腊字母;全小写单词表示小写希腊字母。

你还可以用一些其它的单词表示不同的数学函数,例如 cos、sin、max、lim 等。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

We use $alpha$, $beta$, $gamma$ and $theta$ for angles, but not $Alpha$, $Beta$, $Gamma$ and $Theta$.

The following formula is one of the trigonometric reduction fomulas:
$
  sin theta = cos (pi/2 - theta)
$

你应该注意到了,Typst 的括号默认是根据其中内容调节高度的,而不需要像 LaTeX 一样使用 \left 和 \right。

你可以直接在数学公式代码中使用字符串语法来插入文本。例如:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

We define a function like this:
$
  f(x) = cases(
    1 quad "if" x "is odd",
    0 quad "if" x "is even",
  )
$

你也可以在数学公式中使用代码模式。如果你在数学公式中使用代码模式,并且使用 [] 来创建内容类型的数据,你就可以在数学公式中使用内容块。关于代码模式和内容类型,后面代码模式的部分会详细讲解。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

Then the total complexity of the argorithm is:
$
  O(underbrace(n^2, #[the complexity of `check` function]) times overbrace(n m, #[the complexity of `find` function]))
$

智能括号

在 Typst 数学语法中,小括号(())既可以表示数学公式中的小括号,也有包裹内容的作用(类似 LaTeX 的花括号)。Typst 会根据上下文自动决定该小括号的功能。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

$
  (a + b) dot (a - b)
$

$
  (a + b) / (a - b)
$

现在你应该知道它智能在哪了吧。

数学符号

从上面的示例中,你应该发现了很多 Typst 的数学符号。接下来我们来介绍一些常用的符号。

对于更多符号,可以阅读官方文档 Typst Syntax Math 和 Typst Symbols。

基本运算与比较

含义 代码 示例(代码) 示例(结果)
加法或正号 + $ a + b $
减法或负号 - $ a - b $
正负号 plus.minus $ a plus.minus b $
负正号 minus.plus $ a minus.plus b $
除法 div $ a div b $
叉乘 times $ a times b $
点乘 dot $ a dot b $
分数 / $ a / b $
等于 = $ a = b $
不等于 != $ a != b $
小于 < $ a < b $
不小于 lt.not $ a lt.not b $
小于等于 <= $ a <= b $
不小于等于 lt.eq.not $ a lt.eq.not b $
远小于 << $ a << b $
大于 > $ a > b $
不大于 gt.not $ a gt.not b $
大于等于 >= $ a >= b $
不大于等于 gt.eq.not $ a gt.eq.not b $
远大于 >> $ a >> b $
约等于 approx $ a approx b $
算数平方根 sqrt() $ sqrt(a) $
次方根 root() $ root(n, a) $
幂(上标) ^ $ a^b $
下标 _ $ a_b $
对数 log $ log_a b $
自然对数 ln $ ln a $
常用对数 lg $ lg a $
比 : $ a : b $
整除 divides $ a divides b $
不整除 divides.not $ a divides.not b $
同余 equiv $ a equiv b $
不同余 equiv.not $ a equiv.not b $
向下取整 floor() $ floor(a / b) $
向上取整 ceil() $ ceil(a / b) $
绝对值(也可表示模长) abs() $ abs(a) $

集合

含义 代码 示例(代码) 示例(结果)
空集 emptyset $ A = emptyset $
属于 in $ a in A $
不属于 in.not $ a in.not A $
包含于 subset.eq $ A subset.eq B $
不包含于 subset.eq.not $ A subset.eq.not B $
真子集 subset.neq $ A subset.neq B $
包含 supset.eq $ B supset.eq A $ \
不包含 supset.eq.not $ B supset.eq.not A $
含真子集 supset.neq $ B supset.neq A $
交集 inter $ A inter B $
并集 union $ A union B $
补集 complement $ complement_U A $

几何

含义 代码 示例(代码) 示例(结果)
角 angle $ angle A $
直角 angle.right $ angle.right A $
平行四边形 parallelogram $ parallelogram A B C D $
三角形 triangle $ triangle A B C $
垂直于 perp $ l perp m $
平行于 parallel $ l parallel m $
不平行于 parallel.not $ l parallel.not m $
相似 tilde $ triangle A B C tilde triangle D E F $
全等 tilde.equiv $ triangle A B C tilde.equiv triangle D E F $
包含于(在……上) subset $ l subset alpha $
不包含于(不在……上) subset.not $ l subset.not alpha $
包含(……上有) supset $ alpha supset l $
不包含(……上没有) supset.not $ alpha supset.not l $

大型符号

含义 代码 示例(代码) 示例(结果)
求和 sum $ sum_(i = 1)^n f(i) $
求积 product $ product_(i = 1)^n f(i) $

其他符号

含义 代码 示例(代码) 示例(结果)
无穷大 oo $ a -> +oo $
阿列夫 aleph $ aleph_0 $
存在 exists $ exists a in A $
任意(所有) forall $ forall a in A $
与 and $ a and b $
或 or $ a or b $
非 not $ not p $
右箭头 -> $ a -> b $
左箭头 <- $ a <- b $
推出 => $ p => q $
充要条件 <=> $ p <=> q $
因为 because $ because p $
所以 therefore $ therefore q $
正比于 prop $ y prop x $
省略号 dots $ a_1, a_2, dots, a_n $
居中省略号 dots.c $ a_1, a_2, dots.c, a_n $
二项式系数 binom() $ binom(m, n) $

更多数学排版

注释

使用 /* ... */ 在数学代码中编写注释。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

$
  sum_(i = 1)^n f(i)  /* the sum of f(i) for i from 1 to n */
$

这个就不再配图了。

解释说明

overbrace 和 underbrace 可以进行类似解释说明的功能。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

$
  a times n = underbrace(a + a + dots.c + a, n "copies of" a)
$

cases

使用 cases 可以达到下面这种效果:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

$
  cases(
    2x + 3y + 5z = 2,
    3x - y + 6z = 3,
    -2x + 7y - 3z = 4,
  )
$

对齐点

使用 & 创建对齐点。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

$
  cases(
    2x + 3y + 5z &= 2,
    3x - y + 6z &= 3,
    -2x + 7y - 3z &= 4,
  )
$

$
  & sum_(i = 1)^n a_i^2 + sum_(i = 1)^n b_i^2 - 2 sum_(i = 1)^n a_i b_i \  /* break line */
  =& (a_1^2 + a_2^2 + dots.c + a_n^2) + (b_1^2 + b_2^2 + dots.c b_n^2) - 2 (a_1 b_1 + a_2 b_2 + dots.c + a_n b_n) \
  =& (a_1^2 + b_1^2 - 2 a_1 b_1) + (a_2 ^2 + b_2^2 - 2 a_2 b_2) + dots.c + (a_n^2 + b_n^2 - 2 a_n b_n) \
  =& (a_1 - b_1)^2 + (a_2 - b_2)^2 + dots.c + (a_n - b_n)^2 \
  =& sum_(i = 1)^n (a_i - b_i)^2
$

accent()

accent() 函数可以在某一段数学内容上方添加一些内容。你可以用这个来表示向量等。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

$
  accent(A B, ->) dot accent(C D, ->) = abs(accent(A B, ->)) abs(accent(C D, ->)) cos theta
$

删除线

使用 cancel() 函数可以渲染删除线。这在演示约分等场景时非常有用。

例子:

// Set fonts for text and math.
#set text(font: "Libertinus Serif")
#show math.equation: set text(font: "Libertinus Math")

$
  a/b times b/c = (a times b) / (b times c) = (a times cancel(b)) / (cancel(b) times c) = a/c
$

Typst 代码模式

是不是终于等到了期待已久的代码模式?

Typst 作为一门编程语言,自然是需要能够编写代码的。接下来我们就来讲讲 Typst 强大的代码模式。

Typst 代码模式的内容也很庞大,我们只挑常见的讲解。更多内容可以阅读给出的参考文档。

:::info[编程语言基础]{open} 本部分内容假定你有一定的编程语言基础(Python、Java、Rust 等常见编程语言均可),不会详细讲解编程中的细节和原理,只介绍 Typst 代码模式语法和特性。 :::

进入代码模式

Typst 默认情况下是文本模式。使用井号(#)可以进入代码模式。

你可以在井号后输入一行 Typst 代码。例如:

#let a = 1

你也可以在井号后输入一个 Typst 表达式。Typst 会求值表达式并插入内容中。如果表达式中有运算符,需要用小括号括起来。例如:

If Tom has 3 apples and Tim has 2, then they have #(3 + 2) apples in total.

你也可以在井号后使用花括号来启用一串代码块(也是无参匿名函数)。例如:

#{
  let a = 1
  let b = 2
}

分号(;)在 Typst 中是可选的(一般不使用)。我们一般只有在一行写多个语句或表达式的时候使用分号。

变量

使用 let 语句来绑定一个变量。随后,你可以像在其他编程语言一样使用这个变量。

值得注意的是,Typst 标识符中是可以包含连字符(-)的,也就是 this-is-a-variable 是允许的。这意味着如果你要在表达式中使用减号,必须加空格(事实上本来也建议加空格)。

例子:

#{
  let a = 1
  let b = a + 1  // 2
}

变量遵循词法作用域(代码块为词法作用域单位)。例如:

#let global = 3

#{
  let a = 1
  let b = a + global  // 4
  {
    let c = 2
  }
  let d = c + 2  // Error!
}

#a  // Error!

数据类型

整型(int)

官方文档

整型用于表示整数。你可以对整数进行运算。例如:

#let a = 1
#let b = a + 1

#(b + 2)  // 4

注意,整型的除法(/)在 Typst 中执行浮点除法,将得到浮点型。例如:

#let a = 5

#(a / 2)  // 2.5

Typst 整型使用 64 位有符号整数。请注意可能的溢出。

Typst 整型可以使用 0x 前缀表示十六进制、0o 前缀表示八进制、0b 前缀表示二进制。均不区分大小写。

使用 int() 函数可以将其它类型转换为整型。例如:

#int("123")  // 123

浮点型(float)

官方文档

浮点型用于表示浮点数。例如:

#let pi = 3.14

#(pi * 2 * 3)  // 18.84

使用 float() 函数可以将其他类型转换为浮点型。例如:

#float(true)  // 1

#float("2.3")  // 2.3

长度(length)

官方文档

长度类型用于表示衡量尺寸的数据。

长度类型有以下单位:

在整型和浮点型后面添加相应的单位后缀即可变成长度类型。

长度类型数据运算时会自动转换单位。例如:

#(1cm + 5mm)  // 42.52pt

#(1cm + 1pt)  // 29.35pt

使用 .pt() 方法可以将单位转换为点数;使用 .mm() 方法可以将单位转换为毫米;使用 .cm() 方法可以将单位转换为厘米;使用 .inches() 方法可以将单位转换为英寸。返回值均为浮点型。例如:

#(1cm.pt())  // 28.346456692913385

#(20pt.mm())  // 7.055555555555555

#(20pt.cm())  // 0.7055555555555556

#(20pt.inches())  // 0.2777777777777778

使用 .to-absolute() 方法可以将任意单位的长度类型转换为绝对长度(点数单位)。返回值为长度类型。

注意,这个方法需要在 context 表达式中使用。我们后面会讲这个。

例子:

#set text(size: 15pt)

#(context 1em.to-absolute())  // 15pt

#(context 10mm.to-absolute())  // 28.35pt

分数(fraction)

官方文档

分数类型用于表示将某一长度按比例分配。常见于设置表格列宽等场景中。

在整型或浮点型数字后使用 fr 后缀即可创建分数类型数值。

当多个分数类型同时出现的时候,会按比例分配。

我们来看一个例子:

#table(
  columns: (1fr, 2fr, 3fr),
  align: center + horizon,
  [1], [2], [3],
)

我们可以看到,分数类型将整个文本宽度(略小于页面宽度)按比例分为三个部分。第一部分占 \dfrac{1}{6},第二部分占 \dfrac{2}{6}(即 \dfrac{1}{3}),第三部分占 \dfrac{3}{6}(即 \dfrac{1}{2})。其中 6 = 1 + 2 + 3。

再看一个例子:

#table(
  columns: (1fr, 2fr, 1fr),
  align: center + horizon,
  [1], [2], [3],
)

依然按比例将文本宽度分为三个部分,但是这次第一部分占 \dfrac{1}{4},第二部分占 \dfrac{2}{4}(即 \dfrac{1}{2}),第三部分占 \dfrac{1}{4}。其中 4 = 1 + 2 + 1。

你现在应该理解分数类型是如何按比例分配长度的了。

比例(ratio)

官方文档

比例类型其实就类似于百分数。在整型或浮点型数字后面添加百分号即可。

比例可以与比例、长度、角度、整型、浮点型、相对量、分数运算。例如:

#(27% * 10%)  // with ratio: 2.7%

#(27% * 100pt)  // with length: 27pt

#(27% * (10% + 100pt))  // with relative: 2.7% + 27pt

#(27% * 100deg)  // with angle: 27deg

#(27% * 2)  // with int: 54%

#(27% * 0.37037)  // with float: 10%

#(27% * 3fr)  // with fraction: 0.81fr

相对量(relative)

官方文档

相对量是长度与比例相加而得的复合类型,表示相对于环境的长度的量。

角度(angle)

官方文档

角度类型用于表示角度值。

角度类型有两个单位:

在整型或浮点型数字后面添加单位后缀即可创建角度值。

例子(rotate() 函数用于旋转内容):

#rotate(10deg)[Hello, World!]

#rotate(-9rad)[Hello, World!]

使用 .deg() 方法返回角度的度数值;使用 .rad() 方法返回角度的弧度值。两者返回值均为浮点型。

例子:

#(10deg.deg())  // 10

#(10deg.rad())  // 0.17453292519943295

#(9rad.deg())  // 515.6620156177408

#(9rad.rad())  // 9

布尔型(bool)

官方文档

布尔型用于表示真或假。仅有两个值:true 和 false。

例子:

#true

#false

#(1 < 2)  // true

布尔型的运算符有 and(与)、or(或)和 not(非)。这与 Python 非常类似。例如:

#let score = 100
#let cheat = false

#if score == 100 and not cheat [
  Wonderful!
]

not 运算符可以用在 in 操作符前面,表示判断不存在。这与 Python 也是类似的。例如:

#let names = ("John", "Smith")

#("Alan" not in names)

#(not "Alan" in names)  // Equivalent

// both are true

字符串(str)

官方文档

字符串用于表示一串字符。Typst 中没有单一的字符类型。

使用双引号来标记字符串。不能使用单引号。

字符串中可以使用转义字符,例如 \n(换行符)、\"(双引号)\t(制表符)、\\(反斜杠)、\u{xxxxx}(Unicode 字符)等。

字符串可以使用加号(+)拼接。使用 str() 函数可以将其他类型转换为字符串类型。

例子:

#let a = 123

#("Hello, " + str(a))  // Hello, 123

当使用 str() 函数转换整型的时候,可以用 base 参数指定进制。例如:

#let num = 1357

#str(num, base: 2)  // 10101001101

#str(num, base: 8)  // 2515

#str(num, base: 16)  // 54d

#str(num, base: 36)  // 11p

字符串类型有以下常用方法:

方法 描述
.len() 返回整型。获取字符串长度。
.first() 返回字符串。获取第一个字符。default 参数指定字符串为空时的默认字符。若未指定 default 参数且字符串为空,则报错。
.last() 返回字符串。获取最后一个字符。用法与 .first() 相似。
.at() 返回字符串。传入整型参数 i,获取第 i 个字符(从 0 开始)。有 default 参数。
.find() 返回字符串或 none。传入字符串或正则表达式,找到第一个匹配的位置(如果没找到,返回 none)。
.split() 返回数组。默认按空白符分割字符串。若传入字符串或正则表达式做参数,则按匹配的模式分割字符串。

使用 in 操作符可以判断一个字符串是否在另一个字符串中。这与 .contains() 方法类似。例如:

#("a" in "Hello, World!")  // false

数组(array)

官方文档

数组是一系列元素的集合。元素内类型不要求一致。数组使用圆括号(())来标记。使用空圆括号来创建空数组。数组元素使用逗号分隔。特别地,如果数组元素只有一个,也需要带上尾随逗号(否则将被视为普通的括号运算符)。

例子:

#let a = (1, 2)
#let b = ()
#let c = ("a",)

使用 .at() 方法按下标访问元素;使用 .len() 方法获取长度;使用 .push() 方法追加元素;使用 .pop() 方法弹出尾元素;使用 in 操作符判断包含。

例子:

#let arr = (1, 2, "a", "b", 1pt, 2fr)

#arr.at(2)  // a

#arr.len()  // 6

#arr.push(50%)

#arr.len()  // 7

#arr.pop()

#arr.len()  // 6

#("b" in arr)  // true

使用加号(+)可以直接拼接两个数组。例如:

#let names1 = ("Alan", "Bessie")
#let names2 = ("John",)

#let names = names1 + names2

#names  // ("Alan", "Bessie", "John")

字典(dictionary)

官方文档

字典类似哈希表,存储一系列键值对。

同样使用圆括号创建字典,但是键值对的键与值之间使用冒号(:)分隔。键可以是直接的名称,也可以是字符串,但不能是其他类型。值可以是任意类型。空字典使用 (:) 表示。

例子:

#let dict = (
  name: "Glacyend",
  "born": 2010,
  height: 175cm,
)

使用 .at() 通过键获取值;使用 .len() 获取键值对数量;使用 .insert() 插入键值对;使用 .remove() 删除键值对;使用 .keys() 获取所有键;使用 .values() 获取所有值;使用 .pairs() 获取所有键值对。使用 in 操作符判断键是否存在。如果键是合法的标识符,也可以不使用 .at() 来访问,可以直接使用 . 操作符来访问,类似成员访问语法。

例子:

#let info = (
  name: "Typst",
  "born": 2019,
  "now version": "0.14.2",
)

#info.len()  // 3

#info.at("name")  // Typst

#info.born  // 2019

#info.insert("what", -1)

#info.len()  // 4

#info.what  // -1

#info.remove("what")

#info.len()  // 3

#("what" in info)  // false

#info.keys()  // ("name", "born", "now version")

#info.values()  // ("Typst", 2019, "0.14.2")

#info.pairs()
/*
(
  ("name", "Typst"),
  ("born", 2019),
  ("now version", "0.14.2")
)
/*

使用加号(+)可以拼接两个字典。例如:

#let info1 = (
  name: "Glacyend",
  born: 2010,
)

#let info2 = (
  nationality: "Chinese",
  interests: ("Programming", "Math", "Music"),
)

#let info = info1 + info2

#info
/*
(
  name: "Glacyend",
  born: 2010,
  nationality: "Chinese",
  interests: ("Programming", "Math", "Music"),
)
*/

空类型(none)

官方文档

Typst 中比较特殊的数据类型,只有一个值——空值(none)。

将空值插入文档内容的时候,不会有任何效果。例如:

This is none: #none

自动类型(auto)

官方文档

这又是一个 Typst 中比较特殊的数据类型,只有一个值——自动值(auto)。

它表示对某些值得默认选项,常用于函数参数中。函数体可以对这个值进行特判,并执行默认行为。

代码块与内容块

官方文档

代码块

在代码模式中,使用花括号({})可以创建代码块。代码块将构建一个词法作用域,并且可以将里面的多条语句或表达式集合成一条语句或表达式,称为复合语句或复合表达式。

Typst 语法从 Rust 中吸取了很多灵感。Typst 代码块与 Rust 代码块类似,可以被当作表达式使用。当代码块的最后一句是有值的表达式时,整个代码块就是一个复合表达式,且其值为代码块内最后一句的值。例如:

#let num = 3

#let result = {
  let total = num * 2
  total + 2
}

#result  // 8

如果最后一句不是能求值的表达式(即语句),则代码块被视为复合语句。若非要将复合语句用作表达式,则其值为 none。

内容块与内容类型(content)

在代码模式中,使用方括号([])可以创建内容块。内容块本身是内容类型(content)的字面量。在内容块中,Typst 将进入文本模式,从而可以使用标记语言。当内容类型的数据插入文本模式时,将原样输出其内容。

内容类型的数据可以通过加号(+)拼接。

例子:

#let final = {
  let a = [Hello, ]
  let b = [I'm Typst!]
  a + [*World*! ] + b
}

#final

控制流

条件控制

使用 if 语句进行条件控制。if 语句的条件不需要小括号。条件表达式必须是布尔类型。

一个例子:

#let number = 1

#if number < 2 {
  number *= 2
}

#number  // 2

注意,不像 C++、Java 等编程语言,Typst 所有控制语句后都需要完整的代码块或内容块,不能省略花括号或方括号。

当然,你可以在条件控制语句的子句中使用内容块。例如:

#let show-name = true

My name is #if show-name [Typst] else [???].

#(show-name = false)

My name is #if show-name [
  Typst
] else [
  ???
].

你可以使用很多个分支。例如:

#let score = 94

#let level = if score < 60 [
  Failed
] else if score < 80 [
  Bad
] else if score < 90 [
  Good
] else if score < 100 [
  Wonderful
] else if score == 100 [
  Perfect
]

Your score is #score. Your level is #level!

与 Rust 类似,条件控制语句可以被当作表达式使用。如果子句是代码块,则代码块最后一个表达式将作为该分支的值;如果子句是内容块,则内容块就是该分支的值。上面就是一个例子。我们再来看另一个例子:

#let score = 98
#let cheat = true

#let final = if cheat {
  0
} else {
  score
}

Your final score is #final.

诚信考试,严禁作弊。

循环控制

官方文档

Typst 中有两种循环:while 循环和 for 循环。

while 循环是条件循环。每当条件为 true,则执行循环体;否则跳出循环。条件表达式必须是布尔类型。

例子:

#let count = 1

#while count <= 10 {
  [Counting... (#count) \ ]
  count += 1
}

注意,Typst 不支持无限循环(因为文档编译总要在有限的时间内结束)。所以,下面两段代码都会报错:

#while true {  // Error: condition is always true

}
#let number = 1

#while number < 5 {  // Error: loop seems to be infinite

}

for 循环则适用于对一个可迭代的对象(如数组、字符串、字典)进行迭代。例如:

#let names = ("Tim", "John", "Smith", "Laura")

These are the contributors:
#for name in names [
  - #name
]

若要对一个数字范围进行迭代,可以使用 range() 函数。这个函数与 Python 中的 range() 函数非常相似:第一个参数表示起始(包含),第二个参数表示终止(不包含),第三个参数(具名参数,要带名称 step)表示步长。

例子:

Let's count down:

#for sec in range(10, 0, step: -1) {
  [#sec second] + if sec != 1 [s] + [! \ ]
}

Fire!

两个循环都可以作为表达式使用。作为表达式使用时,若子句为代码块,则每一次循环的值为代码块中最后一个表达式的值;若子句为内容块,则每一次循环的值就是内容本身。循环整体最终的值就是每一次循环的值进行拼接。所以你可以在表达式中使用内容类型、数组类型、字典类型、字符串类型,等等。

例子:

#let sentence = "I'm using LaTeX to write my paper. LaTeX is easy to use and I really like it."

#let corrected = for word in sentence.split() {
  if word == "LaTeX" {
    "Typst"
  } else {
    word
  } + " "
}

#corrected

两种循环都可以使用 break 来退出循环、continue 来跳过当前循环。这两者的功能与其他编程语言基本一致,这里不再过多介绍。

错误与断言

错误

我们可以调用 panic() 函数来引发一个运行时错误。panic() 函数可以接收任意数量的任意类型的参数,表示 Typst 抛出运行时错误时要附带的信息。

例子:

#let score = 101

#let level = if 0 <= score and score < 60 [
  Failed
] else if score < 80 [
  Bad
] else if score < 90 [
  Good
] else if score < 100 [
  Wonderful
] else if score == 100 [
  Perfect
] else {
  panic("Invalid score!")
}

Your score is #score. Your level is #level!

这份代码将报错,报错信息为:

error: panicked with: "Invalid score!"
   ┌─ main.typ:14:2
   │
14 │   panic("Invalid score!")
   │   ^^^^^^^^^^^^^^^^^^^^^^^

断言

你可以使用 assert() 函数来进行断言。使用断言可以保证我们的代码按照预期规定的方式运行。

assert() 的第一个参数是表达式;第二个参数(message,具名参数)是字符串,表示报错信息。当表达式为 false 时,就将引发运行时错误。

例子:

#assert(1 == 1)

#assert(1 == 2, message: "That's what I expect.")

上面的代码会报错,报错信息为:

error: assertion failed: That's what I expect.
  ┌─ main.typ:3:1
  │
3 │ #assert(1 == 2, message: "That's what I expect.")
  │  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

函数

官方文档

定义函数与函数调用

Typst 定义函数也使用 let 进行绑定。与变量的绑定的区别是 let 后的名称后面有参数列表(形参列表),且等号后面是代码块或内容块。

函数调用的方式与其他编程语言类似,给出参数列表(实参列表)即可。

注意,Typst 是动态强类型脚本语言,形参列表不需要标注类型。

函数返回值可以使用 return 语句。当然,如果函数体是代码块,最后一个表达式默认就是返回值;如果函数体是内容块,则内容本身就是返回值。

例子:

#let add(a, b) = {
  a + b
}

#add(1, 2)  // 3

当然,如果函数体只有一条语句或表达式,可以省略括号。例如:

#let add(a, b) = a + b

#add(1, 2)  // 3

这些语法均借鉴自函数式编程语言。在 Typst 中,使用函数式语法能简化很多代码。

但是上面的代码有问题:它将不止对数字有效。例如:

#let add(a, b) = a + b

#add(1, 2)  // 3

#add("abc", "def")  // abcdef

如果我们确实要保证参数只能是数字(整型或浮点型),可以使用断言:

#let add(a, b) = {
  assert(type(a) == int or type(a) == float, message: "Type Error!")
  assert(type(b) == int or type(b) == float, message: "Type Error!")

  a + b
}

#add(1, 2)  // 3

type() 函数用于获取值的类型。这样在调用函数的时候,如果传入非数字,则会报错。

你还可以将类型判断封装成另一个函数 check-type(),以简化和重用代码。例如:

#let check-type(value, types) = {
  assert(type(types) == type or type(types) == array, message: "Type Error in check-type()!")
  if type(types) == type {
    types = (types,)
  }
  for typ in types {
    assert(type(typ) == type, message: "Type Error in check-type()!")
  }

  assert(type(value) in types, message: "Type Error!")
}

#let add(a, b) = {
  check-type(a, (int, float))
  check-type(b, (int, float))

  a + b
}

#add(1, 2)  // 3

你可以在形参列表都某一个参数后面添加冒号,并提供一个默认值。这个参数将从位置参数变为具名参数,在调用时必须写上其名称,并且使用冒号。

#let greet(name, what: "Hello!") = name + ": " + what

#greet("Typst")

#greet("LaTeX", what: "Goodbye!")

注意,具名参数不一定必须在位置参数后面,可以在位置参数前面。调用函数时,如果传入的参数没有指定名称,则传入的目标是位置参数,而不是具名参数,尽管具名参数可能更靠前。例如:

#let two-numbers(a: 1, b) = [
  The first number is #a. \
  The second number is #b.
]

#two-numbers(2)

#two-numbers(a: 2, 3)

具名参数的使用不需要固定的顺序。例如:

#let power(base: 2, exp: 10) = {
  let result = 1
  for i in range(exp) {
    result *= base
  }
  result
}

#power()  // 1024

#power(base: 3, exp: 3)  // 27

#power(exp: 4, base: 4)  // 256

匿名函数

我们可以在 Typst 中创建匿名函数。匿名函数是一个即时值,不需要绑定在某个标识符上。

匿名函数的核心操作符是 =>。在这个符号前面是匿名函数的形参列表(如果只有一个参数,可以省略小括号),而符号后则是函数的函数体(如果只有一条语句或变量,可以省略括号)。

例子:

#let bind-and-call(a, b, c, func1: none, func2: none) = {
  assert(func1 != none)
  assert(func2 != none)

  let temp = func1(a, b)
  let result = func2(temp)
  func1(result, c)
}

#bind-and-call(1, 2, 3, func1: (a, b) => a + b, func2: num => num * 2)  // 9

函数方法

在 Typst 中,函数也是一种数据类型。函数类型的数据有一些常用的方法。

.with() 方法使用非常广泛。它可以将原函数某个具名参数的默认值替换为另一个,并返回这个替换后的新函数。例如:

#let assert-error = assert.with(message: "Error!")

#assert(true, message: "Error!")

#assert-error(true)  // Equivalent

.where() 方法一般用于元素函数(后面会讲)。它将返回一个筛选器,根据 .where() 的参数进行筛选。这个筛选器常用于 set 语句和 show 语句。我们后面再介绍。

解包

Typst 中,可以将数组或字典解包,然后传入参数。如果你未曾接触过解包,可以先理解下面的例子。你可以将其视作将数组或字典抄写一遍,但是不要圆括号。

例子:

#let add-three(a, b, c) = a + b + c

#let numbers = (1, 2, 3)

#add-three(..numbers)  // 6

#let print-level(score, cheat: false, forgive: false) = if cheat and not forgive [0] else { score }

#let info = (cheat: true, forgive: true)

#print-level(100, ..info)  // 100

任意参数与 arguments 类型

官方文档

arguments 又是 Typst 中的另一个数据类型,表示实参。

你可以像调用一个没有任何参数限制的函数一样调用 arguments() 函数,来创建实参数据。例如:

#let args = arguments(1, 2, 3, name: "John", height: 172cm, true)

函数是可以接收任意参数的。在某一个形参名称前面加两个点(..),即可让这个函数接收任意实参。而这个形参将变成 arguments 类型的。

该类型有几个方法:.at() 方法可以获取某个实参。如果传入整数,则获取第几个位置参数(下标从 0 开始);如果传入字符串,则获取字符串对应标识符的具名参数。还有 default 参数,当获取的参数不存在时返回的默认值。

例如:

#let print-info(..args) = [
  - ID: #args.at(0, default: -1)
  - Name: #args.at("name", default: "John")
  - Department: #args.at("dep", default: "Unknown")
]

#print-info(1234, name: "Alice")

此外,还有 .pos() 方法,返回所有位置参数所构成的数组;.named() 方法,返回所有具名参数构成的字典。

再看一个例子:

#let sum(..args) = {
  assert(args.named().len() == 0)

  let result = 0
  for i in args.pos() {
    result += i
  }
  result
}

#sum(1, 3, 4, 6, 9)  // 23

传入内容的语法糖

很多函数(包括你自己定义的函数)中都有一个叫 body 或者 content 等名称的参数,接收一个内容类型的数值。这是,你可以使用 Typst 提供的语法糖,将内容块提到函数调用后面去,来保证代码可读性。例如:

#let show-text(body, reverse: false) = {
  if reverse {
    rotate(180deg, body)
  } else {
    body
  }
}

#show-text(reverse: true)[What?]

当使用这个语法糖时,如果圆括号内的参数列表没有参数,则可以省略圆括号。例如:

#let show-text(body, reverse: false) = {
  if reverse {
    rotate(180deg, body)
  } else {
    body
  }
}

#show-text()[What?]

#show-text[Oh, that's right.]

模式绑定

官方文档

这部分内容需要你有相关的编程知识(如 Rust 和 C# 中的模式匹配、其他编程语言中的解构赋值等),而且也不属于 Typst 中特别重要和常见的内容,所以我们仅一笔带过,不再详细讲述。这里给出官方文档中提供的例子,如果你接触过模式匹配之类的编程技巧,应该能理解这里的例子。

#let (x, y) = (1, 2)
The coordinates are #x, #y.

#let (a, .., b) = (1, 2, 3, 4)
The first element is #a.
The last element is #b.

#let books = (
  Shakespeare: "Hamlet",
  Homer: "The Odyssey",
  Austen: "Persuasion",
)

#let (Austen,) = books
Austen wrote #Austen.

#let (Homer: h) = books
Homer wrote #h.

#let (Homer, ..other) = books
#for (author, title) in other [
  #author wrote #title.
]

#let (_, y, _) = (1, 2, 3)
The y coordinate is #y.

set 语句

官方文档

set 语句可用于元素函数(如文本 text()、标题 heading()、表格 table() 等,这些函数调用时会产生一个在文本模式下可排版的内容),用于设置该元素在当前作用域后文中的一些函数参数默认值。例如:

This is small text.

#set text(size: 20pt)

This is big text.

再看一个例子:

= Introduction

#set heading(numbering: "1.1")

= Section 1

== Subsection 1

=== Subsubsection 1

set-if 语句

官方文档

有时,我们想要根据一些条件来选择性地设置一些格式。我们可能会写下这样的代码:

#let block(body, important: false) = {
  if important {
    set text(fill: red)
  }

  body
}

#block[This is not important.]

#block(important: true)[This is important.]

咦,奇怪了,我们设置的红色格式去哪里了?

仔细阅读上一节的第一段,我们发现:set 语句的效果仅在当前作用域生效。if 语句由于使用花括号新开了一个作用域,所以 set 语句实际上只在 if 的子句内生效。

这难道意味着我们无法选择性设置格式了吗?

好在,Typst 还提供了 set-if 语句。你可以把你的条件判断放在 set 语句后面,来选择性地设置格式。上面的代码改写成这样就能正常工作:

#let block(body, important: false) = {
  set text(fill: red) if important

  body
}

#block[This is not important.]

#block(important: true)[This is important.]

show 语句

官方文档

show 语句主要执行的是类似筛选和内容替换的工作(在当前作用域中)。例如:

#show "something cool": "Typst"

I'm using something cool to write my papers.

show 语句还可以使用标签。这里,对任意内容的标签就其作用了!我们之前讲标签和交叉引用时,讲了一些能被引用的加标签的元素(如标题、图片等),但是对于其他不能被引用的元素(如段落等)的标签难道就没用了吗?这里,它就起作用了。我们来看一个例子:

#show <secret>: "SECRET"

My password is:

123456abcdef <secret>

在 show 语句中,元素函数的 .where() 方法所返回的筛选器就有用武之地了。你可以给 .where() 传入一些参数,表示让其筛选那些满足这些参数的元素函数调用(文本模式下的标记语言语法就属于元素函数调用的语法糖)。例如:

#show heading.where(level: 4): [_Who tells you to use H4?_]

= Section 1

== Subsection 1

=== Subsubsection 1

==== Subsubsubsection 1

show 语句的冒号后面还可以跟随 set 语句,表示将这个 set 语句所设置的格式应用到 show 语句筛选的内容上去。例如:

#show "green": set text(fill: green)

We should plant more green trees to make our planet greener.

此外,show 语句的冒号后还可以跟随一个函数。这种情况下,Typst 会将筛选中的内容传入这个函数,并把这个函数的返回值作为替换的内容。例如:

#show heading: it => {
  if "Introduction" in it.body.at("text") [
    _In this article using "Introduction" is not allowed_.
  ] else {
    it
  }
}

#show "Typst": highlight.with(fill: blue.lighten(50%))

= Introduction

= Section 1

We want to use Typst to write our articles.

show 语句还有全局(严格地讲,是当前作用域)的用法:不指定筛选内容,直接指定替换内容。这个用法常见于应用文档模板。下面这个例子可以让全文都使用大写:

#show: upper

Oh, this article does not have any lowercase letters.

context 表达式

官方文档

context(上下文)表达式是 Typst 中比较强大的功能:当一个表达式的求值与位置相关时,就使用上下文表达式。

这里我觉得官方文档写得很不错,于是我将官方文档的内容贴了一些在下面。

Sometimes, we want to create content that reacts to its location in the document. This could be a localized phrase that depends on the configured text language or something as simple as a heading number which prints the right value based on how many headings came before it. However, Typst code isn't directly aware of its location in the document. Some code at the beginning of the source text could yield content that ends up at the back of the document.

To produce content that is reactive to its surroundings, we must thus specifically instruct Typst: We do this with the context keyword, which precedes an expression and ensures that it is computed with knowledge of its environment. In return, the context expression itself ends up opaque. We cannot directly access whatever results from it in our code, precisely because it is contextual: There is no one correct result, there may be multiple results in different places of the document. For this reason, everything that depends on the contextual data must happen inside of the context expression.

Aside from explicit context expressions, context is also established implicitly in some places that are also aware of their location in the document: Show rules provide context (Currently, all show rules provide style context, but only show rules on locatable elements provide a location context.) and numberings in the outline, for instance, also provide the proper context to resolve counters.

来翻译一下:

有时,我们希望创建能够根据其在文档中的位置做出反应的内容。这可以是一个依赖于所配置文本语言的本地化短语,或者像标题编号那样简单的东西——根据其前面有多少个标题来打印正确的编号。然而,Typst 代码并不能直接感知自身在文档中的位置。源文本开头的某些代码生成的内容,最终可能被放置在文档的末尾。

因此,为了生成能对其周围环境做出反应的内容,我们必须明确地指示 Typst:通过使用 context 关键字来实现,该关键字位于表达式之前,确保该表达式在了解其环境信息的情况下进行计算。作为代价,context 表达式本身会变得不透明。我们无法在代码中直接访问它的结果,这恰恰是因为它是依赖于上下文的:不存在唯一正确的结果,在文档的不同位置可能会产生多个结果。出于这个原因,所有依赖于上下文数据的内容都必须在 context 表达式内部完成。

除了显式的 context 表达式之外,在某些同样能感知自身文档位置的地方,上下文也会被隐式地建立:show 规则提供了上下文(目前,所有显示规则都提供样式上下文,但只有位于可定位元素上的显示规则才提供位置上下文)。此外,例如目录中的编号也会提供正确的上下文来解析计数器。

接下来我们来简单介绍 context 表达式。

样式上下文

官方文档

在 context 表达式中,我们可以获取一些元素的属性(这些属性通常使用 set 语句设置)。例如:

#context text.lang  // en

#context text.font  // libertinus serif

#set text(lang: "fr")

#context text.lang  // fr

#set text(font: "Libertinus Sans")

#context text.font  // libertinus sans

注意,一旦上下文表达式的返回值被捕获,那么捕获者的值将始终跟随上下文而变化。你可以理解为 context 表达式返回的是“引用”而不是“复制”。例如:

#let lang = context text.lang

#lang  // en

#set text(lang: "fr")

#lang  // fr

位置上下文

官方文档

Typst 的 context 表达式还可以确定当前的位置。

这里有一个例子。使用 counter() 函数可以获取元素的计数器(返回计数器 counter 类型)。那么,我们可以使用 context 表达式在文档的不同部分获取计数器:

#set heading(numbering: "1.")

= Introduction

#context counter(heading).get()  // (1,)

== The Project

#context counter(heading).get()  // (1, 1)

== The Authors

#context counter(heading).get()  // (1, 2)

= Background

#context counter(heading).get()  // (2,)

在 context 上下文中,使用 here() 函数可以获取一个 location(位置)类型的值,这个值中保存着当前位置的一些信息。例如:

This is page #(context here().page()).  // This is page 1.

#pagebreak()

This is page #(context here().page()).  // This is page 2.

(两页太长了所有就没截图了。)

再放一个官方文档中的例子:

#set heading(numbering: "1.")

= Introduction <intro>
#lorem(5)

= Background <back>
#lorem(5)

#context [
  #counter(heading).get() \
  #counter(heading).at(here()) \
  #counter(heading).at(<intro>)
]

locate() 函数可以返回当前位置信息。例如:

Background is at: \
#context locate(<back>).position()

= Introduction <intro>
#lorem(5)
#pagebreak()

= Background <back>
#lorem(5)

嵌套 context 表达式与上下文域

官方文档

这一部分内容可能有些难以理解。简单来说就是:context 表达式中可以嵌套另一个 context 表达式。但是,大的 context 表达式只能获取大的 context 表达式所在位置的上下文,只有嵌套在其中的 context 表达式才可以获取更精细的上下文。

如果你能看懂官方文档给的这两个例子,说明你理解能力真不错;如果不能,那你也可以暂时放一放,因为这个功能确实不怎么常用。

#set text(lang: "de")
#context [
  #set text(lang: "fr")
  #text.lang \  // de
  #context text.lang  // fr
]

#let template(body) = {
  set text(lang: "fr")
  upper(body)
}

#set text(lang: "de")
#context [
  #show: template
  #text.lang \  // DE
  #context text.lang  // FR
]

模块

官方文档

使用模块,我们可以像多文件编程一样,更好地组织我们的文档。

包含模块

使用 include 语句可以将另一个 Typst 文件的内容原封不动地复制到当前文件。

下面是一个例子。假如我们有文件 intro.typ,内容如下:

// intro.typ

= Introduction

#lorem(50)

又有文件 main.typ,内容如下:

// main.typ

#include "intro.typ"

= Background

#lorem(30)

编译 main.typ,可以得到:

注意,include 语句会为自己所包含的模块文件创建一个虚拟作用域。因此,你无法使用 intro.typ 中定义的变量、函数等。

导入模板

使用 import 语句可以导入一个模块。注意,这和 include 是不一样的。import 语句不会计算模块文件中的文本模式下的内容,而是将所有在模块文件的全局作用域中的内容导出,封装为 module 类型的值,命名为模块文件的名称,然后变可以通过成员访问操作符(.)进行访问。

这里有一个例子。假如我们有模块文件 mod.typ:

// mod.typ

Something doesn't appear in the final output.

#let language = "English"

然后是主文件 main.typ:

// main.typ

#import "mod.typ"

The language is #mod.language.

编译主文件 main.typ,可以得到:

但如果我们直接将这个 module 类型的值插入文本模式中,将会得到模块文件的内容。例如,改写 main.typ 为:

// main.typ

#import "mod.typ"

#mod

可以得到:

我们还可以使用 Typst 官方维护的模块仓库中的模块(你将来也许也会是一个贡献者呢)。我们需要使用 @preview/ 前缀并指定版本号。这里我们用 Typst 官方提供的专门用来学习使用模块的模块 example 来学习更多模块的用法。

导入 example 模块(使用唯一的版本号 0.1.0),并查看其内容:

#import "@preview/example:0.1.0"

#example

将 module 类型转换为字典类型,可以查看其导出的变量和函数等:

#import "@preview/example:0.1.0"

#dictionary(example)  // (add: add, sub: sub, mul: mul, div: div)

我们可以使用模块中提供的这些四则运算函数:

#import "@preview/example:0.1.0"

The result is: #example.add(1, example.sub(2, -1)).

在 import 语句后面使用冒号,可以指定要导入的内容,但是 Typst 将不再创建 module 类型的值。例如:

#import "@preview/example:0.1.0": add, mul

The result is: #add(12, mul(-1, 3)).

使用星号表示全部导入。例如:

#import "@preview/example:0.1.0": *

The result is: #add(12, mul(-1, 3)).

其结果和上面的是一样的。

使用 as 关键字可以指定导入的模块的 module 类型的值的名称。例如:

#import "@preview/example:0.1.0" as calculator

The result is: #calculator.div(3, 2).

代码模式示例:作者列表

讲了这么多代码模式的东西,都有些什么用呢?这里我们来具体写一段代码,用于根据需要显示作者列表。

首先,我们需要的应该是一个函数,叫做 show-authors() 吧。它接收一个参数,我们把它叫做 authors。它的默认值是 none,表示没有作者,这时我们什么都不做就好了。

#let show-authors(authors: none) = {
  if authors != none {
    // TODO
  }
}

首先,作者可能只有一人。因此,authors 应该可以接收一个普通的字符串,表示作者的名字。我们改写函数,让其居中显示这个名字:

#let show-authors(authors: none) = {
  if authors != none {
    set align(center)

    authors
  }
}

#show-authors(authors: "Glacyend")

但是,如果用户传入其他类型的值怎么办?所以我们需要检查类型。我们写一个函数 check-type() 来检查类型。不要忘记特判 none!

#let check-type(value, types: none, can-be-none: false) = {
  assert(types != none, message: "The expected types are necessary.")

  if value == none and can-be-none {
    return
  }

  assert(type(types) in (type, array), message: "Type Error in check-type().")
  if type(types) == type {
    types = (types,)  // Convert to an array.
  }
  for ele in types {
    assert(type(ele) == type, message: "Type Error in check-type().")
  }

  assert(type(value) in types, message: "Type Error.")
}

#let show-authors(authors: none) = {
  check-type(authors, types: str, can-be-none: true)

  if authors != none {
    set align(center)

    authors
  }
}

随后我们就可以增加更多的内容了。

有时候,作者会不止有一个。我们还需要能够处理字符串数组,来表示多个作者。在添加处理代码之前,我们先来编写 check-element-type() 函数来检查数组中的类型。

#let check-type(value, types: none, can-be-none: false) = {
  // ...
}

#let check-element-type(arr, types: none, can-be-none: false) = {
  assert(types != none, message: "The expected types are necessary.")

  check-type(arr, types: array)
  for ele in arr {
    check-type(ele, types: types, can-be-none: can-be-none)
  }
}

#let show-authors(authors: none) = {
  check-type(authors, types: (str, array), can-be-none: true)

  if authors == none {
    return
  } else if type(authors) == str {
    set align(center)
    authors
  } else if type(authors) == array {
    check-element-type(authors, types: str)

    // TODO
  }
}

随后,我们开始编写对于多个作者的处理代码。

首先,我们需要使用一个叫 grid() 的函数。这个函数接收任意多个内容块,将这些内容块按网格排列。我们规定最多使用 3 列,然后将这些作者按照网格排列。

#let show-authors(authors: none) = {
  check-type(authors, types: (str, array), can-be-none: true)

  set align(center)

  if authors == none {
    return
  } else if type(authors) == str {
    authors
  } else if type(authors) == array {
    check-element-type(authors, types: str)

    grid(
      columns: (8em,) * calc.min(authors.len(), 3),
      align: center + horizon,
      gutter: 5em,
      row-gutter: 2em,
      ..authors,
    )
  }
}

我们使用 columns 参数来设置列数以及列宽。这里,我们使用了内置模块 calc 的 min() 函数来求最小值,保证最大列数为 3。align 参数设置网格排列的每一项都使用水平居中和垂直居中。gutter 用于设置水平项之间的间距。row-gutter 用于设置垂直项之间的间距。最后,将 authors 解包并作为参数传入 grid() 函数。

我们来看看效果:

#show-authors(authors: "Glacyend")

#line(length: 100%)

#show-authors(authors: ("Glacyend", "Star"))

#line(length: 100%)

#show-authors(authors: ("Glacyend", "Star", "Azure", "Youth Computer Promotion Society", "Virtual Organization", "Some Charities"))

漂亮!

但是,我们可能还有更多的需求:有时不同的作者负责不同的工作,例如甲负责绘图、乙负责排版等。我们需要更强大的功能,也需要更复杂的类型:字典。

首先,添加字典的类型检查支持。

#let show-authors(authors: none) = {
  check-type(authors, types: (str, array, dictionary), can-be-none: true)

  set align(center)

  if authors == none {
    return
  } else if type(authors) == str {
    authors
  } else if type(authors) == array {
    // ...
  } else if type(authors) == dictionary {
    check-element-type(authors.keys(), types: str)
    check-element-type(authors.values(), types: str)

    // TODO
  }
}

然后,我们开始处理。这次,我们依然使用 grid(),不过限制最多 2 列。我们将把字典中的键加粗,把值放在后面。调整合适的间距,解析键值对并生成数组,然后将数组解包传入 grid() 即可。

#let show-authors(authors: none) = {
  check-type(authors, types: (str, array, dictionary), can-be-none: true)

  set align(center)

  if authors == none {
    return
  } else if type(authors) == str {
    authors
  } else if type(authors) == array {
    // ...
  } else if type(authors) == dictionary {
    check-element-type(authors.keys(), types: str)
    check-element-type(authors.values(), types: str)

    let authors-list = ()
    for (key, value) in authors {
      authors-list.push(strong(key) + h(1em) + value)
    }

    grid(
      columns: (15em,) * calc.min(authors-list.len(), 2),
      align: center + horizon,
      gutter: 5em,
      row-gutter: 2em,
      ..authors-list
    )
  }
}

因为 grid() 函数只接收多个内容块,所以我们必须解包内容数组而不是字典。我们使用 authors-list 来存储每一个键值对处理后的内容。使用 strong() 函数加粗,使用 h() 函数来创建水平间距。将三个内容拼接,插入 authors-list 后。然后,使用类似的代码来网格排版。

我们来看看效果:

#show-authors(
  authors: (
    "Drawing": "Alice",
    "Mathematical Proof": "Bob",
    "Article Writing": "Charlie",
    "Typesetting": "Glacyend",
  )
)

成功了!

如果想要复用这些效果(也就是复用 show-authors() 函数),你可以将这些代码封装在一个你自己的模块文件中,以后直接使用就可以了(Typst 中有 @local 模块前缀,表示本地模块。感兴趣的可以自行搜索)。如果你想要更进一步地组织一下代码,可以将 check-type() 函数与 check-element-type() 函数单独放在一个模块中,然后在 show-authors() 函数所在的模块导入它。

#let check-type(value, types: none, can-be-none: false) = {
  assert(types != none, message: "The expected types are necessary.")

  if value == none and can-be-none {
    return
  }

  assert(type(types) in (type, array), message: "Type Error in check-type().")
  if type(types) == type {
    types = (types,)  // Convert to an array.
  }
  for ele in types {
    assert(type(ele) == type, message: "Type Error in check-type().")
  }

  assert(type(value) in types, message: "Type Error.")
}

#let check-element-type(arr, types: none, can-be-none: false) = {
  assert(types != none, message: "The expected types are necessary.")

  check-type(arr, types: array)
  for ele in arr {
    check-type(ele, types: types, can-be-none: can-be-none)
  }
}

#let show-authors(authors: none) = {
  check-type(authors, types: (str, array, dictionary), can-be-none: true)

  set align(center)

  if authors == none {
    return
  } else if type(authors) == str {
    authors
  } else if type(authors) == array {
    check-element-type(authors, types: str)

    grid(
      columns: (8em,) * calc.min(authors.len(), 3),
      align: center + horizon,
      gutter: 5em,
      row-gutter: 2em,
      ..authors,
    )
  } else if type(authors) == dictionary {
    check-element-type(authors.keys(), types: str)
    check-element-type(authors.values(), types: str)

    let authors-list = ()
    for (key, value) in authors {
      authors-list.push(strong(key) + h(1em) + value)
    }

    grid(
      columns: (15em,) * calc.min(authors-list.len(), 2),
      align: center + horizon,
      gutter: 5em,
      row-gutter: 2em,
      ..authors-list
    )
  }
}

现在感受到 Typst 这门编程语言的威力了吧!

至此,你已经完成了 Typst 的快速入门之旅。从安装环境、编写第一个文档,到运用基础语法、管理交叉引用,再到设置格式、理解 context 表达式等核心而复杂的概念——现在,你已经具备了用 Typst 高效排版的基本能力。

Typst 的设计初衷,是让排版回归“写作”本身:不再被 LaTeX 中的复杂的宏或引擎配置打断思路,同时又能保持专业级的质量与可控性。你会发现,许多原本在传统工具(如 LaTeX、MS Word、LibreOffice Write 等)中需要反复调试的地方,在 Typst 里往往只是一行自然的代码。

当然,入门只是开始。Typst 还有更丰富的功能等待你去探索:灵活的显示规则、强大的图表绘制、丰富的第三方库……这些都能让你将排版提升到新的高度。

到目前,Typst 还存在一些已知问题:不支持按范围自定义数学字体(类似 LaTeX 的 unicode-math 宏包)、在行内公式使用行间公式符号大小会有行距问题,等等。这些问题在 LaTeX 中都是可以解决的。不过应当给 Typst 一些成长的时间,毕竟它还只是个“孩子”,还在不断更新、不断长大。

希望这份手册曾为你点亮一盏灯,愿你今后在 Typst 的世界里,书写得更快、更自由。

祝你排版愉快!