Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 68 additions & 62 deletions src/tutorial/doc-stateful.typ
Original file line number Diff line number Diff line change
Expand Up @@ -26,81 +26,95 @@
#import "../tutorial/figure-time-travel.typ": figure-time-travel
#align(center + horizon, figure-time-travel())

「状态」基本上是教程中最难的部分。它涉及教程之前所有的知识。具体而言,我们需要理解透排版Ⅱ中的「编译流程」。整个「编译流程」中,排版引擎会储存一个「上下文」(context)状态。首先,「解析器」(Parser)将源代码字符串解析为待求值的「抽象语法树」(AST),接着「表达式求值」(Evaluation)阶段将「抽象语法树」转换为「内容」,然后「排版」(typeset)阶段将「内容」转换为布局好的结果。

== 「`typeset`」阶段的迭代收敛

一个容易值得思考的问题是,如果我在文档的开始位置调用了#typst-func("state.final")方法,那么Typst要如何做才能把文档的最终状态返回给我呢?

容易推测出,原来Typst并不会只对内容执行一遍「`typeset`」。仅考虑我们使用#typst-func("state.final")方法的情况。初始情况下#typst-func("state.final")方法会返回状态默认值,并完成一次布局。接下来的迭代,#typst-func("state.final")方法会返回上一次迭代布局完成时的。直到布局的内容不再发生变化。#typst-func("state.at")会导致相似的布局迭代,只不过情况更为复杂,这里便不再展开细节。

所有对文档的查询都会导致布局的迭代:`query`函数可能会导致布局的迭代;`state.at`函数可能会导致布局的迭代;`state.final`函数一定会导致布局的迭代。

// 延迟执行

// This section mainly talks about `selector` and `state` step by step, to teach how to locate content, create and manipulate states.

本节教你使用选择器(selector)定位到文档的任意部分;也教你创建与查询二维文档状态(state)。
// 本节教你使用选择器(selector)定位到文档的任意部分;也教你创建与查询二维文档状态(state)。

== 时间维度 -- 控制流

// == 自定义标题样式
== 空间维度 -- 文档(内容)树

// 本节讲解的程序是如何在Typst中设置标题样式。我们的目标是:
// + 为每级标题单独设置样式。
// + 设置标题为内容的页眉:
// + 如果当前页眉有二级标题,则是当前页面的第一个二级标题。
// + 否则是之前所有页面的最后一个二级标题。
// == 回顾其一

// 效果如下
// 针对特定的`feat`和`refactor`文本,我们使用`emph`修饰

// #frames-cjk(
// read("./stateful/s1.typ"),
// read("./stateful/s2.typ"),
// code-as: ```typ
// #show: set-heading

// == 雨滴书v0.1.2
// === KiraKira 样式改进
// feat: 改进了样式。
// === FuwaFuwa 脚本改进
// feat: 改进了脚本。

// == 雨滴书v0.1.1
// refactor: 移除了LaTeX。
// #show regex("feat|refactor"): emph
// ```,
// )

// feat: 删除了一个多余的文件夹。
// 对于三级标题,我们将中文文本用下划线标记,同时将特定文本替换成emoji:

// == 雨滴书v0.1.0
// feat: 新建了两个文件夹。
// #frames-cjk(
// read("./stateful/s3.typ"),
// code-as: ```typ
// #let set-heading(content) = {
// show heading.where(level: 3): it => {
// show regex("[\p{hani}\s]+"): underline
// it
// }
// show heading: it => {
// show regex("KiraKira"): box("★", baseline: -20%)
// show regex("FuwaFuwa"): box(text("🪄", size: 0.5em), baseline: -50%)
// it
// }

// content
// }
// #show: set-heading
// ```,
// )

== 回顾其一
== 任务描述

针对特定的`feat`和`refactor`文本,我们使用`emph`修饰:
为举例说明,本节讲解的程序是如何在Typst中设置标题样式。我们的目标是设置标题为内容的页眉:
+ 如果当前页眉有二级标题,则是当前页面的第一个二级标题。
+ 否则是之前所有页面的最后一个二级标题。

效果如下:

#frames-cjk(
read("./stateful/s2.typ"),
read("./stateful/s1.typ"),
code-as: ```typ
#show regex("feat|refactor"): emph
```,
)
#show: set-heading

对于三级标题,我们将中文文本用下划线标记,同时将特定文本替换成emoji:
== 雨滴书v0.1.2
=== KiraKira 样式改进
feat: 改进了样式。
=== FuwaFuwa 脚本改进
feat: 改进了脚本。

#frames-cjk(
read("./stateful/s3.typ"),
code-as: ```typ
#let set-heading(content) = {
show heading.where(level: 3): it => {
show regex("[\p{hani}\s]+"): underline
it
}
show heading: it => {
show regex("KiraKira"): box("★", baseline: -20%)
show regex("FuwaFuwa"): box(text("🪄", size: 0.5em), baseline: -50%)
it
}
== 雨滴书v0.1.1
refactor: 移除了LaTeX。

content
}
#show: set-heading
feat: 删除了一个多余的文件夹。

== 雨滴书v0.1.0
feat: 新建了两个文件夹。
```,
)

