I am not completely satisfied with the two answers that have already been posted, so I’ll try to propose a refinement of the same basic idea. Allow me to explain what I deem to be the relative weaknesses of the other answers.
@cfr’s answer swallows up the whole contents of a section as a macro argument and processes them twice as a token list, the first time in order to decide what to do, and the second time to do the actual typesetting. I think this is the only method that permits to cope with the problem of \includepdf
, since obtaining the correct placement of the included pages entails executing one or more \shipout
at the appropriate times, and \shipout
is a TeX primitive command, so it can only be executed in TeX’s stomach, while digesting tokens (this is not the same way as, e.g., the way in which a \write
is performed). So, in my opinion, @cfr’s answer should be regarded as the most general and powerful solution of the problem, and I suggest that you accept it and award the bounty to her. Notwithstanding this, storing the tokens of a whole section as a macro argument and processing them twice is (1) inefficient, and (2) prone to introducing undesired side effects, as it was the case with the section counters incremented twice; so, we see that it can be worthwhile to look for solutions based on different strategies.
Indeed, the contents of an environment can also be captured at a later point downstream in TeX’s processing line, that is, as a list of boxes; more precisely, contents that (potentially) span several paragraphs can only be collected, in practice, as a vertical list. This is exactly what @ShreevatsaR’s solution does: it measures the height of the collected list and, based on the result of this measurement, hands on to the page builder a suitably manipulated version of that list. Note that this manipulation takes place well downstream of TeX’s stomach in TeX’s processing line, when it is no longer possible to execute primitive commands, and this is the reason that makes it impossible for this solution to cope with the \includepdf
problem: simply, there’s nothing you can put in a list of boxes that can make TeX perform a \shipout
operation at a certain point of that list (again, contrast this with the behavior of \write
).
However, the way in which @ShreevatsaR’s solution implements this general idea is, IMHO, not entirely correct: basically, the main downside that I find in it is that it only considers the natural height of the captured contents, and not their ability to shrink; but there are also a few more problems, that the comments included in the code given below will discuss. For this reason, I dare submit another answer based on the same general principle, but implemented via \vsplit
; it works like this:
it captures the contents of an environment as a vertical list,
exactly as @ShreevatsaR does;
it tries to \vsplit
this list either to the available height
remaining on the current page, or to the full \textheight
(note that \vsplit
will consider the available
shrinkability);
it eventually delivers to the page builder a suitably
manipulated version of the captured list, again exactly as in
@ShreevatsaR’s solution.
Since it is based on the same principle, my solution doesn’t permit to use \includepdf
inside the environment either. I consider it a significant enhancement, though, to have introduced proper management of the interline glue at the environment’s boundaries.
I’ve split the code into two files. The first is a package, named tryfittinginpage
, that defines an environment by the same name, inside which you should put the material that you want to try to make fit in a single page:
\NeedsTeXFormat{LaTeX2e}[2004/06/01] % LaTeX2e is required!
\ProvidesPackage{tryfittinginpage}
[2017/01/17 version information here]
% Diagnostic commands.
\newcommand*\@TFIP@Show[1]{%
\begingroup
\showboxbreadth \@m
\showboxdepth \sixt@@n
#1% what should be shown
\endgroup
}
\newcommand*\ShowList{\@TFIP@Show\showlists}
% In the following command, "\relax" is not needed after "#1" since
% "\endgroup" will follow anyway:
\newcommand*\ShowBox[1]{\@TFIP@Show{\showbox #1}}
\newcommand*\@TFIP@debug@Show[2]{%
\typeout{}%
\typeout{================================================%
================}%
\typeout{#2}%
\typeout{}%
\ShowBox{#1}%
}
\newcommand*\@TFIP@maybe@debug@Show[2]{}
\newcommand*\TFIPDebugInfoOn{%
\let \@TFIP@maybe@debug@Show \@TFIP@debug@Show
}
\newcommand*\TFIPDebugInfoOff{%
\let \@TFIP@maybe@debug@Show \@gobbletwo
}
\DeclareOption{debug}{\TFIPDebugInfoOn}
\DeclareOption{quiet}{\TFIPDebugInfoOff}
\ExecuteOptions{quiet}
\ProcessOptions\relax
\newenvironment*{tryfittinginpage}{%
\par
% The following assignment need not be "\global", bur we prefer to do
% so in order to conserve save stack positions, given the fact that,
% in any case, "\dimen@i" is going to be altered below.
\global \dimen@i \prevdepth
\setbox\z@ \vbox\bgroup % collect environment contents in "\box0"
\prevdepth \dimen@i
}{%
\par
\global \dimen@i \prevdepth
\egroup % end of "\vbox"
\@TFIP@maybe@debug@Show \z@
{Original contents of the environment:}%
\dimen@ \pagegoal
\advance \dimen@ -\pagetotal
% Do not forget that the material accumulated in the page so far can
% cooperate in making our content fit in the current page.
\advance \dimen@ \pagestretch % now "\dimen@" = available space
% Note that we cannot prevent the "\splittopskip" glue from being
% added during a "\vsplit", so we make sure that at least it is zero.
\splittopskip \z@skip
% Use in "\vsplit" the same setting used by the page builder:
\splitmaxdepth \maxdepth
% We'll need to reinsert the discared items if we eventually find that
% the material does not fit:
\savingvdiscards \@ne
% Finally, avoid warnings:
\vbadness \@M
\setbox\tw@ \vsplit\z@ to \dimen@
% Convention: we'll leave in "\box0" the material that should be
% returned to the main vertical list.
\ifvoid\z@ % if everything fits in the current page...
\setbox\z@ \vbox{% ...put it back in "\box0"
\@TFIP@clever@unbox@two \dimen@
}%
\@TFIP@maybe@debug@Show \z@
{It fits in the current page; will append:}%
\else
\@TFIP@reassemble@contents
\@TFIP@maybe@debug@Show \z@
{It does NOT fit in the current page; reassembled contents:}%
% Note that it is wrong, in general, to use the current value of
% "\pagegoal" as the measure of the height of the text.
\setbox\tw@ \vsplit\z@ to \textheight
\ifvoid\z@ % if the contents fit in a single page...
\setbox\z@ \vbox{% ...package them in "\box0" with a page break
% We insert here the equivalent of "\clearpage": the page
% builder will take care of forwarding the "\penalty -\@Mi"
% message to the output routine.
\vfil\break
\write\m@ne{}%
\vbox{}%
\penalty -\@Mi
\@TFIP@clever@unbox@two \textheight
}%
\@TFIP@maybe@debug@Show \z@
{It fits in the next page; will append:}%
\else % if the oontents do not fit in a single page...
\@TFIP@reassemble@contents % ...put them back in "\box0"
\@TFIP@maybe@debug@Show \z@
{It does NOT fit in a page by itself; will append:}%
\fi
\fi
\unvbox\z@
\prevdepth \dimen@i
}
\newcommand*\@TFIP@reassemble@contents{%
% This macro reassembles in "\box0" the original contents of the
% environment after they have been split.
\setbox\z@ \vbox{% now be careful:
\unvbox\tw@ % split-off material
\splitdiscards % includes chosen breakpoint
\nobreak % because the "\splittopskip" glue follows, and of course
% it hadn't been considered as a possible breakpoint in
% the previous steps...
\unvbox\z@ % rest of the contents
}%
}
\newcommand*\@TFIP@clever@unbox@two[1]{%
% This macro unboxes "\box2" while taking care of the following
% problem: if "\vsplit" shrank the contents in order to make them fit
% in the given height (and note that "\vsplit" assumes an implicit
% "\penalty -10000" at the end of the vertical list being split, see
% "The TeXbook", p. 124, lines 6-7), we need to be sure that the page
% builder will choose the same breakpoint; for this, we insert
% "\break" at the end of the current list, but only if the natural
% height of "\box2" exceeds the dimension passed as argument #1.
\ifdim\ht\tw@>#1% assume #1 is a <dimen> token
\unvbox\tw@
\break
\else
\unvbox\tw@
\fi
}
\endinput
Save this code as tryfittinginpage.sty
and put it in a place where TeX looks for input files (e.g., in the same directory as the file you want to compile). Note that the code includes some diagnostic commands you could very well get rid of.
The second file is an ordinary LaTeX document that invokes the tryfittinginpage
package and tests it:
% My standard header for TeX.SX answers:
\documentclass[a4paper]{article} % To avoid confusion, let us explicitly
% declare the paper format.
\usepackage[T1]{fontenc} % Not always necessary, but recommended.
% End of standard header. What follows pertains to the problem at hand.
\usepackage{tryfittinginpage}
\usepackage{caption}
\usepackage{pdfpages}
\usepackage{mwe}
\begin{document}
\tableofcontents
\listoffigures
\section{Some recipes}
\lipsum[1-2]
\begin{figure}[tbp]
\centering
\includegraphics{image}
\caption{An insertion that makes things a little bit more difficult}
\label{fig:image}
\end{figure}
The presence of figure~\ref{fig:image} explains why it is necessary to ensure
that dangling insertions are processed before the contents of our environment
get re-inserted.
% \showthe\prevdepth
%
% \TFIPDebugInfoOn
\begin{tryfittinginpage}
\subsection{Sachertorte} % :-)
\lipsum[3]
\begin{center}
\includegraphics{image-a}
\captionof{figure}{Is this a Sachertorte?}
\end{center}
\lipsum[4]
\end{tryfittinginpage}
Note that some ordinary text may follow on the same page.
\par\lipsum[5-7]
\begin{tryfittinginpage}
\subsection{Gnocchi alla bolognese}
This is expected to fit in the same page.
\par\lipsum[8]
\end{tryfittinginpage}
% \TFIPDebugInfoOff
Again, ordinary text may follow (of course, it's always possible to prevent this
from happening by inserting an explicit \verb|\pagebreak| immediately after the
end of the environment.
\begin{tryfittinginpage}
Now, a section that will not fit in a single page.
\subsection{Canard \`{a} l'orange}
\lipsum[9-16]
\begin{center}
\includegraphics{image-b}
\captionof{figure}{And this? Could it ever be a canard?}
\end{center}
\end{tryfittinginpage}
\section{Technical trials}
Here we check some technical issues.
\begin{tryfittinginpage}
\subsection{Page breaks are honored}
Without the page breaks\ldots
\newpage
\ldots this section would almost certainly\ldots
\newpage
\ldots fit in a single page (but not at the end of the page on which the
environments starts, because of the ``lipsum'' text).
\par\lipsum[17-18]
\end{tryfittinginpage}
\newpage
Text between two sections.
\lipsum*[19]
This text starts on a new page and continues until it gets to the eighth line.
\begin{tryfittinginpage}
\subsection{Without shrinkability}
This section contains an unshrinkable vertical space that prevents it from
fitting into a single page. Lorem ipsum adipisci elit num. This text takes
up three lines, no more and no less.
\par\vspace{.9\textheight}
This text comes at the end of a section, after a big, unshrinkable vertical
space. Lorem ipsum dolor sit amet.
\end{tryfittinginpage}
\newpage
Now we repeat the test under the same conditions, but with shrinkability.
\lipsum*[19]
This text gets to the eighth line too.
\begin{tryfittinginpage}
\subsection{With shrinkability}
This section contains a shrinkable vertical space with the same natural
length as the space contained in the previous section. This text takes up
three lines, no more and no less.
\par\vspace{.9\textheight minus .1\textheight}
This text comes at the end of a section, after a big, but shrinkable
vertical space. Lorem ipsum dolor sit amet.
\end{tryfittinginpage}
\subsection{Including pages}
Unfortunately, it is possible to include a whole page with \verb|\includepdf|
only when you are outside the \texttt{tryfittinginpage} environment.
\includepdf{image-a4}
\lipsum[20]
\end{document}
\usepackage[all]{nowidow}
? Maybe it suits your needs...