diff --git a/src/tutorial/doc-stateful.typ b/src/tutorial/doc-stateful.typ index 4d25866..32d2073 100644 --- a/src/tutorial/doc-stateful.typ +++ b/src/tutorial/doc-stateful.typ @@ -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)) @@ -248,7 +262,7 @@ // + 已经存在对应结果,则不会重新执行查询,而是使用表中的值作为结果。 // ] -== 回顾其二 +== 通过查询内置状态制作页眉 页眉的设置方法是创建一条```typc set page(header)```规则: @@ -630,7 +644,9 @@ for i in range(res-headings.len()) { ```, ) -在上一节(法一)中,我们仅靠「#typst-func("query")」函数就完成制作所要求页眉的功能。 +== 自定义「状态」(state) + +在法一中,我们仅靠「#typst-func("query")」函数就完成制作所要求页眉的功能。 思考下面函数: @@ -654,8 +670,6 @@ for i in range(res-headings.len()) { Typst文档可以很高效,但有些人写出的Typst代码更高效。本节所介绍的法二,让我们变得更接近这种人。 -== 「state」函数 - `state`接收一个名称,并创建该名称对应*全局*唯一的状态变量。 #code(```typ @@ -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`函数一定会导致布局的迭代。 - -== 回顾其三 +== 通过自定义状态制作页眉 本节使用递归的方法完成状态的构建,其更为巧妙。 diff --git a/src/tutorial/reference-utils.typ b/src/tutorial/reference-utils.typ index 5dc3b84..fe92eda 100644 --- a/src/tutorial/reference-utils.typ +++ b/src/tutorial/reference-utils.typ @@ -37,7 +37,7 @@ #table( columns: (1fr, 1fr, 2fr), [函数], [名称], [描述], - ..table-items(typst-v11.funcs, featured-func) + ..table-items(typst-v11.funcs, featured-func), ) == 分类:方法 @@ -45,5 +45,48 @@ #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])) + ```) + + 这也便是我们在「内容类型」小节所述的鸭子类型特性。如果「内容」长得像文本(鸭子),那么它就是文本。 +] diff --git a/src/tutorial/scripting-main.typ b/src/tutorial/scripting-main.typ index 74d5ea7..42f1e35 100644 --- a/src/tutorial/scripting-main.typ +++ b/src/tutorial/scripting-main.typ @@ -49,6 +49,21 @@ 但是对于整个文档,要如何理解对内容块的求值?这就引入了「可折叠」的值(Foldable)的概念。「可折叠」成为块作为表达式的基础。 +== Typst的主函数 + +在Typst的源代码中,有一个Rust函数直接对应整个编译流程,其内容非常简短,便是调用了两个阶段对应的函数。“求值”阶段(`eval`阶段)对应执行一个Rust函数,它的名称为`typst::eval`;“内容排版”阶段(`typeset`阶段)对应执行另一个Rust函数,它的名称为`typst::typeset`。 + +```rs +pub fn compile(world: &dyn World) -> SourceResult { + // 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的完整架构。 @@ -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 { - // 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`。 - == 延迟执行 架构图中还有两个关键的反向箭头,疑问顿生:这两个反向箭头是如何产生的? diff --git a/src/tutorial/scripting-style.typ b/src/tutorial/scripting-style.typ index 8d68ada..ddaf06e 100644 --- a/src/tutorial/scripting-style.typ +++ b/src/tutorial/scripting-style.typ @@ -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`」语法 「`set`」语法是「`show set`」语法的简写。因此,「`show`」语法显然可以比`set`更强大。