// 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-`. #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 , 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: 62034424, sha256: "9db75765a8ad2fc881164b98ff69f0d34ded582304428c28aee7f87ccb211061"), (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: 66620552, sha256: "d788213d14e69ee32348cee9b707b046c71c208c6436d84988e76aaf117095da"), ), ), ) /// A glossary: like #pairs, under its own heading. On paper the heading /// and the table; in HTML a
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 ] 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 ] 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) } }