Skip to content

Commit

Permalink
Introduce an f-specification argument in ltcmd
Browse files Browse the repository at this point in the history
  • Loading branch information
josephwright committed Nov 1, 2024
1 parent 1985482 commit 88e37f9
Show file tree
Hide file tree
Showing 9 changed files with 433 additions and 463 deletions.
4 changes: 4 additions & 0 deletions base/changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ to completeness or accuracy and it contains some references to files that are
not part of the distribution.
================================================================================

2024-11-01 Joseph Wright <Joseph.Wright@latex-project.org>
* ltcmd.dtx, usrguide.tex
New "f"-type argument

================================================================================
All changes above are only part of the development branch for the next release.
================================================================================
Expand Down
33 changes: 33 additions & 0 deletions base/doc/ltnews41.tex
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,39 @@ \section{Introduction}

\section{New or improved commands}

\subsection{Collecting environment bodies verbatim}

The mechanisms in \pkg{ltcmd} (\enquote{\pkg{xparse}}) offer a powerful way to
specify a range of types of document command and environment syntax. This
includes the ability to collect the entire body of an environment, for cases
where treating it as a standard argument is useful. It is also possible in
\pkg{ltcmd} to define argument which grab their content verbatim, another
specialist argument form. To date, however, it was not possible to combine
these two ideas.

