Typst 的 Element Function 与语法糖设计

核心结论

Typst 并不是简单地把所有语法都转换为普通函数调用。更准确的描述是:

可展示的内容最终统一表示为 Content 和 Element;Element 的构造器同时以函数形式暴露给用户,常用 Element 再提供简洁的标记语法糖。

例如:

= Introduction

和:

#heading(level: 1)[Introduction]

最终都产生 Heading Element。但是 Typst 内部不必先把第一种语法改写成第二种函数调用 AST;解释器可以直接从 Heading 语法节点构造 HeadingElem

Typst 的整体流程可以简化为:

源代码
  ↓
Syntax AST
  ↓ eval
Value / Content / Element
  ↓ realization
结构组合、set/show 规则
  ↓ layout
页面布局
  • Value:字符串、整数、数组、函数、Label 等一般值。

  • Content:可展示内容的统一容器,可以包含 Element 序列。

  • Element:Text、Heading、Strong、Link 等具体内容节点。

  • Element function:用户可以调用的 Element 构造函数。

直接对应 Element 的语法糖

表面语法

底层 Element

大致等价写法

Hello

TextElem

#text("Hello")

*strong*

StrongElem

#strong[strong]

_emphasis_

EmphElem

#emph[emphasis]

`raw`

RawElem

#raw("raw")

https://typst.app

LinkElem

#link("https://typst.app")

= Title

HeadingElem

#heading(level: 1)[Title]

\

LinebreakElem

#linebreak()

空行

ParbreakElem

#parbreak()

' / "

SmartQuoteElem

#smartquote(...)

$x$

EquationElem

#math.equation[...]

例如 *Hello* 在语法层是 Strong 节点:

Strong
└─ Text("Hello")

求值时直接构造:

StrongElem {
    body: Content(TextElem("Hello")),
}

它与调用 strong element function 产生相同类型的 Element,因此能被相同的 set/show 规则处理。

标题

= First Level
== Second Level
=== Third Level

Heading 语法节点携带标题深度和正文:

Heading
├─ depth: 2
└─ body: Content(...)

求值后得到 HeadingElem。标题语法没有产生只能由语法创建的特殊对象,因此以下规则对语法糖和函数调用创建的标题都有效:

#set heading(numbering: "1.")
#show heading: it => ...

这是 Typst Element 设计的重要性质:表面语法和显式函数调用最终汇合到相同的内容类型。

列表与后续组合

列表语法并不会立即产生完整列表:

- Apple
- Banana
- Orange

每一行首先产生一个 ListItem

ListItem([Apple])
ListItem([Banana])
ListItem([Orange])

相邻的 ListItem 在 realization 阶段被组合成:

ListElem {
    children: [
        ListItem([Apple]),
        ListItem([Banana]),
        ListItem([Orange]),
    ],
}

有序列表类似:

+ First
+ Second

或:

1. First
2. Second

首先产生 EnumItem,然后组合成 EnumElem。显式编号保存在 item 上。

术语列表:

/ Term: Description

产生:

TermItem {
    term: Content,
    description: Content,
}

连续的 TermItem 再组合成 TermsElem

这种设计允许 parser 只识别局部 item,后续阶段再决定列表边界,也允许 show rule 单独匹配 item。

Label 与引用

Label:

= Introduction <intro>

<intro> 首先产生 Value::Label("intro"),随后 markup evaluator 将它附着到前一个可以被标记的 Element:

HeadingElem + Label("intro")
             ↓
HeadingElem { label: "intro" }

因此 Label 是值和附着操作,不是独立的可见 Element。

引用:

@intro

产生:

RefElem {
    target: Label("intro"),
}

大致对应:

#ref(<intro>)

引用最终显示为章节号、图号或文献引用,由后续语义分析决定。

这与 Notist 的模块引用接近:

[[vault::guide::intro]]

可以降级为:

ModuleRefElem {
    target: ModulePath(["guide", "intro"]),
}

显式函数形式可以设计为:

#ref(module("vault::guide::intro"))

自动链接与 Raw

直接书写 URL:

https://typst.app

会产生 LinkElem。完整的函数形式还能指定展示内容:

#link("https://typst.app")[Typst]

这体现了一种通用原则:

常见用法:简洁语法
完整能力:Element function

行内 Raw 和块级 Raw 使用同一个 RawElem

RawElem {
    text,
    block,
    lang,
}

行内和块级主要通过 blocklang 参数区分,而不是定义完全无关的底层类型。

空格、换行和段落

Typst 将部分看起来不像 Element 的结构也表示为内容:

