From ce990dbe857fbe18b3d8c6698d4425bcbb03ff65 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 7 Oct 2025 18:00:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=96=B0=E5=AE=89=E6=8E=92?= =?UTF-8?q?=E6=8E=92=E7=89=88=E2=85=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/basic/scripting-color.typ | 2 +- src/basic/scripting-content.typ | 366 +++++++++++++++ src/basic/scripting-layout.typ | 221 ++++++++- src/basic/scripting-length.typ | 224 --------- src/basic/scripting-main.typ | 224 +++++++++ src/basic/scripting-style.typ | 547 +++++++--------------- src/book.typ | 13 +- src/intermediate/content-stateful-agg.typ | 5 - src/intermediate/doc-stateful.typ | 415 +--------------- 9 files changed, 1007 insertions(+), 1010 deletions(-) create mode 100644 src/basic/scripting-content.typ delete mode 100644 src/basic/scripting-length.typ create mode 100644 src/basic/scripting-main.typ delete mode 100644 src/intermediate/content-stateful-agg.typ diff --git a/src/basic/scripting-color.typ b/src/basic/scripting-color.typ index 13525e3..173bf10 100644 --- a/src/basic/scripting-color.typ +++ b/src/basic/scripting-color.typ @@ -1,6 +1,6 @@ #import "mod.typ": * -#show: book.page.with(title: "色彩") +#show: book.page.with(title: "颜色类型") == 颜色类型 diff --git a/src/basic/scripting-content.typ b/src/basic/scripting-content.typ new file mode 100644 index 0000000..3b7381f --- /dev/null +++ b/src/basic/scripting-content.typ @@ -0,0 +1,366 @@ +#import "mod.typ": * + +#show: book.page.with(title: "文档树") + +== 「可折叠」的值(Foldable) + +先来看代码块。代码块其实就是一个脚本。既然是脚本,Typst就可以按照语句顺序依次执行「语句」。 + +#pro-tip[ + 准确地来说,按照控制流顺序。 +] + +Typst按控制流顺序执行代码,将所有结果*折叠*成一个值。所谓折叠,就是将所有数值“连接”在一起。这样讲还是太抽象了,来看一些具体的例子。 + +=== 字符串折叠 + +Typst实际上不限制代码块的每个语句将会产生什么结果,只要是结果之间可以*折叠*即可。 + +我们说字符串是可以折叠的: + +#code(```typ +#{"Hello"; " "; "World"} +```) + +实际上折叠操作基本就是#mark("+")操作。那么字符串的折叠就是在做字符串连接操作: + +#code(```typ +#("Hello" + " " + "World") +```) + +再看一个例子: + +#code(```typ +#{ + let hello = "Hello"; + let space = " "; + let world = "World"; + hello; space; world; + let destroy = ", Destroy" + destroy; space; world; "." +} +```) + +如何理解将「变量声明」与表达式混写? + +回忆前文。对了,「变量声明」表达式的结果为```typc none```。 +#code(```typ +#type(let hello = "Hello") +```) + +并且还有一个重点是,字符串与`none`相加是字符串本身,`none`加`none`还是`none`: + +#code(```typ +#("Hello" + none), #(none + "Hello"), #repr(none + none) +```) + +现在可以重新体会这句话了:Typst按控制流顺序执行代码,将所有结果*折叠*成一个值。对于上例,每句话的执行结果分别是: + +```typc +#{ + none; // let hello = "Hello"; + none; // let space = " "; + none; // let world = "World"; + "Hello"; " "; "World"; // hello; space; world; + none; // let destroy = ", Destroy" + ", Destroy"; " "; "World"; "." // destroy; space; world; "." +} +``` + +将结果收集并“折叠”,得到结果: + +#code(```typc +#(none + none + none + "Hello" + " " + "World" + none + ", Destroy" + " " + "World" + ".") +```) + +#pro-tip[ + 还有其他可以折叠的值,例如,数组与字典也是可以折叠的: + + #code(```typ + #for i in range(1, 5) { (i, i * 10) } + ```) + + #code(```typ + #for i in range(1, 5) { let d = (:); d.insert(str(i), i * 10); d } + ```) +] + +=== 其他基本类型的情况 + +那么为什么说折叠操作基本就是#mark("+")操作。那么就是说有的“#mark("+")操作”并非是折叠操作。 + +布尔值、整数和浮点数都不能相互折叠: + +```typ +// 不能编译 +#{ false; true }; #{ 1; 2 }; #{ 1.; 2. } +``` + +那么是否说布尔值、整数和浮点数都不能折叠呢。答案又是否认的,它们都可以与```typc none```折叠(把下面的加号看成折叠操作): + +#code(```typ +#(1 + none) +```) + +所以你可以保证一个代码块中只有一个「语句」产生布尔值、整数或浮点数结果,这样的代码块就又是能编译的了。让我们利用`let _ = `来实现这一点: + +#code(```typ +#{ let _ = 1; true }, +#{ let _ = false; 2. } +```) + +回忆之前所讲的特殊规则:#term("placeholder")用作标识符的作用是“忽略不必要的语句结果”。 + +=== 内容折叠 + +Typst脚本的核心重点就在本段。 + +内容也可以作为代码块的语句结果,这时候内容块的结果是每个语句内容的“折叠”。 + +#code(```typ +#{ + [= 生活在Content树上] + [现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。] + [滥觞于家庭与社会传统的期望正失去它们的借鉴意义。] + [但面对看似无垠的未来天空,我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。] +} +```) + +是不是感觉很熟悉?实际上内容块就是上述代码块的“糖”。所谓糖就是同一事物更方便书写的语法。上述代码块与下述内容块等价: + +```typ +#[ + = 生活在Content树上 + 现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。滥觞于家庭与社会传统的期望正失去它们的借鉴意义。但面对看似无垠的未来天空,我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。 +] +``` + +由于Typst默认以「标记模式」开始解释你的文档,这又与省略`#[]`的写法等价: + +```typ += 生活在Content树上 +现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。滥觞于家庭与社会传统的期望正失去它们的借鉴意义。但面对看似无垠的未来天空,我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。 +``` + +#pro-tip[ + 实际上有区别,由于多两个换行和缩进,前后各多一个Space Element。 +] + +// == Hello World程序 + +// 有的时候,我们想要访问字面量、变量与函数中存储的“信息”。例如,给定一个字符串```typc "Hello World"```,我们想要截取其中的第二个单词。 + +// 单词`World`就在那里,但仅凭我们有限的脚本知识,却没有方法得到它。这是因为字符串本身是一个整体,虽然它具备单词信息,我们却缺乏了*访问*信息的方法。 + +// Typst为我们提供了「成员」和「方法」两种概念访问这些信息。使用「方法」,可以使用以下脚本完成目标: + +// #code(```typ +// #"Hello World".split(" ").at(1) +// ```) + +// 为了方便讲解,我们改写出6行脚本。除了第二行,每一行都输出一段内容: + +// #code(```typ +// #let x = "Hello World"; #x \ +// #let split = str.split +// #split(x, " ") \ +// #str.split(x, " ") \ +// #x.split(" ") \ +// #x.split(" ").at(1) +// ```) + +// 从```typ #x.split(" ").at(1)```的输出可以看出,这一行帮助我们实现了“截取其中的第二个单词”的目标。我们虽然隐隐约约能揣测出其中的意思: + +// ```typ +// #( x .split(" ") .at(1) ) +// // 将字符串 根据字符串拆分 取出其中的第2个单词(字符串) +// ``` + +// 但至少我们对#mark(".")仍是一无所知。 + +// 本节我们就来讲解Typst中较为高级的脚本语法。这些脚本语法与大部分编程语言的语法相同,但是我们假设你并不知道这些语法。 + +== 「内容」是一棵树(Cont.) + +#pro-tip[ + 利用「内容」与「树」的特性,我们可以在Typst中设计出更多优雅的脚本功能。 +] + +=== CeTZ的「树」 + +CeTZ利用内容树制作“内嵌的DSL”。CeTZ的`canvas`函数接收的不完全是内容,而是内容与其IR的混合。 + +例如它的`line`函数的返回值,就完全不是一个内容,而是一个无法窥视的函数。 + +#code(```typ +#import "@preview/cetz:0.3.4" +#repr(cetz.draw.line((0, 0), (1, 1), fill: blue)) +```) + +当你产生一个“混合”的内容并将其传递给`cetz.canvas`,CeTZ就会像`plain-text`一样遍历你的混合内容,并加以区分和处理。如果遇到了他自己特定的IR,例如`cetz.draw.line`,便将其以特殊的方式转换为真正的「内容」。 + +使用混合语言,在Typst中可以很优雅地画多面体: + +#code.with(al: top)(```typ +#import "@preview/cetz:0.3.4" +#align(center, cetz.canvas({ + // 导入cetz的draw方言 + import cetz.draw: *; import cetz.vector: add + let neg(u) = if u == 0 { 1 } else { -1 } + for (p, c) in ( + ((0, 0, 0), black), ((1, 1, 0), red), ((1, 0, 1), blue), ((0, 1, 1), green), + ) { + line(add(p, (0, 0, neg(p.at(2)))), p, stroke: c) + line(add(p, (0, neg(p.at(1)), 0)), p, stroke: c) + line(add(p, (neg(p.at(0)), 0, 0)), p, stroke: c) + } +})) +```) + +=== curryst的「树」 + +我们知道「内容块」与「代码块」没有什么本质区别。 + +如果我们可以基于「代码块」描述一棵「内容」的树,那么逻辑推理的过程也可以被描述为条件、规则、结论的树。 + +#link("https://typst.app/universe/package/curryst/")[curryst]包提供了接收条件、规则、结论参数的`rule`函数,其返回一个包含传入信息的`dict`,并且允许把`rule`函数返回的`dict`作为`rule`的部分参数。于是我们可以通过嵌套`rule`函数建立描述推理过程的树,并通过该包提供的`prooftree`函数把包含推理过程的`dict`树画出来: + +#code(```typ +#import "@preview/curryst:0.5.0": rule, prooftree +#let tree-dict = rule( + name: $R$, + $C_1 or C_2 or C_3$, + rule( + name: $A$, + $C_1 or C_2 or L$, + rule( + $C_1 or L$, + $Pi_1$, + ), + ), + rule( + $C_2 or overline(L)$, + $Pi_2$, + ), +) +`tree-dict`的类型:#type(tree-dict) \ +`tree-dict`代表的树:#prooftree(tree-dict) +```) + +== 内容类型 + +我们已经学过很多元素:段落、标题、代码片段等。这些元素在被创建后都会被包装成为一种被称为「内容」的值。这些值所具有的类型便被称为「内容类型」。同时「内容类型」提供了一组公共方法访问元素本身。 + +乍一听,内容就像是一个“容器”将元素包裹。但内容又不太像是之前所学过的数组或字典那样的复合字面量,或者说这样不方便理解。事实上,每个元素都有各自的特点,但仅仅为了保持动态性,所有的元素都被硬凑在一起,共享一种类型。有两种理解这种类型的视角:从表象论,「内容类型」是一种鸭子类型;从原理论,「内容类型」提供了操控内容的公共方法,即它是一种接口,或称特征(Trait)。 + +=== 特性一:元素包装于「内容」 + +我们知道所有的元素语法都可以等价使用相应的函数构造。例如标题: + +#code(```typ +#repr([= 123]) \ // 语法构造 +#repr(heading(depth: 1)[123]) // 函数构造 + +```) + +一个常见的误区是误认为元素继承自「内容类型」,进而使用以下方法判断一个内容是否为标题元素: + +#code(```typ +标题是heading类型(伪)?#(type([= 123]) == heading) +```) + +但两者类型并不一样。事实上,元素是「函数类型」,元素函数的返回值为「内容类型」。 + +#code(```typ +标题函数的类型:#(type(heading)) \ +标题的类型:#type([= 123]) +```) + +这引出了一个重要的理念,Typst中一切皆组合。Typst中目前没有继承概念,一切功能都是组合出来的,这类似于Rust语言的概念。你可能没有学过Rust语言,但这里有一个冷知识: + +#align(center, [Typst $<=>$ Typ(setting Ru)st $<=>$ Typesetting Rust]) + +即Typst是以Rust语言特性为基础设计出的一个排版(Typesetting)语言。 + +当各式各样的元素函数接受参数时,它们会构造出「元素」,然后将元素包装成一个共同的类型:「内容类型」。`heading`是函数而不是类型。与其他语言不同,没有一个`heading`类型继承`content`。因此不能使用`type([= 123]) == heading`判断一个内容是否为标题元素。 + +=== 特性二:内容类型的`func`方法 + +所有内容都允许使用`func`得到构造这个内容所使用的函数。因此,可以使用以下方法判断一个内容是否为标题元素: + +#code(```typ +标题所使用的构造函数:#([= 123]).func() + +标题的构造函数是`heading`?#(([= 123]).func() == heading) +```) + +// 这一段不要了 +// === 特性二点五:内容类型的`func`方法可以直接拿来用 + +// `func`方法返回的就是函数本身,自然也可以拿来使用: + +// #code(```typ +// 重新构造标题:#(([= 123]).func())([456]) +// ```) + +// 这一般没什么用,但是有的时候可以用于得到一些Typst没有暴露出来的内容函数,例如`styled`。 + +// #code(```typ +// #let type_styled = text(fill: red, "").func() +// #let st = text(fill: blue, "").styles +// #text([abc], st) +// ```) + +=== 特性三:内容类型的`fields`方法 + +Typst中一切皆组合,它将所有内容打包成「内容类型」的值以完成类型上的统一,而非类型继承。 + +但是这也有坏处,坏处是无法“透明”访问内部内容。例如,我们可能希望知道`heading`的级别。如果不提供任何方法访问标题的级别,那么我们就无法编程完成与之相关的排版。 + +为了解决这个问题,Typst提供一个`fields`方法提供一个content的部分信息: + +#code(```typ +#([= 123]).fields() +```) + +`fields()`将部分信息组成字典并返回。如上图所示,我们可以通过这个字典对象进一步访问标题的内容和级别。 + +#code(```typ +#([= 123]).fields().at("depth") +```) + +#pro-tip[ + 这里的“部分信息”描述稍显模糊。具体来说,Typst只允许你直接访问元素中不受样式影响的信息,至少包含语法属性,而不允许你*直接*访问元素的样式。 + + // 如下: + + // #code.with(al: top)(````typ + // #let x = [= 123] + // #rect([#x ]) + // #x.fields() \ + // #locate(loc => query(, loc)) + // ````) +] + +=== 特性四:内容类型与`fields`相关的糖 + +由于我们经常需要与`fields`交互,Typst提供了`has`方法帮助我们判断一个内容的`fields`是否有相关的「键」。 + +#code(```typ +使用`... in x.fields()`判断:#("text" in `x`.fields()) \ +等同于使用`has`方法判断:#(`x`.has("text")) +```) + +Typst提供了`at`方法帮助我们访问一个内容的`fields`中键对应的值。 + +#code(```typ +使用`x.fields().at()`获取值:#(`www`.fields().at("text")) \ +等同于使用`at`方法:#(`www`.at("text")) +```) + +特别地,内容的成员包含`fields`的键,我们可以直接通过成员访问相关信息: + +#code(```typ +使用`at`方法:#(`www`.at("text")) \ +等同于访问`text`成员:#(`www`.text) +```) diff --git a/src/basic/scripting-layout.typ b/src/basic/scripting-layout.typ index 3615d3c..490403d 100644 --- a/src/basic/scripting-layout.typ +++ b/src/basic/scripting-layout.typ @@ -1,12 +1,231 @@ #import "mod.typ": * -#show: book.page.with(title: "布局") +#show: book.page.with(title: "长度与布局") 本章我们再度回到排版专题,拓宽制作文档的能力。 #let absent(content) = underline(offset: 1.5pt, underline(offset: 3pt, text(red, content))) #let ng(content) = underline(offset: 1.5pt, text(blue, content)) +== 长度类型 + +#show quote: it => { + box( + stroke: ( + left: 2pt + main-color, + ), + inset: ( + left: 12pt, + y: 8pt, + ), + { + stack( + { + set text(style: "italic") + it.body + }, + if "attribution" in it.fields() { + 1em + }, + if "attribution" in it.fields() { + align(right, [——] + it.attribution) + }, + ) + }, + ) +} + +Typst有三种长度单位,它们是:绝对长度(absolute length)、相对长度(relative length)与上下文有关长度(context-sensitive length)。 + ++ 绝对长度:人们最熟知的长度单位。例如,```typc 1cm```恰等于真实的一厘米。 ++ 相对长度:与父元素长度相关联的长度单位,例如```typc 70%```恰等于父元素高度或宽度的70%。 ++ 上下文有关长度:与样式等上下文有关的长度单位,例如```typc 1em```恰等于当前位置设定的字体长度。 + +掌握不同种类的长度单位对构建期望的布局非常重要。它们是相辅相成的。 + +== 绝对长度 + +目前Typst一共提供四种绝对长度。除了公制长度单位```typc 1cm```与```typc 1mm```与英制长度单位```typc 1in```,Typst还提供排版专用的长度单位“点”,即```typc 1pt```。 + +#quote(attribution: link("https://zh.wikipedia.org/wiki/%E9%BB%9E_(%E5%8D%B0%E5%88%B7)")[维基百科:点 (印刷)])[ + 点(英语:point),pt,是印刷所使用的长度单位,用于表示字型的大小,也用于余白(字距、行距)等其他版面构成要素的长度。作为铸字行业内部的一个专用单位,1 点的长度在世界各地、各个时代曾经有过不同定义,并不统一。当代最通行的是广泛应用于桌面排版软件的 DTP 点,72 点等于 1英寸(1 point = 127⁄360 mm = 0.352777... mm)。中国传统字体排印上的字号单位是号,而后采用“点”“号”兼容的体制。 +] + +Typst会将你提供的任意长度单位都统一成点单位,以便进行长度运算。 + +#{ + set align(center) + let units = ((1pt, "pt"), (1mm, "mm"), (1cm, "cm"), (1in, "in")) + let methods = (length.pt, length.mm, length.cm, length.inches) + table( + columns: 5, + "", + ..("pt", "mm", "cm", "in").map(e => raw("=?" + e)), + ..units + .map(((l, u)) => { + (raw("1" + u, lang: "typc"),) + methods.map(method => [#calc.round(method(l), digits: 2)]) + }) + .flatten(), + ) +} + +== 绝对长度的运算 + +长度单位可以参与任意多个浮点值的运算。一个长度表达式是合法的当且仅当运算结果*保持长度量纲*。请观察下列算式,它们都可以编译: + +#code(```typ +#(1cm * 3), #(1cm / 3), #(2 * 1cm * 3 / 2), #(1cm + 3in) +```) + +请观察下列算式,它们都不能编译: + +#( + ```typ + #(1cm + 3), #(3 / 1cm), #(1cm * 1cm) + ``` +) + +所谓*保持长度量纲*,即它存在一系列判别规则: + +- 由于`1cm`与`3in`量纲均为长度量纲(`m`),它们之间*可以*进行*加减*运算。 +- 由于`3`无量纲,`1cm`与`3`之间*不能*进行*加减*运算。 + +进一步,通过量纲运算,可以判断一个长度算术表达式是否合法: + +#{ + set align(center) + table( + columns: 4, + [长度表达式], + [量纲运算], + [检查合法性], + [判断结果], + ```typc 1cm * 3```, + $bold(sans(m dot 1 = m))$, + $bold(sans(m = m))$, + table.cell(rowspan: 2, align: horizon)[合法], + ```typc 1cm / 3```, + $bold(sans(m op(slash) 1 = m))$, + $bold(sans(m = m))$, + ```typc 3 / 1cm```, + $bold(sans(1 op(slash) m = m^(-1)))$, + $bold(sans(m^(-1) = m))$, + table.cell(rowspan: 2, align: horizon)[非法], + ```typc 1cm * 1cm```, + $bold(sans(m dot m = m^2))$, + $bold(sans(m^2 = m))$, + ) +} + +== 绝对长度的转换 + +你可以使用`length`类型上的「方法」实现不同单位到浮点数的转换: + +#code(```typ +#1cm 是 #1cm.pt() 点 \ +#1cm 是 #1cm.inches() 英尺 +```) + +你可以使用乘法实现浮点数到长度上的转换,例如```typc 28.3465 * 1pt```: + +#code(```typ +#1cm 是 #(28.3465 * 1pt).cm() 厘米 +```) + +== 相对长度 + +有两种相对长度(Relative Length),一是百分比(Ratio),一是分数比(Fraction)。 + +=== 百分比 + +#let p(w, f, ..args) = box( + width: w, + height: 10pt, + fill: f, + ..args, +) +#let code = code.with(scope: (p: p)) + +当「百分比」用作长度时,其实际值取决于父容器宽度: + +#code(```typ +#let p(w, f, ..args) = box( + width: w, height: 10pt, fill: f, ..args) +4比6:#p(100pt, blue, p(40%, red)) +```) + +Typst还支持以「分数比」作长度单位。当分数比作长度单位时,Typst按比例分配长度。 + +#code(```typ +4比6:#p(100pt, blue, + p(4fr, red) + p(6fr, blue)) +```) + +结合代码与图例理解,`N fr`代表:在总的比例中,这个元素应当占有其中`N`份长度。 + +当同级元素既有分数比长度元素,又有其他长度单位元素时,优先将空间分配给其他长度单位元素。 + +#code(```typ +绿色先占60%: #p(100pt, blue, p(1fr, red) + p(2fr, blue) + p(60%, green)) \ +绿色先占110%: #p(100pt, blue, p(1fr, red) + p(1fr, blue) + p(110%, green)) \ +红色先占30pt: #p(100pt, blue, p(30pt, red) + p(1fr, blue) + p(110%, green)) +```) + +建议结合下文中grid布局关于长度的使用,加深对相对长度的理解。 + +== 上下文有关长度 + +目前Typst仅提供一种上下文有关长度,即当前上下文中的字体大小。历史上,定义该字体中大写字母`M`的宽度为`1em`,但是现代排版中,`1em`可以比`M`的宽度要更窄或者更宽。 + +上下文有关长度是与相对长度相区分的。区别是上下文有关长度的取值从「样式链」获取,而相对长度相对于父元素宽度。事实上`1em`的具体值可以通过上下文有关表达式获取: + +#code(```typ +#let _1em = context measure( + line(length: 1em)).width +#text(size: 10pt, [1em等于] + _1em) \ +#text(size: 20pt, [1em等于] + _1em) \ +```) + +相比较,`1em`更好用一点,因为`text.size`只允许在上下文有关表达式内部使用。 + +== 混合长度 + +以上所介绍的各种长度可以通过「加号」任意混合成单个长度的值,其长度的值为每个分量总和: + +#code(```typ +#(1pt + 1em + 100%) +```) + +== 长度的内省或评估 + +你可以通过`measure`获取一个长度在当前位置的具体值: + +#let length-of(l) = (measure(line(length: l)).width) +#let code = code.with(scope: (length-of: length-of)) + +#code(```typ +#let length-of(l) = measure( + line(length: l)).width +#context [长度等于#length-of(1em+1pt)。] +```) + +但是该方式是不被推荐的,因为一个长度值中的相对长度分量会被评估为`0pt`,从而导致计算失真: + +#code(```typ +长度等于 #context length-of(1em+1pt+100%)。 +```) + +这是因为在评估的时候,`measure`没有为内容锚定一个“父元素”。 + +一种更为鲁棒的方法是使用`layout`函数获取`layout`位置的宽度和高度信息: + +#code(```typ +长度等于#box(width: 1em+1pt+100%, + layout(l => l.width)) +```) + +但是使用`layout`会导致布局的多轮迭代,有可能*严重*降低编译性能。 + == 布局概览与布局模型 Typst的布局引擎仍未完成,其主要#absent[缺失]或#ng[不足]的内容为: diff --git a/src/basic/scripting-length.typ b/src/basic/scripting-length.typ deleted file mode 100644 index 274e284..0000000 --- a/src/basic/scripting-length.typ +++ /dev/null @@ -1,224 +0,0 @@ -#import "mod.typ": * - -#show: book.page.with(title: "度量") - -本章我们再度回到排版专题,拓宽制作文档的能力。 - -== 长度类型 - -#show quote: it => { - box( - stroke: ( - left: 2pt + main-color, - ), - inset: ( - left: 12pt, - y: 8pt, - ), - { - stack( - { - set text(style: "italic") - it.body - }, - if "attribution" in it.fields() { - 1em - }, - if "attribution" in it.fields() { - align(right, [——] + it.attribution) - }, - ) - }, - ) -} - -Typst有三种长度单位,它们是:绝对长度(absolute length)、相对长度(relative length)与上下文有关长度(context-sensitive length)。 - -+ 绝对长度:人们最熟知的长度单位。例如,```typc 1cm```恰等于真实的一厘米。 -+ 相对长度:与父元素长度相关联的长度单位,例如```typc 70%```恰等于父元素高度或宽度的70%。 -+ 上下文有关长度:与样式等上下文有关的长度单位,例如```typc 1em```恰等于当前位置设定的字体长度。 - -掌握不同种类的长度单位对构建期望的布局非常重要。它们是相辅相成的。 - -== 绝对长度 - -目前Typst一共提供四种绝对长度。除了公制长度单位```typc 1cm```与```typc 1mm```与英制长度单位```typc 1in```,Typst还提供排版专用的长度单位“点”,即```typc 1pt```。 - -#quote(attribution: link("https://zh.wikipedia.org/wiki/%E9%BB%9E_(%E5%8D%B0%E5%88%B7)")[维基百科:点 (印刷)])[ - 点(英语:point),pt,是印刷所使用的长度单位,用于表示字型的大小,也用于余白(字距、行距)等其他版面构成要素的长度。作为铸字行业内部的一个专用单位,1 点的长度在世界各地、各个时代曾经有过不同定义,并不统一。当代最通行的是广泛应用于桌面排版软件的 DTP 点,72 点等于 1英寸(1 point = 127⁄360 mm = 0.352777... mm)。中国传统字体排印上的字号单位是号,而后采用“点”“号”兼容的体制。 -] - -Typst会将你提供的任意长度单位都统一成点单位,以便进行长度运算。 - -#{ - set align(center) - let units = ((1pt, "pt"), (1mm, "mm"), (1cm, "cm"), (1in, "in")) - let methods = (length.pt, length.mm, length.cm, length.inches) - table( - columns: 5, - "", - ..("pt", "mm", "cm", "in").map(e => raw("=?" + e)), - ..units - .map(((l, u)) => { - (raw("1" + u, lang: "typc"),) + methods.map(method => [#calc.round(method(l), digits: 2)]) - }) - .flatten(), - ) -} - -== 绝对长度的运算 - -长度单位可以参与任意多个浮点值的运算。一个长度表达式是合法的当且仅当运算结果*保持长度量纲*。请观察下列算式,它们都可以编译: - -#code(```typ -#(1cm * 3), #(1cm / 3), #(2 * 1cm * 3 / 2), #(1cm + 3in) -```) - -请观察下列算式,它们都不能编译: - -#( - ```typ - #(1cm + 3), #(3 / 1cm), #(1cm * 1cm) - ``` -) - -所谓*保持长度量纲*,即它存在一系列判别规则: - -- 由于`1cm`与`3in`量纲均为长度量纲(`m`),它们之间*可以*进行*加减*运算。 -- 由于`3`无量纲,`1cm`与`3`之间*不能*进行*加减*运算。 - -进一步,通过量纲运算,可以判断一个长度算术表达式是否合法: - -#{ - set align(center) - table( - columns: 4, - [长度表达式], - [量纲运算], - [检查合法性], - [判断结果], - ```typc 1cm * 3```, - $bold(sans(m dot 1 = m))$, - $bold(sans(m = m))$, - table.cell(rowspan: 2, align: horizon)[合法], - ```typc 1cm / 3```, - $bold(sans(m op(slash) 1 = m))$, - $bold(sans(m = m))$, - ```typc 3 / 1cm```, - $bold(sans(1 op(slash) m = m^(-1)))$, - $bold(sans(m^(-1) = m))$, - table.cell(rowspan: 2, align: horizon)[非法], - ```typc 1cm * 1cm```, - $bold(sans(m dot m = m^2))$, - $bold(sans(m^2 = m))$, - ) -} - -== 绝对长度的转换 - -你可以使用`length`类型上的「方法」实现不同单位到浮点数的转换: - -#code(```typ -#1cm 是 #1cm.pt() 点 \ -#1cm 是 #1cm.inches() 英尺 -```) - -你可以使用乘法实现浮点数到长度上的转换,例如```typc 28.3465 * 1pt```: - -#code(```typ -#1cm 是 #(28.3465 * 1pt).cm() 厘米 -```) - -== 相对长度 - -有两种相对长度(Relative Length),一是百分比(Ratio),一是分数比(Fraction)。 - -=== 百分比 - -#let p(w, f, ..args) = box( - width: w, - height: 10pt, - fill: f, - ..args, -) -#let code = code.with(scope: (p: p)) - -当「百分比」用作长度时,其实际值取决于父容器宽度: - -#code(```typ -#let p(w, f, ..args) = box( - width: w, height: 10pt, fill: f, ..args) -4比6:#p(100pt, blue, p(40%, red)) -```) - -Typst还支持以「分数比」作长度单位。当分数比作长度单位时,Typst按比例分配长度。 - -#code(```typ -4比6:#p(100pt, blue, - p(4fr, red) + p(6fr, blue)) -```) - -结合代码与图例理解,`N fr`代表:在总的比例中,这个元素应当占有其中`N`份长度。 - -当同级元素既有分数比长度元素,又有其他长度单位元素时,优先将空间分配给其他长度单位元素。 - -#code(```typ -绿色先占60%: #p(100pt, blue, p(1fr, red) + p(2fr, blue) + p(60%, green)) \ -绿色先占110%: #p(100pt, blue, p(1fr, red) + p(1fr, blue) + p(110%, green)) \ -红色先占30pt: #p(100pt, blue, p(30pt, red) + p(1fr, blue) + p(110%, green)) -```) - -建议结合下文中grid布局关于长度的使用,加深对相对长度的理解。 - -== 上下文有关长度 - -目前Typst仅提供一种上下文有关长度,即当前上下文中的字体大小。历史上,定义该字体中大写字母`M`的宽度为`1em`,但是现代排版中,`1em`可以比`M`的宽度要更窄或者更宽。 - -上下文有关长度是与相对长度相区分的。区别是上下文有关长度的取值从「样式链」获取,而相对长度相对于父元素宽度。事实上`1em`的具体值可以通过上下文有关表达式获取: - -#code(```typ -#let _1em = context measure( - line(length: 1em)).width -#text(size: 10pt, [1em等于] + _1em) \ -#text(size: 20pt, [1em等于] + _1em) \ -```) - -相比较,`1em`更好用一点,因为`text.size`只允许在上下文有关表达式内部使用。 - -== 混合长度 - -以上所介绍的各种长度可以通过「加号」任意混合成单个长度的值,其长度的值为每个分量总和: - -#code(```typ -#(1pt + 1em + 100%) -```) - -== 长度的内省或评估 - -你可以通过`measure`获取一个长度在当前位置的具体值: - -#let length-of(l) = (measure(line(length: l)).width) -#let code = code.with(scope: (length-of: length-of)) - -#code(```typ -#let length-of(l) = measure( - line(length: l)).width -#context [长度等于#length-of(1em+1pt)。] -```) - -但是该方式是不被推荐的,因为一个长度值中的相对长度分量会被评估为`0pt`,从而导致计算失真: - -#code(```typ -长度等于 #context length-of(1em+1pt+100%)。 -```) - -这是因为在评估的时候,`measure`没有为内容锚定一个“父元素”。 - -一种更为鲁棒的方法是使用`layout`函数获取`layout`位置的宽度和高度信息: - -#code(```typ -长度等于#box(width: 1em+1pt+100%, - layout(l => l.width)) -```) - -但是使用`layout`会导致布局的多轮迭代,有可能*严重*降低编译性能。 diff --git a/src/basic/scripting-main.typ b/src/basic/scripting-main.typ new file mode 100644 index 0000000..74d5ea7 --- /dev/null +++ b/src/basic/scripting-main.typ @@ -0,0 +1,224 @@ +#import "mod.typ": * + +#show: book.page.with(title: "编译流程") + +经过几节稍显枯燥的脚本教程,我们继续回到排版本身。 + +在#(refs.writing-markup)[《初识标记模式》]中,我们学到了很多各式各样的内容。我们学到了段落、标题、代码片段...... + +接着我们又花费了三节的篇幅,讲授了各式各样的脚本技巧。我们学到了字面量、变量、闭包...... + +但是它们之间似乎隔有一层厚障壁,阻止了我们进行更高级的排版。是了,如果「内容」也是一种值,那么我们应该也可以更随心所欲地使用脚本操控它们。Typst以排版为核心,应当也对「内容类型」有着精心设计。 + +本节主要介绍如何使用脚本排版内容。这也是Typst的核心功能,并在语法上*与很多其他语言有着不同之处*。不用担心,在我们已经学了很多Typst语言的知识的基础上,本节也仅仅更进一步,教你如何真正以脚本视角看待一篇文档。 + +纵览Typst的编译流程,其大致分为4个阶段,解析、求值、排版和导出。 + +// todo: 介绍Typst的多种概念 + +// Source Code +// Value +// Type +// Content + +// todo: 简化下面的的图片 +#import "../figures.typ": figure-typst-arch +#align(center + horizon, figure-typst-arch()) + +// ,层层有缓存 + +为了方便排版,Typst首先使用了一个函数“解析和评估”你的代码。有趣地是,我们之前已经学过了这个函数。事实上,它就是#typst-func("eval")。 + +#code(```typ +#repr(eval("#[一段内容]", mode: "markup")) +```) + +流程图展现了编译阶段间的关系,也包含了本节「块」与「表达式」两个概念之间的关系。 + +- #typst-func("eval")输入:在文件解析阶段,*代码字符串*被解析成一个语法结构,即「表达式」。古人云,世界是一个巨大的表达式。作为世界的一部分,Typst文档本身也是一个巨大的表达式。事实上,它就是我们在上一章提及的「内容块」。文档的本身是一个内容块,其由一个个标记串联形成。 + +- #typst-func("eval")输出:在内容排版阶段,排版引擎事实不作任何计算。用TeX黑话来说,文档被“解析和评估”完了之后,就成为了一个个「材料」(material)。排版引擎将材料。 + +在求值阶段。「表达式」被计算成一个方便排版引擎操作的值,即「材料」。一般来说,我们所谓的表达式是诸如`1+1`的算式,而对其求值则是做算数。 + +#code(```typ +#eval("1+1") +```) + +显然,如果意图是让排版引擎输出计算结果,让排版引擎直接排版2要比排版“1+1”更简单。 + +但是对于整个文档,要如何理解对内容块的求值?这就引入了「可折叠」的值(Foldable)的概念。「可折叠」成为块作为表达式的基础。 + +== 「`eval`阶段」与「`typeset`阶段」 + +现在我们介绍Typst的完整架构。 + +当Typst接受到一个编译请求时,他会使用「解析器」(Parser)从`main`文件开始解析整个项目;对于每个文件,Typst使用「评估器」(Evaluator)执行脚本并得到「内容」;对于每个「内容」,Typst使用「排版引擎」(Typesetting Engine)计算布局与合成样式。 + +当一切布局与样式都计算好后,Typst将最终结果导出为各种格式的文件,例如PDF格式。 + +我们回忆上一节讲过的内容,Typst大致上分为四个执行阶段。这四个执行阶段并不完全相互独立,但有明显的先后顺序: + +#import "../figures.typ": figure-typst-arch +#align(center + horizon, figure-typst-arch()) + +我们在上一节着重讲解了前两个阶段。这里,我们着重讲解“表达式求值”阶段与“内容排版”阶段。 + +事实上,Typst直接在脚本中提供了对应“求值”阶段的函数,它就是我们之前已经介绍过的函数`eval`。你可以使用`eval`函数,将一个字符串对象「评估」为「内容」: + +#code(```typ +以代码模式评估:#eval("repr(str(1 + 1))") \ +以标记模式评估:#eval("repr(str(1 + 1))", mode: "markup") \ +以标记模式评估2:#eval("#show: it => [c] + it + [t];a", mode: "markup") +```) + +由于技术原因,Typst并不提供对应“内容排版”阶段的函数,如果有的话这个函数的名称应该为`typeset`。已经有很多地方介绍了潜在的`typeset`函数: ++ #link("https://github.com/andreasKroepelin/polylux")[Polylux], #link("https://github.com/touying-typ/touying")[Touying]等演示文档(PPT)框架需要将一部分内容固定为特定结果的能力。 ++ Typst的作者在其博客中提及#link("https://laurmaedje.github.io/posts/frozen-state/")[Frozen State + ]的可能性。 + + 他提及数学公式的编号在演示文档框架。 + + 即便不涉及用户需求,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`。 + +== 延迟执行 + +架构图中还有两个关键的反向箭头,疑问顿生:这两个反向箭头是如何产生的? + +我们首先关注与本节直接相关的「样式化」内容。当`eval`阶段结束时,「`show`」语法将会对应产生一个`styled`元素,其包含了被设置样式的内容,以及设置样式的「回调」: + +#code(```typ +内容是:#repr({show: set text(fill: blue); [abc]}) \ +样式无法描述,但它在这里:#repr({show: set text(fill: blue); [abc]}.styles) +```) + +也就是说`eval`并不具备任何排版能力,它只能为排版准备好各种“素材”,并把素材交给排版引擎完成排版。 + +这里的「回调」术语很关键:它是一个计算机术语。所谓「回调函数」就是一个临时的函数,它会在后续执行过程的合适时机“回过头来被调用”。例如,我们写了一个这样的「`show`」规则: + +#code(```typ +#repr({ + show raw: content => layout(parent => if parent.width > 100pt { + set text(fill: red); content + } else { + content + }) + `a` +}) +```) + +这里`parent.width > 100pt`是说当且仅当父元素的宽度大于`100pt`时,才为该代码片段设置红色字体样式。其中,`parent.width`与排版相关。那么,自然`eval`也不知道该如何评估该条件的真正结果。*计算因此被停滞*。 + +于是,`eval`干脆将整个`show`右侧的函数都作为“素材”交给了排版引擎。当排版引擎计算好了相关内容,才回到评估阶段,执行这一小部分“素材”函数中的脚本,得到为正确的内容。我们可以看出,`show`右侧的函数*被延后执行*可。 + +这种被延后执行零次、一次或多次的函数便被称为「回调函数」。相关的计算方法也有对应的术语,被称为「延迟执行」。 + +我们对每个术语咬文嚼字一番,它们都很准确: + +1. *「表达式求值」*阶段仅仅“评估”出*「内容排版」*阶段所需的素材.*「评估器」*并不具备排版能力。 +2. 对于依赖排版产生的内容,「表达式求值」产生包含*「回调函数」*的内容,让「排版引擎」在合适的时机“回过头来调用”。 +3. 相关的计算方法又被称为*「延迟执行」*。因为现在不具备执行条件,所以延迟到条件满足时才继续执行。 + +现在我们可以理解两个反向箭头是如何产生的了。它们是下一阶段的回调,用于完成阶段之间复杂的协作。评估阶段可能会`import`或`include`文件,这时候会重新让解析器解析文件的字符串内容。排版阶段也可能会继续根据`styled`等元素产生复杂的内容,这时候依靠评估器执行脚本并产生或改变内容。 + +== 模拟Typst的执行 + +我们来模拟一遍上述示例的执行,以加深理解: + +#code(```typ +#show raw: content => layout(parent => if parent.width < 100pt { + set text(fill: red); content +} else { + content +}) +#box(width: 50pt, `a`) +`b` +```) + +首先进行表达式求值得到: + +```typ +#styled((box(width: 50pt, `a`), `b`), styles: content => ..) +``` + +排版引擎遇到``` `a` ```。由于``` `a` ```是`raw`元素,它「回调」了对应`show`规则右侧的函数。待执行的代码如下: + +```typc +layout(parent => if parent.width < 100pt { + set text(fill: red); `a` +} else { + `a` +}) +``` + +此时`parent`即为`box(width: 50pt)`。排版引擎将这个`parent`的具体内容交给「评估器」,待执行的代码如下: + +```typc +if box(width: 50pt).width < 100pt { + set text(fill: red); `a` +} else { + `a` +} +``` + +由于此时父元素(`box`元素)宽度只有`50pt`,评估器进入了`then`分支,其为代码片段设置了红色样式。内容变为: + +```typ +#(box(width: 50pt, {set text(fill: red); `a`}), styled((`b`, ), styles: content => ..)) +``` + +待执行的代码如下: + +```typc +set text(fill: red); text("a", font: "monospace") +``` + +排版引擎遇到``` `a` ```中的`text`元素。由于其是`text`元素,「回调」了`text`元素的「`show`」规则。记得我们之前说过`set`是一种特殊的`show`,于是排版器执行了`set text(fill: red)`。 + +```typ +#(box(width: 50pt, text(fill: red, "a", ..)), styled((`b`, ), styles: content => ..)) +``` + +排版引擎离开了`show`规则右侧的函数,该函数调用由``` `a` ```触发。同时`set text(fill: red)`规则也被解除,因为离开了相关作用域。 + +回到文档顶层,待执行的代码如下: + +```typc +#show raw: ... +`b` +``` + +排版引擎遇到``` `b` ```,再度「回调」了对应`show`规则右侧的函数。由于此时父元素(`page`元素,即整个页面)宽度有`500pt`,我们没有为代码片段设置样式。 + +```typ +#(box(width: 50pt, text(fill: red, "a", ..)), text("b", ..)) +``` + +至此,文档的内容已经准备好「导出」(Export)了。 + +#pro-tip[ + 有时候`show`规则会原地执行,这属于一种细节上的优化,例如: + + #code(```typ + #repr({ show: it => it; [a] }) \ + #repr({ show: it => [c] + it + [d]; [a] }) + ```) + + 这个时候`show`规则不会对应一个`styled`元素。 + + 这种优化告诉你前面手动描述的过程仅作理解。一旦涉及更复杂的环境,Typst的实际执行过程就会产生诸多变化。因此,你不应该依赖以上某步中排版引擎的瞬间状态。这些瞬间状态将产生「未注明特性」(undocumented details),并随时有可能在未来被打破。 +] diff --git a/src/basic/scripting-style.typ b/src/basic/scripting-style.typ index bc4137d..8d68ada 100644 --- a/src/basic/scripting-style.typ +++ b/src/basic/scripting-style.typ @@ -1,349 +1,66 @@ #import "mod.typ": * -#show: book.page.with(title: "样式") +#show: book.page.with(title: "选择器与样式") -经过几节稍显枯燥的脚本教程,我们继续回到排版本身。 +== 「样式化」内容 -在#(refs.writing-markup)[《初识标记模式》]中,我们学到了很多各式各样的内容。我们学到了段落、标题、代码片段...... +当我们有一个`repr`玩具的时候,总想着对着各种各样的对象使用`repr`。我们在上一节讲解了「`set`」和「`show`」语法。现在让我们稍微深挖一些。 -接着我们又花费了三节的篇幅,讲授了各式各样的脚本技巧。我们学到了字面量、变量、闭包...... - -但是它们之间似乎隔有一层厚障壁,阻止了我们进行更高级的排版。是了,如果「内容」也是一种值,那么我们应该也可以更随心所欲地使用脚本操控它们。Typst以排版为核心,应当也对「内容类型」有着精心设计。 - -本节主要介绍如何使用脚本排版内容。这也是Typst的核心功能,并在语法上*与很多其他语言有着不同之处*。不用担心,在我们已经学了很多Typst语言的知识的基础上,本节也仅仅更进一步,教你如何真正以脚本视角看待一篇文档。 - -纵览Typst的编译流程,其大致分为4个阶段,解析、求值、排版和导出。 - -// todo: 介绍Typst的多种概念 - -// Source Code -// Value -// Type -// Content - -// todo: 简化下面的的图片 -#import "../figures.typ": figure-typst-arch -#align(center + horizon, figure-typst-arch()) - -// ,层层有缓存 - -为了方便排版,Typst首先使用了一个函数“解析和评估”你的代码。有趣地是,我们之前已经学过了这个函数。事实上,它就是#typst-func("eval")。 - -#code(```typ -#repr(eval("#[一段内容]", mode: "markup")) -```) - -流程图展现了编译阶段间的关系,也包含了本节「块」与「表达式」两个概念之间的关系。 - -- #typst-func("eval")输入:在文件解析阶段,*代码字符串*被解析成一个语法结构,即「表达式」。古人云,世界是一个巨大的表达式。作为世界的一部分,Typst文档本身也是一个巨大的表达式。事实上,它就是我们在上一章提及的「内容块」。文档的本身是一个内容块,其由一个个标记串联形成。 - -- #typst-func("eval")输出:在内容排版阶段,排版引擎事实不作任何计算。用TeX黑话来说,文档被“解析和评估”完了之后,就成为了一个个「材料」(material)。排版引擎将材料。 - -在求值阶段。「表达式」被计算成一个方便排版引擎操作的值,即「材料」。一般来说,我们所谓的表达式是诸如`1+1`的算式,而对其求值则是做算数。 - -#code(```typ -#eval("1+1") -```) - -显然,如果意图是让排版引擎输出计算结果,让排版引擎直接排版2要比排版“1+1”更简单。 - -但是对于整个文档,要如何理解对内容块的求值?这就引入了「可折叠」的值(Foldable)的概念。「可折叠」成为块作为表达式的基础。 - -== 「可折叠」的值(Foldable) - -先来看代码块。代码块其实就是一个脚本。既然是脚本,Typst就可以按照语句顺序依次执行「语句」。 - -#pro-tip[ - 准确地来说,按照控制流顺序。 -] - -Typst按控制流顺序执行代码,将所有结果*折叠*成一个值。所谓折叠,就是将所有数值“连接”在一起。这样讲还是太抽象了,来看一些具体的例子。 - -=== 字符串折叠 - -Typst实际上不限制代码块的每个语句将会产生什么结果,只要是结果之间可以*折叠*即可。 - -我们说字符串是可以折叠的: - -#code(```typ -#{"Hello"; " "; "World"} -```) - -实际上折叠操作基本就是#mark("+")操作。那么字符串的折叠就是在做字符串连接操作: +「`set`」是什么,`repr`一下: #code(```typ -#("Hello" + " " + "World") +#repr({ + [a]; set text(fill: blue); [b] +}) ```) -再看一个例子: +「`show`」是什么,`repr`一下: #code(```typ -#{ - let hello = "Hello"; - let space = " "; - let world = "World"; - hello; space; world; - let destroy = ", Destroy" - destroy; space; world; "." -} +#repr({ + [b]; show raw: set text(fill: red) + [a] +}) ```) -如何理解将「变量声明」与表达式混写? - -回忆前文。对了,「变量声明」表达式的结果为```typc none```。 -#code(```typ -#type(let hello = "Hello") -```) +我们知道`set text(fill: blue)`是`show: set text(fill: blue)`的简写,因此「`set`」语法和「`show`」语法都可以统合到第二个例子来理解。 -并且还有一个重点是,字符串与`none`相加是字符串本身,`none`加`none`还是`none`: +对于第二个例子,我们发现`show`语句之后的内容都被重新包裹在`styled`元素中。虽然我们不知道`styled`做了什么事情,但是简单的事实是: #code(```typ -#("Hello" + none), #(none + "Hello"), #repr(none + none) -```) - -现在可以重新体会这句话了:Typst按控制流顺序执行代码,将所有结果*折叠*成一个值。对于上例,每句话的执行结果分别是: - -```typc -#{ - none; // let hello = "Hello"; - none; // let space = " "; - none; // let world = "World"; - "Hello"; " "; "World"; // hello; space; world; - none; // let destroy = ", Destroy" - ", Destroy"; " "; "World"; "." // destroy; space; world; "." -} -``` - -将结果收集并“折叠”,得到结果: - -#code(```typc -#(none + none + none + "Hello" + " " + "World" + none + ", Destroy" + " " + "World" + ".") +该元素的类型是:#type({show: set text(fill: blue)}) \ +该元素的构造函数是:#({show: set text(fill: blue)}).func() ```) -#pro-tip[ - 还有其他可以折叠的值,例如,数组与字典也是可以折叠的: - - #code(```typ - #for i in range(1, 5) { (i, i * 10) } - ```) - - #code(```typ - #for i in range(1, 5) { let d = (:); d.insert(str(i), i * 10); d } - ```) -] - -=== 其他基本类型的情况 - -那么为什么说折叠操作基本就是#mark("+")操作。那么就是说有的“#mark("+")操作”并非是折叠操作。 +原来,你也是内容。从图中,我们可以看到被`show`过的内容会被封装成「样式化」内容,即图中构造函数为`styled`的内容。 -布尔值、整数和浮点数都不能相互折叠: +关于`styled`的知识便涉及到Typst的核心架构。 -```typ -// 不能编译 -#{ false; true }; #{ 1; 2 }; #{ 1.; 2. } -``` - -那么是否说布尔值、整数和浮点数都不能折叠呢。答案又是否认的,它们都可以与```typc none```折叠(把下面的加号看成折叠操作): - -#code(```typ -#(1 + none) -```) +// == 「可定位」的内容 -所以你可以保证一个代码块中只有一个「语句」产生布尔值、整数或浮点数结果,这样的代码块就又是能编译的了。让我们利用`let _ = `来实现这一点: +// 在过去的章节中,我们了解了评估结果的具体结构,也大致了解了排版引擎的工作方式。 -#code(```typ -#{ let _ = 1; true }, -#{ let _ = false; 2. } -```) +// 接下来,我们介绍一类内容的「可定位」(Locatable)特征。你可以与前文中的「可折叠」(Foldable)特征对照理解。 -回忆之前所讲的特殊规则:#term("placeholder")用作标识符的作用是“忽略不必要的语句结果”。 +// 一个内容是可定位的,如果它可以以某种方式被索引得到。 -=== 内容折叠 - -Typst脚本的核心重点就在本段。 - -内容也可以作为代码块的语句结果,这时候内容块的结果是每个语句内容的“折叠”。 - -#code(```typ -#{ - [= 生活在Content树上] - [现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。] - [滥觞于家庭与社会传统的期望正失去它们的借鉴意义。] - [但面对看似无垠的未来天空,我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。] -} -```) - -是不是感觉很熟悉?实际上内容块就是上述代码块的“糖”。所谓糖就是同一事物更方便书写的语法。上述代码块与下述内容块等价: - -```typ -#[ - = 生活在Content树上 - 现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。滥觞于家庭与社会传统的期望正失去它们的借鉴意义。但面对看似无垠的未来天空,我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。 -] -``` - -由于Typst默认以「标记模式」开始解释你的文档,这又与省略`#[]`的写法等价: - -```typ -= 生活在Content树上 -现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。滥觞于家庭与社会传统的期望正失去它们的借鉴意义。但面对看似无垠的未来天空,我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。 -``` - -#pro-tip[ - 实际上有区别,由于多两个换行和缩进,前后各多一个Space Element。 -] - -// == Hello World程序 - -// 有的时候,我们想要访问字面量、变量与函数中存储的“信息”。例如,给定一个字符串```typc "Hello World"```,我们想要截取其中的第二个单词。 - -// 单词`World`就在那里,但仅凭我们有限的脚本知识,却没有方法得到它。这是因为字符串本身是一个整体,虽然它具备单词信息,我们却缺乏了*访问*信息的方法。 - -// Typst为我们提供了「成员」和「方法」两种概念访问这些信息。使用「方法」,可以使用以下脚本完成目标: - -// #code(```typ -// #"Hello World".split(" ").at(1) -// ```) - -// 为了方便讲解,我们改写出6行脚本。除了第二行,每一行都输出一段内容: - -// #code(```typ -// #let x = "Hello World"; #x \ -// #let split = str.split -// #split(x, " ") \ -// #str.split(x, " ") \ -// #x.split(" ") \ -// #x.split(" ").at(1) -// ```) - -// 从```typ #x.split(" ").at(1)```的输出可以看出,这一行帮助我们实现了“截取其中的第二个单词”的目标。我们虽然隐隐约约能揣测出其中的意思: +// 如果一个内容在代码块中,并未被使用,那么显然这种内容是不可定位的。 // ```typ -// #( x .split(" ") .at(1) ) -// // 将字符串 根据字符串拆分 取出其中的第2个单词(字符串) +// #{ let unused-content = [一段不可定位的内容]; } // ``` -// 但至少我们对#mark(".")仍是一无所知。 +// 理论上文档中所有内容都是可定位的,但由于*性能限制*,Typst无法允许你定位文档中的所有内容。 -// 本节我们就来讲解Typst中较为高级的脚本语法。这些脚本语法与大部分编程语言的语法相同,但是我们假设你并不知道这些语法。 +// 我们已经学习过元素函数可以用来定位内容。如下: -== 内容类型 - -我们已经学过很多元素:段落、标题、代码片段等。这些元素在被创建后都会被包装成为一种被称为「内容」的值。这些值所具有的类型便被称为「内容类型」。同时「内容类型」提供了一组公共方法访问元素本身。 - -乍一听,内容就像是一个“容器”将元素包裹。但内容又不太像是之前所学过的数组或字典那样的复合字面量,或者说这样不方便理解。事实上,每个元素都有各自的特点,但仅仅为了保持动态性,所有的元素都被硬凑在一起,共享一种类型。有两种理解这种类型的视角:从表象论,「内容类型」是一种鸭子类型;从原理论,「内容类型」提供了操控内容的公共方法,即它是一种接口,或称特征(Trait)。 - -=== 特性一:元素包装于「内容」 - -我们知道所有的元素语法都可以等价使用相应的函数构造。例如标题: - -#code(```typ -#repr([= 123]) \ // 语法构造 -#repr(heading(depth: 1)[123]) // 函数构造 - -```) - -一个常见的误区是误认为元素继承自「内容类型」,进而使用以下方法判断一个内容是否为标题元素: - -#code(```typ -标题是heading类型(伪)?#(type([= 123]) == heading) -```) - -但两者类型并不一样。事实上,元素是「函数类型」,元素函数的返回值为「内容类型」。 - -#code(```typ -标题函数的类型:#(type(heading)) \ -标题的类型:#type([= 123]) -```) - -这引出了一个重要的理念,Typst中一切皆组合。Typst中目前没有继承概念,一切功能都是组合出来的,这类似于Rust语言的概念。你可能没有学过Rust语言,但这里有一个冷知识: - -#align(center, [Typst $<=>$ Typ(setting Ru)st $<=>$ Typesetting Rust]) - -即Typst是以Rust语言特性为基础设计出的一个排版(Typesetting)语言。 - -当各式各样的元素函数接受参数时,它们会构造出「元素」,然后将元素包装成一个共同的类型:「内容类型」。`heading`是函数而不是类型。与其他语言不同,没有一个`heading`类型继承`content`。因此不能使用`type([= 123]) == heading`判断一个内容是否为标题元素。 - -=== 特性二:内容类型的`func`方法 - -所有内容都允许使用`func`得到构造这个内容所使用的函数。因此,可以使用以下方法判断一个内容是否为标题元素: - -#code(```typ -标题所使用的构造函数:#([= 123]).func() - -标题的构造函数是`heading`?#(([= 123]).func() == heading) -```) - -// 这一段不要了 -// === 特性二点五:内容类型的`func`方法可以直接拿来用 - -// `func`方法返回的就是函数本身,自然也可以拿来使用: - -// #code(```typ -// 重新构造标题:#(([= 123]).func())([456]) -// ```) +// #code(````typ +// #show heading: set text(fill: blue) +// = 蓝色标题 +// 段落中的内容保持为原色。 +// ````) -// 这一般没什么用,但是有的时候可以用于得到一些Typst没有暴露出来的内容函数,例如`styled`。 - -// #code(```typ -// #let type_styled = text(fill: red, "").func() -// #let st = text(fill: blue, "").styles -// #text([abc], st) -// ```) - -=== 特性三:内容类型的`fields`方法 - -Typst中一切皆组合,它将所有内容打包成「内容类型」的值以完成类型上的统一,而非类型继承。 - -但是这也有坏处,坏处是无法“透明”访问内部内容。例如,我们可能希望知道`heading`的级别。如果不提供任何方法访问标题的级别,那么我们就无法编程完成与之相关的排版。 - -为了解决这个问题,Typst提供一个`fields`方法提供一个content的部分信息: - -#code(```typ -#([= 123]).fields() -```) - -`fields()`将部分信息组成字典并返回。如上图所示,我们可以通过这个字典对象进一步访问标题的内容和级别。 - -#code(```typ -#([= 123]).fields().at("depth") -```) - -#pro-tip[ - 这里的“部分信息”描述稍显模糊。具体来说,Typst只允许你直接访问元素中不受样式影响的信息,至少包含语法属性,而不允许你*直接*访问元素的样式。 - - // 如下: - - // #code.with(al: top)(````typ - // #let x = [= 123] - // #rect([#x ]) - // #x.fields() \ - // #locate(loc => query(, loc)) - // ````) -] - -=== 特性四:内容类型与`fields`相关的糖 - -由于我们经常需要与`fields`交互,Typst提供了`has`方法帮助我们判断一个内容的`fields`是否有相关的「键」。 - -#code(```typ -使用`... in x.fields()`判断:#("text" in `x`.fields()) \ -等同于使用`has`方法判断:#(`x`.has("text")) -```) - -Typst提供了`at`方法帮助我们访问一个内容的`fields`中键对应的值。 - -#code(```typ -使用`x.fields().at()`获取值:#(`www`.fields().at("text")) \ -等同于使用`at`方法:#(`www`.at("text")) -```) - -特别地,内容的成员包含`fields`的键,我们可以直接通过成员访问相关信息: - -#code(```typ -使用`at`方法:#(`www`.at("text")) \ -等同于访问`text`成员:#(`www`.text) -```) +// 接下来我们继续学习更多选择器。 == 内容的「样式」 @@ -465,73 +182,6 @@ Typst对代码块有着的一系列语法设计,让代码块非常适合描述 这也便是我们在「内容类型」小节所述的鸭子类型特性。如果「内容」长得像文本(鸭子),那么它就是文本。 -== 「内容」是一棵树(Cont.) - -#pro-tip[ - 利用「内容」与「树」的特性,我们可以在Typst中设计出更多优雅的脚本功能。 -] - -=== CeTZ的「树」 - -CeTZ利用内容树制作“内嵌的DSL”。CeTZ的`canvas`函数接收的不完全是内容,而是内容与其IR的混合。 - -例如它的`line`函数的返回值,就完全不是一个内容,而是一个无法窥视的函数。 - -#code(```typ -#import "@preview/cetz:0.3.4" -#repr(cetz.draw.line((0, 0), (1, 1), fill: blue)) -```) - -当你产生一个“混合”的内容并将其传递给`cetz.canvas`,CeTZ就会像`plain-text`一样遍历你的混合内容,并加以区分和处理。如果遇到了他自己特定的IR,例如`cetz.draw.line`,便将其以特殊的方式转换为真正的「内容」。 - -使用混合语言,在Typst中可以很优雅地画多面体: - -#code.with(al: top)(```typ -#import "@preview/cetz:0.3.4" -#align(center, cetz.canvas({ - // 导入cetz的draw方言 - import cetz.draw: *; import cetz.vector: add - let neg(u) = if u == 0 { 1 } else { -1 } - for (p, c) in ( - ((0, 0, 0), black), ((1, 1, 0), red), ((1, 0, 1), blue), ((0, 1, 1), green), - ) { - line(add(p, (0, 0, neg(p.at(2)))), p, stroke: c) - line(add(p, (0, neg(p.at(1)), 0)), p, stroke: c) - line(add(p, (neg(p.at(0)), 0, 0)), p, stroke: c) - } -})) -```) - -=== curryst的「树」 - -我们知道「内容块」与「代码块」没有什么本质区别。 - -如果我们可以基于「代码块」描述一棵「内容」的树,那么逻辑推理的过程也可以被描述为条件、规则、结论的树。 - -#link("https://typst.app/universe/package/curryst/")[curryst]包提供了接收条件、规则、结论参数的`rule`函数,其返回一个包含传入信息的`dict`,并且允许把`rule`函数返回的`dict`作为`rule`的部分参数。于是我们可以通过嵌套`rule`函数建立描述推理过程的树,并通过该包提供的`prooftree`函数把包含推理过程的`dict`树画出来: - -#code(```typ -#import "@preview/curryst:0.5.0": rule, prooftree -#let tree-dict = rule( - name: $R$, - $C_1 or C_2 or C_3$, - rule( - name: $A$, - $C_1 or C_2 or L$, - rule( - $C_1 or L$, - $Pi_1$, - ), - ), - rule( - $C_2 or overline(L)$, - $Pi_2$, - ), -) -`tree-dict`的类型:#type(tree-dict) \ -`tree-dict`代表的树:#prooftree(tree-dict) -```) - == 「`show`」语法 「`set`」语法是「`show set`」语法的简写。因此,「`show`」语法显然可以比`set`更强大。 @@ -677,6 +327,139 @@ set text(fill: true) // `include`的文件是一个「内容块」,自带一个作用域。 +== 文本选择器 + +你可以使用「字符串」或「正则表达式」(`regex`)匹配文本中的特定内容,例如为`c++`文本特别设置样式: + +#code(````typ +#show "cpp": strong(emph(box("C++"))) +在古代,cpp是一门常用语言。 +````) + +这与使用正则表达式的效果相同: + +#code(````typ +#show regex("cp{2}"): strong(emph(box("C++"))) +在古代,cpp是一门常用语言。 +````) + +关于正则表达式的知识,推荐在#link("https://regex101.com")[Regex 101]中继续学习。 + +这里讲述一个关于`regex`选择器的重要知识。当文本被元素选中时,会创建一个不可见的分界,导致分界之间无法继续被正则匹配: + +#code(````typ +#show "ab": set text(fill: blue) +#show "a": set text(fill: red) +ababababababa +````) + +因为`"a"`规则比`"ab"`规则更早应用,每个`a`都被单独分隔,所以`"ab"`规则无法匹配到任何本文。 + +#code(````typ +#show "a": set text(fill: red) +#show "ab": set text(fill: blue) +ababababababa +````) + +虽然每个`ab`都被单独分隔,但是`"a"`规则可以继续在分界内继续匹配文本。 + +这个特征在设置文本的字体时需要特别注意: + +为引号单独设置字体会导致错误的排版结果。因为句号与双引号之间产生了分界,使得Typst无法应用标点挤压规则: + +#code(````typ +#show "”": it => { + set text(font: "KaiTi") + highlight(it, fill: yellow) +} +“无名,万物之始也;有名,万物之母也。” +````) + +以下正则匹配也会导致句号与双引号之间产生分界,因为没有对两个标点进行贪婪匹配: + +#code(````typ +#show regex("[”。]"): it => { + set text(font: "KaiTi") + highlight(it, fill: yellow) +} +“无名,万物之始也;有名,万物之母也。” +````) + +以下正则匹配没有在句号与双引号之间创建分界。考虑两个标点的字体设置规则,Typst能排版出这句话的正确结果: + +#code(````typ +#show regex("[”。]+"): it => { + set text(font: "KaiTi") + highlight(it, fill: yellow) +} +“无名,万物之始也;有名,万物之母也。” +````) + +== 标签选择器 + +基本上,任何元素都包含文本。这使得你很难对一段话针对性排版应用排版规则。「标签」有助于改善这一点。标签是「内容」,由一对「尖括号」(`<`和`>`)包裹: + +#code(````typ +一句话 +````) + +「标签」可以选中恰好在它*之前*的一个内容。示例中,``选中了文本内容`一句话`。 + +也就是说,「标签」无法选中在它*之前*的多个内容。以下选择器选中了`#[]`后的一句话: + +#code(````typ +#show <一句话>: set text(fill: blue) +#[一句话。]还是一句话。 <一句话> + +另一句话。 +````) + +这是因为`#[一句话。]`被分隔为了单独的内容。 + +我们很难判断一段话中有多少个内容。因此为了可控性,我们可以使用内容块将一段话括起来,然后使用标签准确选中这一整段话: + +#code(````typ +#show <一整段话>: set text(fill: blue) +#[ + $lambda$语言是世界上最好的语言。#[]还是一句话。 +] <一整段话> + +另一段话。 +````) + +== 选择器表达式 + +任意「内容」可以使用「`where`」方法创建选中满足条件的选择器。 + +例如我们可以选中二级标题: + +#code(````typ +#show heading.where(level: 2): set text(fill: blue) += 一级标题 +== 二级标题 +````) + +这里`heading`是一个元素,`heading.where`创建一个选择器: + +#code(````typ +选择器是:#repr(heading.where(level: 2)) \ +类型是:#type(heading.where(level: 2)) +````) + +同理我们可以选中行内的代码片段而不选中代码块: + +#code(````typ +#show raw.where(block:false): set text(fill: blue) +`php`是世界上最好的语言。 +``` +typst也是。 +``` +````) + +// == 「`numbering`」函数 + +// 略 + == 总结 本节仅以文本、代码块和内容块为例讲清楚了文件、作用域、「set」语法和「show」语法。为了拓展广度,你还需要查看《基本参考》中各种元素的用法,这样才能随心所欲排版任何「内容」。 diff --git a/src/book.typ b/src/book.typ index fdf268a..61417c7 100644 --- a/src/book.typ +++ b/src/book.typ @@ -23,17 +23,18 @@ - #chapter("basic/scripting-variable.typ")[变量与函数] - #chapter("basic/scripting-composite.typ")[复合类型] = 基础教程 — 排版Ⅱ - - #chapter("basic/scripting-style.typ")[样式] - - #chapter("basic/scripting-color.typ")[色彩] - - #chapter("basic/scripting-shape.typ")[图表] - - #chapter("basic/scripting-length.typ")[度量] - - #chapter("basic/scripting-layout.typ")[布局] + - #chapter("basic/scripting-main.typ")[编译流程] + - #chapter("basic/scripting-layout.typ")[长度与布局] + - #chapter("basic/scripting-content.typ")[文档树] + - #chapter("basic/scripting-color.typ")[颜色类型] + - #chapter("basic/scripting-style.typ")[选择器与样式] = 基础教程 — 脚本Ⅱ - #chapter("basic/scripting-block-and-expression.typ")[表达式] - #chapter("basic/scripting-control-flow.typ")[控制流] - - #chapter("intermediate/doc-modulize.typ")[模块化] + - #chapter("intermediate/doc-modulize.typ")[模块化(多文件)] - #chapter("intermediate/doc-stateful.typ")[状态化] = 基础教程 — 排版Ⅲ + - #chapter("basic/scripting-shape.typ")[图表] - #chapter("intermediate/writing-chinese.typ")[中文排版] - #chapter("intermediate/writing-math.typ")[数学排版] = 基础教程 — 附录Ⅰ diff --git a/src/intermediate/content-stateful-agg.typ b/src/intermediate/content-stateful-agg.typ deleted file mode 100644 index 93df718..0000000 --- a/src/intermediate/content-stateful-agg.typ +++ /dev/null @@ -1,5 +0,0 @@ - - -#include "content-stateful.typ" -#include "content-stateful-2.typ" -#include "content-stateful-3.typ" diff --git a/src/intermediate/doc-stateful.typ b/src/intermediate/doc-stateful.typ index 92ac0a2..7313043 100644 --- a/src/intermediate/doc-stateful.typ +++ b/src/intermediate/doc-stateful.typ @@ -1,6 +1,6 @@ #import "mod.typ": * -#import "/typ/embedded-typst/lib.typ": svg-doc, default-fonts, default-cjk-fonts +#import "/typ/embedded-typst/lib.typ": default-cjk-fonts, default-fonts, svg-doc #show: book.page.with(title: [状态化]) @@ -30,403 +30,36 @@ 本节教你使用选择器(selector)定位到文档的任意部分;也教你创建与查询二维文档状态(state)。 -== 自定义标题样式 +// == 自定义标题样式 -本节讲解的程序是如何在Typst中设置标题样式。我们的目标是: -+ 为每级标题单独设置样式。 -+ 设置标题为内容的页眉: - + 如果当前页眉有二级标题,则是当前页面的第一个二级标题。 - + 否则是之前所有页面的最后一个二级标题。 +// 本节讲解的程序是如何在Typst中设置标题样式。我们的目标是: +// + 为每级标题单独设置样式。 +// + 设置标题为内容的页眉: +// + 如果当前页眉有二级标题,则是当前页面的第一个二级标题。 +// + 否则是之前所有页面的最后一个二级标题。 -效果如下: +// 效果如下: -#frames-cjk( - read("./stateful/s1.typ"), - code-as: ```typ - #show: set-heading - - == 雨滴书v0.1.2 - === KiraKira 样式改进 - feat: 改进了样式。 - === FuwaFuwa 脚本改进 - feat: 改进了脚本。 - - == 雨滴书v0.1.1 - refactor: 移除了LaTeX。 - - feat: 删除了一个多余的文件夹。 - - == 雨滴书v0.1.0 - feat: 新建了两个文件夹。 - ```, -) - -== 「样式化」内容 - -当我们有一个`repr`玩具的时候,总想着对着各种各样的对象使用`repr`。我们在上一节讲解了「`set`」和「`show`」语法。现在让我们稍微深挖一些。 - -「`set`」是什么,`repr`一下: - -#code(```typ -#repr({ - [a]; set text(fill: blue); [b] -}) -```) - -「`show`」是什么,`repr`一下: - -#code(```typ -#repr({ - [b]; show raw: set text(fill: red) - [a] -}) -```) - -我们知道`set text(fill: blue)`是`show: set text(fill: blue)`的简写,因此「`set`」语法和「`show`」语法都可以统合到第二个例子来理解。 - -对于第二个例子,我们发现`show`语句之后的内容都被重新包裹在`styled`元素中。虽然我们不知道`styled`做了什么事情,但是简单的事实是: - -#code(```typ -该元素的类型是:#type({show: set text(fill: blue)}) \ -该元素的构造函数是:#({show: set text(fill: blue)}).func() -```) - -原来,你也是内容。从图中,我们可以看到被`show`过的内容会被封装成「样式化」内容,即图中构造函数为`styled`的内容。 - -关于`styled`的知识便涉及到Typst的核心架构。 - -== 「`eval`阶段」与「`typeset`阶段」 - -现在我们介绍Typst的完整架构。 - -当Typst接受到一个编译请求时,他会使用「解析器」(Parser)从`main`文件开始解析整个项目;对于每个文件,Typst使用「评估器」(Evaluator)执行脚本并得到「内容」;对于每个「内容」,Typst使用「排版引擎」(Typesetting Engine)计算布局与合成样式。 - -当一切布局与样式都计算好后,Typst将最终结果导出为各种格式的文件,例如PDF格式。 - -我们回忆上一节讲过的内容,Typst大致上分为四个执行阶段。这四个执行阶段并不完全相互独立,但有明显的先后顺序: - -#import "../figures.typ": figure-typst-arch -#align(center + horizon, figure-typst-arch()) - -我们在上一节着重讲解了前两个阶段。这里,我们着重讲解“表达式求值”阶段与“内容排版”阶段。 - -事实上,Typst直接在脚本中提供了对应“求值”阶段的函数,它就是我们之前已经介绍过的函数`eval`。你可以使用`eval`函数,将一个字符串对象「评估」为「内容」: - -#code(```typ -以代码模式评估:#eval("repr(str(1 + 1))") \ -以标记模式评估:#eval("repr(str(1 + 1))", mode: "markup") \ -以标记模式评估2:#eval("#show: it => [c] + it + [t];a", mode: "markup") -```) - -由于技术原因,Typst并不提供对应“内容排版”阶段的函数,如果有的话这个函数的名称应该为`typeset`。已经有很多地方介绍了潜在的`typeset`函数: -+ #link("https://github.com/andreasKroepelin/polylux")[Polylux], #link("https://github.com/touying-typ/touying")[Touying]等演示文档(PPT)框架需要将一部分内容固定为特定结果的能力。 -+ Typst的作者在其博客中提及#link("https://laurmaedje.github.io/posts/frozen-state/")[Frozen State - ]的可能性。 - + 他提及数学公式的编号在演示文档框架。 - + 即便不涉及用户需求,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`。 - -== 延迟执行 - -架构图中还有两个关键的反向箭头,疑问顿生:这两个反向箭头是如何产生的? - -我们首先关注与本节直接相关的「样式化」内容。当`eval`阶段结束时,「`show`」语法将会对应产生一个`styled`元素,其包含了被设置样式的内容,以及设置样式的「回调」: - -#code(```typ -内容是:#repr({show: set text(fill: blue); [abc]}) \ -样式无法描述,但它在这里:#repr({show: set text(fill: blue); [abc]}.styles) -```) - -也就是说`eval`并不具备任何排版能力,它只能为排版准备好各种“素材”,并把素材交给排版引擎完成排版。 - -这里的「回调」术语很关键:它是一个计算机术语。所谓「回调函数」就是一个临时的函数,它会在后续执行过程的合适时机“回过头来被调用”。例如,我们写了一个这样的「`show`」规则: - -#code(```typ -#repr({ - show raw: content => layout(parent => if parent.width > 100pt { - set text(fill: red); content - } else { - content - }) - `a` -}) -```) - -这里`parent.width > 100pt`是说当且仅当父元素的宽度大于`100pt`时,才为该代码片段设置红色字体样式。其中,`parent.width`与排版相关。那么,自然`eval`也不知道该如何评估该条件的真正结果。*计算因此被停滞*。 - -于是,`eval`干脆将整个`show`右侧的函数都作为“素材”交给了排版引擎。当排版引擎计算好了相关内容,才回到评估阶段,执行这一小部分“素材”函数中的脚本,得到为正确的内容。我们可以看出,`show`右侧的函数*被延后执行*可。 - -这种被延后执行零次、一次或多次的函数便被称为「回调函数」。相关的计算方法也有对应的术语,被称为「延迟执行」。 - -我们对每个术语咬文嚼字一番,它们都很准确: - -1. *「表达式求值」*阶段仅仅“评估”出*「内容排版」*阶段所需的素材.*「评估器」*并不具备排版能力。 -2. 对于依赖排版产生的内容,「表达式求值」产生包含*「回调函数」*的内容,让「排版引擎」在合适的时机“回过头来调用”。 -3. 相关的计算方法又被称为*「延迟执行」*。因为现在不具备执行条件,所以延迟到条件满足时才继续执行。 - -现在我们可以理解两个反向箭头是如何产生的了。它们是下一阶段的回调,用于完成阶段之间复杂的协作。评估阶段可能会`import`或`include`文件,这时候会重新让解析器解析文件的字符串内容。排版阶段也可能会继续根据`styled`等元素产生复杂的内容,这时候依靠评估器执行脚本并产生或改变内容。 - -== 模拟Typst的执行 - -我们来模拟一遍上述示例的执行,以加深理解: - -#code(```typ -#show raw: content => layout(parent => if parent.width < 100pt { - set text(fill: red); content -} else { - content -}) -#box(width: 50pt, `a`) -`b` -```) - -首先进行表达式求值得到: - -```typ -#styled((box(width: 50pt, `a`), `b`), styles: content => ..) -``` - -排版引擎遇到``` `a` ```。由于``` `a` ```是`raw`元素,它「回调」了对应`show`规则右侧的函数。待执行的代码如下: - -```typc -layout(parent => if parent.width < 100pt { - set text(fill: red); `a` -} else { - `a` -}) -``` - -此时`parent`即为`box(width: 50pt)`。排版引擎将这个`parent`的具体内容交给「评估器」,待执行的代码如下: - -```typc -if box(width: 50pt).width < 100pt { - set text(fill: red); `a` -} else { - `a` -} -``` - -由于此时父元素(`box`元素)宽度只有`50pt`,评估器进入了`then`分支,其为代码片段设置了红色样式。内容变为: - -```typ -#(box(width: 50pt, {set text(fill: red); `a`}), styled((`b`, ), styles: content => ..)) -``` - -待执行的代码如下: - -```typc -set text(fill: red); text("a", font: "monospace") -``` - -排版引擎遇到``` `a` ```中的`text`元素。由于其是`text`元素,「回调」了`text`元素的「`show`」规则。记得我们之前说过`set`是一种特殊的`show`,于是排版器执行了`set text(fill: red)`。 - -```typ -#(box(width: 50pt, text(fill: red, "a", ..)), styled((`b`, ), styles: content => ..)) -``` - -排版引擎离开了`show`规则右侧的函数,该函数调用由``` `a` ```触发。同时`set text(fill: red)`规则也被解除,因为离开了相关作用域。 - -回到文档顶层,待执行的代码如下: - -```typc -#show raw: ... -`b` -``` - -排版引擎遇到``` `b` ```,再度「回调」了对应`show`规则右侧的函数。由于此时父元素(`page`元素,即整个页面)宽度有`500pt`,我们没有为代码片段设置样式。 - -```typ -#(box(width: 50pt, text(fill: red, "a", ..)), text("b", ..)) -``` - -至此,文档的内容已经准备好「导出」(Export)了。 - -== 「样式化」内容的补充 - -有时候`show`规则会原地执行,这属于一种细节上的优化,例如: - -#code(```typ -#repr({ show: it => it; [a] }) \ -#repr({ show: it => [c] + it + [d]; [a] }) -```) - -这个时候`show`规则不会对应一个`styled`元素。 - -这种优化告诉你前面手动描述的过程仅作理解。一旦涉及更复杂的环境,Typst的实际执行过程就会产生诸多变化。因此,你不应该依赖以上某步中排版引擎的瞬间状态。这些瞬间状态将产生「未注明特性」(undocumented details),并随时有可能在未来被打破。 - -== 「可定位」的内容 - -在过去的章节中,我们了解了评估结果的具体结构,也大致了解了排版引擎的工作方式。 - -接下来,我们介绍一类内容的「可定位」(Locatable)特征。你可以与前文中的「可折叠」(Foldable)特征对照理解。 - -一个内容是可定位的,如果它可以以某种方式被索引得到。 - -如果一个内容在代码块中,并未被使用,那么显然这种内容是不可定位的。 - -```typ -#{ let unused-content = [一段不可定位的内容]; } -``` - -理论上文档中所有内容都是可定位的,但由于*性能限制*,Typst无法允许你定位文档中的所有内容。 - -我们已经学习过元素函数可以用来定位内容。如下: +// #frames-cjk( +// read("./stateful/s1.typ"), +// code-as: ```typ +// #show: set-heading -#code(````typ -#show heading: set text(fill: blue) -= 蓝色标题 -段落中的内容保持为原色。 -````) +// == 雨滴书v0.1.2 +// === KiraKira 样式改进 +// feat: 改进了样式。 +// === FuwaFuwa 脚本改进 +// feat: 改进了脚本。 -接下来我们继续学习更多选择器。 - -== 文本选择器 - -你可以使用「字符串」或「正则表达式」(`regex`)匹配文本中的特定内容,例如为`c++`文本特别设置样式: - -#code(````typ -#show "cpp": strong(emph(box("C++"))) -在古代,cpp是一门常用语言。 -````) - -这与使用正则表达式的效果相同: - -#code(````typ -#show regex("cp{2}"): strong(emph(box("C++"))) -在古代,cpp是一门常用语言。 -````) - -关于正则表达式的知识,推荐在#link("https://regex101.com")[Regex 101]中继续学习。 - -这里讲述一个关于`regex`选择器的重要知识。当文本被元素选中时,会创建一个不可见的分界,导致分界之间无法继续被正则匹配: - -#code(````typ -#show "ab": set text(fill: blue) -#show "a": set text(fill: red) -ababababababa -````) - -因为`"a"`规则比`"ab"`规则更早应用,每个`a`都被单独分隔,所以`"ab"`规则无法匹配到任何本文。 - -#code(````typ -#show "a": set text(fill: red) -#show "ab": set text(fill: blue) -ababababababa -````) - -虽然每个`ab`都被单独分隔,但是`"a"`规则可以继续在分界内继续匹配文本。 - -这个特征在设置文本的字体时需要特别注意: - -为引号单独设置字体会导致错误的排版结果。因为句号与双引号之间产生了分界,使得Typst无法应用标点挤压规则: - -#code(````typ -#show "”": it => { - set text(font: "KaiTi") - highlight(it, fill: yellow) -} -“无名,万物之始也;有名,万物之母也。” -````) - -以下正则匹配也会导致句号与双引号之间产生分界,因为没有对两个标点进行贪婪匹配: - -#code(````typ -#show regex("[”。]"): it => { - set text(font: "KaiTi") - highlight(it, fill: yellow) -} -“无名,万物之始也;有名,万物之母也。” -````) - -以下正则匹配没有在句号与双引号之间创建分界。考虑两个标点的字体设置规则,Typst能排版出这句话的正确结果: - -#code(````typ -#show regex("[”。]+"): it => { - set text(font: "KaiTi") - highlight(it, fill: yellow) -} -“无名,万物之始也;有名,万物之母也。” -````) - -== 标签选择器 - -基本上,任何元素都包含文本。这使得你很难对一段话针对性排版应用排版规则。「标签」有助于改善这一点。标签是「内容」,由一对「尖括号」(`<`和`>`)包裹: - -#code(````typ -一句话 -````) - -「标签」可以选中恰好在它*之前*的一个内容。示例中,``选中了文本内容`一句话`。 - -也就是说,「标签」无法选中在它*之前*的多个内容。以下选择器选中了`#[]`后的一句话: - -#code(````typ -#show <一句话>: set text(fill: blue) -#[一句话。]还是一句话。 <一句话> - -另一句话。 -````) - -这是因为`#[一句话。]`被分隔为了单独的内容。 - -我们很难判断一段话中有多少个内容。因此为了可控性,我们可以使用内容块将一段话括起来,然后使用标签准确选中这一整段话: - -#code(````typ -#show <一整段话>: set text(fill: blue) -#[ - $lambda$语言是世界上最好的语言。#[]还是一句话。 -] <一整段话> - -另一段话。 -````) - -== 选择器表达式 - -任意「内容」可以使用「`where`」方法创建选中满足条件的选择器。 - -例如我们可以选中二级标题: - -#code(````typ -#show heading.where(level: 2): set text(fill: blue) -= 一级标题 -== 二级标题 -````) - -这里`heading`是一个元素,`heading.where`创建一个选择器: - -#code(````typ -选择器是:#repr(heading.where(level: 2)) \ -类型是:#type(heading.where(level: 2)) -````) - -同理我们可以选中行内的代码片段而不选中代码块: - -#code(````typ -#show raw.where(block:false): set text(fill: blue) -`php`是世界上最好的语言。 -``` -typst也是。 -``` -````) +// == 雨滴书v0.1.1 +// refactor: 移除了LaTeX。 -// == 「`numbering`」函数 +// feat: 删除了一个多余的文件夹。 -// 略 +// == 雨滴书v0.1.0 +// feat: 新建了两个文件夹。 +// ```, +// ) == 回顾其一