A frame whose only <2> sat on a \note lost its second slide: beamer counts note specs toward the slide count, and talk-notes did not. Feed the spec through the class's overlay test, discarding the result; its side effect asks for the slides the spec names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
92 lines
3.4 KiB
TeX
92 lines
3.4 KiB
TeX
% 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>[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<> o +m }
|
|
{
|
|
% Run the spec through the class's overlay test, discarding the result:
|
|
% its side effect asks for the slides the spec names, so a frame whose
|
|
% only <2> is on a note still gets two slides, as under beamer.
|
|
\IfNoValueF {#1}
|
|
{
|
|
\cs_if_exist:NT \__talk_if_overlay:nT
|
|
{ \__talk_if_overlay:nT {#1} { } }
|
|
}
|
|
\iow_now:Nx \g__talknotes_iow
|
|
{
|
|
NOTE ~ FRAME ~ \int_use:N \g__talk_frame_int |
|
|
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 {#3} }
|
|
\iow_now:Nn \g__talknotes_iow { ENDNOTE }
|
|
}
|
|
|
|
\ExplSyntaxOff
|