== 制作页眉标题的两种方法
// == 制作页眉标题的两种方法

制作页眉标题至少有两种方法。一是直接查询文档内容;二是创建状态,利用布局迭代收敛的特性获得每个页面的首标题。
// 制作页眉标题至少有两种方法。一是直接查询文档内容;二是创建状态,利用布局迭代收敛的特性获得每个页面的首标题。

在接下来的两节中我们将分别介绍这两种方法。
// 在接下来的两节中我们将分别介绍这两种方法。

本节我们讲解制作页眉标题的第一种方法,即通过查询文档状态直接估计当前页眉应当填入的内容。
// 本节我们讲解制作页眉标题的第一种方法,即通过查询文档状态直接估计当前页眉应当填入的内容。

// #locate(loc => query(heading, loc))
// #locate(loc => query(heading.where(level: 2), loc))
Expand Down Expand Up @@ -248,7 +262,7 @@
// + 已经存在对应结果,则不会重新执行查询,而是使用表中的值作为结果。
// ]

== 回顾其二
== 通过查询内置状态制作页眉

页眉的设置方法是创建一条```typc set page(header)```规则:

Expand Down Expand Up @@ -630,7 +644,9 @@ for i in range(res-headings.len()) {
```,
)

在上一节(法一)中,我们仅靠「#typst-func("query")」函数就完成制作所要求页眉的功能。
== 自定义「状态」(state)<grammar-state>

在法一中,我们仅靠「#typst-func("query")」函数就完成制作所要求页眉的功能。

思考下面函数:

Expand All @@ -654,8 +670,6 @@ for i in range(res-headings.len()) {

Typst文档可以很高效,但有些人写出的Typst代码更高效。本节所介绍的法二,让我们变得更接近这种人。

== 「state」函数<grammar-state>

`state`接收一个名称,并创建该名称对应*全局*唯一的状态变量。

#code(```typ
Expand Down Expand Up @@ -800,15 +814,7 @@ Typst提供两个方法查询特定时间点的「状态」:

这就是允许我们进行时光回溯的基础。

== 「`typeset`」阶段的迭代收敛

一个容易值得思考的问题是,如果我在文档的开始位置调用了#typst-func("state.final")方法,那么Typst要如何做才能把文档的最终状态返回给我呢?

容易推测出,原来Typst并不会只对内容执行一遍「`typeset`」。仅考虑我们使用#typst-func("state.final")方法的情况。初始情况下#typst-func("state.final")方法会返回状态默认值,并完成一次布局。接下来的迭代,#typst-func("state.final")方法会返回上一次迭代布局完成时的。直到布局的内容不再发生变化。#typst-func("state.at")会导致相似的布局迭代,只不过情况更为复杂,这里便不再展开细节。

所有对文档的查询都会导致布局的迭代:`query`函数可能会导致布局的迭代;`state.at`函数可能会导致布局的迭代;`state.final`函数一定会导致布局的迭代。

== 回顾其三
== 通过自定义状态制作页眉

本节使用递归的方法完成状态的构建,其更为巧妙。

Expand Down
47 changes: 45 additions & 2 deletions src/tutorial/reference-utils.typ
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,56 @@
#table(
columns: (1fr, 1fr, 2fr),
[函数], [名称], [描述],
..table-items(typst-v11.funcs, featured-func)
..table-items(typst-v11.funcs, featured-func),
)

== 分类:方法

#table(
columns: (1fr, 1fr, 2fr),
[方法], [名称], [描述],
..table-items(typst-v11.scoped-items, featured-scope-item)
..table-items(typst-v11.scoped-items, featured-scope-item),
)

#if false [
== `plain-text`,以及递归函数

如果我们想要实现一个函数`plain-text`,它将一段文本转换为字符串。它便可以在树上递归遍历:

```typ
#let plain-text(it) = if it.has("text") {
it.text
} else if it.has("children") {
("", ..it.children.map(plain-text)).join()
} else if it.has("child") {
plain-text(it.child)
} else { ... }
```

所谓递归是一种特殊的函数实现技巧:
- 递归总有一个不调用其自身的分支,称其为递归基。这里递归基就是返回`it.text`的分支。
- 函数体中包含它自身的函数调用。例如,`plain-text(it.child)`便再度调用了自身。

这个函数充分利用了内容类型的特性实现了遍历。首先它使用了`has`函数检查内容的成员。

如果一个内容有孩子,那么对其每个孩子都继续调用`plain-text`函数并组合在一起:

```typ
#if it.has("children") { ("", ..it.children.map(plain-text)).join() }
#if it.has("child") { plain-text(it.child) }
```

限于篇幅,我们没有提供`plain-text`的完整实现,你可以试着在课后完成。

== 鸭子类型

这里值得注意的是,`it.text`具有多态行为。即便没有继承,这里通过一定动态特性,允许我们同时访问「代码片段」的`text`和「文本」的text。例如:

