Skip to content

Commit 3aedeb8

Browse files
feat: 重新安排排版Ⅱ (#35)
1 parent cee4597 commit 3aedeb8

File tree

9 files changed

+1007
-1010
lines changed

9 files changed

+1007
-1010
lines changed

src/basic/scripting-color.typ

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#import "mod.typ": *
22

3-
#show: book.page.with(title: "色彩")
3+
#show: book.page.with(title: "颜色类型")
44

55
== 颜色类型
66

src/basic/scripting-content.typ

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
#import "mod.typ": *
2+
3+
#show: book.page.with(title: "文档树")
4+
5+
== 「可折叠」的值(Foldable)
6+
7+
先来看代码块。代码块其实就是一个脚本。既然是脚本,Typst就可以按照语句顺序依次执行「语句」。
8+
9+
#pro-tip[
10+
准确地来说,按照控制流顺序。
11+
]
12+
13+
Typst按控制流顺序执行代码,将所有结果*折叠*成一个值。所谓折叠,就是将所有数值“连接”在一起。这样讲还是太抽象了,来看一些具体的例子。
14+
15+
=== 字符串折叠
16+
17+
Typst实际上不限制代码块的每个语句将会产生什么结果,只要是结果之间可以*折叠*即可。
18+
19+
我们说字符串是可以折叠的:
20+
21+
#code(```typ
22+
#{"Hello"; " "; "World"}
23+
```)
24+
25+
实际上折叠操作基本就是#mark("+")操作。那么字符串的折叠就是在做字符串连接操作:
26+
27+
#code(```typ
28+
#("Hello" + " " + "World")
29+
```)
30+
31+
再看一个例子:
32+
33+
#code(```typ
34+
#{
35+
let hello = "Hello";
36+
let space = " ";
37+
let world = "World";
38+
hello; space; world;
39+
let destroy = ", Destroy"
40+
destroy; space; world; "."
41+
}
42+
```)
43+
44+
如何理解将「变量声明」与表达式混写?
45+
46+
回忆前文。对了,「变量声明」表达式的结果为```typc none```
47+
#code(```typ
48+
#type(let hello = "Hello")
49+
```)
50+
51+
并且还有一个重点是,字符串与`none`相加是字符串本身,`none``none`还是`none`
52+
53+
#code(```typ
54+
#("Hello" + none), #(none + "Hello"), #repr(none + none)
55+
```)
56+
57+
现在可以重新体会这句话了:Typst按控制流顺序执行代码,将所有结果*折叠*成一个值。对于上例,每句话的执行结果分别是:
58+
59+
```typc
60+
#{
61+
none; // let hello = "Hello";
62+
none; // let space = " ";
63+
none; // let world = "World";
64+
"Hello"; " "; "World"; // hello; space; world;
65+
none; // let destroy = ", Destroy"
66+
", Destroy"; " "; "World"; "." // destroy; space; world; "."
67+
}
68+
```
69+
70+
将结果收集并“折叠”,得到结果:
71+
72+
#code(```typc
73+
#(none + none + none + "Hello" + " " + "World" + none + ", Destroy" + " " + "World" + ".")
74+
```)
75+
76+
#pro-tip[
77+
还有其他可以折叠的值,例如,数组与字典也是可以折叠的:
78+
79+
#code(```typ
80+
#for i in range(1, 5) { (i, i * 10) }
81+
```)
82+
83+
#code(```typ
84+
#for i in range(1, 5) { let d = (:); d.insert(str(i), i * 10); d }
85+
```)
86+
]
87+
88+
=== 其他基本类型的情况
89+
90+
那么为什么说折叠操作基本就是#mark("+")操作。那么就是说有的“#mark("+")操作”并非是折叠操作。
91+
92+
布尔值、整数和浮点数都不能相互折叠:
93+
94+
```typ
95+
// 不能编译
96+
#{ false; true }; #{ 1; 2 }; #{ 1.; 2. }
97+
```
98+
99+
那么是否说布尔值、整数和浮点数都不能折叠呢。答案又是否认的,它们都可以与```typc none```折叠(把下面的加号看成折叠操作):
100+
101+
#code(```typ
102+
#(1 + none)
103+
```)
104+
105+
所以你可以保证一个代码块中只有一个「语句」产生布尔值、整数或浮点数结果,这样的代码块就又是能编译的了。让我们利用`let _ = `来实现这一点:
106+
107+
#code(```typ
108+
#{ let _ = 1; true },
109+
#{ let _ = false; 2. }
110+
```)
111+
112+
回忆之前所讲的特殊规则:#term("placeholder")用作标识符的作用是“忽略不必要的语句结果”。
113+
114+
=== 内容折叠
115+
116+
Typst脚本的核心重点就在本段。
117+
118+
内容也可以作为代码块的语句结果,这时候内容块的结果是每个语句内容的“折叠”。
119+
120+
#code(```typ
121+
#{
122+
[= 生活在Content树上]
123+
[现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。]
124+
[滥觞于家庭与社会传统的期望正失去它们的借鉴意义。]
125+
[但面对看似无垠的未来天空,我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。]
126+
}
127+
```)
128+
129+
是不是感觉很熟悉?实际上内容块就是上述代码块的“糖”。所谓糖就是同一事物更方便书写的语法。上述代码块与下述内容块等价:
130+
131+
```typ
132+
#[
133+
= 生活在Content树上
134+
现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。滥觞于家庭与社会传统的期望正失去它们的借鉴意义。但面对看似无垠的未来天空,我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。
135+
]
136+
```
137+
138+
由于Typst默认以「标记模式」开始解释你的文档,这又与省略`#[]`的写法等价:
139+
140+
```typ
141+
= 生活在Content树上
142+
现代社会以海德格尔的一句“一切实践传统都已经瓦解完了”为嚆矢。滥觞于家庭与社会传统的期望正失去它们的借鉴意义。但面对看似无垠的未来天空,我想循卡尔维诺“树上的男爵”的生活好过过早地振翮。
143+
```
144+
145+
#pro-tip[
146+
实际上有区别,由于多两个换行和缩进,前后各多一个Space Element。
147+
]
148+
149+
// == Hello World程序
150+
151+
// 有的时候,我们想要访问字面量、变量与函数中存储的“信息”。例如,给定一个字符串```typc "Hello World"```,我们想要截取其中的第二个单词。
152+
153+
// 单词`World`就在那里,但仅凭我们有限的脚本知识,却没有方法得到它。这是因为字符串本身是一个整体,虽然它具备单词信息,我们却缺乏了*访问*信息的方法。
154+
155+
// Typst为我们提供了「成员」和「方法」两种概念访问这些信息。使用「方法」,可以使用以下脚本完成目标:
156+
157+
// #code(```typ
158+
// #"Hello World".split(" ").at(1)
159+
// ```)
160+
161+
// 为了方便讲解,我们改写出6行脚本。除了第二行,每一行都输出一段内容:
162+
163+
// #code(```typ
164+
// #let x = "Hello World"; #x \
165+
// #let split = str.split
166+
// #split(x, " ") \
167+
// #str.split(x, " ") \
168+
// #x.split(" ") \
169+
// #x.split(" ").at(1)
170+
// ```)
171+
172+
// 从```typ #x.split(" ").at(1)```的输出可以看出,这一行帮助我们实现了“截取其中的第二个单词”的目标。我们虽然隐隐约约能揣测出其中的意思:
173+
174+
// ```typ
175+
// #( x .split(" ") .at(1) )
176+
// // 将字符串 根据字符串拆分 取出其中的第2个单词(字符串)
177+
// ```
178+
179+
// 但至少我们对#mark(".")仍是一无所知。
180+
181+
// 本节我们就来讲解Typst中较为高级的脚本语法。这些脚本语法与大部分编程语言的语法相同,但是我们假设你并不知道这些语法。
182+
183+
== 「内容」是一棵树(Cont.)
184+
185+
#pro-tip[
186+
利用「内容」与「树」的特性,我们可以在Typst中设计出更多优雅的脚本功能。
187+
]
188+
189+
=== CeTZ的「树」
190+
191+
CeTZ利用内容树制作“内嵌的DSL”。CeTZ的`canvas`函数接收的不完全是内容,而是内容与其IR的混合。
192+
193+
例如它的`line`函数的返回值,就完全不是一个内容,而是一个无法窥视的函数。
194+
195+
#code(```typ
196+
#import "@preview/cetz:0.3.4"
197+
#repr(cetz.draw.line((0, 0), (1, 1), fill: blue))
198+
```)
199+
200+
当你产生一个“混合”的内容并将其传递给`cetz.canvas`,CeTZ就会像`plain-text`一样遍历你的混合内容,并加以区分和处理。如果遇到了他自己特定的IR,例如`cetz.draw.line`,便将其以特殊的方式转换为真正的「内容」。
201+
202+
使用混合语言,在Typst中可以很优雅地画多面体:
203+
204+
#code.with(al: top)(```typ
205+
#import "@preview/cetz:0.3.4"
206+
#align(center, cetz.canvas({
207+
// 导入cetz的draw方言
208+
import cetz.draw: *; import cetz.vector: add
209+
let neg(u) = if u == 0 { 1 } else { -1 }
210+
for (p, c) in (
211+
((0, 0, 0), black), ((1, 1, 0), red), ((1, 0, 1), blue), ((0, 1, 1), green),
212+
) {
213+
line(add(p, (0, 0, neg(p.at(2)))), p, stroke: c)
214+
line(add(p, (0, neg(p.at(1)), 0)), p, stroke: c)
215+
line(add(p, (neg(p.at(0)), 0, 0)), p, stroke: c)
216+
}
217+
}))
218+
```)
219+
220+
=== curryst的「树」
221+
222+
我们知道「内容块」与「代码块」没有什么本质区别。
223+
224+
如果我们可以基于「代码块」描述一棵「内容」的树,那么逻辑推理的过程也可以被描述为条件、规则、结论的树。
225+
226+
#link("https://typst.app/universe/package/curryst/")[curryst]包提供了接收条件、规则、结论参数的`rule`函数,其返回一个包含传入信息的`dict`,并且允许把`rule`函数返回的`dict`作为`rule`的部分参数。于是我们可以通过嵌套`rule`函数建立描述推理过程的树,并通过该包提供的`prooftree`函数把包含推理过程的`dict`树画出来:
227+
228+
#code(```typ
229+
#import "@preview/curryst:0.5.0": rule, prooftree
230+
#let tree-dict = rule(
231+
name: $R$,
232+
$C_1 or C_2 or C_3$,
233+
rule(
234+
name: $A$,
235+
$C_1 or C_2 or L$,
236+
rule(
237+
$C_1 or L$,
238+
$Pi_1$,
239+
),
240+
),
241+
rule(
242+
$C_2 or overline(L)$,
243+
$Pi_2$,
244+
),
245+
)
246+
`tree-dict`的类型:#type(tree-dict) \
247+
`tree-dict`代表的树:#prooftree(tree-dict)
248+
```)
249+
250+
== 内容类型 <content-type-feature>
251+
252+
我们已经学过很多元素:段落、标题、代码片段等。这些元素在被创建后都会被包装成为一种被称为「内容」的值。这些值所具有的类型便被称为「内容类型」。同时「内容类型」提供了一组公共方法访问元素本身。
253+
254+
乍一听,内容就像是一个“容器”将元素包裹。但内容又不太像是之前所学过的数组或字典那样的复合字面量,或者说这样不方便理解。事实上,每个元素都有各自的特点,但仅仅为了保持动态性,所有的元素都被硬凑在一起,共享一种类型。有两种理解这种类型的视角:从表象论,「内容类型」是一种鸭子类型;从原理论,「内容类型」提供了操控内容的公共方法,即它是一种接口,或称特征(Trait)。
255+
256+
=== 特性一:元素包装于「内容」
257+
258+
我们知道所有的元素语法都可以等价使用相应的函数构造。例如标题:
259+
260+
#code(```typ
261+
#repr([= 123]) \ // 语法构造
262+
#repr(heading(depth: 1)[123]) // 函数构造
263+
264+
```)
265+
266+
一个常见的误区是误认为元素继承自「内容类型」,进而使用以下方法判断一个内容是否为标题元素:
267+
268+
#code(```typ
269+
标题是heading类型(伪)?#(type([= 123]) == heading)
270+
```)
271+
272+
但两者类型并不一样。事实上,元素是「函数类型」,元素函数的返回值为「内容类型」。
273+
274+
#code(```typ
275+
标题函数的类型:#(type(heading)) \
276+
标题的类型:#type([= 123])
277+
```)
278+
279+
这引出了一个重要的理念,Typst中一切皆组合。Typst中目前没有继承概念,一切功能都是组合出来的,这类似于Rust语言的概念。你可能没有学过Rust语言,但这里有一个冷知识:
280+
281+
#align(center, [Typst $<=>$ Typ(setting Ru)st $<=>$ Typesetting Rust])
282+
283+
即Typst是以Rust语言特性为基础设计出的一个排版(Typesetting)语言。
284+
285+
当各式各样的元素函数接受参数时,它们会构造出「元素」,然后将元素包装成一个共同的类型:「内容类型」。`heading`是函数而不是类型。与其他语言不同,没有一个`heading`类型继承`content`。因此不能使用`type([= 123]) == heading`判断一个内容是否为标题元素。
286+
287+
=== 特性二:内容类型的`func`方法
288+
289+
所有内容都允许使用`func`得到构造这个内容所使用的函数。因此,可以使用以下方法判断一个内容是否为标题元素:
290+
291+
#code(```typ
292+
标题所使用的构造函数:#([= 123]).func()
293+
294+
标题的构造函数是`heading`?#(([= 123]).func() == heading)
295+
```)
296+
297+
// 这一段不要了
298+
// === 特性二点五:内容类型的`func`方法可以直接拿来用
299+
300+
// `func`方法返回的就是函数本身,自然也可以拿来使用:
301+
302+
// #code(```typ
303+
// 重新构造标题:#(([= 123]).func())([456])
304+
// ```)
305+
306+
// 这一般没什么用,但是有的时候可以用于得到一些Typst没有暴露出来的内容函数,例如`styled`。
307+
308+
// #code(```typ
309+
// #let type_styled = text(fill: red, "").func()
310+
// #let st = text(fill: blue, "").styles
311+
// #text([abc], st)
312+
// ```)
313+
314+
=== 特性三:内容类型的`fields`方法
315+
316+
Typst中一切皆组合,它将所有内容打包成「内容类型」的值以完成类型上的统一,而非类型继承。
317+
318+
但是这也有坏处,坏处是无法“透明”访问内部内容。例如,我们可能希望知道`heading`的级别。如果不提供任何方法访问标题的级别,那么我们就无法编程完成与之相关的排版。
319+
320+
为了解决这个问题,Typst提供一个`fields`方法提供一个content的部分信息:
321+
322+
#code(```typ
323+
#([= 123]).fields()
324+
```)
325+
326+
`fields()`将部分信息组成字典并返回。如上图所示,我们可以通过这个字典对象进一步访问标题的内容和级别。
327+
328+
#code(```typ
329+
#([= 123]).fields().at("depth")
330+
```)
331+
332+
#pro-tip[
333+
这里的“部分信息”描述稍显模糊。具体来说,Typst只允许你直接访问元素中不受样式影响的信息,至少包含语法属性,而不允许你*直接*访问元素的样式。
334+
335+
// 如下:
336+
337+
// #code.with(al: top)(````typ
338+
// #let x = [= 123]
339+
// #rect([#x <the-heading>])
340+
// #x.fields() \
341+
// #locate(loc => query(<the-heading>, loc))
342+
// ````)
343+
]
344+
345+
=== 特性四:内容类型与`fields`相关的糖 <grammar-content-member-exp>
346+
347+
由于我们经常需要与`fields`交互,Typst提供了`has`方法帮助我们判断一个内容的`fields`是否有相关的「键」。
348+
349+
#code(```typ
350+
使用`... in x.fields()`判断:#("text" in `x`.fields()) \
351+
等同于使用`has`方法判断:#(`x`.has("text"))
352+
```)
353+
354+
Typst提供了`at`方法帮助我们访问一个内容的`fields`中键对应的值。
355+
356+
#code(```typ
357+
使用`x.fields().at()`获取值:#(`www`.fields().at("text")) \
358+
等同于使用`at`方法:#(`www`.at("text"))
359+
```)
360+
361+
特别地,内容的成员包含`fields`的键,我们可以直接通过成员访问相关信息:
362+
363+
#code(```typ
364+
使用`at`方法:#(`www`.at("text")) \
365+
等同于访问`text`成员:#(`www`.text)
366+
```)

0 commit comments

Comments
 (0)