Vertical list customization like enumitem's itemjoin?

I like the flexibility in editing afforded by custom inline enumitem lists; for example:

\documentclass{article}
\usepackage{enumitem}

\newlist{inlineitem}{itemize*}{1}
\setlist[inlineitem]{       % Customize itemize* environ
before=\unskip{:},     % Colon before list
itemjoin={{;}},        % Join with semicolons
itemjoin*={{; and}},   % Last join has "; and "
label={},               % No label
after=.}                % Period at end of last item

\begin{document}

Here is some text that will have a list of
\begin{inlineitem}
\item a list item that could go first but also last
\item another list item that I'm not sure about
\item this one should go last unless I think of another one
\end{inlineitem}

It's convenient because I can add, remove, and rearrange items without worrying about punctuation.

\end{document}


I'd like to do the same thing with a non-inline list; however, enumitem's docs state that "itemjoin is ignored in vertical mode", and so it is.

• What does "the same thing" mean? Have you tried label={; and}? – Andrew Sep 13 '18 at 14:39
• @Andrew "the same thing" means automatically adding before before the list, itemjoin to the end of every item in the list except the penultimate and last ones, itemjoin* to the penultimate item, and after to the last item. Using label={; and} changes the label of every item to be ; and, which is definitely not the effect I'm going for. – Dan Sep 14 '18 at 7:39
• I'm reminded this bounty needs to be allocated soon, so just a reminder to raise any concerns with either answer before I have to make a decision. – user170109 Feb 3 '19 at 17:44

This answer has grown a bit since I first wrote it. I've come up with three solutions and didn't want to throw any of them away.

1. The first (original) solution is simple because it doesn't add “and” to the penultimate item.
2. The second solution does do this, but it is a little bit of a hack. I can't guarantee that it will continue to work if enumitem receives a big update.
3. The third solution works pretty much like enumitem's inline lists. It has a huge limitation though: each items should be just a single paragraph and can't contain display equations.

1. Simple solution (no “and”)

Here's a possible solution that works by redefining \item. This may seem a little heavy-handed, but enumitems's inline lists do this as well. This won't add “and” to the penultimate item (see below for that).

I'm using SetEnumitemKey to define a new key, sentence, that can be supplied to any list environment that enumitem knows about. It will append a period (.) to the last item and a semicolon (;) to every other item. You can use the itemjoin key after sentence to change the semicolon to something else.

\documentclass{article}

\usepackage{enumitem}
\SetEnumitemKey{sentence}{%
before*=\sentencelistprep,
after*={\unskip.},
itemjoin={;},
}

\let\sentenceitemjoin\empty
\edef\sentenceitem{\noexpand\sentenceitemjoin\unexpanded\expandafter{\item}}%
\makeatletter %% <- make @ usable in command sequences
\newcommand*\sentencelistprep{%
\def\sentenceitemjoin{\def\sentenceitemjoin{\unskip\enit@itemjoin}}%
\let\item\sentenceitem
}
\makeatother  %% <- revert @

\begin{document}

Here is an \texttt{itemize} environment whose items are part of this sentence and therefore come with appropriate punctuation:
\begin{itemize}[sentence]
\item a list item that could go first but also last
\item another list item that I'm not sure about that is long enough to span two lines
\item this one should go last unless I think of another one
\end{itemize}

It's convenient because I can add, remove, and rearrange items without worrying about punctuation.

\end{document}


