From c779140c015ebcd5f6b2bbfd53862ace7bd48c7e Mon Sep 17 00:00:00 2001 From: Benjamin Mako Hill Date: Fri, 14 Aug 2026 07:21:37 -0700 Subject: [PATCH] Follow beamer's model for notes, and lay the page out like it Notes were rendered as a bullet list, which beamer does not do. Reading beamerbasenotes.sty: a plain \note appends its text to a running block with no separator at all, and \note[item] accumulates separately into an enumerate printed after that block. Match both. talk-notes.sty takes \note[item]{text}, as beamer does, and records which kind it is. The one deviation is a paragraph break between plain notes instead of running them together, which is easier to read on a page. The notes half is laid out like beamer's too: a grey band across the head carrying the section and the position in the talk, the current slide flush into the outer corner, and the notes below. The heading is set in the sans face against the roman body so it reads as apparatus rather than as part of the notes. Co-Authored-By: Claude Opus 5 (1M context) --- slides_template/mknotes | 38 ++++++++++++++++++++++++---------- slides_template/talk-notes.sty | 12 +++++++---- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/slides_template/mknotes b/slides_template/mknotes index 7ecb1f4..571ec99 100755 --- a/slides_template/mknotes +++ b/slides_template/mknotes @@ -44,6 +44,7 @@ class Page: self.section = "" self.subsection = "" self.notes = [] + self.items = [] def parse_notes_file(path): @@ -77,9 +78,10 @@ def parse_notes_file(path): i += 1 continue - match = re.match(r"^NOTE FRAME (\d+)\|SPEC (.*)$", line) + match = re.match(r"^NOTE FRAME (\d+)\|SPEC (.*)\|KIND (.*)$", line) if match: - frame, spec = int(match.group(1)), match.group(2).strip() + frame = int(match.group(1)) + spec, kind = match.group(2).strip(), match.group(3).strip() body = [] i += 1 while i < len(lines) and lines[i].strip() != "ENDNOTE": @@ -90,10 +92,10 @@ def parse_notes_file(path): text = " ".join(part.strip() for part in body).strip() # A note is re-run for every slide of its frame, so the same record # arrives once per slide. Keep the first occurrence only. - key = (frame, spec, text) + key = (frame, spec, kind, text) if key not in seen: seen.add(key) - notes.setdefault(frame, []).append((spec, text)) + notes.setdefault(frame, []).append((spec, kind, text)) continue i += 1 @@ -141,9 +143,12 @@ def attach_notes(pages, notes): last_slide[page.frame] = max(last_slide.get(page.frame, 0), page.slide) for page in pages: - for spec, text in notes.get(page.frame, []): + for spec, kind, text in notes.get(page.frame, []): if spec_matches(spec, page.slide, last_slide[page.frame]): - page.notes.append(text) + if kind == "item": + page.items.append(text) + else: + page.notes.append(text) # ---------------------------------------------------------------- typesetting @@ -243,14 +248,25 @@ def build_notes_pdf(pages, width, height, thumb_w, thumb_h, workdir, body.append(r"\noindent\colorbox{notegrey}{%s}%%" % band) body.append(r"\par\vspace{%.2fbp}%%" % gap) + # Paragraph spacing is set here rather than in the preamble so that it + # separates the notes without also loosening the heading band above. body.append(r"\noindent\hspace{%.2fbp}\begin{minipage}[t]{%.2fbp}" + r"\setlength{\parskip}{0.7em}" % (hmargin, textwidth)) + # As in beamer, plain \note commands run together as text and + # \note[item] ones become a numbered list printed after it. beamer + # concatenates the plain ones with no separator at all; a paragraph + # break between them is easier to read and is the one deviation. if page.notes: - body.append(r"\begin{itemize}\setlength{\itemsep}{0.6em}" - r"\setlength{\leftmargini}{1.2em}") - for note in page.notes: - body.append(r"\item %s" % escape_for_context(note)) - body.append(r"\end{itemize}") + body.append("\n\n".join( + escape_for_context(n) for n in page.notes)) + if page.items: + body.append(r"\par\begin{enumerate}\setlength{\itemsep}{0pt}" + r"\setlength{\parskip}{0pt}" + r"\setlength{\leftmargini}{1.4em}") + for item in page.items: + body.append(r"\item %s" % escape_for_context(item)) + body.append(r"\end{enumerate}") body.append(r"\end{minipage}") if index != total - 1: diff --git a/slides_template/talk-notes.sty b/slides_template/talk-notes.sty index 25e9403..456b668 100644 --- a/slides_template/talk-notes.sty +++ b/slides_template/talk-notes.sty @@ -58,21 +58,25 @@ { SUBSECTION ~ \tl_to_str:N \g__talknotes_subsection_tl } } -% \note[]{text} +% \note[item]{text} +% +% Follows beamer's interface. A plain \note contributes running text; \note[item] +% contributes an entry to a numbered list printed after that text. % % The body is written out stringified, so LaTeX markup inside a note survives % to be typeset again on the notes page. A note is recorded once per slide of % its frame (the frame body is re-run for each), so mknotes deduplicates. % Records are bracketed by ENDNOTE because a long note may be broken across % output lines. -\NewDocumentCommand \note { d<> +m } +\NewDocumentCommand \note { d<> o +m } { \iow_now:Nx \g__talknotes_iow { NOTE ~ FRAME ~ \int_use:N \g__talk_frame_int | - SPEC ~ \IfNoValueTF {#1} { - } { \tl_to_str:n {#1} } + SPEC ~ \IfNoValueTF {#1} { - } { \tl_to_str:n {#1} } | + KIND ~ \IfNoValueTF {#2} { text } { \tl_to_str:n {#2} } } - \iow_now:Nx \g__talknotes_iow { TEXT ~ \tl_to_str:n {#2} } + \iow_now:Nx \g__talknotes_iow { TEXT ~ \tl_to_str:n {#3} } \iow_now:Nn \g__talknotes_iow { ENDNOTE } }