From 2910a2a800718ec5df4f6437ad760085c64ffac5 Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Thu, 1 Oct 2026 20:40:55 +0200 Subject: [PATCH] slides: light theme, final edits, single-file bundle As presented. The palette is light on request of the reviewer; code blocks and byte dumps keep a dark ground so the byte colours still work. The OPEN walk lights the hovered bytes as an inverted block and its table cells are dark text on a tint of their group colour. build.py inlines reveal.js, the stylesheets, scripts and Markdown sections into slides/dist/hands-on-bgp.html for uploading. Co-Authored-By: Claude Fable 5.1 --- slides/README.md | 14 +++++ slides/build.py | 89 ++++++++++++++++++++++++++++ slides/dns-lookup.js | 35 +++++++++++ slides/index.html | 10 ++-- slides/sections/01-resources.md | 8 ++- slides/sections/03-bgp-update.md | 77 ++++++++++++++++++------ slides/sections/04-transit-and-ix.md | 26 +++++--- slides/sections/05-lab.md | 36 +++++------ slides/style.css | 72 ++++++++++++++++++---- slides/theme.css | 69 +++++++++++---------- 10 files changed, 338 insertions(+), 98 deletions(-) create mode 100755 slides/build.py diff --git a/slides/README.md b/slides/README.md index 3479ba8..24d84e9 100644 --- a/slides/README.md +++ b/slides/README.md @@ -105,6 +105,20 @@ page (add `focus`, and `on` to the spans to keep lit, to dim the rest), `` drops the panel, the motif and the wordmark for one slide, e.g. a full-bleed image. The title slide does so by itself. +## Publish as one file + + slides/vendor-reveal.sh # once, if slides/reveal.js is missing + python3 slides/build.py # writes slides/dist/hands-on-bgp.html + +`build.py` inlines everything the page would otherwise fetch: reveal.js and +its plugins, the stylesheets, the favicon, our scripts and the Markdown +sections. The one file (about 1.3 MB) can be uploaded anywhere and also +opens straight from disk. It still uses the network for two things: the IBM +Plex fonts from Google Fonts (without them the browser's fallback fonts are +used) and the live DNS and RIPE lookups in section 1. Speaker view (`S`) +works from the bundle too. `slides/dist/` is not committed; rebuild after +editing. + ## PDF Open in Chrome and print to PDF with diff --git a/slides/build.py b/slides/build.py new file mode 100755 index 0000000..665cc33 --- /dev/null +++ b/slides/build.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Bundle the deck into one self-contained HTML file for uploading. + + python3 slides/build.py # writes slides/dist/hands-on-bgp.html + +Everything the page fetches at runtime is inlined: reveal.js and its plugins +(from the vendored copy, so run vendor-reveal.sh first), the stylesheets, the +favicon, our scripts and the Markdown sections. The result opens from any web +server and from disk. Two things still need a network: the IBM Plex fonts +(Google Fonts) and the live DNS and RIPE lookups. +""" +import base64 +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +REVEAL = os.path.join(HERE, "reveal.js", "dist") +OUT = os.path.join(HERE, "dist", "hands-on-bgp.html") + + +def read(*parts): + with open(os.path.join(HERE, *parts), encoding="utf-8") as f: + return f.read() + + +def main(): + if not os.path.isdir(REVEAL): + sys.exit("reveal.js is not vendored; run slides/vendor-reveal.sh first") + html = read("index.html") + + # Stylesheets: reveal's first, then ours, as the loader orders them. + css = "\n".join(read("reveal.js", "dist", p) for p in ["reset.css", "reveal.css", "plugin/highlight/monokai.css"]) + css += "\n" + read("theme.css") + "\n" + read("style.css") + html = re.sub(r'\s*', "", html) + html = re.sub(r'\s*', f"\n ", html, count=1) + + icon = "data:image/svg+xml;base64," + base64.b64encode(read("images", "favicon.svg").encode()).decode() + html = html.replace('href="images/favicon.svg"', f'href="{icon}"') + + # Sections: the Markdown goes inline in a text/template script. + def inline_section(m): + attrs, path = m.group(1), m.group(2) + text = read(path) + if "\n\n' + html, n = re.subn(r'', + lambda m: inline_section(type("M", (), {"group": lambda self, i: (None, m.group(2), m.group(1))[i]})()), html) + if n == 0: + sys.exit("no data-markdown sections found in index.html") + + # Our scripts. + for name in ["dns-lookup.js", "prefix-explorer.js"]: + html, k = re.subn(rf'', lambda m: f"", html) + if k != 1: + sys.exit(f"expected one ', "", html, count=1, flags=re.S) + + # The loader: the module script that picks a reveal.js location and + # imports it becomes reveal.js itself, its plugins, and the deck setup. + m = re.search(r'', html, flags=re.S) + if not m: + sys.exit("loader script not found") + loader = m.group(1) + setup_start = loader.index("// BIRD configuration") + setup = loader[setup_start:] + setup = setup.replace("await deck.initialize().catch(fail(\"reveal.js failed to start\"));", "deck.initialize().then(() => {") + setup = setup.replace("plugins: [Markdown, Highlight, Notes],", "plugins: [RevealMarkdown, RevealHighlight, RevealNotes],") + setup = setup.rstrip() + "\n });\n" + libs = "\n".join(read("reveal.js", "dist", p) for p in ["reveal.js", "plugin/markdown.js", "plugin/highlight.js", "plugin/notes.js"]) + html = html[: m.start()] + f"\n " + html[m.end():] + if "await " in setup or "import(" in setup: + sys.exit("loader still contains module-only code") + + html = html.replace("reveal.js is loaded from ./reveal.js/dist if present", "Built by build.py from index.html: everything is inlined. reveal.js was loaded from ./reveal.js/dist if present") + + os.makedirs(os.path.dirname(OUT), exist_ok=True) + with open(OUT, "w", encoding="utf-8") as f: + f.write(html) + print(f"wrote {os.path.relpath(OUT)} ({os.path.getsize(OUT) // 1024} KB, {n} sections)") + + +if __name__ == "__main__": + main() diff --git a/slides/dns-lookup.js b/slides/dns-lookup.js index d040890..0d0d7f5 100644 --- a/slides/dns-lookup.js +++ b/slides/dns-lookup.js @@ -103,3 +103,38 @@ relayout(); }); })(); + +// Byte walks: hovering (or tabbing to) a field row lights its bytes in the +// dump above it, and hovering bytes in the dump lights their row and scrolls +// it into view, so the dump can be used as the index while presenting. +(function () { + const pair = (el) => { + const walk = el.closest(".bytewalk"); + if (!walk) return []; + const i = el.dataset.i; + return [walk.querySelector(`.bw-dump span[data-i="${i}"]`), walk.querySelector(`.bw-row[data-i="${i}"]`)].filter(Boolean); + }; + const target = (e) => e.target.closest && e.target.closest(".bw-row, .bw-dump span[data-i]"); + const on = (e) => { + const el = target(e); + if (!el) return; + const both = pair(el); + both.forEach((x) => x.classList.add("on")); + if (el.matches(".bw-dump span")) { + // Scroll the box itself, and instantly: scrollIntoView would also + // move the slide, and Chrome drops smooth scrolls inside the + // transformed slide that reveal positions. + const row = both.find((x) => x.classList.contains("bw-row")); + const box = row && row.closest(".bw-rows"); + if (box) box.scrollTop = row.offsetTop - box.clientHeight / 2 + row.offsetHeight / 2; + } + }; + const off = (e) => { + const el = target(e); + if (el) pair(el).forEach((x) => x.classList.remove("on")); + }; + document.addEventListener("mouseover", on); + document.addEventListener("focusin", on); + document.addEventListener("mouseout", off); + document.addEventListener("focusout", off); +})(); diff --git a/slides/index.html b/slides/index.html index f0029d7..77cd7c7 100644 --- a/slides/index.html +++ b/slides/index.html @@ -15,8 +15,8 @@ stylesheets are inserted BEFORE ours by the loader below, so theme.css and style.css always have the last word. --> - - + + @@ -29,8 +29,8 @@

Hands-on BGP

Insights into real world operations

@@ -113,7 +113,7 @@ })(); - +