commit - a0b9e5dea49c842bbb03baa65d0de54e01acdccc
commit + b1f38837a95ad615458029e67dd2f3c8a40702f3
blob - 831bfad5f935135afedacb20444085599ba04f44
blob + c13d40130ab9288d30d00873846d76289864ac96
--- .gitignore
+++ .gitignore
# Nix
/result
/result-*
+
+# personal example presentations, kept local only
+/examples/arquimedes/
+/examples/matlab-laser-mic/
blob - ab4242d360f3c856a526cb5e59ad618c46cfe834
blob + 2fd121479cbecb4be3d91cad9ff1b7e712aff070
--- internal/bib/cite.go
+++ internal/bib/cite.go
var citeRe = regexp.MustCompile(`\[@([A-Za-z0-9_:.-]+)\]`)
// Process scans slides in order for [@key] citations, numbers them by
-// first appearance, rewrites each [@key] into a [n] styled with the
-// theme's link_text color (a real markdown link to a #key anchor -- it
-// never resolves to anywhere, it's just how glamour picks up the color,
-// same trick as everything else in this fork that wants themed styling
-// without hardcoding a color), and appends a References slide listing
-// every cited entry in citation-number order. Citation keys not found in
-// entries are left as literal [@key] text, a visible signal that
-// something's missing rather than a silent drop.
+// first appearance, rewrites each [@key] into a [n] markdown link to a
+// #key anchor (it never resolves anywhere -- it's just how glamour picks
+// up the theme's link_text color without hardcoding one), and appends a
+// References slide listing every cited entry in citation-number order.
+// Citation keys not found in entries are left as literal [@key] text.
func Process(slides []string, entries map[string]Entry) []string {
numbers := make(map[string]int)
var order []string
blob - a78c4a610dfa667a4892c3db19a9f03a377645b1
blob + f1f8e8365a4ecf0f3b58528c1fd588b9f75ef57a
--- internal/bib/parse.go
+++ internal/bib/parse.go
Fields map[string]string
}
-// ParseFile reads and parses a .bib file.
func ParseFile(path string) (map[string]Entry, error) {
f, err := os.Open(path)
if err != nil {
blob - 759d1a43a60d40455242080b54222905f85546e4
blob + 7f505567cf54150d321c3de354850408031306e9
--- internal/image/image.go
+++ internal/image/image.go
// resolved (lookup returns ok) with a sentinel line, plus a caption
// paragraph if present. References not yet resolved get loading (e.g. a
// spinner frame) instead, plus the caption if any -- callers only reach
-// here once they already know graphics are supported, so "not ready" only
-// ever means "still decoding", never "never will". The returned
-// placements are indexed exactly as the sentinels appear, for Expand to
-// consume after markdown rendering.
+// here once they already know graphics are supported, so "not ready"
+// only ever means "still decoding". The returned placements are indexed
+// exactly as the sentinels appear, for Expand to consume after markdown
+// rendering.
func Preprocess(markdown, loading string, lookup func(Ref) (Entry, bool)) (string, []Placement) {
var placements []Placement
for _, ref := range Parse(markdown) {
blob - 408a82584e7d1dac81abd7bee7bf905b2182a1cd
blob + 72b5f2ddc5df96f6deb9fa698fc274c3ea83ebf6
--- internal/image/kitty.go
+++ internal/image/kitty.go
// PlaceholderBlock returns the Unicode-placeholder text for a placed
// image: only the first cell of each row carries the row+column
-// diacritics, the terminal infers the rest from position -- validated
-// against Bubble Tea v2's cell-diffing renderer in cmd/spike-images.
+// diacritics, the terminal infers the rest from position.
func PlaceholderBlock(id, cols, rows int) string {
fg := fgSGR(id)
const reset = "\x1b[0m"
blob - b1e9d434686c65576176dc14ef4aafa9578116c5
blob + b2c3d3f78e161755025b6fe027b53a4b02d925bb
--- internal/latex/unicode.go
+++ internal/latex/unicode.go
"deg": true, "dim": true, "ker": true, "hom": true,
}
-// accentSuffix covers the physics notation combining marks can't:
-// combining-mark glyphs (arrow/hat/dot above) get silently dropped
-// somewhere in slides' own render pipeline (confirmed: they show up fine
-// printed directly to the terminal, but not once routed through
-// glamour/reflow), so these decorate their argument with an ordinary,
-// non-combining suffix character instead -- never as good-looking as a
-// mark sitting directly above the letter, but guaranteed to actually
-// show up.
+// accentSuffix covers physics vector/derivative notation with a
+// non-combining suffix: combining-mark glyphs get silently dropped
+// somewhere in slides' render pipeline (glamour/reflow), so a mark
+// directly above the letter isn't an option here.
var accentSuffix = map[string]string{
"vec": "→", "hat": "^", "dot": "′", "ddot": "″",
}
blob - eebe29b53bffdb5c59f459bb4faaea9a769ed017
blob + ce607787710a9f3f38f76d0b18321a4ac18023d4
--- internal/model/model.go
+++ internal/model/model.go
}
// reservedRows is a fixed allowance for whatever text shares the slide
-// with a figure -- title, intro sentence, caption, status bar. A fixed
-// row count holds up better than a fraction of the viewport once the
-// viewport gets small (zooming in shrinks the cell grid), where a
-// percentage-based cut stops leaving enough room for fixed-size text.
-// It's still just a budget: a slide that also stacks a full table (or
-// more) alongside the figure can still run past it -- JoinVertical clips
-// the overflow so the status bar never drifts, but the figure itself may
-// get visually cut off. Splitting such a slide in two is the real fix.
+// with a figure -- title, intro sentence, caption, status bar. Fixed
+// rather than a fraction of the viewport, since a percentage stops
+// leaving enough room for fixed-size text once the viewport gets small
+// (e.g. after zooming in).
const reservedRows = 10
// mediaSize returns the column/row budget for a single image or LaTeX
blob - bba8a9eb08582a14e32b58443ce217d040f58c29
blob + c0d60cd468ff6ad414be7d008b573a36aa8580b0
--- styles/styles.go
+++ styles/styles.go
}
// JoinVertical joins two strings vertically and fills the space in-between,
-// centering top within it rather than pinning it to the first line -- on
-// a tall terminal, a short slide otherwise reads as stranded at the top
-// with a large dead gap below it.
+// centering top rather than pinning it to the first line.
//
-// top is always clipped to at most height-lipgloss.Height(bottom) lines:
-// PlaceVertical is a no-op when its content is already taller than the
-// requested height, so without this, bottom (the status bar) would drift
-// away from the last line whenever top overflows -- which becomes more
-// likely the smaller the viewport gets, e.g. after zooming in.
-//
-// The concatenation below relies on top's own last line being blank: with
-// no room left to spare, that's where bottom's leading (padding) line
-// merges in. When clipping kicks in, the last surviving line is real
-// content instead, so it's swapped for an explicit blank one to keep
-// that seam clean rather than smushing text into the status bar.
+// top is clipped to at most height-lipgloss.Height(bottom) lines, since
+// lipgloss.PlaceVertical is a no-op once its content already exceeds the
+// requested height -- without the clip, bottom (the status bar) would
+// drift away from the last line whenever top overflows (e.g. after
+// zooming in shrinks the viewport). The last surviving line is swapped
+// for a blank one so bottom's own leading blank line merges into it
+// cleanly instead of smushing into real content.
func JoinVertical(top, bottom string, height int) string {
h := height - lipgloss.Height(bottom)
if h < 0 {