Fix infinite recursion in the \caption redefinition (#316) - #317
Open
wkjarosz wants to merge 1 commit into
Open
Conversation
kao.sty detects whether a float contains a \caption so that uncaptioned
floats can be reset with \RawFloats. This was implemented by saving \caption
and replacing it from inside \AtBeginEnvironment for figure and table:
\let\oldcaption\caption
\RenewDocumentCommand{\caption}{s o m}{... \oldcaption ...}
Because the save happens every time a float begins, the hook can run at a
point where \caption has already been replaced, so the replacement is saved
as its own target and calls itself indefinitely. On LaTeX 2026-06-01 with
KOMA-Script 3.49.2 this happens for any float carrying a caption, and the
run aborts with
! TeX capacity exceeded, sorry [parameter stack size=20000].
Two of the four bundled examples, documentation and
machine_learning_project, currently fail to build for this reason.
Instead of re-wrapping \caption per float, the flag is now raised by a hook
attached to \caption once. Nothing is re-saved, so the recursion cannot
arise. Three details are required for this to work and are documented beside
the code:
* The hook must be attached after the preamble: the caption package
installs its own \caption from an \AtBeginDocument hook and would
otherwise discard it. \AfterEndPreamble is used rather than
\AddToHook{begindocument/end} so that no LaTeX newer than the current
\NeedsTeXFormat{LaTeX2e} is required.
* The flag must be set \global, since \caption runs several groups deep
inside floatrow's boxes; a local flag is restored before the
end-of-environment hook reads it, which surfaces as floatrow's
"Caption(s) lost".
* The flag is cleared when a float begins rather than when it ends, so a
captioned marginfigure cannot leave it raised for the next float.
The etoolbox toggle is replaced by a plain \newif, because etoolbox toggles
are group-local by design and expose no public global setter.
wkjarosz
marked this pull request as ready for review
August 14, 2026 21:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Disclaimer: this fix was written with the help of Claude Opus 5, and while I have been writing LaTeX for 20+ years, I do not have much experience writing complex templates. I therefore do not personally understand every detail of the mechanism it touches. I am opening this PR anyway because it makes a problem go away, it is small, and I have checked the results fairly carefully — but please review it with that in mind rather than taking my word for it. I am happy to hand it over, adjust it, or close it if you would rather fix this differently.
The problem
On LaTeX 2026-06-01 with KOMA-Script 3.49.2, any float carrying a caption aborts the run with
Two of the four bundled examples —
documentationandmachine_learning_project— currently fail to build for this reason. This is issue #316, and #308 looks like the same thing.Why it happens
kao.styneeds to know whether a float contains a\caption, so that uncaptioned floats can be reset with\RawFloats. It worked this out by saving\captionunder another name and replacing it, from inside\AtBeginEnvironmentforfigureandtable:The save happens every time a float begins. On these newer releases the hook can run at a moment when
\captionhas already been replaced — so the replacement gets saved as its own target. It then calls itself, forever.The fix
Stop replacing
\captionper float. Attach the flag-raising to\captiononce, and leave it there. Nothing is ever re-saved, so the recursion cannot occur at all.Three details are needed to make that work. Each of them silently breaks the detection in a different way if you get it wrong, so they are commented in the source:
captionpackage installs its own\captionfrom an\AtBeginDocumenthook, and discards anything attached before that. Instrumenting an earlier attempt showed the hook firing zero times while captions were plainly being typeset.\AfterEndPreamble(etoolbox, already a dependency) is used rather than\AddToHook{begindocument/end}so that nothing newer than the current\NeedsTeXFormat{LaTeX2e}is required.\global.\captionruns several groups deep inside floatrow's boxes, so a group-local flag is restored before the end-of-environment hook can read it. That failure mode shows up as floatrow'sCaption(s) lost.marginfigureleaves it raised and the next uncaptioned float is mistaken for a captioned one.One API change: the etoolbox toggle
kaocaptionbecomes a plain\newif\ifkaosawcaption, because etoolbox toggles are group-local by design and expose no public global setter. Happy to keep the old name defined as well if you would prefer not to drop it.Testing
Bundled examples, before and after, LaTeX 2026-06-01 / KOMA-Script 3.49.2:
documentationmachine_learning_projectminimal_bookminimal_reportThe two that already built are byte-identical when rendered to images page by page, so this restores the detection without changing any output.
Also exercised, all with no errors:
kaobookone-sided and two-sided,kaohandt, a4/11pt,lstlistingandmarginlisting, and a document covering captioned and uncaptionedfigures andtables, a captionedmarginfigurefollowed by an uncaptionedfigure, and\caption*.Separately, on a ~90 page book of my own: it could not be compiled locally at all before this change, and afterwards renders byte-identically to a reference build with the whole detection mechanism removed. That book builds under both pdfLaTeX and XeLaTeX.
Tested under TeX Live 2026 locally, and under TeX Live 2025, 2022 and 2021 via Overleaf.
What I have not tested
instructions/material.A note on the suggestion in #316
The
\pretocmd-based approach sketched in the issue is the right idea, but as written it runs into points 1 and 2 above: attached from\AtBeginDocumentit is discarded by thecaptionpackage, and with a group-local toggle it producesCaption(s) lostinstead of the crash. Recording that here in case it saves someone the same detour.