[latex3-commits] [git/LaTeX3-latex3-latex2e] keyval-detect: Add "={...}" modifier and suppress brace stripping (44d00d1f)
Joseph Wright
joseph.wright at morningstar2.co.uk
Wed Aug 10 14:15:41 CEST 2022
Repository : https://github.com/latex3/latex2e
On branch : keyval-detect
Link : https://github.com/latex3/latex2e/commit/44d00d1fb4f0987c112353a62cc4c407b6bbcf56
>---------------------------------------------------------------
commit 44d00d1fb4f0987c112353a62cc4c407b6bbcf56
Author: Joseph Wright <joseph.wright at morningstar2.co.uk>
Date: Wed Aug 10 08:12:30 2022 +0100
Add "={...}" modifier and suppress brace stripping
>---------------------------------------------------------------
44d00d1fb4f0987c112353a62cc4c407b6bbcf56
base/changes.txt | 5 +
base/doc/ltnews36.tex | 21 +++
base/doc/usrguide3.tex | 77 +++++++++
base/ltcmd.dtx | 331 ++++++++++++++++++++++++++++++++------
base/testfiles-ltcmd/ltcmd008.lvt | 41 +++++
base/testfiles-ltcmd/ltcmd008.tlg | 27 ++++
6 files changed, 457 insertions(+), 45 deletions(-)
diff --git a/base/changes.txt b/base/changes.txt
index cef69f97..1f53c851 100644
--- a/base/changes.txt
+++ b/base/changes.txt
@@ -6,6 +6,11 @@ completeness or accuracy and it contains some references to files that
are not part of the distribution.
================================================================================
+2022-08-10 Joseph Wright <Joseph.Wright at latex-project.org>
+
+ * ltcmd.dtx:
+ Add support for keyval detection using "={...}" syntax
+
2022-07-23 Joseph Wright <Joseph.Wright at latex-project.org>
* ltkeys.dtx:
diff --git a/base/doc/ltnews36.tex b/base/doc/ltnews36.tex
index 88f2b855..c06a9bdd 100644
--- a/base/doc/ltnews36.tex
+++ b/base/doc/ltnews36.tex
@@ -180,6 +180,27 @@ font instead.
%
\githubissue{879}
+\subsection{Auto-detecting key--value arguments}
+
+To allow extension of the core \LaTeX{} syntax, \pkg{ltcmd} now supports
+a \texttt{={...}} modifier when grabbing arguments. This modifier instructs
+\LaTeX{} that the argument should be passed to the underlying code as
+a set of keyvals. If the argument does not \enquote{look like} a set
+of keyvals, it will be converted into a single key--value pair, with
+the argument to \texttt{=} specifying the name of that key. For
+example, the \cs{caption} command could be defined as
+\begin{verbatim}
+ \DeclareDocumentCommand\caption{s ={TOC-text}+O{#3} +m}
+\end{verbatim}
+which would mean that if the optional argument does \emph{not}
+contain keyval data, it will be converted to a single keyval
+pair with the key name \texttt{TOC-text}.
+
+Arguments which begin with \texttt{=,} are always interpreted as
+keyvals even if they do not contain further \texttt{=} signs.
+Any \texttt{=} signs within math mode are ignored, meaning that
+only \texttt{=} outside of math mode will generally cause
+interpretation as keyval material.
\section{Bug fixes}
diff --git a/base/doc/usrguide3.tex b/base/doc/usrguide3.tex
index 2d7f295a..f757314c 100644
--- a/base/doc/usrguide3.tex
+++ b/base/doc/usrguide3.tex
@@ -195,6 +195,10 @@ optional arguments. There are some subtleties to this, as \TeX{} itself
has some restrictions on where spaces can be `detected': more detail
is given in Section~\ref{sec:cmd:opt-space}.
+Thirdly, \texttt{=} is used to declare that the following argument should
+be interpreted as a series of keyvals. See Section~\ref{sec:cmd:keyval}
+for more details.
+
Finally, the character \texttt{>} is used to declare so-called
`argument processors', which can be used to modify the contents of an
argument before it is passed to the macro definition. The use of argument
@@ -489,6 +493,79 @@ Used to test if \meta{argument} (|#1|, |#2|, \emph{etc.}) is
checks for a star as the first argument, then chooses the action to
take based on this information.
+\subsection{Auto-converting to key--value format}
+\label{sec:cmd:keyval}
+
+Some document commands have a long history of accepting a `free text' optional
+argument, for example \cs{caption} and the sectioning commands \cs{section},
+etc. Introducing more sophisticated (keyval) options to these commands
+therefore needs a method to interpret the optional argument \emph{either} as
+free text \emph{or} as a series of keyvals. This needs to take place
+during argument grabbing as there is a need for careful treatment of
+braces to obtain the correct result.
+
+The \texttt{=} modifier is available to allow \pkg{ltcmd} to correctly
+implement this process. The modifier guarantees that the argument will be
+passed to further code as a series of keyvals. To do that, the \texttt{=}
+should be followed by an argument containing the default key name. This is used
+as the key in a key--value pair \emph{if} the `raw' argument does \emph{not}
+have the correct form to be interpreted as a set of keyvals.
+
+Taking \cs{caption} as an example, with the demonstration implementation
+\begin{verbatim}
+\DeclareDocumentCommand
+ \caption
+ {s ={short-text} +O{#3} +m}
+ {%
+ \showtokens{Grabbed arguments:^^J(#2)^^Jand^^J(#3)}%
+ }
+\end{verbatim}
+the default key name is \texttt{short-text}. When the command \cs{caption} is
+then used, if the mandatory argument is free text such as
+\begin{verbatim}
+\caption[Some short text]{A much longer and more detailed text for
+ demonstration purposes}
+\end{verbatim}
+then the output will be
+\begin{verbatim}
+Grabbed arguments:
+(short-text={Some short text})
+and
+(A much longer and more detailed text for demonstration purposes)
+\end{verbatim}
+On the other hand, if the caption is given with a keyval-form argument
+\begin{verbatim}
+\caption[label = cap:demo]%
+ {A much longer and more detailed text for demonstration purposes}
+\end{verbatim}
+then this will be respected
+\begin{verbatim}
+Grabbed arguments:
+(label = cap:demo)
+and
+(A much longer and more detailed text for demonstration purposes)
+\end{verbatim}
+
+Interpretation as keyval form is determinded by the presence of \texttt{=}
+characters within the argument. Those in math mode (inside \verb|$...$| or
+\verb|\(...\)|) are ignored. An argument can be forced to be read as keyvals by
+including an empty entry at the start
+\begin{verbatim}
+\caption[=,This is now a keyval]%
+\end{verbatim}
+This empty entry is \emph{not} passed to the underlying code, so will not lead
+to issues with keyval parsers that do not allow an empty key name. Any text-mode
+\texttt{=} signs will need to be braced to avoid being misinterpreted: this
+is likely most conveniently handled by bracing the entire argument
+\begin{verbatim}
+\caption[{Not = to a keyval!}]%
+\end{verbatim}
+which will be passed correctly as
+\begin{verbatim}
+Grabbed arguments:
+(short-text = {Not = to a keyval!})
+\end{verbatim}
+
\subsection{Argument processors}
\label{sec:cmd:processors}
diff --git a/base/ltcmd.dtx b/base/ltcmd.dtx
index 59aa5bf4..bb967068 100644
--- a/base/ltcmd.dtx
+++ b/base/ltcmd.dtx
@@ -34,8 +34,8 @@
%%% From File: ltcmd.dtx
%
% \begin{macrocode}
-\def\ltcmdversion{v1.0l}
-\def\ltcmddate{2022-03-18}
+\def\ltcmdversion{v1.1a}
+\def\ltcmddate{2022-08-10}
% \end{macrocode}
%
%<*driver>
@@ -247,6 +247,15 @@
% \end{macrocode}
% \end{variable}
%
+% \begin{variable}{\l_@@_suppress_strip_bool}
+% \changes{v1.1a}{2022/08/10}{New switch}
+% Used to indicate that an a pair of braces should not be stripped from
+% an optional argument.
+% \begin{macrocode}
+\bool_new:N \l_@@_suppress_strip_bool
+% \end{macrocode}
+% \end{variable}
+%
% \begin{variable}{\l_@@_m_args_int}
% The number of \texttt{m} arguments: if this is the same as the total
% number of arguments, then a short-cut can be taken in the creation of
@@ -874,7 +883,7 @@
% \item Check that each argument has the correct number of data items
% associated with it, and that where a single character is required,
% one has actually been supplied.
-% \item Check that processors and the markers~|+| and~|!| are followed
+% \item Check that processors and the markers~|+|, |!| and~|=| are followed
% by an argument for which they make sense, and are not redundant.
% \item Check the absence of forbidden types for expandable commands,
% namely \texttt{G}/\texttt{v} always, and \texttt{l}/\texttt{u}
@@ -918,6 +927,7 @@
\bool_set_true:N \l_@@_grab_expandably_bool
\bool_set_false:N \l_@@_obey_spaces_bool
\bool_set_false:N \l_@@_long_bool
+ \bool_set_false:N \l_@@_suppress_strip_bool
\bool_set_false:N \l_@@_some_obey_spaces_bool
\bool_set_false:N \l_@@_some_long_bool
\bool_set_false:N \l_@@_some_short_bool
@@ -1020,7 +1030,11 @@
% \@@_normalize_type_>:w,
% \@@_normalize_type_+:w,
% \@@_normalize_type_!:w,
+% \@@_normalize_type_=:w
% }
+% \changes{v1.1a}{2022/08/10}{Refactor to use common auxiliary}
+% \changes{v1.1a}{2022/08/10}{Add support for \texttt{=} modifier}
+% \begin{macro}{\@@_normalize_type_aux:NnNn}
% Check that these prefixes have arguments, namely that the next token
% is not \cs{q_recursion_tail}, and remember to leave it after the
% looping macro. Processors are forbidden in expandable commands.
@@ -1044,33 +1058,46 @@
}
\cs_new_protected:cpn { @@_normalize_type_+:w } #1
{
- \quark_if_recursion_tail_stop_do:nn {#1} { \@@_bad_arg_spec:wn }
- \bool_if:NT \l_@@_long_bool
+ \@@_normalize_type_aux:NnNn + {#1}
+ \l_@@_long_bool
+ { \bool_set_true:N \l_@@_long_bool }
+ }
+\cs_new_protected:cpn { @@_normalize_type_!:w } #1
+ {
+ \@@_normalize_type_aux:NnNn ! {#1}
+ \l_@@_obey_spaces_bool
{
- \msg_error:nnxx { cmd } { two-markers }
- { \@@_environment_or_command: } { + }
- \@@_bad_def:wn
+ \bool_set_true:N \l_@@_obey_spaces_bool
+ \bool_set_true:N \l_@@_some_obey_spaces_bool
}
- \bool_set_true:N \l_@@_long_bool
- \int_decr:N \l_@@_current_arg_int
- \@@_normalize_arg_spec_loop:n {#1}
}
-\cs_new_protected:cpn { @@_normalize_type_!:w } #1
+\cs_new_protected:cpn { @@_normalize_type_=:w } #1#2
{
- \quark_if_recursion_tail_stop_do:nn {#1} { \@@_bad_arg_spec:wn }
- \bool_if:NT \l_@@_obey_spaces_bool
+ \@@_normalize_type_aux:NnNn = {#2}
+ \l_@@_suppress_strip_bool
+ {
+ \bool_set_true:N \l_@@_suppress_strip_bool
+ \bool_set_false:N \l_@@_grab_expandably_bool
+ \tl_put_right:Nx \l_@@_arg_spec_tl
+ { > { \@@_arg_to_keyvalue:nn { \tl_trim_spaces:n {#1} } } }
+ }
+ }
+\cs_new_protected:Npn \@@_normalize_type_aux:NnNn #1#2#3#4
+ {
+ \quark_if_recursion_tail_stop_do:nn {#2} { \@@_bad_arg_spec:wn }
+ \bool_if:NT #3
{
\msg_error:nnxx { cmd } { two-markers }
- { \@@_environment_or_command: } { ! }
+ { \@@_environment_or_command: } { #1 }
\@@_bad_def:wn
}
- \bool_set_true:N \l_@@_obey_spaces_bool
- \bool_set_true:N \l_@@_some_obey_spaces_bool
+ #4
\int_decr:N \l_@@_current_arg_int
- \@@_normalize_arg_spec_loop:n {#1}
+ \@@_normalize_arg_spec_loop:n {#2}
}
% \end{macrocode}
% \end{macro}
+% \end{macro}
%
% \begin{macro}
% {
@@ -1224,12 +1251,12 @@
% \end{macro}
%
% \begin{macro}{\@@_allowed_token_check:N}
-% Some tokens are now allowed as delimiters for some argument types,
+% Some tokens are not allowed as delimiters for some argument types,
% notably implicit begin/end-group tokens (|\bgroup|/|\egroup|).
% The major problem with these tokens is that for |\peek_...| functions,
% a literal~|{|$_1$. is virtually indistinguishable from a |\bgroup| or
% other token which was |\let| to a~|{|$_1$, and the same goes
-% for~|}|$_2$. All other tokens can be easily distingushed from their
+% for~|}|$_2$. All other tokens can be easily distinguished from their
% implicit counterparts by grabbing them and looking at the string
% length (see \cs{@@_token_if_cs:NTF}), but for begin/end group tokens
% that is not possible without the risk of mistakenly grabbing the
@@ -1324,13 +1351,13 @@
% \begin{macro}{\@@_add_arg_spec:n, \@@_add_arg_spec_mandatory:n}
% When adding an argument to the argument specification, set the
% \texttt{some_long} or \texttt{some_short} booleans as appropriate
-% and clear the booleans keeping track of |+| and |!| markers.
+% and clear the booleans keeping track of |+|, |!| and |=| markers.
% Before that, test for a short argument following some long
% arguments: this is forbidden for expandable commands and prevents
% grabbing arguments expandably.
%
% For mandatory arguments do some more work, in particular complain if
-% they were preceeded by~|!|.
+% they were preceded by~|!|.
% \begin{macrocode}
\cs_new_protected:Npn \@@_add_arg_spec:n #1
{
@@ -1387,6 +1414,7 @@
\int_zero:N \l_@@_current_arg_int
\bool_set_false:N \l_@@_long_bool
\bool_set_false:N \l_@@_obey_spaces_bool
+ \bool_set_false:N \l_@@_suppress_strip_bool
\int_zero:N \l_@@_m_args_int
\bool_set_false:N \l_@@_defaults_bool
\tl_clear:N \l_@@_defaults_tl
@@ -1484,6 +1512,22 @@
% \end{macrocode}
% \end{macro}
%
+% \begin{macro}{\@@_add_type_=:}
+% \changes{v1.1a}{2022/08/10}{Add support for \texttt{=} modifier}
+% A mix of the ideas from above: set a flag and add a processor.
+% \begin{macrocode}
+\cs_new_protected:cpn { @@_add_type_=:w } #1
+ {
+ \@@_flush_m_args:
+ \bool_set_true:N \l_@@_prefixed_bool
+ \bool_set_true:N \l_@@_suppress_sstrip_Bool
+ \bool_set_true:N \l_@@_process_some_bool
+ \tl_put_left:Nn \l_@@_process_one_tl { \@@_arg_to_keyvalue:nn {#1} }
+ \@@_prepare_signature_bypass:N
+ }
+% \end{macrocode}
+% \end{macro}
+%
% \begin{macro}{\@@_add_type_b:w}
% \begin{macrocode}
\cs_new_protected:Npn \@@_add_type_b:w
@@ -1677,11 +1721,16 @@
@@_grab_ #1
\bool_if:NT \l_@@_long_bool { _long }
\bool_if:NT \l_@@_obey_spaces_bool { _obey_spaces }
+ \bool_lazy_and:nnT
+ { \l_@@_suppress_strip_bool }
+ { \str_if_eq_p:nn {#1} { D } }
+ { _no_strip }
:w
}
}
\bool_set_false:N \l_@@_long_bool
\bool_set_false:N \l_@@_obey_spaces_bool
+ \bool_set_false:N \l_@@_suppress_strip_bool
\tl_put_right:Nx \l_@@_process_all_tl
{
{
@@ -2615,6 +2664,7 @@
% \@@_grab_b_aux:NNw,
% \@@_grab_b_end:Nw
% }
+% \changes{v1.1a}{2022/08/10}{Track changes in \texttt{D}-type implementation}
% This uses the well-tested code of \texttt{D}-type arguments,
% skipping the peeking step because the \texttt{b}-type argument is
% always present, and adding a cleanup stage at the end by hijacking
@@ -2640,7 +2690,7 @@
{ \@@_grab_b_aux:NNw \cs_set_protected:Npn \exp_not:n }
\cs_new_protected:Npn \@@_grab_b_aux:NNw #1#2#3 \@@_run_code:
{
- \@@_grab_D_aux:NNnN \begin \end {#3} #1
+ \@@_grab_D_aux:NNnNN \begin \end {#3} #1 \use_ii:nn
\tl_put_left:Nn \l_@@_signature_tl { \@@_grab_b_end:Nw #2 }
\tl_set_eq:NN \l_@@_saved_args_tl \l_@@_args_tl
\tl_clear:N \l_@@_args_tl
@@ -2660,37 +2710,69 @@
% \end{macrocode}
% \end{macro}
%
-% \begin{macro}{\@@_grab_D:w}
-% \begin{macro}{\@@_grab_D_long:w}
-% \begin{macro}{\@@_grab_D_obey_spaces:w}
-% \begin{macro}{\@@_grab_D_long_obey_spaces:w}
+% \begin{macro}
+% {
+% \@@_grab_D:w ,
+% \@@_grab_D_long:w ,
+% \@@_grab_D_obey_spaces:w ,
+% \@@_grab_D_long_obey_spaces:w ,
+% \@@_grab_D_no_strip:w ,
+% \@@_grab_D_long_no_strip:w ,
+% \@@_grab_D_obey_spaces_no_strip:w ,
+% \@@_grab_D_long_obey_spaces_no_strip:w
+% }
+% \changes{v1.1a}{2022/08/10}{Add support for skipping brace stripping}
% The generic delimited argument grabber. The auxiliary function does
% a peek test before calling \cs{@@_grab_D_call:Nw}, so that the
% optional nature of the argument works as expected.
% \begin{macrocode}
\cs_new_protected:Npn \@@_grab_D:w #1#2#3 \@@_run_code:
{
- \@@_grab_D_aux:NNnNN #1 #2 {#3} \cs_set_protected_nopar:Npn
- \@@_peek_nonspace_remove:NTF
+ \@@_grab_D_aux:NNnNNN #1 #2 {#3} \cs_set_protected_nopar:Npn
+ \@@_peek_nonspace_remove:NTF \use_ii:nn
}
\cs_new_protected:Npn \@@_grab_D_long:w #1#2#3 \@@_run_code:
{
- \@@_grab_D_aux:NNnNN #1 #2 {#3} \cs_set_protected:Npn
- \@@_peek_nonspace_remove:NTF
+ \@@_grab_D_aux:NNnNNN #1 #2 {#3} \cs_set_protected:Npn
+ \@@_peek_nonspace_remove:NTF \use_ii:nn
}
\cs_new_protected:Npn \@@_grab_D_obey_spaces:w #1#2#3 \@@_run_code:
{
- \@@_grab_D_aux:NNnNN #1 #2 {#3} \cs_set_protected_nopar:Npn
- \@@_peek_meaning_remove:NTF
+ \@@_grab_D_aux:NNnNNN #1 #2 {#3} \cs_set_protected_nopar:Npn
+ \@@_peek_meaning_remove:NTF \use_ii:nn
}
\cs_new_protected:Npn \@@_grab_D_long_obey_spaces:w #1#2#3 \@@_run_code:
{
- \@@_grab_D_aux:NNnNN #1 #2 {#3} \cs_set_protected:Npn
- \@@_peek_meaning_remove:NTF
+ \@@_grab_D_aux:NNnNNN #1 #2 {#3} \cs_set_protected:Npn
+ \@@_peek_meaning_remove:NTF \use_ii:nn
+ }
+\cs_new_protected:Npn \@@_grab_D_no_strip:w
+ #1#2#3 \@@_run_code:
+ {
+ \@@_grab_D_aux:NNnNNN #1 #2 {#3} \cs_set_protected_nopar:Npn
+ \@@_peek_nonspace_remove:NTF \use_none:n
+ }
+\cs_new_protected:Npn \@@_grab_D_long_no_strip:w
+ #1#2#3 \@@_run_code:
+ {
+ \@@_grab_D_aux:NNnNNN #1 #2 {#3} \cs_set_protected:Npn
+ \@@_peek_nonspace_remove:NTF \use_none:n
+ }
+\cs_new_protected:Npn \@@_grab_D_obey_spaces_no_strip:w
+ #1#2#3 \@@_run_code:
+ {
+ \@@_grab_D_aux:NNnNNN #1 #2 {#3} \cs_set_protected_nopar:Npn
+ \@@_peek_meaning_remove:NTF \use_none:n
+ }
+\cs_new_protected:Npn \@@_grab_D_long_obey_spaces_no_strip:w
+ #1#2#3 \@@_run_code:
+ {
+ \@@_grab_D_aux:NNnNNN #1 #2 {#3} \cs_set_protected:Npn
+ \@@_peek_meaning_remove:NTF \use_none:n
}
% \end{macrocode}
+% \begin{macro}{\@@_grab_D_aux:NNnNNN}
% \begin{macro}{\@@_grab_D_aux:NNnNN}
-% \begin{macro}{\@@_grab_D_aux:NNnN}
% This is a bit complicated. The idea is that, in order to check for
% nested optional argument tokens (\texttt{[[...]]} and so on) the
% argument needs to be grabbed without removing any braces at all. If
@@ -2699,9 +2781,9 @@
% prevents loss of braces, and there is then a test to see if there are
% nested delimiters to handle.
% \begin{macrocode}
-\cs_new_protected:Npn \@@_grab_D_aux:NNnNN #1#2#3#4#5
+\cs_new_protected:Npn \@@_grab_D_aux:NNnNNN #1#2#3#4#5#6
{
- \@@_grab_D_aux:NNnN #1#2 {#3} #4
+ \@@_grab_D_aux:NNnNN #1#2 {#3} #4 #6
#5 #1
{ \@@_grab_D_call:Nw #1 }
{ \@@_add_arg:o \c_novalue_tl }
@@ -2712,9 +2794,10 @@
% extra factors to allow for: the argument might be entirely empty, and
% spaces at the start and end of the input must be retained around a brace
% group. Also notice that a \emph{blank} argument might still contain
-% spaces.
+% spaces. To allow for suppression of brace stripping, the business end
+% is passed here as |#5|.
% \begin{macrocode}
-\cs_new_protected:Npn \@@_grab_D_aux:NNnN #1#2#3#4
+\cs_new_protected:Npn \@@_grab_D_aux:NNnNN #1#2#3#4#5
{
\tl_set:Nn \l_@@_signature_tl {#3}
\exp_after:wN #4 \l_@@_fn_tl ##1 #2
@@ -2728,7 +2811,7 @@
\str_if_eq:eeTF
{ \exp_not:o { \use_none:n ##1 } }
{ { \exp_not:o { \use_ii:nnn ##1 \q_nil } } }
- { \@@_add_arg:o { \use_ii:nn ##1 } }
+ { \@@_add_arg:o { #5 ##1 } }
{ \@@_add_arg:o { \use_none:n ##1 } }
}
}
@@ -2738,9 +2821,6 @@
% \end{macro}
% \end{macro}
% \end{macro}
-% \end{macro}
-% \end{macro}
-% \end{macro}
%
% \begin{macro}{\@@_grab_D_nested:NNnN}
% \begin{macro}{\@@_grab_D_nested:w}
@@ -3039,6 +3119,7 @@
%
% \begin{macro}{\@@_grab_R:w, \@@_grab_R_long:w}
% \begin{macro}{\@@_grab_R_aux:NNnN}
+% \changes{v1.1a}{2022/08/10}{Track changes in \texttt{D}-type implementation}
% The grabber for \texttt{R}-type arguments is basically the same as
% that for \texttt{D}-type ones, but always skips spaces (as it is mandatory)
% and has a hard-coded error message.
@@ -3049,7 +3130,7 @@
{ \@@_grab_R_aux:NNnN #1 #2 {#3} \cs_set_protected:Npn }
\cs_new_protected:Npn \@@_grab_R_aux:NNnN #1#2#3#4
{
- \@@_grab_D_aux:NNnN #1 #2 {#3} #4
+ \@@_grab_D_aux:NNnNN #1 #2 {#3} #4 \use_ii:nn
\@@_peek_nonspace_remove:NTF #1
{ \@@_grab_D_call:Nw #1 }
{
@@ -3873,6 +3954,166 @@
% \end{macrocode}
% \end{macro}
%
+% \subsection{Conversion to key--value form}
+%
+% This is implemented as a process but with no public interfaces,
+% hence is treated separately from the others: it's a feature of
+% \pkg{ltcmd} which just happens to use the same mechanism as a processor.
+%
+% \begin{macro}{\@@_arg_to_keyvalue:nn}
+% \changes{v1.1a}{2022/08/10}{New internal arg-to-keyval processor}
+% \begin{macro}{\@@_arg_to_keyvalue_auxi:nnn}
+% \begin{macro}{\@@_arg_to_keyvalue_auxii:nnNw}
+% \begin{macro}{\@@_arg_to_keyvalue_auxiii:nnn}
+% \begin{macro}{\@@_arg_to_keyvalue_auxiv:nnNw}
+% \begin{macro}{\@@_arg_to_keyvalue_auxv:nn}
+% \begin{macro}{\@@_arg_to_keyvalue_loop:nnw}
+% \begin{macro}{\@@_arg_to_keyvalue_loop_group:nnn}
+% \begin{macro}{\@@_arg_to_keyvalue_loop_space:nnw}
+% \begin{macro}{\@@_arg_to_keyvalue_loop_N_type:nnN}
+% \begin{macro}{\@@_arg_to_keyvalue_math:nnw}
+% \begin{macro}{\@@_arg_to_keyvalue_math_N_type:nnN}
+% \begin{macro}{\@@_arg_to_keyvalue_math_group:nnn}
+% \begin{macro}{\@@_arg_to_keyvalue_math_space:nnw}
+% If the entire argument is braced, we treat as free text and return as
+% the value for the text key. Alternatively, if the start of the input is
+% |=,| then it is forced to be key--value. To avoid needing to worry about
+% catcodes for this, and to allow spaces around the |=|, we use a
+% series of steps rather than a delimited argument.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_arg_to_keyvalue:nn #1#2
+ {
+ \str_if_eq:eeTF { \exp_not:n {#2} } { \exp_not:o { { \use:n #2 } } }
+ { \tl_set:Nn \ProcessedArgument { #1 = #2 } }
+ {
+ \exp_args:Ne \@@_arg_to_keyvalue_auxi:nnn
+ { \tl_trim_spaces:n {#2} } {#1} {#2}
+ }
+ }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_auxi:nnn #1#2#3
+ {
+ \tl_if_head_is_N_type:nTF {#1}
+ { \@@_arg_to_keyvalue_auxii:nnNw {#2} {#3} #1 \q_@@_stop }
+ { \@@_arg_to_keyvalue_auxv:nn {#2} {#3} }
+ }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_auxii:nnNw
+ #1#2#3#4 \q_@@_stop
+ {
+ \str_if_eq:nnTF {#3} { = }
+ {
+ \exp_args:Ne \@@_arg_to_keyvalue_auxiii:nnn
+ { \tl_trim_spaces:n {#4} } {#1} {#2}
+ }
+ { \@@_arg_to_keyvalue_auxv:nn {#1} {#2} }
+ }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_auxiii:nnn #1#2#3
+ {
+ \tl_if_head_is_N_type:nTF {#1}
+ { \@@_arg_to_keyvalue_auxiv:nnNw {#2} {#3} #1 \q_@@_stop }
+ { \@@_arg_to_keyvalue_auxv:nn {#2} {#3} }
+ }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_auxiv:nnNw
+ #1#2#3#4 \q_@@_stop
+ {
+ \str_if_eq:nnTF {#3} { , }
+ { \tl_set:Nn \ProcessedArgument {#4} }
+ { \@@_arg_to_keyvalue_auxv:nn {#1} {#2} }
+ }
+% \end{macrocode}
+% The two clear-cut cases have been eliminated, and we therefore have to deal
+% with a search for |=| signs. We need an \enquote{action} loop here
+% so we do not get mislead by for example |{=}|. As the code here is for
+% very much predictable types of input, we hard-code what constitutes
+% math mode opening and closing.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_arg_to_keyvalue_auxv:nn #1#2
+ {
+ \@@_arg_to_keyvalue_loop:nnw {#1} {#2} #2
+ \q_recursion_tail \q_recursion_stop
+ }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_loop:nnw
+ #1#2#3 \q_recursion_stop
+ {
+ \tl_if_head_is_N_type:nTF {#3}
+ { \@@_arg_to_keyvalue_loop_N_type:nnN }
+ {
+ \tl_if_head_is_group:nTF {#3}
+ { \@@_arg_to_keyvalue_loop_group:nnn }
+ { \@@_arg_to_keyvalue_loop_space:nnw }
+ }
+ {#1} {#2} #3 \q_recursion_stop
+ }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_loop_group:nnn #1#2#3
+ { \@@_arg_to_keyvalue_loop:nnw {#1} {#2} }
+\use:x
+ {
+ \cs_new_protected:Npn
+ \exp_not:N \@@_arg_to_keyvalue_loop_space:nnw ##1##2 \c_space_tl
+ }
+ { \@@_arg_to_keyvalue_loop:nnw {#1} {#2} }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_loop_N_type:nnN #1#2#3
+ {
+ \quark_if_recursion_tail_stop_do:Nn #3
+ { \tl_set:Nn \ProcessedArgument { #1 = {#2} } }
+ \str_if_eq:nnTF {#1} { = }
+ {
+ \use_i_delimit_by_q_recursion_stop:nw
+ { \tl_set:Nn \ProcessedArgument {#2} }
+ }
+ {
+ \bool_lazy_or:nnTF
+ { \str_if_eq_p:nn {#1} { $ } }
+ { \str_if_eq_p:nn {#1} { \( } }
+ { \@@_arg_to_keyvalue_math:nnw {#1} {#2} }
+ { \@@_arg_to_keyvalue_loop:nnw {#1} {#2} }
+ }
+ }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_math:nnw
+ #1#2#3 \q_recursion_stop
+ {
+ \tl_if_head_is_N_type:nTF {#3}
+ { \@@_arg_to_keyvalue_math_N_type:nnN }
+ {
+ \tl_if_head_is_group:nTF {#3}
+ { \@@_arg_to_keyvalue_math_group:nnn }
+ { \@@_arg_to_keyvalue_math_space:nnw }
+ }
+ {#1} {#2} #3 \q_recursion_stop
+ }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_math_N_type:nnN #1#2#3
+ {
+ \quark_if_recursion_tail_stop_do:Nn #3
+ { \tl_set:Nn \ProcessedArgument { #1 = {#2} } }
+ \bool_lazy_or:nnTF
+ { \str_if_eq_p:nn {#1} { $ } }
+ { \str_if_eq_p:nn {#1} { \) } }
+ { \@@_arg_to_keyvalue_loop:nnw {#1} {#2} }
+ { \@@_arg_to_keyvalue_math:nnw {#1} {#2} }
+ }
+\cs_new_protected:Npn \@@_arg_to_keyvalue_math_group:nnn #1#2#3
+ { \@@_arg_to_keyvalue_math:nnw {#1} {#2} }
+\use:x
+ {
+ \cs_new_protected:Npn
+ \exp_not:N \@@_arg_to_keyvalue_loop_math:nnw ##1##2 \c_space_tl
+ }
+ { \@@_arg_to_keyvalue_math:nnw {#1} {#2} }
+% \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}
+% \end{macro}
+% \end{macro}
+%
% \subsection{Access to the argument specification}
%
% \begin{macro}{\@@_get_arg_spec_error:N, \@@_get_arg_spec_error:n}
diff --git a/base/testfiles-ltcmd/ltcmd008.lvt b/base/testfiles-ltcmd/ltcmd008.lvt
new file mode 100644
index 00000000..0876de67
--- /dev/null
+++ b/base/testfiles-ltcmd/ltcmd008.lvt
@@ -0,0 +1,41 @@
+
+\documentclass{minimal}
+\input{regression-test}
+\RequirePackage[check-declarations]{expl3}
+\ExplSyntaxOn
+\debug_on:n { deprecation }
+\ExplSyntaxOff
+
+\START
+\AUTHOR{Joseph Wright}
+
+\ExplSyntaxOn
+
+\TEST { Basic~definitions~using~={...} }
+ {
+ \DeclareDocumentCommand \foo { = { TOC-entry } O{#2} m } { \TYPE { \detokenize { (#1) (#2) } } }
+ \foo { bar }
+ \foo [ bar ] { baz }
+ }
+
+\TEST { Collecting~keyvals }
+ {
+ \DeclareDocumentCommand \foo { = { TOC-entry } O{ } m } { \TYPE { \detokenize { (#1) (#2) } } }
+ \foo [ bong ] { baz }
+ \foo [ bong = bang ] { baz }
+ \foo [ = , bong ] { baz }
+ \foo [ { bong = bang } ] { baz }
+ \foo [ $y = mx + c$ ] { baz }
+ \foo [ \( y = mx + c \) ] { baz }
+ \foo [ math = $y \mathbin{=} mx + c$ ] { baz }
+ }
+
+\TEST { Mixed|argument~types~using~={...} }
+ {
+ \DeclareDocumentCommand \foo { = { TOC-entry } m } { \TYPE { \detokenize { (#1) } } }
+ \foo { bar }
+ \foo { { baz } }
+ \foo { bong = bing }
+ }
+
+\END
\ No newline at end of file
diff --git a/base/testfiles-ltcmd/ltcmd008.tlg b/base/testfiles-ltcmd/ltcmd008.tlg
new file mode 100644
index 00000000..463e4e27
--- /dev/null
+++ b/base/testfiles-ltcmd/ltcmd008.tlg
@@ -0,0 +1,27 @@
+This is a generated file for the LaTeX2e validation system.
+Don't change this file in any respect.
+Author: Joseph Wright
+============================================================
+TEST 1: Basic definitions using ={...}
+============================================================
+(TOC-entry={bar})(bar)
+(TOC-entry={bar})(baz)
+============================================================
+============================================================
+TEST 2: Collecting keyvals
+============================================================
+(TOC-entry={bong})(baz)
+(TOC-entry={bong=bang})(baz)
+(bong)(baz)
+(TOC-entry={bong=bang})(baz)
+(TOC-entry={$y=mx+c$})(baz)
+(TOC-entry={\(y=mx+c\)})(baz)
+(TOC-entry={math=$y\mathbin {=}mx+c$})(baz)
+============================================================
+============================================================
+TEST 3: Mixed|argument types using ={...}
+============================================================
+(TOC-entry={bar})
+(TOC-entry={{baz}})
+(TOC-entry={bong=bing})
+============================================================
More information about the latex3-commits
mailing list.