Vertical list customization like enumitem's itemjoin?

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:

enter image description here

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 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}

output

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}

output


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}

output


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}

enter image description here

Tags:

Lists

Enumitem