jbezos / enumitem

Customize enumerate, itemize and description
MIT License
48 stars 5 forks source link

Using `[resume*]` without antecedent leads to edge case #48

Open wwywong opened 7 months ago

wwywong commented 7 months ago

Consider the (somewhat unrealistic) MWE

\documentclass{article}

\usepackage{enumitem}

\begin{document}

\begin{enumerate}[resume*]
        \item Test
\end{enumerate}
\end{document}

This will print the text resume,, before the list starts.

My TeX.SE answer describes why, and this can be solved by using a different token to check of list termination in the definition of \enitkv@do#1, (currently it uses \relax to mark end of list, which is the cause of the problem as \csname ... \endcsname returns \relax when the macro is undefined.


A more realistic use case which leads to this showing up is if a user used \begin{enumerate}...\end{enumerate} within an environment (so in a group), and then tried to call \begin{enumerate}[resume*] ... \end{enumerate} afterwards from outside the group. This can be abstracted as

\documentclass{article}

\usepackage{enumitem}

\begin{document}

\bgroup
\begin{enumerate}
        \item Test 1
\end{enumerate}
Text
\begin{enumerate}[resume*]
        \item Test 2
\end{enumerate}
\egroup

\begin{enumerate}[resume*]
        \item Test
\end{enumerate}
\end{document}

In this case, we see a very strange behavior. The first time the resume* environment hits, everything is okay. The second time the resume* environment hits, we get the weird resume,, text showing up, but the counter is resumed correctly!

This is because for resume* it doesn't save the optional arguments, but it saves the counter via \global. While the first call to enumerate saves both but only locally. From the package:

\def\enit@endlist{%
  \enit@after
  \endlist
  \ifx\enit@series\relax\else % discards resume*, too
    \ifnum\enit@resuming=\@ne % ie, series=
      \enit@setresumekeys{series@\enit@series}\global\global
    \else % ie, resume=, resume*= (save count, but not keys)
      \enit@setresumekeys{series@\enit@series}\@gobblefour\global
    \fi
    \enit@afterlist
  \fi
  \ifnum\enit@resuming=\thr@@ % ie, resume* list (save count only)
    \enit@setresumekeys\@currenvir\@gobblefour\global
  \else
    \enit@setresumekeys\@currenvir\@empty\@empty
  \fi
  \aftergroup\enit@afterlist}

Part of me is also wondering whether for the resume* (but not series) case, it would make more sense to have \@gobblefour\@empty instead of \@gobblefour\global to line up with the other non-series cases.

muzimuzhi commented 7 months ago

Here is a patch which makes use of resume* without antecedent behaves the same as such use of resume: input is accepted silently and nothing is resumed.

\documentclass{article}
\usepackage{enumitem}

\makeatletter
\@namedef{enitkv@enumitem-resume@resume*@default}{%
   \let\enit@resuming\thr@@
  \enit@ifunset{enit@resumekeys@\@currenvir}
    % Nothing to resume if this is the first occurrance of \@currenvir.
    % An empty \enit@resumekeys results in \enitkv@setkeys{enumitem}{,resume}
    % called in \enit@setresume.
    {\def\enit@resumekeys{}}
    {\let\enit@resuming\thr@@
     \expandafter\let\expandafter\enit@resumekeys
       \csname enit@resumekeys@\@currenvir\endcsname
     \@nameuse{enit@resume@\@currenvir}\relax}%
  }
\makeatother

\newlist{myenum}{enumerate}{3}
\setlist[myenum]{label=\roman*}

\begin{document}

\begin{enumerate}[resume*]
  \item Test \texttt{resume*} without antecedent
\end{enumerate}

\begin{enumerate}[resume*]
  \item b
  \item c
\end{enumerate}

\begin{myenum}[resume]
  \item Test \texttt{resume} without antecedent
\end{myenum}

\begin{myenum}[resume]
  \item b
  \item c
\end{myenum}

\end{document}

image

https://github.com/jbezos/enumitem/blob/1bdcad0987823b3716c86b126b3863f895ea9c4a/enumitem.sty#L393-L406