// 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)), )) // " 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] The tutor as the editor shows it (#raw("Tutor"), or #raw("SPC h t")). #el("pre", (class: "tutor"))[#tutor-text(read(tutor-path))] ])