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
|
// The website: one Typst bundle run writes every page, the stylesheet, the
// fonts and the media. Pages come from `chapters` in ../typ/style.typ, so a
// new chapter is a new page; the only prose of its own is the landing page.
// typst compile --root . --format bundle --features bundle,html docs/site/site.typ OUT
// Every link is relative and every page sits at the top of OUT, so the site
// works from file:// and from any static server.
#import "../typ/style.typ": chapters, tutor-path, doc-links, project, btn, chord
#let clips = toml("media/clips.toml").clip
#let casts = toml("casts/casts.toml").cast
#let themes = toml("media/themes/themes.toml")
/// The version, from build.zig.zon: 0.23.0 reads 0.23.
#let version = {
let v = read("/build.zig.zon").match(regex("\\.version\\s*=\\s*\"([^\"]+)\"")).captures.first()
if v.ends-with(".0") and v.split(".").len() == 3 { v.slice(0, -2) } else { v }
}
// --- the landing page's own words: everything a visitor reads first ----------
// Change the landing here; the rest of the site is the chapters.
#let landing = (
lead: [pardes is a text environment, after acme and helix, where any text
is live: right-click `main.c:12` to open that line, middle-click `make`
to run it, in source, PDFs and terminals alike, in a terminal or in its
own window.],
credit: [It is built on ideas from #link("https://9p.io/plan9/")[Plan 9] from
Bell Labs: #link("https://9p.io/sys/doc/acme/acme.html")[acme]'s text as
the interface, and the 9P protocol.],
status: [Linux; macOS in progress. Version #version.],
// what's in it: a word each, linked to where it is told; no prose
// what's in it: each word jumps to its recording on this page when it
// has one, and that recording's caption links on to the docs
inside: (
(name: "Terminals", rec: "terminals", docs: "guide.html#terminals"),
(name: "PDFs", rec: "pdf-navigation", docs: "builtins.html#builtin-PdfSections"),
(name: "Images", rec: "image-pane", docs: "reference.html#panes"),
(name: "Language servers", rec: none, docs: "setup.html#language-servers"),
(name: "Diff review", rec: "diff-review", docs: "guide.html#reviewing-diffs"),
(name: "Scripting", rec: "scripting-over-9p", docs: "scripting.html"),
(name: "Themes", rec: none, docs: "themes.html"),
),
terminal: [In a terminal],
window: [In its own window],
start: (
(id: "guide", label: [Start: the guide], note: [about 15 min]),
(id: "tutor", label: [Hands on: the tutor], note: [run `Tutor` inside pardes]),
),
install: [Install],
)
#let pages = (
(id: "index", title: "pardes"),
..chapters.filter(c => c.file != none).map(c => (id: c.id, title: c.title, file: c.file)),
(id: "tutor", title: "The tutor"),
)
#let href(id) = id + ".html"
// --- the files that are not pages --------------------------------------------
#asset("site.css", read("site.css", encoding: none))
#for f in ("DepartureMono-Regular.woff2", "CrimsonPro-VF-normal.woff2", "CrimsonPro-VF-italic.woff2",
"LICENSE-DepartureMono.txt", "LICENSE-CrimsonPro.txt") {
asset("fonts/" + f, read("fonts/" + f, encoding: none))
}
#asset("media/placeholder.svg", read("media/placeholder.svg", encoding: none))
#for f in ("asciinema-player.min.js", "asciinema-player.css", "LICENSE-asciinema-player.txt") {
asset("player/" + f, read("player/" + f, encoding: none))
}
#if themes.at("ready", default: true) {
for t in themes.theme { asset("media/themes/" + t.file, read("media/themes/" + t.file, encoding: none)) }
}
#for c in clips.filter(c => c.ready) {
for ext in ("webm", "mp4", "png") {
asset("media/" + c.name + "." + ext, read("media/" + c.name + "." + ext, encoding: none))
}
}
// --- the shell every page sits in ---------------------------------------------
/// An HTML element: el("a", (href: "x.html"))[text], or el("li")[text].
#let el(tag, ..args) = {
let p = args.pos()
html.elem(tag, attrs: if p.len() > 1 { p.first() } else { (:) }, p.last())
}
/// The running head: a tagline, the way a pane's tag reads, with the path
/// of this page and the chapters as its words.
#let trail(id, title) = el("header", (class: "trail"))[
#el("span", (class: "grip", "aria-hidden": "true"))[]
#el("a", (class: "home", href: href("index")))[pardes]
#if id != "index" {
el("span", (class: "sep", "aria-hidden": "true"))[/]
el("span", (class: "here", "aria-current": "page"))[#title]
}
#el("nav", (class: "words", "aria-label": "Chapters"))[
#for p in pages.slice(1) {
if p.id == id { el("a", (href: href(p.id), "aria-current": "page"))[#p.title] }
else { el("a", (href: href(p.id)))[#p.title] }
}
]
]
#let in-view-script = el("script")[#(
"(() => { if (!('IntersectionObserver' in window)) return;
" +
" const p = el => el.player || (el.tagName === 'VIDEO' ? el : null);
" +
" const io = new IntersectionObserver(es => { for (const e of es) { const r = p(e.target); if (!r) continue;
" +
" if (e.isIntersecting) Promise.resolve(r.play()).catch(() => {}); else r.pause(); } }, {threshold: 0.25});
" +
" for (const el of document.querySelectorAll('.clip video, .cast .player')) io.observe(el);
" +
"})();"
)]
#let shell(id, title, body) = html.html(lang: "en", {
html.head({
html.meta(charset: "utf-8")
el("meta", (name: "viewport", content: "width=device-width, initial-scale=1"))[]
el("meta", (name: "color-scheme", content: "dark light"))[]
html.title(if id == "index" { "pardes" } else { title + " · pardes" })
el("link", (rel: "stylesheet", href: "site.css"))[]
})
html.body({
el("div", (class: "shell"))[
#trail(id, title)
#el("main", (class: "panel", "data-page": id))[
#el("article", (class: "content"))[#body]
]
#el("footer", (class: "colophon"))[
pardes #version ·
source: #project.mirrors.map(m => el("a", (href: m.web))[#m.name]).join(", ") ·
#el("a", (href: project.changes))[changes] ·
#if project.license != none [#project.license license · ]
built from `docs/typ` with Typst ·
#el("a", (href: "fonts/LICENSE-DepartureMono.txt"))[fonts] (OFL) ·
#el("a", (href: "player/LICENSE-asciinema-player.txt"))[asciinema-player] (Apache-2.0)
]
]
// after the page's own scripts (the landing's players), on every page
// that has a recording
in-view-script
})
})
/// The headings of this page only, as a contents list, when there are
/// enough of them to need one.
#let toc(id) = context {
let hs = query(selector(heading.where(level: 2)).after(label("start-" + id)).before(label("end-" + id)))
if hs.len() >= 4 {
el("nav", (class: "toc", "aria-label": "On this page"))[
#el("div", (class: "toc-bar"))[#el("span")[On this page]]
#el("ol")[#for h in hs { el("li")[#link(h.location(), h.body)] }]
]
}
}
// --- the pages ------------------------------------------------------------------
#doc-links.update(true)
// Every recording plays by itself; the script below plays each only while
// it is on screen. Without the script, `autoplay` still plays them.
#let video(c) = el("video", (poster: "media/" + c.name + ".png", autoplay: "", muted: "", loop: "", playsinline: "",
controls: ""))[
#el("source", (src: "media/" + c.name + ".webm", type: "video/webm"))[]
#el("source", (src: "media/" + c.name + ".mp4", type: "video/mp4"))[]
]
#let placeholder = el("img", (src: "media/placeholder.svg", alt: "", width: "640", height: "400"))[]
/// A recording's caption: its text (which may hold #link markup), then the
/// docs of the "What's in it" word that jumps here, if any.
#let caption(c) = {
if c.caption.contains("#") { eval(c.caption, mode: "markup") } else { c.caption }
let to = landing.inside.find(i => i.rec == c.name)
if to != none [ #el("a", (class: "more", href: to.docs))[#to.name in the docs →]]
}
/// A terminal recording: a box the player fills, its data inline (so it
/// plays from file://), its caption under it.
#let ready-casts = casts.filter(c => c.ready)
#let cast(c, i) = el("figure", (class: if i == 0 { "clip cast first" } else { "clip cast" }, id: "rec-" + c.name))[
#if c.ready { el("div", (class: "player", id: "cast-" + c.name, "aria-label": c.title))[] } else { placeholder }
#el("figcaption")[#el("strong")[#c.title.] #caption(c)]
]
#let player-script = {
let list = ready-casts.enumerate().map(((i, c)) => (
id: "cast-" + c.name, data: read("casts/" + c.name + ".cast"),
cols: c.cols, rows: c.rows, poster: "npt:0:" + str(c.at("poster_at", default: 2)),
))
// "</" may not appear inside a script element
let data = json.encode(list, pretty: false).replace("</", "<\\/")
el("script", (src: "player/asciinema-player.min.js"))[]
el("script")[#("for (const c of " + data + ") { const el = document.getElementById(c.id); el.player = AsciinemaPlayer.create({data: c.data}, el, {cols: c.cols, rows: c.rows, autoPlay: false, loop: true, fit: 'width', poster: c.poster}); }")]
}
/// Plays a recording while it is on screen and pauses it once it leaves.
/// The casts wait for it (autoPlay off) so each shows its poster frame
/// until then; the videos autoplay by themselves too.
#document(href("index"), title: "pardes", shell("index", "pardes")[
#el("h1", (class: "masthead"))[pardes]
#el("p", (class: "lead"))[#landing.lead]
#el("ul", (class: "inside", "aria-label": "What's in it"))[
#for i in landing.inside {
let to = if i.rec != none { "#rec-" + i.rec } else { i.docs }
el("li")[#el("a", (href: to, title: if i.rec != none { "watch it here; the docs: " + i.docs } else { "the docs" }))[#i.name]]
}
]
#el("p", (class: "credit"))[#landing.credit]
#el("p", (class: "runs"))[#landing.status]
#el("div", (class: "start"))[
#for s in landing.start {
el("a", (class: "go", href: href(s.id)))[#s.label#if s.note != none [ #el("span")[(#s.note)]]]
}
]
#el("p", (class: "rest"))[
#for p in pages.slice(1).filter(p => p.id not in landing.start.map(s => s.id)) {
el("a", (href: href(p.id)))[#p.title]
}
]
#el("h2", (class: "shelf"))[#landing.terminal]
#el("div", (class: "casts"))[#for (i, c) in casts.enumerate() { cast(c, i) }]
#el("h2", (class: "shelf"))[#landing.window]
#el("div", (class: "clips"))[
#for c in clips.filter(c => c.at("landing", default: true)) {
el("figure", (class: "clip", id: "rec-" + c.name))[
#if c.ready { video(c) } else { placeholder }
#el("figcaption")[#caption(c)]
]
}
]
#el("h2", (class: "install"))[#landing.install]
#include "../typ/install.typ"
#if ready-casts.len() > 0 {
el("link", (rel: "stylesheet", href: "player/asciinema-player.css"))[]
player-script
}
])
#for p in pages.slice(1, -1) {
document(href(p.id), title: p.title, shell(p.id, p.title)[
#metadata(none) #label("start-" + p.id)
#heading(level: 1, p.title) #label("ch-" + p.id)
#toc(p.id)
#{
set heading(offset: 1)
include "../typ/" + p.file
}
#metadata(none) #label("end-" + p.id)
])
}
/// The tutor's text with its mouse buttons drawn: B1, B2, B3 and the
/// chords it writes 1-2 (hold 1, click 2) become the docs' mouse glyphs,
/// each with its hidden words, inside the preformatted block.
#let tutor-text(t) = {
// the guide's sections by title, for "-> Guide: Modes, Config"
let guide-sections = (:)
for m in read("../typ/guide.typ").matches(regex("(?m)^= (.+?) <([a-z0-9-]+)>$")) {
guide-sections.insert(m.captures.at(0), m.captures.at(1))
}
let tokens = regex("\\bB([123])\\b|\\b([123])-([123])\\b|docs/typ/([a-z]+)\\.typ|(?m)(?:^ -> Guide: )(.+)$|https://[^\\s)]+")
let at = 0
for m in t.matches(tokens) {
t.slice(at, m.start)
let (b, c1, c2, page, sections) = m.captures
if b != none { btn("B" + b) }
else if c1 != none { chord("B" + c1, "B" + c2) }
else if page != none { link(page + ".html", m.text) }
else if sections != none {
" -> Guide: "
sections.split(", ").map(name => {
let l = guide-sections.at(name, default: none)
if l != none { link("guide.html#" + l, name) } else { name }
}).join(", ")
}
else { link(m.text, m.text) }
at = m.end
}
t.slice(at)
}
#document(href("tutor"), title: "The tutor", shell("tutor", "The tutor")[
#heading(level: 1)[The tutor] <tutor>
The tutor as the editor shows it (#raw("Tutor"), or #raw("SPC h t")).
#el("pre", (class: "tutor"))[#tutor-text(read(tutor-path))]
])
|