6

A colleague asked this.

Can you automatically demand that items in an enumitem custom list should not spread across a page break? Say this is for an exam where it is preferred that each question is displayed all together, and that questions are not longer than a page.

A simple version of the original context is below.

\documentclass{article}
\usepackage[english]{babel} % needed by blindtext
\usepackage[pangram]{blindtext} % to produce dummy text only
\usepackage{enumitem}
\newlist{questions}{enumerate}{3}
\setlist[questions,1]{label*=\textbf{\arabic*}.}
\setlist[questions,2]{label=(\alph{questionsii})}
\setlist[questions,3]{label=(\roman{questionsiii})}

\begin{document}
    ~\vspace{15cm} % to push the text near the page break
    
    \begin{questions}
        \item \Blindtext[1][4]
        \item \Blindtext[2][4]
        \begin{questions}
            \item \Blindtext[1][4]
            \item \Blindtext[1][4]
            \item \Blindtext[1][4]
        \end{questions}
    \end{questions}
\end{document}

I have a couple of ideas, which I'll post here as answers because (a) he said his searching on this question hadn't led him to anything he could get working; (b) I'm interested in other solutions to the problem that might be better.

2
  • I don't know about automatic handling of your formatting objective. To do it manually, one could load the needspace package and issue the instruction Needspace{6\baselineskip} right before \item \Blindtext[2][4]. That said, I can't see what's supposed to be so objectionable about a mid-paragraph page break.
    – Mico
    Jun 9 at 17:43
  • Interesting, thanks. With needspace though I'd need to know how many lines my question item was going to be, which feels like quite a lot of additional hassle. The context is something like an exam where there might be 4-6 questions and each question might take up a third to a half of a page and it's considered clearer for students (who might be extremely stressed by the exam) to keep all parts of a single question on the same page. Jun 9 at 17:48

2 Answers 2

6

This should work in most cases. We set \interlinepenalty=10000 in the questions environment and also add \nobreak after \par; since \item issues a penalty, a break will be possible between items.

\documentclass{article}
\usepackage[english]{babel} % needed by blindtext
\usepackage[pangram]{blindtext} % to produce dummy text only
\usepackage{enumitem}
\newlist{questions}{enumerate}{3}
\setlist[questions,1]{label*=\textbf{\arabic*}.}
\setlist[questions,2]{label=(\alph{questionsii})}
\setlist[questions,3]{label=(\roman{questionsiii})}

\makeatletter
\AddToHook{env/questions/begin}{%
  \ifnum\enitdp@questions=0
    \interlinepenalty=10000
    \AddToHook{para/after}[nobreak]{\nobreak}%
  \fi
}
\AddToHook{env/questions/end}{%
  \ifnum\enitdp@questions=1
    \RemoveFromHook{para/after}[nobreak]%
  \fi
}
\makeatother

\begin{document}

\vspace*{15cm} % to push the text near the page break
    
    \begin{questions}
        \item \Blindtext[1][4]
        \item \Blindtext[2][4]
        \begin{questions}
            \item \Blindtext[1][4]
            \item \Blindtext[1][4]
            \item \Blindtext[1][4]
        \end{questions}
    \end{questions}

\end{document}

Due to the possibility of nesting questions we want to add to the hooks just once, so we do it when the depth counter is zero (as it is at the moment env/questions/begin is looked at. The removal of the \nobreak is done only when the depth counter is one, that is, at the \end{questions} for the outer level.

enter image description here

5
  • Is \AddToHook local? I never knew.
    – gusbrs
    Jun 9 at 21:37
  • @gusbrs Oops. Really fixed now.
    – egreg
    Jun 9 at 22:13
  • You had made me go "Wow, new possibilities!" :-)
    – gusbrs
    Jun 9 at 22:23
  • Thanks, that's really interesting and looks good because it completely preserves the use of \begin{questions}. Could you elaborate on what you mean by "This should work in most cases"? Are you aware of edge cases where it doesn't, or just concerned such an edge case might exist? Jun 10 at 5:12
  • @PeterRowlett More testing should be done with real documents in order to lift the doubt.
    – egreg
    Jun 10 at 7:24
1

To get a block of text to not break across pages you can put it inside a \begin{minipage}...\end{minipage} environment. A few ways I can think to do this. All these produce the same result, the difference is in the implementation. I think I prefer option 2, which is more complicated in the preamble but seems to me simpler to use.

1. Putting each item in its own minipage.

Basically, you need to do this, and can do so manually:

\begin{questions}
    \begin{minipage}{\linewidth}
        \item question text
    \end{minipage}
\end{questions}

This is perhaps a bit of a faff to remember, so you could define a new command to do this for you. This could be done either as an environment or as a command, though note both would change the usual practice of using \item text because a minipage has a beginning and an end, whereas \item just has a beginning.

