20
0

Add speaker notes for ltx-talk, captured and assembled outside LaTeX

ltx-talk has no \note command. Its README and website list speaker notes
among what the class is for, but the feature is unwritten: the request is
open upstream as issue #156, where the maintainer has said notes are
wanted but that the interface needs thought first. Nothing has landed as
of v0.5.3.

So notes are captured rather than typeset. talk-notes.sty defines \note
to contribute nothing to the slides and instead record its text, and
mknotes reads that afterwards to assemble the presenter PDF: slide on the
left, and on the right a grey header band, a thumbnail of the current
slide flush into the corner, and the notes below. Previous and next
slides are left to dspdfviewer and pdfpc, which show them already.

The hard part is that a frame is not a page, since overlays expand one
frame into an unpredictable number of slides. Records are therefore
written from the shipout hook, the first point at which the frame and
slide identity of a page is settled, which makes the mapping exact rather
than guessed. Notes honour the usual overlay specifications, so
\note<1>{...} appears beside only the first slide of its frame.

The assembled PDF is not tagged and is not meant to be shared; only the
slides build is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 19:12:03 -07:00
parent 9c473a8419
commit 2b0c14209a
2 changed files with 418 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
% talk-notes.sty -- capture speaker notes for assembly outside LaTeX.
%
% The ltx-talk class has no \note command (upstream issue #156 is open, and the
% maintainer has said notes are wanted but unimplemented). This package fills
% the gap without touching the slides themselves: \note{...} typesets nothing
% and instead records its text to \jobname.notes, which the mknotes program
% reads to build the presenter PDF.
%
% The awkward part of doing this outside LaTeX is that a frame does not
% correspond to a page: overlays expand one frame into several slides, and the
% source cannot predict how many. That is why the page records are written from
% the shipout hook, which is the first point at which the frame and slide
% identity of a page is actually known.
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{talk-notes}[2026-08-14 v0.1 Capture speaker notes for ltx-talk]
\ExplSyntaxOn
\iow_new:N \g__talknotes_iow
\tl_new:N \g__talknotes_section_tl
\tl_new:N \g__talknotes_subsection_tl
\AddToHook { begindocument }
{ \iow_open:Nn \g__talknotes_iow { \c_sys_jobname_str .notes } }
% The stream is deliberately not closed by hand: LaTeX closes it at the end of
% the run, and closing it from the enddocument hook races the final shipout.
% Track the section and subsection so the notes page can show where we are.
% ltx-talk exposes the current title as \l__talk_section_tl; guard the lookup
% so that a class change does not turn this into an error.
\AddToHook { section/begin }
{
\cs_if_exist:NT \l__talk_section_tl
{
\tl_gset:NV \g__talknotes_section_tl \l__talk_section_tl
\tl_gclear:N \g__talknotes_subsection_tl
}
}
\AddToHook { subsection/begin }
{
\cs_if_exist:NT \l__talk_subsection_tl
{ \tl_gset:NV \g__talknotes_subsection_tl \l__talk_subsection_tl }
}
% One record per shipped page.
\AddToHook { shipout/before }
{
\iow_now:Nx \g__talknotes_iow
{
PAGE ~ \arabic { page } |
FRAME ~ \int_use:N \g__talk_frame_int |
SLIDE ~ \int_use:N \g__talk_slide_int
}
\iow_now:Nx \g__talknotes_iow
{ SECTION ~ \tl_to_str:N \g__talknotes_section_tl }
\iow_now:Nx \g__talknotes_iow
{ SUBSECTION ~ \tl_to_str:N \g__talknotes_subsection_tl }
}
% \note[<overlay spec>]{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 }
{
\iow_now:Nx \g__talknotes_iow
{
NOTE ~ FRAME ~ \int_use:N \g__talk_frame_int |
SPEC ~ \IfNoValueTF {#1} { - } { \tl_to_str:n {#1} }
}
\iow_now:Nx \g__talknotes_iow { TEXT ~ \tl_to_str:n {#2} }
\iow_now:Nn \g__talknotes_iow { ENDNOTE }
}
\ExplSyntaxOff