Warning: you can't use any other list environments inside one with the sentence key because the redefinition of \item will also apply to those and will insert additional semicolons there as well.
(You probably wouldn't want to nest lists inside a running sentence, but if you have to you can add \let\sentenceitemjoin\empty inside the inner list.)

Remarks

• I didn't include the colon (:) because does not feel like it is a part of the list (and not affected if items are reordered). If you do want the colon to automatically be inserted you can replace the value of before* above by {\unskip:\sentencelistprep}.
• If you don't want to have to supply the sentence key every time, you can create a custom list environment that includes it with e.g.

\newlist{myitemize}{itemize}{1}
\setlist[myitemize]{label=\textbullet,sentence}

• The before and after keys overwrite sentence if supplied later, but the converse is not true because sentence uses the starred versions before* and after*.

• \sentencelistprep, which is called at the start of a list with sentence, does the following: it defines \sentenceitemjoin as\def\sentenceitemjoin{\unskip\enit@itemjoin} and replaces \item by a version that includes \sentenceitemjoin. The first \item therefore redefines \sentenceitemjoin to \unskip\enit@itemjoin (the value of the itemjoin key), and every \item after that consequently inserts a ; before it.

2. Somewhat hacky solution (includes “and”)

Per your request, here is a way to add “; and” to the penultimate item. I couldn't do this by simply modifying the other solution because an \items in a list environment can't really know if it is the final one.

The following works by first scanning the contents of the entire list environment and inserting ;s at the end of every item but the last two, and adding ; and to the penultimate item. It's a little bit of a hack, but it works.

You can use the itemjoin, itemjoin* and itemjoin** keys to change ;, ; and and . to something else.

\documentclass{article}

\usepackage{enumitem}
\usepackage{environ} %% <- for \Collect@Body

\makeatletter %% <- make @ usable in command sequences
\long\def\sentencelist@afterfi#1\fi{ %% <- insert sentencelist code after \fi (hack!)
#1\fi\Collect@Body\sentencelist    %% <- apply \sentencelist to the body of the environment
}
\long\def\sentencelist#1{\@sentencelist#1\item\@sentencelist}
\long\def\@sentencelist#1\item#2\@sentencelist{
#1\@@sentencelist#2\@@sentencelist           %% <- run preamble as normal
}
\long\def\@@sentencelist#1\item#2\@@sentencelist{%
\if\relax\detokenize{#2}\relax               %% <- if last item
\if@newlist\else\unskip\enit@itemjoin@s\fi %% <- ..insert "; and" if not also the first item
\item #1\unskip\enit@itemjoin@ss%          %% <- ..insert the \item and a "."
\else                                        %% <- otherwise
\if@newlist\else\unskip\enit@itemjoin\fi   %% <- .. insert ";" if not also the first item
\item #1%                                  %% <- ..insert the item and a ;
\@@sentencelist#2\@@sentencelist           %% <- ..and repeat
\fi
}
\let\enit@itemjoin@ss\@empty
\enitkv@key{}{itemjoin**}{% %% <- create "itemjoin**" key
\def\enit@itemjoin@ss{#1}}

\SetEnumitemKey{sentence}{% %% <- create "sentence" key
before*=\sentencelist@afterfi,
itemjoin={;}, itemjoin*={; and}, itemjoin**={.}
}
\makeatother  %% <- revert @

\begin{document}

Here is an \texttt{itemize} environment whose items are part of this sentence and therefore come with appropriate punctuation:
\begin{itemize}[sentence]
\item a list item that could go first but also last
\item another list item that I'm not sure about that is long enough to span two lines
\item this one should go last unless I think of another one
\end{itemize}

It's convenient because I can add, remove, and rearrange items without worrying about punctuation.

\end{document}


3. Solution that mimicks enumitem's inline lists

Here is a method that very closely resembles how enumitem's inline lists work. It's quite ingenious and I don't think I would have been able to come up with it myself. You can customise it with the itemjoin, itemjoin* and itemjoin** keys.

A big limitation is that the item's in this list can only be single paragraphs: they can't contain paragraph breaks, display equations, other lists, etc. I don't think there's not much that can be done about this (without switching to a different method) and I suspect this is the reason why enumitem's itemjoin key has not been implemented for normal lists.

\documentclass{article}

\usepackage{enumitem,amsmath}

\begin{document}

\makeatletter
\newif\ifsentence@firstitem
\newcommand*\sentencebefore{%
\let\sentence@olditem\item
\let\item\sentence@item
\sentence@firstitemtrue
}
\newcommand*\sentenceafter{%
\egroup
\ifhmode\unskip\enit@itemjoin@s\fi
\sentence@olditem\unhbox\enit@inbox\unskip\enit@itemjoin@ss
}
\def\sentence@item{%
\ifhmode
\egroup
\ifsentence@firstitem
\sentence@firstitemfalse
\else
\unskip\enit@itemjoin
\fi
\sentence@olditem \unhbox\enit@inbox
\fi
\setbox\enit@inbox=\hbox\bgroup%\parshape1\@totalleftmargin\linewidth
}
\let\enit@itemjoin@ss\@empty
\enitkv@key{}{itemjoin**}{%
\def\enit@itemjoin@ss{#1}}
\makeatother

\SetEnumitemKey{sentence}{%
before*=\sentencebefore, after*=\sentenceafter,
itemjoin=;, itemjoin*={; and}, itemjoin**=.
}

Here is an \texttt{itemize} environment whose items are part of this sentence and therefore come with appropriate punctuation:
\begin{itemize}[sentence]
\item a list item that could go first but also last
\item another list item that I'm not sure about that is long enough to span two lines
\item this one should go last unless I think of another one
\end{itemize}

It's convenient because I can add, remove, and rearrange items without worrying about punctuation.

\end{document}


• Is there a way to get the "; and" at the end of the penultimate item? – Dan Mar 7 '19 at 10:45
• The answer is of course “yes”, but it won't be a simple modification. I'm using \item to print the preceding ;, but it is impossible to tell if any particular \item is the last one without doing something like counting \items (and storing the result in the aux file) or scanning the contents of the list environment beforehand. I'll get back to you on this (but it may be a while; I'm a little busy atm). – Circumscribe Mar 7 '19 at 15:43
• @Dan See my updated answer. – Circumscribe Mar 10 '19 at 7:35

Here is a version that sort of reproduces a display itemize allowing for the features of an itemize*. Below is the same content using inlineitem (provided in question), displayitem and a regular itemize for comparison purposes:

Notes:

• The color red has been added to make it easier to compare the three versions. You can replace the definition of \Show with

\newcommand*{\Show}[1]{#1}


to eliminate the highlighting or simply remove all references to the \Show macro.

• The "sort of" is because I wasn't able to get the vertical spacing of the very first item to match what itemize yields.

Code:

\documentclass{article}
\usepackage{enumitem}
\usepackage{xcolor}

\newcommand*{\NextLine}{%
\newline
\vspace*{0.5\baselineskip}%
\hspace*{\dimexpr\labelindent+\labelwidth-\labelsep\relax}%
}%
\newcommand*{\Show}[1]{\textcolor{red}{\bfseries#1}}% Make it easier to see difference

\newlist{inlineitem}{itemize*}{1}
\setlist[inlineitem]{             % Customize itemize* environ
before={\unskip\Show:},       % Colon before list
itemjoin={{\Show;}},          % Join with semicolons
itemjoin*={{\Show{; and}}},   % Last join has "; and "
label={},                     % No label
after=.,                      % Period at end of last item
}

\newlist{displayitem}{itemize*}{1}
\setlist[displayitem]{                      % Customize itemize* environ
before={\unskip\Show:\NextLine},        % Colon before list
itemjoin={\Show;\NextLine},             % Join with semicolons
itemjoin*={\Show{; and}\NextLine},      % Last join has "; and "
label={$\bullet$},                      % Label
after={\unskip\Show.\endgraf\noindent}, % Period at end of last item
}

\pagecolor{white}
\begin{document}
\noindent
Here is some text in a \verb|inlineitem|
\begin{inlineitem}
\item a list item that could go first but also last
\item another list item that I'm not sure about
\item this one should go last unless I think of another one
\end{inlineitem}
Some text after \verb|inlineitem|.

\bigskip

\noindent
Here is the same text in a \verb|displayitem|
\begin{displayitem}
\item a list item that could go first but also last
\item another list item that I'm not sure about
\item this one should go last unless I think of another one
\end{displayitem}
Some text after \verb|displayitem|.

\bigskip

\noindent
Here is the same text in an \verb|itemize|
\begin{itemize}
\item a list item that could go first but also last
\item another list item that I'm not sure about
\item this one should go last unless I think of another one
\end{itemize}
Some text after regular \verb|itemize|.
\end{document}


This does not allow for nested lists, but the list you have in mind should be the most inner level anyway.

This requires xparse released 2019-03-05 or later (already available for MiKTeX, soon to be available with TeX Live 2019).

The main difference with inline lists is that blank lines should be taken care of, so trailing spaces and \par tokens are removed with the \__dan_genlist_purify:N that also puts back \item that was removed when the body was absorbed and split. A new sequence is built and used at the end with the desired separators.

\documentclass{article}
\usepackage{enumitem,xparse}

\ExplSyntaxOn
\NewDocumentEnvironment{genlist}{mO{}+b}
{% #1 = list type, #2 = options, #3 = body
\dan_genlist_make:nnn { #1 } { #2 } { #3 }
}
{}

\seq_new:N \l_dan_genlist_body_in_seq
\seq_new:N \l_dan_genlist_body_out_seq
\clist_new:N \l_dan_genlist_opt_clist
\cs_generate_variant:Nn \seq_use:Nnnn { NVVV }

% borrow itemjoin, itemjoin* and after from enumitem;
% all other options are passed to enumitem
\keys_define:nn { dan/genlist }
{
itemjoin  .tl_set:N = \l_dan_genlist_more_tl,
itemjoin* .tl_set:N = \l_dan_genlist_last_tl,
after     .tl_set:N = \l_dan_genlist_after_tl,
unknown   .code:n   = \clist_put_right:Nx \l_dan_genlist_opt_clist
{ \l_keys_key_tl = #1 },
}

\cs_new_protected:Nn \dan_genlist_make:nnn
{
\keys_set:nn { dan/genlist } { #2 }
\seq_set_split:Nnn \l_dan_genlist_body_in_seq { \item } { #3 }
\seq_pop_left:NN \l_dan_genlist_body_in_seq \l_tmpa_tl % discard the empty item
\seq_clear:N \l_dan_genlist_body_out_seq
\seq_map_variable:NNn \l_dan_genlist_body_in_seq \l_dan_genlist_temp_tl
{
\__dan_genlist_purify:N \l_dan_genlist_temp_tl
}
\use:e { \exp_not:N \begin{#1}[ \clist_use:Nn \l_dan_genlist_opt_clist {,} ] }
\seq_use:NVVV \l_dan_genlist_body_out_seq
\l_dan_genlist_last_tl
\l_dan_genlist_more_tl
\l_dan_genlist_last_tl
\tl_use:N \l_dan_genlist_after_tl
\end{#1}
}

\cs_new_protected:Nn \__dan_genlist_purify:N
{
\regex_replace_once:nnN { \s* \c{par}* \Z } { } #1
\tl_put_left:Nn #1 { \item }
\seq_put_right:NV \l_dan_genlist_body_out_seq #1
}

\ExplSyntaxOff

\begin{document}

\begin{genlist}{itemize}[
itemjoin=;,      % Join with semicolons
itemjoin*=; and, % Last join has "; and "
label={},        % No label
nosep,
after=.          % Period at end of last item
]
\item a list item that could go first but also last

\item another list item that I'm not sure about

\item this one should go last unless I think of another one

\end{genlist}

\end{document}


• I should really learn the expl3 syntax one of these days… Does the new b argument type correspond to the environment body? (If so that's pretty nice.) – Circumscribe Mar 10 '19 at 18:20
• @Circumscribe Yes, pretty cool! – egreg Mar 10 '19 at 19:56