普通空格     → SpaceElem
强制换行 `\` → LinebreakElem
空行         → ParbreakElem

例如:

Hello \
World

可以理解为:

Content::sequence([
    TextElem("Hello"),
    LinebreakElem,
    TextElem("World"),
])

段落、列表等更高层结构可以在后续 realization/layout 阶段形成,而不是全部由 parser 一次决定。

Content Block 与尾随内容参数

Typst 中:

#strong[Hello]

可以理解为:

#strong([Hello])

[...] 创建 Content 值,并作为尾随位置参数传给函数:

#rect(
  fill: blue,
  inset: 8pt,
)[Hello]

这种调用形式适合文档语言:

函数名
(配置参数)
[内容参数]

Notist 未来也可以采用类似形式:

#note(kind: warning)[
  This is important.
]

底层等价于:

note(
    kind = "warning",
    body = Content(...),
)

数学语法

数学模式不仅是一个字符串节点:

$ x^2 / 2 $

会继续产生数学 Element:

数学语法

底层 Element

$...$

EquationElem

x^2

AttachElem

x_1

AttachElem

a/b

FracElem

根号

RootElem

自动伸缩括号

LrElem

对齐点 &

AlignPointElem

数学模式还拥有独立的名字解析规则,可以从 math scope 中查找函数和符号。

不是普通 Element Function 的语法

Typst 并非所有语言结构都是 Element function 的糖。

变量与控制流

#let x = 1
#if condition [...] else [...]
#for item in items [...]

这些结构控制作用域和求值过程,是语言级构造,不是可展示 Element。

Set Rule

#set text(size: 12pt)

Set rule 会生成样式设置,并作用于后续内容。它不是普通的 set(...) 函数调用。

Show Rule

#show heading: it => ...

Show rule 注册内容转换规则:

HeadingElem
   ↓ show recipe
New Content

它更接近模式匹配和内容重写系统。

Import、Include 与 Context

#import "utils.typ": foo
#include "chapter.typ"
#context counter(heading).display()

这些结构会修改作用域、加载文件或改变求值环境,也不应强行解释为普通 Element function。

Typst 语法的四种类型

Element Sugar

直接生成可展示 Element:

*text*  → strong
_text_  → emph
= title → heading
`code`  → raw
URL     → link
@target → ref
\       → linebreak

Structural Sugar

先产生局部结构,后续再组合:

- item  → list.item
+ item  → enum.item
/ a: b  → terms.item
空行     → parbreak

Value Sugar

产生值而不是独立的可展示 Element:

<label> → Label value
[...]   → Content value

Language Constructs

控制求值、作用域和样式:

let
if
for
while
set
show
import
include
context
return

对 Notist 的设计建议

Notist 当前已经采用统一 Content/Element 模型。以下是与本文讨论直接相关的核心子集;完整枚举还包含 Table、Task、Callout、Details、Image 等节点:

pub enum Element {
    Text(TextElem),
    Paragraph(ParagraphElem),
    Heading(HeadingElem),
    Reference(ReferenceElem),
    List(ListElem),
    ListItem(ListItemElem),
    Raw(RawElem),
}

每种 Element 都具有概念上的构造函数:

text(value)
paragraph(body)
heading(level, body)
ref(target)
list(body)
list::item(body)
raw(text, lang?)
code(text, lang?, block?)

表面语法映射到这些 Element:

[[guide::intro]]
    → ref(module("guide::intro"))

= Introduction
    → heading(level=1, body=[Introduction])

*important*
    → strong([important])

- first
    → list::item([first])

模块路径应当是独立值,而不是始终作为未经验证的字符串存在:

pub struct ModulePath {
    segments: Vec<ModuleSegment>,
}

引用底层使用:

ref(target="guide::intro")

内部不必全部动态化

对用户可以呈现为:

heading(level=1, body=...)

但 Rust 内部仍适合使用强类型结构:

pub struct Heading {
    pub level: u8,
    pub body: Content,
    pub span: TextRange,
}

语法糖可以直接构造:

Element::Heading(Heading {
    level: 1,
    body,
    span,
})

不必先构造动态调用:

Call {
    function: "heading",
    arguments: ...,
}

Notist 应复用的核心原则是:

表面上:
所有 Element 都能通过函数构造。

内部:
语法糖和函数调用最终汇合到同一种强类型 Element。

不要求:
所有语法糖必须先改写成字符串形式的函数调用 AST。

参考实现位置

  • Typst markup 求值:crates/typst-eval/src/markup.rs

  • Typst math 求值:crates/typst-eval/src/math.rs

  • Typst 语法 AST:crates/typst-syntax/src/ast.rs

  • Typst Element 定义:crates/typst-library/src/model/crates/typst-library/src/text/

  • 列表组合阶段:crates/typst-realize/src/lib.rs