1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
|
// The semantic elements every docs/typ topic uses, and the one place they
// are defined. Content files call them ("this is a key", "this is a
// builtin"); how each looks is decided here, per output: paged outputs
// (the A4 cheatsheet, later the book) draw them, HTML export gives them
// elements and classes for a stylesheet. Page geometry and fonts belong to
// the output wrappers beside this file.
/// The `atelier` theme (src/themes/atelier.zig): pardes's acme homage,
/// butter paper and blue-green tags.
#let palette = (
paper: rgb("#fffdeb"),
ink: rgb("#293f35"),
tag-bg: rgb("#e4f0ed"),
tag-fg: rgb("#3d6055"),
tag-name: rgb("#6e4774"),
grip: rgb("#36826b"),
col-grip: rgb("#955815"),
str: rgb("#34743d"),
num: rgb("#9a5810"),
rule: rgb("#95baa2"),
mouse: rgb("#8a3a00"),
)
#let mono = "DejaVu Sans Mono"
/// Topic ids for #doc: each names a chapter (an id in `chapters`) and,
/// optionally, the label of a section in it. Old ids stay so older calls
/// keep working.
#let topics = (
guide: ("guide", none),
setup: ("setup", none),
cheatsheet: ("cheatsheet", none),
scripting: ("scripting", none),
reference: ("reference", none),
themes: ("themes", none),
building: ("building", none),
builtins: ("builtins", none),
tags: ("guide", none),
fs: ("reference", none),
config: ("guide", "config"),
keys: ("guide", "keys"),
detached: ("guide", "sessions"),
)
/// Set by a wrapper that holds every chapter (the book, the HTML): #doc
/// then links to the chapter or section. Alone (the A4 sheet) it names it.
#let doc-links = state("doc-links", false)
/// The book's and the HTML's reading order. `file` is the content file
/// in docs/typ, or none while the topic is not written yet (the outputs
/// then show a placeholder). Wrappers include each with heading offset 1,
/// so a file's `=` headings sit under its chapter title, labelled
/// `ch-<id>`.
#let chapters = (
(id: "guide", title: "Guide", file: "guide.typ"),
(id: "setup", title: "Setting up your environment", file: "setup.typ"),
(id: "cheatsheet", title: "Cheatsheet", file: "cheatsheet.typ"),
(id: "scripting", title: "Scripting", file: "scripting.typ"),
(id: "reference", title: "Reference", file: "reference.typ"),
(id: "builtins", title: "Builtins", file: "builtins.typ"),
(id: "themes", title: "Themes", file: "themes.typ"),
(id: "building", title: "Building, for contributors", file: "building.typ"),
)
/// The one-line pitch: the A4 sheet's header and the book's title page.
#let tagline = "acme's tags and three buttons, helix's keys, terminals as panes, the editor as a virtual filesystem over 9P"
/// The tutor, shown as it is in the editor (`Tutor`), from the repo root.
#let tutor-path = "/src/tutor.txt"
/// How much room tables and shell lines get: "roomy" (the book, the HTML)
/// or "tight" (the A4 sheet sets it).
#let density = state("density", "roomy")
#let html-out() = target() == "html"
#let span(class, body) = html.elem("span", attrs: (class: class), body)
#let m(s, fill: palette.ink) = text(font: mono, size: 0.92em, fill: fill, s)
/// A key: one press (`x`, `Ctrl-w`, `Shift-Esc`), or a sequence pressed one
/// after another, written space-separated (`g g`, `SPC f c`). Either is ONE
/// pill: a combination's parts joined by a muted +, a sequence's steps by a
/// muted › (`Ctrl-w h` reads Ctrl + w › h).
#let key(k) = context if html-out() {
let sep(c, cls) = html.elem("span", attrs: (class: cls, "aria-hidden": "true"), c)
let press(t) = (if t.len() > 1 and t.contains("-") { t.split("-") } else { (t,) }).join(sep("+", "plus"))
html.elem("kbd", attrs: (class: "key"), k.split(" ").map(press).join(sep("›", "then")))
} else {
let muted(c) = text(fill: palette.grip.transparentize(25%), size: 0.85em, c)
let press(t) = (if t.len() > 1 and t.contains("-") { t.split("-") } else { (t,) }).join(muted[+])
box(
fill: palette.grip.transparentize(84%), radius: 2pt,
inset: (x: 1.4pt, y: 0pt), outset: (y: 1.5pt),
text(font: mono, size: 0.92em, fill: palette.ink,
k.split(" ").map(press).join(muted[#h(1pt)›#h(1pt)])),
)
}
/// Alternatives: separate keys listed together, each its own command.
#let keys(..ks) = context ks.pos().map(key).join(if html-out() [ ] else { h(0.25em) })
#let button-names = ("B1": "left button", "B2": "wheel", "B3": "right button")
#let click-names = ("B1": "left", "B2": "middle", "B3": "right")
/// Text that is read and copied but not shown, beside a drawn glyph.
#let hidden(t) = html.elem("span", attrs: (class: "vh"), t)
/// A mouse, about cap height: left button, wheel, right button. The parts
/// in `pressed` ("B1", "B2", "B3") are filled.
#let mouse(pressed) = {
let (w, h, top) = (0.84em, 1.2em, 0.56em)
let ink = palette.mouse
let s = 0.7pt + ink
let on(b) = if b in pressed { ink } else { none }
box(width: w, height: h, baseline: 0.24em, {
place(rect(width: w / 2, height: top, radius: (top-left: w / 2), fill: on("B1"), stroke: s))
place(dx: w / 2, rect(width: w / 2, height: top, radius: (top-right: w / 2), fill: on("B3"), stroke: s))
place(dy: top, rect(width: w, height: h - top, radius: (bottom: w / 2), stroke: s))
// the wheel: a narrow tall slot over the buttons' seam, on a paper
// halo so a filled wheel stands apart from the button outlines
let (ww, wh, halo) = (0.26em, 0.44em, 0.07em)
let (wx, wy) = ((w - ww) / 2, 0.06em)
place(dx: wx - halo, dy: wy - halo / 2, rect(
width: ww + 2 * halo, height: wh + halo, radius: ww, fill: palette.paper))
place(dx: wx, dy: wy, rect(
width: ww, height: wh, radius: ww / 2, stroke: 0.5pt + ink,
fill: if "B2" in pressed { ink } else { palette.paper },
))
})
}
/// The same mouse as inline SVG for HTML. Each part is classed `on`
/// (pressed) or `off`, and the stylesheet colours them: the pressed part
/// filled in the accent, the rest outlined and muted.
#let mouse-svg(pressed) = {
let cls(b) = if b in pressed { "on" } else { "off" }
let part(d, b) = html.elem("path", attrs: (class: cls(b), d: d))
html.elem("svg", attrs: (class: "mouse", viewBox: "-6 -6 96 132", "aria-hidden": "true"), {
// taller buttons and a slimmer wheel than on paper, so a pressed
// button shows as a solid block at body size
part("M0 64H84V78A42 42 0 0 1 0 78Z", none)
part("M0 64V42A42 42 0 0 1 42 0V64Z", "B1")
part("M42 0A42 42 0 0 1 84 42V64H42Z", "B3")
// a pressed wheel is drawn wider, so it reads as pressed, not as a seam
let (hx, hw, wx, ww) = if "B2" in pressed { ("27", "30", "31", "22") } else { ("31", "22", "35", "14") }
html.elem("rect", attrs: (class: "halo", x: hx, y: "4", width: hw, height: "48", rx: "13"))
html.elem("rect", attrs: (class: "wheel " + cls("B2"), x: wx, y: "8", width: ww, height: "40", rx: "9"))
})
}
/// A mouse button; `shift: true` for the button with Shift held.
#let btn(b, shift: false) = context if html-out() {
let label = (if shift { "shift " } else { "" }) + button-names.at(b)
html.elem("span", attrs: (class: "btn", title: label), {
if shift { html.elem("span", attrs: (class: "shift", "aria-hidden": "true"), "⇧") }
mouse-svg((b,))
hidden((if shift { "Shift-" } else { "" }) + click-names.at(b) + "-click")
})
} else {
if shift { text(size: 0.8em, fill: palette.col-grip)[⇧] }
mouse((b,))
}
/// A chord: the first button held, each next one clicked while it is.
/// Drawn as the held button, then for each click the held one and that one
/// down, with arrows between.
#let chord(..bs) = context if html-out() {
let label = bs.pos().map(b => button-names.at(b)).join(" then ")
let (held, ..clicks) = bs.pos()
let arrow = html.elem("span", attrs: (class: "then", "aria-hidden": "true"), "›")
let said = "hold " + click-names.at(held) + ", click " + clicks.map(c => click-names.at(c)).join(", then ")
html.elem("span", attrs: (class: "chord", title: label), {
((mouse-svg((held,)),) + clicks.map(c => mouse-svg((held, c)))).join(arrow)
hidden(said)
})
} else {
let (held, ..clicks) = bs.pos()
let arrow = text(size: 0.75em, fill: palette.col-grip)[#h(0.5pt)›#h(0.5pt)]
((mouse((held,)),) + clicks.map(c => mouse((held, c)))).join(arrow)
}
/// A tag word or builtin, drawn as a thing to click: the tag's ground and
/// ink in the tag's monospace, over a rule. Look and Exec take acme's look
/// and exec sweep colours (green and red, src/themes/acme.zig) for the
/// rule; every other word the tag's box colour. In HTML a <code
/// class="word">, so a text browser still shows the plain word.
#let word-accent = (Look: rgb("#006600"), Exec: rgb("#aa0000"))
/// Every builtin of either build, written by `zig build builtins-json`
/// from the registry and the Help rows (src/builtins_json.zig), a file per
/// build: name, leader, also (Help's other shortcuts), arg, scope, setting,
/// choices, doc. Merged here, each with `platforms`: "both", "tty" (the
/// terminal only) or "gui" (the window only).
#let builtin-data = {
let tty = json("builtins-tty.json")
let gui = json("builtins-gui.json")
let in-tty = tty.map(b => b.name)
let in-gui = gui.map(b => b.name)
let all = (tty + gui.filter(b => b.name not in in-tty)).sorted(key: b => b.name)
all.map(b => {
b.insert("platforms", if b.name in in-tty and b.name in in-gui { "both" } else if b.name in in-tty { "tty" } else { "gui" })
b
})
}
#let builtin-names = builtin-data.map(b => b.name)
#let word-chip(w) = context if html-out() {
let base = w.split("+").first()
let kind = if base == "Look" { " look" } else if base == "Exec" { " exec" } else { "" }
html.elem("code", attrs: (class: "word" + kind), w)
} else {
let rule = word-accent.at(w.split("+").first(), default: palette.grip)
box(
fill: palette.tag-bg, inset: (x: 1.4pt, y: 0pt), outset: (y: 1.4pt),
stroke: (bottom: 0.6pt + rule),
m(w, fill: palette.tag-fg),
)
}
/// A builtin's word links to its glossary entry wherever the glossary is
/// part of the same output (the book, the site); elsewhere it is the chip.
#let word(w) = context {
let base = w.split("+").first()
let target = label("builtin-" + base)
if doc-links.get() and base in builtin-names and query(target).len() > 0 {
link(target, word-chip(w))
} else { word-chip(w) }
}
/// A whole tagline: its grip, the path, then its words.
#let tag(words, path: none) = context if html-out() {
html.elem("div", attrs: (class: "tag"), {
html.elem("span", attrs: (class: "grip", "aria-hidden": "true"))[]
if path != none { span("path", path); [ ] }
raw(words)
})
} else {
block(width: 100%, fill: palette.tag-bg, inset: (x: 2pt, y: 2.4pt), spacing: 0.5em, {
box(fill: palette.grip, width: 0.55em, height: 0.75em, baseline: 0.1em)
h(3pt)
if path != none { m(path, fill: palette.tag-name); h(0.6em) }
m(words, fill: palette.tag-fg)
})
}
/// A look address.
#let addr(a) = context if html-out() { span("addr", raw(a)) } else { m(a, fill: palette.tag-name) }
/// A 9P path.
#let file(p) = context if html-out() { span("file", raw(p)) } else { m(p, fill: palette.str) }
/// A shell line.
#let cmd(c) = context if html-out() { html.elem("div", attrs: (class: "cmd"), raw(c, block: true, lang: "sh")) } else {
let tight = density.get() == "tight"
block(
width: 100%, spacing: if tight { 0.4em } else { 0.9em },
inset: if tight { (left: 3pt, y: 1.2pt) } else { (left: 6pt, y: 3pt) },
stroke: (left: (if tight { 1pt } else { 1.5pt }) + palette.rule),
fill: if tight { none } else { palette.tag-bg.transparentize(50%) },
text(font: mono, size: 0.85em, fill: palette.ink, c),
)
}
/// Where the full story is: a topic id from `topics`, and a section label
/// in it. A link where the wrapper holds the chapters, else its name.
#let doc(id, section: none) = {
let (chapter, default) = topics.at(id)
let section = if section != none { section } else { default }
let title = chapters.find(c => c.id == chapter).title
let to = label(if section != none { section } else { "ch-" + chapter })
let named = title + if section != none { ": " + section.replace("-", " ") } else { "" }
context {
if doc-links.get() {
let found = query(to)
let shown = if found.len() > 0 and found.first().func() == heading {
if section != none [#title: #found.first().body] else [#title]
} else [#named]
link(to, text(fill: palette.num)[→ #shown])
} else {
text(fill: palette.num)[→ #named]
}
}
}
/// Pairs: a thing and what it means, as a two-column table, key column
/// first, rows on alternating tints. `split: 2` sets the rows as that many
/// tables side by side (on paper; HTML gets one table).
#let pairs(split: 1, ..cells) = {
let rows = cells.pos().chunks(2)
context {
let tight = density.get() == "tight"
let one(rows) = table(
columns: (auto, 1fr), stroke: none,
inset: if tight { (x: 2pt, y: 1.6pt) } else { (x: 7pt, y: 4pt) },
fill: (_, y) => if calc.even(y) { palette.tag-bg.transparentize(35%) },
..rows.flatten(),
)
if html-out() or split == 1 { one(rows) } else {
let n = calc.ceil(rows.len() / split)
grid(columns: (1fr,) * split, column-gutter: if tight { 2pt } else { 10pt }, ..rows.chunks(n).map(one))
}
}
}
/// Where pardes lives and under what licence: install.typ, the site's
/// landing and its footer all read it. Every mirror holds the same tree;
/// the first is the one the install line clones. A new mirror is one line.
#let project = (
mirrors: (
(name: "sourcehut", web: "https://git.sr.ht/~gbrls/pardes", clone: "https://git.sr.ht/~gbrls/pardes"),
(name: "git.0x4200.cafe", web: "https://git.0x4200.cafe/pardes", clone: "https://git.0x4200.cafe/pardes.git"),
),
license: "ISC",
changes: "https://git.sr.ht/~gbrls/pardes/tree/main/item/src/CHANGELOG.md",
// built binaries to download: the version they are, where the
// checksums are, and a file per program (install.typ shows them)
download: (
version: "0.23",
sums: "https://pub-66384ba880784f119ce95123b92fa5c6.r2.dev/SHA256SUMS",
files: (
(program: "pardes", what: "the terminal build, static, any x86_64 Linux",
url: "https://pub-66384ba880784f119ce95123b92fa5c6.r2.dev/pardes-x86_64-linux-static", bytes: 62223912,
sha256: "740992b18bdcfefc4db449dcbdb12423b4505eb2b3fb360aaa6dc57cc2fdda9f"),
(program: "pardes-gui", what: "the window, x86_64 Linux with glibc 2.29 or later; it loads Wayland or X11 and GL or Vulkan from the system",
url: "https://pub-66384ba880784f119ce95123b92fa5c6.r2.dev/pardes-gui-x86_64-linux", bytes: 66786824,
sha256: "a1c68b347d424b8077fb5af4050dd8455dc849adb0c1e2fc325b1646f5a255e5"),
),
),
)
/// A glossary: like #pairs, under its own heading. On paper the heading
/// and the table; in HTML a <details> that starts closed, its summary the
/// heading and the count, so a reader who knows the words skips them.
#let glossary(title, lbl, ..cells) = context if html-out() {
// a real definition list, so a text browser reads term, then meaning
html.elem("details", attrs: (class: "glossary"), {
html.elem("summary", [#heading(title) #lbl])
html.elem("dl", for (term, meaning) in cells.pos().chunks(2) {
html.elem("dt", term)
html.elem("dd", meaning)
})
})
} else {
[#heading(title) #lbl]
pairs(..cells)
}
/// Every theme as a picture, on the site's Themes page; nothing on paper,
/// where forty-odd screenshots would only add weight. Reads
/// docs/site/media/themes/themes.toml: `ready` (absent means true) and a
/// [[theme]] per theme, in NextColor's order, with `name`, `file` (under
/// media/themes/) and `kind` ("light" or "dark").
#let theme-gallery() = context if html-out() {
let g = toml("../site/media/themes/themes.toml")
let ready = g.at("ready", default: true)
heading[Every theme]
html.elem("div", attrs: (class: "gallery"), for t in g.theme {
let src = "media/themes/" + t.file
html.elem("figure", attrs: (class: "shot " + t.kind), {
if ready {
html.elem("a", attrs: (href: src), html.elem("img", attrs: (src: src, alt: t.name + " (" + t.kind + ")", loading: "lazy")))
} else {
html.elem("div", attrs: (class: "pending"), "screenshot to come")
}
html.elem("figcaption", raw(t.name))
})
})
}
/// The newcomer's words, first in the glossary; every other builtin goes
/// under its group below. Each must be in builtins.json.
#let everyday = ("Look", "Exec", "Save", "Undo", "Redo", "New", "Newcol", "Del", "Delcol",
"Tty", "Kill", "Find", "Grep", "Recent", "Dump", "Restore", "Exit", "Help", "Tutor", "Themes", "Config")
/// Debugging and developer words, last whatever their leader path.
#let internals = ("Debug", "EffectCode", "Lspwhy", "Messages", "DumpConfig", "DumpThemes")
/// The glossary's groups, in order: by the leader path's first key, the
/// way Help groups them, then settings, the words with no path, internals.
#let builtin-groups = (
(title: "Files", keys: ("f",)),
(title: "Panes and columns", keys: ("c", "d")),
(title: "Moving and jumps", keys: ("w", "j")),
(title: "Terminals", keys: ("n",)),
(title: "Language server", keys: ("l",)),
(title: "Sessions", keys: ("s", "q")),
(title: "Help", keys: ("h", "?")),
(title: "Clipboard", keys: ("y", "Y", "p", "P", "R")),
(title: "Toggles", keys: ("t",)),
(title: "Settings and themes", keys: "setting"),
(title: "Animations", keys: ("a",)),
(title: "Other words", keys: "none"),
)
#let group-of(b) = {
if b.name in internals { return "internals" }
if b.setting { return "setting" }
if b.leader == none { return "none" }
b.leader.split(" ").at(1)
}
/// Help spells a shortcut its own way (C-w left, right-click, topbar);
/// this draws it as the docs do.
#let shortcut(s) = {
if s == "topbar" { return [in the workspace tag] }
if s == "middle-click" { return btn("B2") }
if s == "right-click" { return btn("B3") }
if s == "left-click" { return btn("B1") }
let names = (enter: "Enter", tab: "Tab", escape: "Esc", left: "Left", right: "Right", up: "Up", down: "Down",
home: "Home", end: "End", backspace: "Backspace", delete: "Delete", page_up: "PgUp", page_down: "PgDn")
key(s.split(" ").map(t => {
let mods = ""
while t.len() > 2 and t.at(1) == "-" and t.at(0) in ("C", "A", "S") {
mods += (C: "Ctrl-", A: "Alt-", S: "Shift-").at(t.at(0))
t = t.slice(2)
}
mods + names.at(t, default: t)
}).join(" "))
}
/// A doc comment's text, its `code` spans set as code.
#let prose(t) = t.split("`").enumerate().map(((i, part)) => if calc.odd(i) { raw(part) } else { part }).join()
#let builtin-entry(b) = [
#heading(depth: 2, raw(b.name)) #label("builtin-" + b.name)
#let keys = (if b.leader != none { (key(b.leader),) } else { () }) + b.also.map(shortcut)
#if b.platforms != "both" [#emph(if b.platforms == "gui" [window only] else [terminal only]) · ]
#if keys.len() > 0 [#keys.join([, ]) · ]
#if b.choices != none [#raw(b.choices.replace(", ", "|")) · ] else if b.arg [takes an argument · ]
#prose(b.doc)
]
/// The glossary: everyday words first, then the groups, then internals,
/// each alphabetical. A level-1 heading per group in the content file.
#let builtin-glossary() = {
let by-name = (:)
for b in builtin-data { by-name.insert(b.name, b) }
[= Everyday <builtins-everyday>]
for n in everyday.sorted() { builtin-entry(by-name.at(n)) }
let rest = builtin-data.filter(b => b.name not in everyday)
for g in builtin-groups {
let members = rest.filter(b => {
let k = group-of(b)
if type(g.keys) == str { k == g.keys } else { k in g.keys }
})
if members.len() > 0 {
heading(depth: 1, g.title)
for b in members { builtin-entry(b) }
}
}
let inner = rest.filter(b => group-of(b) == "internals")
if inner.len() > 0 {
[= Debugging and internals]
for b in inner { builtin-entry(b) }
}
// a builtin no group took is a bug in builtin-groups, not a silent drop
let placed = everyday + rest.filter(b => {
let k = group-of(b)
k == "internals" or builtin-groups.any(g => if type(g.keys) == str { k == g.keys } else { k in g.keys })
}).map(b => b.name)
for b in builtin-data { assert(b.name in placed, message: "builtin " + b.name + " is in no glossary group") }
// last, every word at once, A to Z, each a link to its entry
[= All builtins, A–Z <builtins-a-z>]
context if html-out() {
html.elem("p", attrs: (class: "all-words"),
builtin-data.map(b => link(label("builtin-" + b.name), word-chip(b.name))).join(" "))
} else {
par(builtin-data.map(b => link(label("builtin-" + b.name), word-chip(b.name))).join(h(0.4em)))
}
}
/// A directory tree drawn as `tree` draws one. `spec` is a raw block, a
/// line per file: two spaces of indent per level, the name (a directory
/// ends in `/`), two or more spaces, a one-line description, and an
/// optional `@label` naming the section that documents it, which becomes
/// a link wherever that section is in the same output. A description may
/// call #word, #key, #keys or #btn. src/config.zig's
/// test reads the same block in reference.typ against the served tree.
#let fstree(spec, root: "/") = {
// a description is text, unless it calls #word, #key or #btn
let said(d) = if d.contains("#") { eval(d, mode: "markup", scope: (word: word, key: key, keys: keys, btn: btn)) } else { d }
let rows = spec.text.split("\n").filter(l => l.trim() != "").map(l => {
let depth = calc.quo(l.len() - l.trim(at: start).len(), 2)
let m = l.trim().match(regex("^(\S+)(?:\s{2,}(.*?))?(?:\s+@([a-z0-9-]+))?$"))
(depth: depth, name: m.captures.at(0), desc: m.captures.at(1), anchor: m.captures.at(2))
})
// the connector for each row: is each ancestor, and the row itself, the
// last among its siblings?
let last(i) = {
let d = rows.at(i).depth
for j in range(i + 1, rows.len()) {
if rows.at(j).depth < d { return true }
if rows.at(j).depth == d { return false }
}
true
}
let lasts = range(rows.len()).map(last)
let prefix(i) = {
let d = rows.at(i).depth
let out = ""
// for each ancestor depth, the nearest row above at that depth
for k in range(d) {
let a = range(i).rev().find(j => rows.at(j).depth == k)
out += if lasts.at(a) { " " } else { "│ " }
}
out + if lasts.at(i) { "└── " } else { "├── " }
}
let named(r) = context {
let t = if r.anchor != none { label(r.anchor) } else { none }
if t != none and query(t).len() > 0 { link(t, r.name) } else { r.name }
}
let width = calc.max(..range(rows.len()).map(i => prefix(i).len() + rows.at(i).name.len()))
context if html-out() {
let nl = html.elem("span", attrs: (class: "nl"), "\n")
html.elem("pre", attrs: (class: "fstree", style: "--name:" + str(width + 2) + "ch"), {
html.elem("span", attrs: (class: "row root"), [#html.elem("span", attrs: (class: "name dir"), root)#nl])
for (i, r) in rows.enumerate() {
let pad = " " * (width - prefix(i).clusters().len() - r.name.len() + 2)
html.elem("span", attrs: (class: "row"), {
html.elem("span", attrs: (class: if r.name.ends-with("/") { "name dir" } else { "name" }),
[#html.elem("span", attrs: (class: "branch"), prefix(i))#named(r)#pad])
if r.desc != none {
html.elem("span", attrs: (class: "desc", style: "--cols:" + str(prefix(i).clusters().len())), said(r.desc))
}
nl
})
}
})
} else {
set text(font: mono, size: 0.82em)
block(width: 100%, grid(columns: (auto, 1fr), column-gutter: 1.2em, row-gutter: 0pt, inset: (y: 0.12em),
text(weight: "bold", fill: palette.grip, root), [],
..rows.enumerate().map(((i, r)) => (
[#text(fill: palette.rule, prefix(i))#text(fill: if r.name.ends-with("/") { palette.grip } else { palette.ink },
weight: if r.name.ends-with("/") { "bold" } else { "regular" }, named(r))],
text(font: "Libertinus Serif", size: 1.2em, fill: palette.tag-fg, if r.desc == none { [] } else { said(r.desc) }),
)).flatten()))
}
}
/// A screenshot with numbered pins on it and their labels as text beside
/// it, so the labels read, search and translate as text. `src` is from
/// the repository root (the site serves it from the same path under
/// docs/site/); `size` is its pixel (width, height); each pin is (label,
/// x, y) in pixels of that image.
#let annotated(src, size, alt: "", ..pins) = {
let (w, h) = size
let pins = pins.pos()
context if html-out() {
let web = src.trim("/docs/site/", at: start)
html.elem("figure", attrs: (class: "annotated"), {
html.elem("div", attrs: (class: "frame"), {
html.elem("img", attrs: (src: web, alt: alt, width: str(w), height: str(h)))
for (i, (label, x, y)) in pins.enumerate() {
html.elem("span", attrs: (class: "pin", "aria-hidden": "true",
// repr, not str: str writes a negative with U+2212, which CSS rejects
style: "left:" + repr(calc.round(100 * x / w, digits: 2)) + "%;top:" + repr(calc.round(100 * y / h, digits: 2)) + "%"), str(i + 1))
}
})
html.elem("ol", attrs: (class: "pins"), for (label, x, y) in pins { html.elem("li", label) })
})
} else {
let width = 11cm
let k = width / w
let pin(n) = box(width: 1.25em, height: 1.25em, radius: 50%, fill: palette.mouse,
align(center + horizon, text(size: 0.75em, weight: "bold", fill: white, str(n))))
block(breakable: false, grid(columns: (width, 1fr), column-gutter: 1.2em, align: horizon,
box(width: width, height: h * k, {
place(top + left, image(src, width: width, alt: alt))
for (i, (label, x, y)) in pins.enumerate() {
place(top + left, dx: x * k - 0.625em, dy: y * k - 0.625em, pin(i + 1))
}
}),
enum(..pins.map(p => p.at(0)), spacing: 0.7em),
))
}
}
/// A pane's box as it looks: blank in normal mode, `^` in insert, `$` raw
/// (a terminal's own input); `unsaved: true` fills it, as an unsaved
/// file's is. In HTML a span the stylesheet draws, its mode said in words.
#let mode-box(mark, unsaved: false) = {
let mode = if mark == "^" { "insert" } else if mark == "$" { "raw" } else { "normal" }
context if html-out() {
html.elem("span", attrs: (class: "mode-box" + if unsaved { " unsaved" } else { "" },
role: "img", "aria-label": mode + " mode box" + if unsaved { ", unsaved" } else { "" }), mark)
} else {
box(width: 1.05em, height: 1.05em, baseline: 0.15em, radius: 1pt,
fill: if unsaved { palette.col-grip } else { palette.tag-bg },
stroke: 0.7pt + palette.grip,
align(center + horizon, text(font: mono, size: 0.8em, weight: "bold",
fill: if unsaved { white } else { palette.ink }, mark)))
}
}
/// A recording from docs/site/media/clips.toml, by name. On the site the
/// same video as the landing's (poster, autoplay muted loop, played while
/// in view) and its caption; in print its poster, small, and the caption.
/// `wide: true` lets it take the page's full width (a recording of many
/// narrow columns).
#let clip(name, wide: false) = {
let c = toml("../site/media/clips.toml").clip.find(c => c.name == name)
assert(c != none, message: "no clip named " + name + " in clips.toml")
let said = if c.caption.contains("#") { eval(c.caption, mode: "markup") } else { c.caption }
context if html-out() {
html.elem("figure", attrs: (class: "clip inline" + if wide { " wide" } else { "" }, id: "rec-" + c.name), {
html.elem("video", attrs: (poster: "media/" + c.name + ".png", autoplay: "", muted: "", loop: "",
playsinline: "", controls: ""), {
html.elem("source", attrs: (src: "media/" + c.name + ".webm", type: "video/webm"))
html.elem("source", attrs: (src: "media/" + c.name + ".mp4", type: "video/mp4"))
})
html.elem("figcaption", said)
})
} else {
figure(image("/docs/site/media/" + c.name + ".png", width: if wide { 11cm } else { 7cm }, alt: c.caption),
caption: text(size: 0.9em, said), numbering: none, kind: "clip", supplement: none)
}
}
|