#code(```typ
#let plain-mini(it) = if it.has("text") { it.text }
#repr(plain-mini(`代码片段中的text`)) \
#repr(plain-mini([文本中的text]))
```)

这也便是我们在「内容类型」小节所述的鸭子类型特性。如果「内容」长得像文本(鸭子),那么它就是文本。
]
30 changes: 15 additions & 15 deletions src/tutorial/scripting-main.typ
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@

但是对于整个文档,要如何理解对内容块的求值?这就引入了「可折叠」的值(Foldable)的概念。「可折叠」成为块作为表达式的基础。

== Typst的主函数

在Typst的源代码中,有一个Rust函数直接对应整个编译流程,其内容非常简短,便是调用了两个阶段对应的函数。“求值”阶段(`eval`阶段)对应执行一个Rust函数,它的名称为`typst::eval`;“内容排版”阶段(`typeset`阶段)对应执行另一个Rust函数,它的名称为`typst::typeset`。

```rs
pub fn compile(world: &dyn World) -> SourceResult<Document> {
// Try to evaluate the source file into a module.
let module = crate::eval::eval(world, &world.main())?;
// Typeset the module's content, relayouting until convergence.
typeset(world, &module.content())
}
```

从代码逻辑上来看,它有明显的先后顺序,似乎与我们所展示的架构略有不同。其`typst::eval`的输出为一个文件模块`module`;其`typst::typeset`仅接受文件的内容`module.content()`并产生一个已经排版好的文档对象`typst::Document`。

== 「`eval`阶段」与「`typeset`阶段」

现在我们介绍Typst的完整架构。
Expand Down Expand Up @@ -80,21 +95,6 @@
+ 即便不涉及用户需求,Typst的排版引擎已经自然存在Frozen State的需求。
+ 本文档也需要`typeset`的能力为你展示特定页面的最终结果而不影响全局状态。

== Typst的主函数

在Typst的源代码中,有一个Rust函数直接对应整个编译流程,其内容非常简短,便是调用了两个阶段对应的函数。“求值”阶段(`eval`阶段)对应执行一个Rust函数,它的名称为`typst::eval`;“内容排版”阶段(`typeset`阶段)对应执行另一个Rust函数,它的名称为`typst::typeset`。

```rs
pub fn compile(world: &dyn World) -> SourceResult<Document> {
// Try to evaluate the source file into a module.
let module = crate::eval::eval(world, &world.main())?;
// Typeset the module's content, relayouting until convergence.
typeset(world, &module.content())
}
```

从代码逻辑上来看,它有明显的先后顺序,似乎与我们所展示的架构略有不同。其`typst::eval`的输出为一个文件模块`module`;其`typst::typeset`仅接受文件的内容`module.content()`并产生一个已经排版好的文档对象`typst::Document`。

== 延迟执行

架构图中还有两个关键的反向箭头,疑问顿生:这两个反向箭头是如何产生的?
Expand Down
41 changes: 0 additions & 41 deletions src/tutorial/scripting-style.typ
Original file line number Diff line number Diff line change
Expand Up @@ -141,47 +141,6 @@ Typst对代码块有着的一系列语法设计,让代码块非常适合描述

// 理解「作用域」对

== `plain-text`,以及递归函数

如果我们想要实现一个函数`plain-text`,它将一段文本转换为字符串。它便可以在树上递归遍历:

```typ
#let plain-text(it) = if it.has("text") {
it.text
} else if it.has("children") {
("", ..it.children.map(plain-text)).join()
} else if it.has("child") {
plain-text(it.child)
} else { ... }
```

所谓递归是一种特殊的函数实现技巧:
- 递归总有一个不调用其自身的分支,称其为递归基。这里递归基就是返回`it.text`的分支。
- 函数体中包含它自身的函数调用。例如,`plain-text(it.child)`便再度调用了自身。

这个函数充分利用了内容类型的特性实现了遍历。首先它使用了`has`函数检查内容的成员。

如果一个内容有孩子,那么对其每个孩子都继续调用`plain-text`函数并组合在一起:

```typ
#if it.has("children") { ("", ..it.children.map(plain-text)).join() }
#if it.has("child") { plain-text(it.child) }
```

限于篇幅,我们没有提供`plain-text`的完整实现,你可以试着在课后完成。

== 鸭子类型

这里值得注意的是,`it.text`具有多态行为。即便没有继承,这里通过一定动态特性,允许我们同时访问「代码片段」的`text`和「文本」的text。例如:

#code(```typ
#let plain-mini(it) = if it.has("text") { it.text }
#repr(plain-mini(`代码片段中的text`)) \
#repr(plain-mini([文本中的text]))
```)

这也便是我们在「内容类型」小节所述的鸭子类型特性。如果「内容」长得像文本(鸭子),那么它就是文本。

== 「`show`」语法 <grammar-show>

「`set`」语法是「`show set`」语法的简写。因此,「`show`」语法显然可以比`set`更强大。<grammar-show-set>
Expand Down