1a. As an environment.

We define an environment called nobreakitem in the premble. This environment is used instead of the \item command. The two line breaks before \begin{minipage} are necessary because otherwise it thinks the minipage is part of the previous \item and doesn't put a line break between them. It could be just used for a problematic \item, or for all \items. This, to me, is a bit weird because you are doing \begin and \end instead of the usual \item and it's not saving much vs. writing the above code yourself.

\documentclass{article}
\usepackage[english]{babel} % needed by blindtext
\usepackage[pangram]{blindtext} % to produce dummy text only
\usepackage{enumitem}
\newlist{questions}{enumerate}{3}
\setlist[questions,1]{label*=\textbf{\arabic*}.}
\setlist[questions,2]{label=(\alph{questionsii})}
\setlist[questions,3]{label=(\roman{questionsiii})}

\newenvironment{nobreakitem}{
    
\begin{minipage}{\linewidth}\item}{\end{minipage}}

\begin{document}
    ~\vspace{15cm} % to push the text near the page break
    
    \begin{questions}
        \begin{nobreakitem}
            \Blindtext[1][4]
        \end{nobreakitem}
        \begin{nobreakitem}
            \Blindtext[2][4]
            \begin{questions}
                \item \Blindtext[1][4]
                \item \Blindtext[1][4]
                \item \Blindtext[1][4]
            \end{questions}
        \end{nobreakitem}
    \end{questions}
\end{document}

1b. As a command.

We define a command called \nobreakitem in the premble. The standard \item just applies to whatever comes after it, but here we need to tell it where to put the \end{minipage}, so we must enclose what comes after with {} and pass this to our command, which is an unfortunate change over \item. Again, the line breaks in the \newcommand definition are needed.

\documentclass{article}
\usepackage[english]{babel} % needed by blindtext
\usepackage[pangram]{blindtext} % to produce dummy text only
\usepackage{enumitem}
\newlist{questions}{enumerate}{3}
\setlist[questions,1]{label*=\textbf{\arabic*}.}
\setlist[questions,2]{label=(\alph{questionsii})}
\setlist[questions,3]{label=(\roman{questionsiii})}

\newcommand{\nobreakitem}[1]{
    
\begin{minipage}{\linewidth}\item #1\end{minipage}}

\begin{document}
    ~\vspace{15cm} % to push the text near the page break
    
    \begin{questions}
        \nobreakitem{\Blindtext[1][4]}
        \nobreakitem{\Blindtext[2][4]
        \begin{questions}
            \item \Blindtext[1][4]
            \item \Blindtext[1][4]
            \item \Blindtext[1][4]
        \end{questions}}
    \end{questions}
\end{document}

2. An alternative that preserves the usual approach to using \item

To preserve the use of \item in the usual way within questions, we can use a feature of enumitem where a list can resume a previous list. Here each \begin{question} is its own instance of \begin{questions} wrapped inside a minipage, but numbered as a continuation of the global set of questions. Each \begin{question} should contain only one top-level \item, but it can contain sub-parts (defined in the usual way via \begin{questions}). Then each top-level question and its sub-parts won't break across pages.

The \ifthenelse is used because the continuation counting in enumitem requires a different command to be used for the first list than subsequent ones, and I wanted the user to avoid having to think about this.

\documentclass{article}
\usepackage[english]{babel} % needed by blindtext
\usepackage[pangram]{blindtext} % to produce dummy text only
\usepackage{enumitem}
\newlist{questions}{enumerate}{3}
\setlist[questions,1]{label*=\textbf{\arabic*}.}
\setlist[questions,2]{label=(\alph{questionsii})}
\setlist[questions,3]{label=(\roman{questionsiii})}

\usepackage{ifthen}
\gdef\questionsstarted{0}
\newenvironment{question}{\ifthenelse{\equal{\questionsstarted}{0}}{\gdef\questionsstarted{1}\begin{minipage}{\textwidth}\begin{questions}[series=questions,start=1,leftmargin=*,resume]}{\begin{minipage}{\textwidth}\begin{questions}[resume*=questions]}}{\end{questions}\end{minipage}\bigskip}

\begin{document}
    ~\vspace{15cm} % to push the text near the page break
    
    \begin{question}
        \item \Blindtext[1][4]
    \end{question}

    \begin{question}
        \item \Blindtext[2][4]
        \begin{questions}
            \item \Blindtext[1][4]
            \item \Blindtext[1][4]
            \item \Blindtext[1][4]
        \end{questions}
    \end{question}
\end{document}
1
  • Provided each item is within a local group, \interlinepenalty=10000 within that same group should do what you want. Jun 9 at 20:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.