In this release, a new specifier~\texttt{f} is introduced, which collects the
body of an environment in a verbatim-like way. Like the existing
\texttt{v}~specification, each separate line is marked by the special
\cs{obeyedline} marker, which as standard issues a normal paragraph. Thus, this
new specifier is usable both for typesetting and collecting file contents (the
\texttt{f} indicates \enquote{\texttt{filecontents}-like}). Thus, we may use
\begin{verbatim}
\NewDocumentEnvironment
{MyVerbatim}{!O{\ttfamily} +f}
{\begin{center} #1 #2\end{center}} {}
\begin{MyVerbatim}[\ttfamily\itshape]
% Some code is show here
$y = mx + c$
\end{MyVerbatim}
\end{verbatim}
to obtain
\NewDocumentEnvironment{MyVerbatim}{!O{\ttfamily} +f}
{\begin{center} #1 #2\end{center}} {}
\begin{MyVerbatim}[\ttfamily\itshape]
% Some code is show here
$y = mx + c$
\end{MyVerbatim}

\section{Code improvements}

\section{Bug fixes}
Expand Down
36 changes: 36 additions & 0 deletions base/doc/usrguide.tex
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,42 @@ \subsection{Typesetting verbatim-like material}
Similarly, the \texttt{verbatim} environment sets up the meaning of \cs{par}
suitable for breaking lines.

\subsection{Verbatim environments}
\label{sec:cmd:verbenv}

In some cases, as well as grabbing an environment body you will want the
contents to be treated verbatim. This is available using the argument
specification~\texttt{f}. Like the \texttt{b} specification, this has to be the
last one. Thus for example
\begin{verbatim}
\NewDocumentEnvironment{MyVerbatim}{!O{\ttfamily} +f}
{\begin{center} #1 #2\end{center}} {}
\begin{MyVerbatim}[\ttfamily\itshape]
% Some code is show here
$y = mx + c$
\end{MyVerbatim}
\end{verbatim}
will typeset
\NewDocumentEnvironment{MyVerbatim}{!O{\ttfamily} +f}
{\begin{center} #1 #2\end{center}} {}
\begin{MyVerbatim}[\ttfamily\itshape]
% Some code is show here
$y = mx + c$
\end{MyVerbatim}

As grabbing the entire contents verbatim, newlines are always permitted: if you
use \texttt{f} rather than \texttt{+f}, a warning will be issued and \LaTeX{}
will assume you meant to include the \texttt{+}. As for the \texttt{v}
specification, new lines are stored as \cs{ObeyedLine}. In a similar fashion to
the \texttt{b}~specification, by default \emph{newlines} are trimmed at both
ends of the body. Putting the prefix |!| before \texttt{b} suppresses
space-trimming.

Notice that for technical reasons, we recommend that an optional argument
coming immediately before an \texttt{f} specification should not allow any
spaces, achieved by adding the \texttt{!} as showing in the example. However,
this is left as a choice for the user.

\subsection{Performance}

For document commands where the argument specification is entirely
Expand Down
221 changes: 211 additions & 10 deletions base/ltcmd.dtx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
%%% From File: ltcmd.dtx
%
% \begin{macrocode}
\def\ltcmdversion{v1.2f}
\def\ltcmddate{2024-07-06}
\def\ltcmdversion{v1.3a}
\def\ltcmddate{2024-11-01}
% \end{macrocode}
%
%<*driver>
Expand Down Expand Up @@ -1331,28 +1331,47 @@
% \end{macro}
%
% \begin{macro}{\@@_normalize_type_b:w}
% This argument type is not allowed for commands. This is only
% \changes{v1.3a}{2024-11-01}{Extend to cover \texttt{f}-type grabbing}
% \begin{macro}{\@@_normalize_type_f:w}
% \changes{v1.3a}{2024-11-01}{New function}
% \begin{macro}{\@@_normalize_type_b_or_f:nn}
% \changes{v1.3a}{2024-11-01}{New function}
% These argument type is not allowed for commands. They is only
% allowed at the end of the argument specification, hence we check
% that |#1| is the end.
% \begin{macrocode}
\cs_new_protected:Npn \@@_normalize_type_b:w #1
{ \@@_normalize_type_b_or_f:nn {#1} { b } }
\cs_new_protected:Npn \@@_normalize_type_f:w #1
{
\bool_if:NF \l_@@_long_bool
{
\msg_warning:nn { cmd } { verb-collection-always-long }
\bool_set_true:N \l_@@_long_bool
}
\@@_normalize_type_b_or_f:nn {#1} { f }
}
\cs_new_protected:Npn \@@_normalize_type_b_or_f:nn #1#2
{
\bool_if:NF \l_@@_environment_bool
{
\msg_error:nnxx { cmd } { invalid-command-arg }
{ \@@_environment_or_command: } { b }
{ \@@_environment_or_command: } {#2}
\@@_bad_def:wn
}
\tl_clear:N \l_@@_last_delimiters_tl
\@@_add_arg_spec:n { b }
\@@_add_arg_spec:n {#2}
\quark_if_recursion_tail_stop:n {#1}
\msg_error:nnxx { cmd } { arg-after-body }
\msg_error:nnxxx { cmd } { arg-after-body }
{#2}
{ \@@_environment_or_command: }
{ \tl_to_str:n {#1} }
\@@_bad_def:wn
}
% \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_single_token_check:n}
% Checks that the argument is a single (non-space) token (possibly
Expand Down Expand Up @@ -1658,16 +1677,27 @@
% \end{macro}
%
% \begin{macro}{\@@_add_type_b:w}
% \changes{v1.3a}{2024-11-01}{Extend to cover \texttt{f}-type grabbing}
% \begin{macro}{\@@_add_type_f:w}
% \changes{v1.3a}{2024-11-01}{New function}
% \begin{macro}{\@@_add_type_b_or_c:N}
% \changes{v1.3a}{2024-11-01}{New function}
% \begin{macrocode}
\cs_new_protected:Npn \@@_add_type_b:w
{ \@@_add_type_b_or_f:N b }
\cs_new_protected:Npn \@@_add_type_f:w
{ \@@_add_type_b_or_f:N f }
\cs_new_protected:Npn \@@_add_type_b_or_f:N #1
{
\@@_flush_m_args:
\@@_add_default:
\@@_add_grabber:N b
\@@_add_grabber:N #1
\@@_prepare_signature:N
}
% \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_add_type_D:w}
% \begin{macrocode}
Expand Down Expand Up @@ -3002,6 +3032,173 @@
% \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_grab_f_long:w}
% \changes{v1.3a}{2024-11-01}{New \texttt{f}-type grabbing function}
% \begin{macro}{\@@_grab_f_long_obey_spaces:w}
% \begin{macro}{\@@_grab_f_aux:n}
% \begin{macro}{\@@_grab_f_aux:w}
% \begin{macro}{\@@_grab_f_check:w}
% \begin{macro}{\@@_grab_f_loop:w}
% \begin{macro}{\@@_grab_f_space_check:w}
% \begin{macro}{\@@_grab_f_space_aux:w}
% \begin{macro}{\@@_grab_f_end:nn}
% \begin{macro}{\@@_grab_f_end:n}
% \begin{macro}{\@@_grab_f_end_auxi:w}
% \begin{macro}{\@@_grab_f_end_auxii:w}
% Collecting an environment body verbatim shares some ideas with the
% \texttt{v}-type grabber, and others with the standard \texttt{filecontents}
% environment. The start is to set the end-of-line to a predictable value
% and to deactivate the specials. We then define the end marker: this
% has to have the correct category codes, so is a token list not a string
% (\texttt{end} and the end-of-env name are catcode-11). We use this to set
% up the end-of-env test, then hand over to an auxiliary.
% \begin{macrocode}
\cs_new_protected:Npn \@@_grab_f_long:w #1 \@@_run_code:
{
\bool_set_false:N \l_@@_obey_spaces_bool
\@@_grab_f_aux:n {#1}
}
\cs_new_protected:Npn \@@_grab_f_long_obey_spaces:w #1 \@@_run_code:
{
\bool_set_true:N \l_@@_obey_spaces_bool
\@@_grab_f_aux:n {#1}
}
\cs_new_protected:Npn \@@_grab_f_aux:n #1
{
\tl_set:Nn \l_@@_signature_tl {#1}
\group_begin:
\tl_clear:N \l_@@_v_arg_tl
\tex_escapechar:D = 92 \scan_stop:
\tex_endlinechar:D = `\^^M \scan_stop:
\cs_set_eq:NN \do \char_set_catcode_other:N
\dospecials
\tl_set:Ne \l_@@_tmpa_tl
{
\c_backslash_str end
\c_left_brace_str \@currenvir \c_right_brace_str
}
\use:e
{
\cs_set_protected:Npn \@@_grab_f_check:w
##1 \l_@@_tmpa_tl ##2 \l_@@_tmpa_tl ##3
\scan_stop:
}
{
\tl_if_empty:nTF {##3}
{
\tl_put_right:Nn \l_@@_v_arg_tl
{
##1
\obeyedline
}
\@@_grab_f_loop:w
}
{ \@@_grab_f_end:nn {##1} {##2} }
}
\@@_grab_f_aux:w
}
% \end{macrocode}
% To allow for the need to change the category code of end-of-lines, we
% put this in a small auxiliary. At the end of this, we deal with the
% a special case for the first line before looping.
% \begin{macrocode}
\group_begin:
\char_set_catcode_other:N \^^M %
\cs_new_protected:Npn \@@_grab_f_aux:w %
{ %
\cs_set_protected:Npe \@@_grab_f_loop:w ##1 ^^M %
{ %
\@@_grab_f_check:w ##1 %
\l_@@_tmpa_tl %
\l_@@_tmpa_tl %
\scan_stop: %
} %
\char_set_catcode_other:N \^^M %
\group_align_safe_begin: %
\peek_after:Nw \@@_grab_f_space_check:w
} %
\group_end: %
\cs_new_protected:Npn \@@_grab_f_check:w { }
\cs_new_protected:Npn \@@_grab_f_loop:w { }
% \end{macrocode}
% If there was a potential optional argument before the \texttt{f}-type
% one, but it was missed out, the next character will already have been
% tokenized. The most common case will be a newline, which will have
% been converted to a space: that is picked up here. (It is impossible
% to filter out all possible cases, as the underlying behavior of
% \tn{futurelet} means that for example a \verb=%= would be a comment
% and be lost.) Alternative approaches require that a category code
% change for \verb=^^M= takes place before searching for any optional
% arguments (see for example \pkg{fancyvrb}).
% \begin{macrocode}
\cs_new_protected:Npn \@@_grab_f_space_check:w
{
\if_meaning:w \l_peek_token \c_space_token
\exp_after:wN \@@_grab_f_space_aux:w
\else:
\group_align_safe_end:
\exp_after:wN \@@_grab_f_loop:w
\fi:
}
\cs_new_protected:Npn \@@_grab_f_space_aux:w
{
\group_align_safe_end:
\tl_set:Nn \l_@@_v_arg_tl { \obeyedline }
\peek_remove_spaces:n
{ \@@_grab_f_loop:w }
}
\cs_new_protected:Npn \@@_grab_f_end:nn #1#2
{
\tl_if_blank:nF {#1#2}
\ERROR
\exp_args:NNNo \group_end:
\tl_set:Nn \l_@@_v_arg_tl { \l_@@_v_arg_tl }
\@@_add_arg:x
{
\bool_if:NTF \l_@@_obey_spaces_bool
{ \exp_not:V }
{ \exp_args:NV \@@_grab_f_end:n }
\l_@@_v_arg_tl
}
\exp_args:NV \end \@currenvir
}
% \end{macrocode}
% Look for line markers at each end and tidy up if required.
% \begin{macrocode}
\cs_new:Npn \@@_grab_f_end:n #1
{
\@@_grab_f_end_auxi:w {#1} #1
\q_nil \obeyedline \q_nil \obeyedline \q_nil \q_stop
}
\cs_new:Npn \@@_grab_f_end_auxi:w
#1#2 \obeyedline \q_nil \obeyedline \q_nil #3 \q_stop
{
\tl_if_empty:nTF {#3}
{ \@@_grab_f_end_auxii:w {#1} #1 }
{ \@@_grab_f_end_auxii:w {#2} #2 }
\obeyedline \q_stop
}
\cs_new:Npn \@@_grab_f_end_auxii:w
#1#2 \obeyedline #3 \q_stop
{
\tl_if_blank:nTF {#2}
{ \tl_tail:n {#1} }
{ \exp_not:n {#1} }
}
% \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}
% {
% \@@_grab_D:w ,
Expand Down Expand Up @@ -4716,12 +4913,13 @@
% \changes{v1.0f}{2021/06/04}{Normalize various error messages}
% \changes{v1.2c}{2023/12/22}
% {Generalize message \texttt{invalid-bang} (gh/1198)}
% \changes{v1.3a}{2024-11-01}{Generalize message \texttt{arg-after-body}}
% \begin{macrocode}
\msg_new:nnnn { cmd } { arg-after-body }
{ Argument~type~'b'~must~be~last~in~#1. }
{ Argument~type~'#1'~must~be~last~in~#2. }
{
The~'b'~argument~type~must~come~last~but~it~is~followed~
by~'#2'~in~the~argument~specification.~This~is~not~allowed.
The~'#1'~argument~type~must~come~last~but~it~is~followed~
by~'#3'~in~the~argument~specification.~This~is~not~allowed.
\c_@@_ignore_def_tl
}
\msg_new:nnnn { cmd } { bad-arg-spec }
Expand Down Expand Up @@ -4934,6 +5132,7 @@
% \end{macrocode}
%
% Intended more for information.
% \changes{v1.3a}{2024-11-01}{New message \texttt{verb-collection-always-long}}
% \begin{macrocode}
\msg_new:nnn { cmd } { define-command } % should be just ``define'' but dep in xparse
{
Expand Down Expand Up @@ -4973,6 +5172,8 @@
'#1~code'~and/or~'#1~defaults'.~Maybe~you~tried~using~
\iow_char:N\\let.~This~may~lead~to~an~infinite~loop.
}
\msg_new:nnn { cmd } { verb-collection-always-long }
{ Verbatim~body~collection~always~permits~newlines. }
% \end{macrocode}
%
% \subsection{User functions}
Expand Down
Loading

0 comments on commit 88e37f9

Please sign in to comment.