summaryrefslogtreecommitdiff
path: root/macros/latex-dev/base/ltcmdhooks.dtx
diff options
context:
space:
mode:
Diffstat (limited to 'macros/latex-dev/base/ltcmdhooks.dtx')
-rw-r--r--macros/latex-dev/base/ltcmdhooks.dtx1093
1 files changed, 1093 insertions, 0 deletions
diff --git a/macros/latex-dev/base/ltcmdhooks.dtx b/macros/latex-dev/base/ltcmdhooks.dtx
new file mode 100644
index 0000000000..cc67f5eaa7
--- /dev/null
+++ b/macros/latex-dev/base/ltcmdhooks.dtx
@@ -0,0 +1,1093 @@
+% \iffalse meta-comment
+%
+%% File: ltcmdhooks.dtx (C) Copyright 2020-2021
+%% Frank Mittelbach, Phelype Oleinik, LaTeX Team
+%
+% It may be distributed and/or modified under the conditions of the
+% LaTeX Project Public License (LPPL), either version 1.3c of this
+% license or (at your option) any later version. The latest version
+% of this license is in the file
+%
+% https://www.latex-project.org/lppl.txt
+%
+%
+%%% From File: ltcmdhooks.dtx
+%
+\def\ltcmdhooksversion{v1.0a}
+\def\ltcmdhooksdate{2021/04/30}
+%
+%
+%
+%<*driver>
+\documentclass{l3doc}
+%\usepackage{ltcmdhooks}
+\makeatletter
+\providecommand\footref[1]{%
+ \begingroup
+ \unrestored@protected@xdef\@thefnmark{\ref{#1}}%
+ \endgroup
+ \@footnotemark}
+\makeatother
+\EnableCrossrefs
+\CodelineIndex
+\begin{document}
+ \DocInput{ltcmdhooks.dtx}
+\end{document}
+%</driver>
+%
+% \fi
+%
+%
+% \providecommand\hook[1]{\texttt{#1}}
+% \providecommand\fmi[1]{\marginpar{\footnotesize FMi: #1}}
+% \providecommand\pho[1]{\marginpar{\footnotesize PhO: #1}}
+% \providecommand\phoinline[1]{\begin{quote}\itshape\footnotesize PhO: #1\end{quote}}
+%
+% \title{The \texttt{ltcmdhooks} module\thanks{This file has version
+% \ltcmdhooksversion\ dated \ltcmdhooksdate, \copyright\ \LaTeX\
+% Project.}}
+% \author{Frank Mittelbach \and Phelype Oleinik}
+%
+% \maketitle
+%
+%
+% \tableofcontents
+%
+%
+% \section{Introduction}
+%
+% This file implements generic hooks for (arbitrary) commands.
+% In theory every command \tn[no-index]{\meta{name}} offers now two
+% associated hooks to which code can be added using \tn{AddToHook}
+% or \tn{AddToHookNext}.\footnote{In practice this is not supported
+% for all types of commands, see section~\ref{sec:look-ahead} for
+% the restrictions that apply and what happens if one tries to use
+% this with commands for which this is not supported.} These are
+% \begin{description}
+% \item[\hook{cmd/\meta{name}/before}]
+%
+% This hook is executed at the very start of the command
+% execution after its arguments (if any) are parsed. The hook
+% \meta{code} is wrapped in the command inside a call to
+% \cs{UseHook}|{cmd/|\meta{name}|/before}|, so the arguments
+% passed to the command are \emph{not} available in the hook
+% \meta{code}.
+%
+% \item[\hook{cmd/\meta{name}/after}]
+%
+% This hook is similar to \hook{cmd/\meta{name}/before}, but it is
+% executed at the very end of the command body. This hook is
+% implemented as a reversed hook.
+% \end{description}
+%
+% The hooks are not physically present before
+% \verb=\begin{document}= (i.e., using a command in the preamble
+% will never execute them) and if nobody has declared any code for
+% them, then they are not added to the command code ever. For
+% example, if we have the following definition
+%\begin{verbatim}
+% \newcommand\foo[2]{Code #1 for #2!}
+%\end{verbatim}
+% then executing \verb=\foo{A}{B}= will simply run
+% \verb*=Code A for B!=
+% as it was always the case. However, if somebody, somewhere (e.g.,
+% in a package) adds
+%\begin{verbatim}
+% \AddToHook{cmd/foo/before}{<before code>}
+%\end{verbatim}
+% then, after |\begin{document}| the definition of \cs[no-index]{foo} will be:
+%\begin{verbatim}
+% \renewcommand\foo[2]{\UseHook{cmd/foo/before}Code #1 for #2!}
+%\end{verbatim}
+% and similarly \verb=\AddToHook{cmd/foo/after}{<after code>}=
+% alters the definition to
+%\begin{verbatim}
+% \renewcommand\foo[2]{Code #1 for #2!\UseHook{cmd/foo/after}}
+%\end{verbatim}
+%
+% In other words, the mechanism is similar to what \pkg{etoolbox}
+% offers with \tn{pretocmd} and \tn{apptocmd} with the important
+% differences
+% \begin{itemize}
+% \item
+%
+% that code can be prepended or appended (i.e., added to the
+% hooks) even if the command itself is not defined, because the
+% defining package has not yet been loaded
+%
+% \item
+%
+% and that by using the hook management interface it is now
+% possible to define how the code chunks added in these places
+% are ordered, if different packages want to add code at these
+% points.
+%
+% \end{itemize}
+%
+%
+%
+%
+% \section{Restrictions and Operational details}
+% \label{sec:restrictions}
+%
+% Adding arbitrary material to commands is tricky because most of the
+% time we do not know what the macro expects as arguments when expanding
+% and \TeX{} doesn't have a reliable way to see that, so some guesswork
+% has to be employed.
+%
+% \subsection{Patching}
+%
+% The code here tries to find out if a command was defined with
+% \tn{newcommand} or \tn{DeclareRobustCommand} or
+% \tn{NewDocumentCommand}, and if so it \emph{assumes} that the argument
+% specification of the command is as expected (which is not fail-proof,
+% if someone redefines the internals of these commands in devious ways,
+% but is a reasonable assumption).
+%
+% If the command is one of the defined types, the code here does a
+% sandboxed expansion of the command such that it can be redefined again
+% exactly as before, but with the hook code added.
+%
+% If however the command is not a known type (it was defined with
+% \tn{def}, for example), then the code uses an approach similar to
+% \pkg{etoolbox}'s \tn{patchcmd} to retokenize the command with the hook
+% code in place. This procedure, however, is more likely to fail if the
+% catcode settings are not the same as the ones at the time of command's
+% definition, so not always adding a hook to a command will work.
+%
+% \subsubsection{Timing}
+%
+% When \cs{AddToHook} (or its \pkg{expl3} equivalent) is called with
+% a generic |cmd| hook, say, \hook{cmd/foo/before}, for the first time
+% (that is, no code was added to that same hook before), in the preamble
+% of a document, it will store a patch instruction for that command
+% until |\begin{document}|, and only then all the commands which had
+% hooks added will be patched in one go. That means that no command in
+% the preamble will have hooks patched into them.
+%
+% At |\begin{document}| all the delayed patches will be executed, and
+% if the command doesn't exist the code is still added to the hook,
+% but it will not be executed. After |\begin{document}|, when
+% \cs{AddToHook} is called with a generic |cmd| hook the first time,
+% the command will be immediately patched to include the hook, and if
+% it doesn't exist or if it can't be patched for any reason, an error
+% is thrown; if \cs{AddToHook} was already used in the preamble no new
+% patching is attempted.
+%
+% This has the consequence that a command defined or redefined after
+% |\begin{document}| only uses generic |cmd| hook code if
+% \cs{AddToHook} is called for the first time after the definition is
+% made, or if the command explicitly uses the generic hook in its
+% definition by declaring it with \cs{NewHookPair} adding \cs{UseHook} as
+% part of the code.\footnote{We might change this behavior in the main
+% document slightly after gaining some usage experience.}
+%
+%
+% \subsection{Commands that look ahead}
+% \label{sec:look-ahead}
+%
+% Some commands are defined in different ``steps'' and they look ahead
+% in the input stream to find more arguments. If you try to add some
+% code to the \hook{cmd/\meta{name}/after} hook of such command, it will
+% not work, and it is not possible to detect that programmatically, so
+% the user has to know (or find out) which commands can or cannot have
+% hooks attached to them.
+%
+% One good example is the \tn{section} command. You can add something
+% to the \hook{cmd/section/before} hook, but if you try to add something
+% to the \hook{cmd/section/after} hook, \tn{section} will no longer
+% work. That happens because the \tn{section} macro takes no argument,
+% but instead calls a few internal \LaTeX{} macros to look for the
+% optional and mandatory arguments. By adding code to the
+% \hook{cmd/section/after} hook, you get in the way of that scanning.
+%
+%
+%
+% \section{Package Author Interface}
+%
+% The \hook{cmd} hooks are, by default, available for all commands
+% that can be patched to add the hooks. For some commands, however,
+% the very beginning or the very end of the code is not the best place
+% to put the hooks, for example, if the command looks ahead for
+% arguments (see section~\ref{sec:look-ahead}).
+%
+% If you are a package author and you want to add the hooks to your
+% own commands in the proper position you can define the command and
+% manually add the \cs{UseHook} calls inside the command in the proper
+% positions, and manually define the hooks with \cs{NewHook} or
+% \cs{NewReversedHook}. When the hooks are explicitly defined,
+% patching is not attempted so you can make sure your command works
+% properly. For example, an (admittedly not really useful) command
+% that typesets its contents in a framed box with width optionally
+% given in parentheses:
+% \begin{verbatim}
+% \newcommand\fancybox{\@ifnextchar({\@fancybox}{\@fancybox(5cm)}}
+% \def\@fancybox(#1)#2{\fbox{\parbox{#1}{#2}}}
+% \end{verbatim}
+% If you try that definition, then add some code after it with
+% \begin{verbatim}
+% \AddToHook{cmd/fancybox/after}{<code>}
+% \end{verbatim}
+% and then use the \cs[no-index]{fancybox} command you will see that it
+% will be completely broken, because the hook will get executed in the
+% middle of parsing for optional \texttt{(...)} argument.
+%
+% If, on the other hand, you want to add hooks to your command you can
+% do something like:
+% \begin{verbatim}
+% \newcommand\fancybox{\@ifnextchar({\@fancybox}{\@fancybox(5cm)}}
+% \def\@fancybox(#1)#2{\fbox{%
+% \UseHook{cmd/fancybox/before}%
+% \parbox{#1}{#2}%
+% \UseHook{cmd/fancybox/after}}}
+% \NewHook{cmd/fancybox/before}
+% \NewReversedHook{cmd/fancybox/after}
+% \end{verbatim}
+% then the hooks will be executed where they should and no patching
+% will be attempted. It is important that the hooks are declared with
+% \cs{NewHook} or \cs{NewReversedHook}, otherwise the command hook
+% code will try to patch the command. Note also that the call to
+% |\UseHook{cmd/fancybox/before}| does not need to be in the
+% definition of \cs[no-index]{fancybox}, but anywhere it makes sense
+% to insert it (in this case in the internal
+% \cs[no-index]{@fancybox}).
+%
+% Alternatively, if for whatever reason your command does not support
+% the generic hooks provided here, you can disable a hook with
+% \cs{DisableHook}\footnote{Please use \cs{DisableHook} if at all, only
+% on hooks that you \enquote{own}, i.e., for commands that your
+% package or class defines and not second guess
+% whether or not hooks of other packages should get disabled!}, so
+% that when someone tries to add code to it they will get an error.
+% Or if you don't want the error, you can simply declare the hook with
+% \cs{NewHook} and never use it.
+%
+%
+% The above approach is useful for really complex commands where for
+% one or the other reason the hooks can't be placed at the very
+% beginning and end of the command body and some hand-crafting is
+% needed. However, in the example above the real (and in fact only)
+% issue is the cascading argument parsing in the style developed long
+% ago in \LaTeX~2.09. Thus, a much simpler solution for this case is
+% to replace it with the modern \cs{NewDocumentCommand} syntax and
+% define the command as follows:
+% \begin{verbatim}
+% \DeclareDocumentCommand\fancybox{D(){5cm}m}{\fbox{\parbox{#1}{#2}}}
+% \end{verbatim}
+% If you do that then both hooks automatically work and are patched
+% into the right places.
+%
+% \StopEventually{\setlength\IndexMin{200pt} \PrintIndex }
+%
+%
+%
+% \section{The Implementation}
+%
+% \subsection{Execution plan}
+%
+% To add |before| and |after| hooks to a command we will need to peek
+% into the definition of a command, which is always a tricky thing to
+% do. Some cases are easy because we know how the command was defined,
+% so we can assume how its \meta{parameter text} looks like (for example
+% a command defined with \tn{newcommand} may have an optional argument
+% followed by a run of mandatory arguments), so we can just expand that
+% command and make it grab |#1|, |#2|, etc.\@ as arguments and
+% define it all back with the hooks added.
+%
+% Life's usually not that easy, so with some commands we can't do that
+% (a |#1| might as well be |#|$_{12}$|1|$_{12}$ instead of the expected
+% |#|$_{6}$|1|$_{12}$, for example) so we need to resort to ``patching''
+% the command: read its \tn{meaning}, and tokenize it again with
+% \tn{scantokens} and hope for the best.
+%
+% So the overall plan is:
+% \begin{enumerate}
+% \item
+% Check if a command is of a known type (that is, defined with
+% \tn{newcommand}\footnote{It's not always possible to reliably
+% detect this case because a command defined with no optional
+% argument is indistinguishable from a \tn{def}ed command.},
+% \cs[no-index]{DeclareRobustCommand}, or
+% \cs[no-index]{New(Expandable)DocumentCommand}), and if is, take
+% appropriate action.
+% \item
+% If the command is not a known type, we'll check if the command can
+% be patched. Two things will prevent a command from being
+% patched: if it was defined in a nonstandard catcode setting, or
+% if it is an internal expl3 command with |__|\meta{module} in its
+% name, in which case we refuse to patch.
+% \item
+% If the command was defined in nonstandard catcode settings, we
+% will try a few standard ones to try our best to carry out the
+% pathing. If this doesn't help either, the code will give up and
+% throw an error.
+% \end{enumerate}
+%
+% \begin{macrocode}
+%<@@=hook>
+% \end{macrocode}
+%
+% \begin{macrocode}
+%<*2ekernel|latexrelease>
+\ExplSyntaxOn
+%<latexrelease>\NewModuleRelease{2021/06/01}{ltcmdhooks}
+%<latexrelease> {The~hook~management~system~for~commands}
+% \end{macrocode}
+%
+% \subsection{Variables}
+%
+% \begin{macro}[int]{\g_hook_patch_action_list_tl}
+% Pairs of |\if<cmd>..\patch<cmd>| to be used with
+% \tn{robust@command@act} when looking for a known patching
+% rule. This token list is exposed because we see some future
+% applications (with very specialized packages, such as
+% \pkg{etoolbox} that may want to extend the pairs processed. It is
+% not meant for general use which is why it is not documented in
+% the interface documentation above.
+% \begin{macrocode}
+\tl_new:N \g_hook_patch_action_list_tl
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\l_@@_patch_num_args_int}
+% The number of arguments in a macro being patched.
+% \begin{macrocode}
+\int_new:N \l_@@_patch_num_args_int
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\l_@@_patch_prefixes_tl}
+% \begin{macro}{\l_@@_patch_param_text_tl}
+% \begin{macro}{\l_@@_patch_replacement_tl}
+% The prefixes and parameters of the definition for the macro being
+% patched.
+% \begin{macrocode}
+\tl_new:N \l_@@_patch_prefixes_tl
+\tl_new:N \l_@@_patch_param_text_tl
+\tl_new:N \l_@@_patch_replacement_tl
+% \end{macrocode}
+% \end{macro}
+% \end{macro}
+% \end{macro}
+%
+% \begin{macro}{\g_@@_delayed_patches_prop}
+% A list containing the patches delayed to |\begin{document}|, so that
+% patching is not attempted twice.
+% \begin{macrocode}
+\prop_new:N \g_@@_delayed_patches_prop
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\@@_patch_debug:x}
+% A helper for patching debug info.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_patch_debug:x #1
+ { \@@_debug:n { \iow_term:x { [lthooks]~#1 } } }
+% \end{macrocode}
+% \end{macro}
+%
+% \subsection{Variants}
+%
+% \begin{macro}[int]{\tl_rescan:nV}
+% \pkg{expl3} function variants used throughout the code.
+% \begin{macrocode}
+\cs_generate_variant:Nn \tl_rescan:nn { nV }
+% \end{macrocode}
+% \end{macro}
+%
+% \subsection{Patching or delaying}
+%
+% \begin{macro}{\@@_try_put_cmd_hook:n,\@@_try_put_cmd_hook:w}
+% Before |\begin{document}| all patching is delayed.
+% This function is called from within \cs{AddToHook}, when code is
+% added to a generic |cmd| hook is newly declared. It checks whether
+% the patch position is valid, then proceeds to trying to patch or
+% delaying to |\begin{document}| if in the preamble.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_try_put_cmd_hook:n #1
+ { \@@_try_put_cmd_hook:w #1 / / / \s_@@_mark {#1} }
+\cs_new_protected:Npn \@@_try_put_cmd_hook:w
+ #1 / #2 / #3 / #4 \s_@@_mark #5
+ {
+ \@@_debug:n { \iow_term:n { ->~Adding~cmd~hook~to~'#2'~(#3): } }
+ \str_case:nnTF {#3}
+ { { before } { } { after } { } }
+ { \exp_args:Nc \@@_patch_cmd_or_delay:Nnn {#2} {#2} {#3} }
+ { \__kernel_msg_error:nnn { hooks } { wrong-cmd-hook } {#2} {#3} }
+ }
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\@@_patch_cmd_or_delay:Nnn}
+% \begin{macro}{\@@_cmd_begindocument_code:}
+% In the preamble, \cs{@@_patch_cmd_or_delay:Nnn} just adds the patch
+% instruction to a property list to be executed later.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_patch_cmd_or_delay:Nnn #1 #2 #3
+ {
+ \@@_debug:n { \iow_term:n { ->~Add~generic~cmd~hook~for~#2~(#3). } }
+ \@@_debug:n
+ { \iow_term:n { !~In~the~preamble:~delaying. } }
+ \prop_gput:Nnn \g_@@_delayed_patches_prop { #2 / #3 }
+ { \@@_cmd_try_patch:nn {#2} {#3} }
+ }
+% \end{macrocode}
+%
+% The delayed patches are added to a property list to prevent
+% duplication, and the code stored in the property list for each
+% key is executed. The function \cs{@@_patch_cmd_or_delay:Nnn} is
+% also redefined to be \cs{@@_patch_command:Nnn} so that no further
+% delaying is attempted.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_cmd_begindocument_code:
+ {
+ \cs_gset_eq:NN \@@_patch_cmd_or_delay:Nnn \@@_patch_command:Nnn
+ \prop_map_function:NN \g_@@_delayed_patches_prop { \use_ii:nn }
+ \prop_gclear:N \g_@@_delayed_patches_prop
+ \cs_undefine:N \@@_cmd_begindocument_code:
+ }
+\g@addto@macro \@kernel@after@begindocument
+ { \@@_cmd_begindocument_code: }
+% \end{macrocode}
+% \end{macro}
+% \end{macro}
+%
+% \begin{macro}{\@@_cmd_try_patch:nn}
+% At |\begin{document}| tries patching the command if the hook
+% was not manually created in the meantime. If the document does not
+% exist, no error is raised here as it may hook into a package that
+% wasn't loaded. Hooks added to commands in the document body still
+% raise an error if the command is not defined.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_cmd_try_patch:nn #1 #2
+ {
+ \@@_debug:n
+ { \iow_term:x { ->~\string\begin{document}~try~cmd / #1 / #2. } }
+ \@@_if_declared:nTF { cmd / #1 / #2 }
+ {
+ \@@_debug:n
+ { \iow_term:n { .->~Giving~up:~hook~already~created. } }
+ }
+ {
+ \cs_if_exist:cT {#1}
+ { \exp_args:Nc \@@_patch_command:Nnn {#1} {#1} {#2} }
+ }
+ }
+% \end{macrocode}
+% \end{macro}
+%
+%
+%
+%
+%
+% \subsection{Patching commands}
+%
+% \begin{macro}{\@@_patch_command:Nnn}
+% \begin{macro}{\@@_patch_check:NNnn}
+% \begin{macro}[TF]{\@@_if_public_command:N}
+% \begin{macro}{\@@_if_public_command:w}
+% \cs{@@_patch_command:Nnn} will do some sanity checks on the
+% argument to detect if it is possible to add hooks to the command,
+% and raises an error otherwise. If the command can contain hooks,
+% then it uses \tn{robust@command@act} to find out what type is the
+% command, and patch it accordingly.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_patch_command:Nnn #1 #2 #3
+ {
+ \@@_patch_debug:x { analyzing~'\token_to_str:N #1' }
+ \@@_patch_debug:x { \token_to_str:N #1 = \token_to_meaning:N #1 }
+ \@@_patch_check:NNnn \cs_if_exist:NTF #1 { undef }
+ {
+ \@@_patch_debug:x { ++~control~sequence~is~defined }
+ \@@_patch_check:NNnn \token_if_macro:NTF #1 { macro }
+ {
+ \@@_patch_debug:x { ++~control~sequence~is~a~macro }
+ \@@_patch_check:NNnn \@@_if_public_command:NTF #1 { expl3 }
+ {
+ \@@_patch_debug:x { ++~macro~is~not~private }
+ \robust@command@act
+ \g_hook_patch_action_list_tl #1
+ \@@_retokenize_patch:Nnn { #1 {#2} {#3} }
+ }
+ }
+ }
+ }
+% \end{macrocode}
+%
+% And here's the auxiliary used above:
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_patch_check:NNnn #1 #2 #3 #4
+ {
+ #1 #2 {#4}
+ {
+ \__kernel_msg_error:nnxx { hooks } { cant-patch }
+ { \token_to_str:N #2 } {#3}
+ }
+ }
+% \end{macrocode}
+% and a conditional \cs{@@_if_public_command:N} to check if a command
+% has |__| in its name (no other checking is performed). Primitives
+% with |:D| in their name could be included here, but they are already
+% discarded in the \cs{token_if_macro:NTF} test above.
+% \begin{macrocode}
+\use:x
+ {
+ \prg_new_protected_conditional:Npnn
+ \exp_not:N \@@_if_public_command:N ##1 { TF }
+ {
+ \exp_not:N \exp_last_unbraced:Nf
+ \exp_not:N \@@_if_public_command:w
+ { \exp_not:N \cs_to_str:N ##1 }
+ \tl_to_str:n { _ _ } \s_@@_mark
+ }
+ }
+\exp_last_unbraced:NNNNo
+\cs_new_protected:Npn \@@_if_public_command:w
+ #1 \tl_to_str:n { _ _ } #2 \s_@@_mark
+ {
+ \tl_if_empty:nTF {#2}
+ { \prg_return_true: }
+ { \prg_return_false: }
+ }
+% \end{macrocode}
+% \end{macro}
+% \end{macro}
+% \end{macro}
+% \end{macro}
+%
+%
+%
+%
+%
+%
+%
+% \subsubsection{Patching by expansion and redefinition}
+%
+% \begin{macro}[int]{\g_hook_patch_action_list_tl}
+% This is the list of known command types and the function that
+% patches the command hooks into them. The conditionals are taken
+% from \tn{ShowCommand}, \tn{NewCommandCopy} and
+% \cs{__kernel_cmd_if_xparse:NTF} defined in \texttt{ltcmd}.
+% \begin{macrocode}
+\tl_gset:Nn \g_hook_patch_action_list_tl
+ {
+ { \@if@DeclareRobustCommand \@@_patch_DeclareRobustCommand:Nnn }
+ { \@if@newcommand \@@_patch_newcommand:Nnn }
+ { \__kernel_cmd_if_xparse:NTF \@@_cmd_patch_xparse:Nnn }
+ }
+% \end{macrocode}
+% \end{macro}
+%
+%
+%
+%
+% \begin{macro}{\@@_patch_DeclareRobustCommand:Nnn}
+% At this point we know that the commands can be patched by expanding
+% then redefining. These are the cases of commands defined with
+% \tn{newcommand} with an optional argument or with
+% \tn{DeclareRobustCommand}.
+%
+% With \cs{@@_patch_DeclareRobustCommand:Nnn} we check if the command
+% has an optional argument (with a test counter-intuitively called
+% \tn{@if@newcommand}). If so, we forward the action to
+% \cs{@@_patch_newcommand:Nnn}, otherwise call the patching engine
+% \cs{@@_patch_expand_redefine:NNnn} with a \cs{c_false_bool} to
+% indicate that there is no optional argument.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_patch_DeclareRobustCommand:Nnn #1
+ {
+ \exp_args:Nc \@if@newcommand { \cs_to_str:N #1 ~ }
+ { \exp_args:Nc \@@_patch_newcommand:Nnn }
+ { \exp_args:NNc \@@_patch_expand_redefine:NNnn \c_false_bool }
+ { \cs_to_str:N #1 ~ }
+ }
+% \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \begin{macro}{\@@_patch_newcommand:Nnn}
+% If the command was defined with \tn{newcommand} and an optional
+% argument, call the patching engine with a \cs{c_true_bool} to flag
+% the presence of an optional argument, and with
+% \cs[no-index]{\string\command} to patch the actual code for
+% \cs[no-index]{command}.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_patch_newcommand:Nnn #1
+ {
+ \exp_args:NNc \@@_patch_expand_redefine:NNnn \c_true_bool
+ { \c_backslash_str \cs_to_str:N #1 }
+ }
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\@@_cmd_patch_xparse:Nnn}
+% And for commands defined by the \pkg{xparse} commands use this
+% for patching:
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_cmd_patch_xparse:Nnn #1
+ {
+ \exp_args:NNc \@@_patch_expand_redefine:NNnn \c_false_bool
+ { \cs_to_str:N #1 ~ code }
+ }
+% \end{macrocode}
+% \end{macro}
+%
+%
+%
+%
+% \begin{macro}{\@@_patch_expand_redefine:NNnn}
+% \begin{macro}[EXP]{\@@_make_prefixes:w}
+% Now the real action begins. Here we have in |#1| a boolean
+% indicating if the command has a |[|\ldots|]|-delimited argument, in
+% |#2| the command control sequence, in |#3| the name of the command
+% (note that |#1|${}\ne{}$|\csname#2\endcsname| at this point!), and
+% in |#4| the hook position, either |before| or |after|.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_patch_expand_redefine:NNnn #1 #2 #3 #4
+ {
+ \@@_patch_debug:x { ++~command~can~be~patched~without~rescanning }
+% \end{macrocode}
+% We'll start by counting the number of arguments in the command by
+% counting the number of characters in the \cs{cs_argument_spec:N} of
+% the macro, divided by two, and subtracting one if the command has an
+% optional argument (that is, an extra |[]| in its
+% \meta{parameter text}).
+% \begin{macrocode}
+ \int_set:Nn \l_@@_patch_num_args_int
+ {
+ \exp_args:Nf \str_count:n { \cs_argument_spec:N #2 } / 2
+ \bool_if:NT #1 { -1 }
+ }
+% \end{macrocode}
+% Now build two token lists:
+% \begin{description}
+% \item[\cs{l_@@_patch_param_text_tl}] will contain the
+% \meta{parameter text} to be used when redefining the macro. It
+% should be identical to the \meta{parameter text} used when
+% originally defining that macro.
+% \item[\cs{l_@@_patch_replacement_tl}] will contain braced pairs of
+% |#|$_{12}$\meta{num} to feed to the macro when expanded. This
+% token list as well as the previous will have the first item
+% surrounded by |[|\ldots|]| in the case of an optional argument.
+% \end{description}
+% \begin{macrocode}
+ \int_compare:nNnTF { \l_@@_patch_num_args_int } > { \c_zero_int }
+ {
+ \tl_set:Nx \l_@@_patch_param_text_tl
+ { \bool_if:NTF #1 { [####1] } { ####1 } }
+ \tl_set:Nx \l_@@_patch_replacement_tl
+ { \bool_if:NTF #1 { [####1] } { {####1} } }
+ \int_step_inline:nnn { 2 } { \l_@@_patch_num_args_int }
+ {
+ \tl_put_right:Nn \l_@@_patch_param_text_tl { ## ####1 }
+ \tl_put_right:Nn \l_@@_patch_replacement_tl { { ## ####1 } }
+ }
+ }
+ {
+ \tl_clear:N \l_@@_patch_param_text_tl
+ \tl_clear:N \l_@@_patch_replacement_tl
+ }
+% \end{macrocode}
+% Finally, before redefining, we need to also get the prefixes used
+% when defining the command. Here we ensure that the \tn{escapechar}
+% is printable, otherwise a macro defined with prefixes
+% |\protected \long| will have it \tn{meaning} printed as
+% |protectedlong|, making life unnecessarily complicated. Here the
+% \tn{escapechar} is changed to |/|, then we loop between pairs of
+% |/|\ldots|/| extracting the prefixes.
+% \begin{macrocode}
+ \group_begin:
+ \int_set:Nn \tex_escapechar:D { `\/ }
+ \use:x
+ {
+ \group_end:
+ \tl_set:Nx \exp_not:N \l_@@_patch_prefixes_tl
+ { \exp_not:N \@@_make_prefixes:w \cs_prefix_spec:N #2 / / }
+ }
+% \end{macrocode}
+% Now that all the needed tools are ready, without further ado we'll
+% redefine the command |#2|. The definition uses the prefixes
+% gathered in \cs{l_@@_patch_prefixes_tl}, a primitive \cs{tex_def:D}
+% to avoid adding extra prefixes, and the \meta{parameter text} from
+% \cs{l_@@_patch_param_text_tl}.
+%
+% Then finally, in the body of the definition, we insert
+% \hook{cmd/\#3/before} if |#4| is |before|, the code of the
+% command expanded once grabbing the parameters in
+% \cs{l_@@_patch_replacement_tl}, and \hook{cmd/\#3/after} if |#4| is
+% |after|.
+% \begin{macrocode}
+ \use:x
+ {
+ \l_@@_patch_prefixes_tl \tex_def:D
+ \exp_not:N #2 \exp_not:V \l_@@_patch_param_text_tl
+ {
+ \str_if_eq:nnT {#4} { before }
+ { \exp_not:N \UseHook { cmd / #3 / #4 } }
+ \exp_args:No \exp_not:o
+ { \exp_after:wN #2 \l_@@_patch_replacement_tl }
+ \str_if_eq:nnT {#4} { after }
+ { \exp_not:N \UseHook { cmd / #3 / #4 } }
+ }
+ }
+ }
+% \end{macrocode}
+%
+% Here's the auxiliary that makes the prefix control sequences for the
+% redefinition. Each item has to be \cs{tl_trim_spaces:n}'d because
+% the last item (and not any other) has a trailing space.
+% \begin{macrocode}
+\cs_new:Npn \@@_make_prefixes:w / #1 /
+ {
+ \tl_if_empty:nF {#1}
+ {
+ \exp_not:c { tex_ \tl_trim_spaces:n {#1} :D }
+ \@@_make_prefixes:w /
+ }
+ }
+% \end{macrocode}
+% \end{macro}
+% \end{macro}
+%
+%
+%
+%
+%
+%
+% \subsubsection{Patching by retokenization}
+%
+% At this point we've drained the possibilities of patching a command by
+% expansion-and-redefinition, so we have to resort to patching by
+% retokenizing the command. Patching by retokenization is done by
+% getting the \tn{meaning} of the command, doing the necessary
+% manipulations on the generated string, and the retokenizing that again
+% by using \tn{scantokens}.
+%
+% Patching by retokenization is definitely a riskier business, because
+% it relies that the tokens printed by \tn{meaning} produce the exact
+% same tokens as the ones in the original definition. That is, the
+% catcode régime must be exactly(ish) the same, and there is no way of
+% telling except by trial and error.
+%
+% \begin{macro}{\@@_retokenize_patch:Nnn}
+% This is the macro that will control the whole process. First we'll
+% try out one final, rather trivial case, of a command with no
+% arguments; that is, a token list. This case can be patched with
+% the expand-and-redefine routine but it has to be the very last case
+% tested for, because most (all?) robust commands start with a
+% top-level macro with no arguments, so testing this first would
+% short-circuit \tn{robust@command@act} and the top-level macros would
+% be incorrectly patched. In that case, we just check if the
+% \cs{cs_argument_spec:N} is empty, and call
+% \cs{@@_patch_expand_redefine:NNnn}.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_retokenize_patch:Nnn #1 #2 #3
+ {
+ \@@_patch_debug:x { ..~command~can~only~be~patched~by~rescanning }
+ \str_if_eq:eeTF { \cs_argument_spec:N #1 } { }
+ { \@@_patch_expand_redefine:NNnn \c_false_bool #1 {#2} {#3} }
+ {
+% \end{macrocode}
+%
+% Otherwise, we start the actual patching by retokenization job. The
+% code calls \cs{@@_try_patch_with_catcodes:Nnnnw} with a different
+% catcode setting:
+% \begin{itemize}
+% \item The current catcode setting;
+% \item Switching the catcode of |@|;
+% \item Switching the \pkg{expl3} syntax on or off;
+% \item Both of the above.
+% \end{itemize}
+%
+% If patching succeeds, \cs{@@_try_patch_with_catcodes:Nnnnw} has the
+% side-effect of patching the macro |#1| (which may be an internal
+% from the command whose name is~|#2|).
+% \begin{macrocode}
+ \tl_set:Nx \l_@@_tmpa_tl
+ {
+ \int_compare:nNnTF { \char_value_catcode:n {`\@ } } = { 12 }
+ { \exp_not:N \makeatletter } { \exp_not:N \makeatother }
+ }
+ \tl_set:Nx \l_@@_tmpb_tl
+ {
+ \bool_if:NTF \l__kernel_expl_bool
+ { \ExplSyntaxOff } { \ExplSyntaxOn }
+ }
+ \use:x
+ {
+ \exp_not:N \@@_try_patch_with_catcodes:Nnnnw
+ \exp_not:n { #1 {#2} {#3} }
+ { \prg_do_nothing: }
+ { \exp_not:V \l_@@_tmpa_tl } % @
+ { \exp_not:V \l_@@_tmpb_tl } % _:
+ {
+ \exp_not:V \l_@@_tmpa_tl % @
+ \exp_not:V \l_@@_tmpb_tl % _:
+ }
+ }
+ \q_recursion_tail \q_recursion_stop
+% \end{macrocode}
+%
+% If no catcode setting succeeds, give up and raise an error. The
+% command isn't changed in any way in that case.
+% \begin{macrocode}
+ {
+ \__kernel_msg_error:nnxx { hooks } { cant-patch }
+ { \c_backslash_str #2 } { retok }
+ }
+ }
+ }
+% \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \begin{macro}{\@@_try_patch_with_catcodes:Nnnnw}
+% This function is a simple wrapper around
+% \cs{@@_cmd_if_scanable:NnTF} and \cs{@@_patch_retokenize:Nnnn} if
+% the former returns \meta{true}, plus some debug messages.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_try_patch_with_catcodes:Nnnnw #1 #2 #3 #4
+ {
+ \quark_if_recursion_tail_stop_do:nn {#4} { \use:n }
+ \@@_patch_debug:x { ++~trying~to~patch~by~retokenization }
+ \@@_cmd_if_scanable:NnTF {#1} {#4}
+ {
+ \@@_patch_debug:x { ++~macro~can~be~retokenized~cleanly }
+ \@@_patch_debug:x { ==~retokenizing~macro~now }
+ \@@_patch_retokenize:Nnnn #1 {#2} {#3} {#4}
+ \use_i_delimit_by_q_recursion_stop:nw \use_none:n
+ }
+ {
+ \@@_patch_debug:x { --~macro~cannot~be~retokenized~cleanly }
+ \@@_try_patch_with_catcodes:Nnnnw #1 {#2} {#3}
+ }
+ }
+% \end{macrocode}
+% \end{macro}
+%
+%
+%
+%
+% \begin{macro}[int]{\kerneltmpDoNotUse}
+% This is an oddity required to be safe (as safe as reasonably
+% possible) when patching the command. The entirety of
+% \begin{quote}
+% \meta{prefixes} \tn{def} \meta{cs} \meta{parameter text}
+% |{|\meta{replacement text}|}|
+% \end{quote}
+% will go through \tn{scantokens}. The \meta{parameter text} and
+% \meta{replacement text} are what we are trying to retokenize, so not
+% much worry there. The other items, however, should ``just work'',
+% so some care is needed to not use too fancy catcode settings.
+% Therefore we can't use an \pkg{expl3}-named macro for \meta{cs}, nor
+% the \pkg{expl3} versions of \tn{def} or the \meta{prefixes}.
+% That is why the definitions that will eventually go into
+% \tn{scantokens} will use the oddly (but hopefully clearly)-named
+% \cs{kerneltmpDoNotUse}:
+% \begin{macrocode}
+\cs_new_eq:NN \kerneltmpDoNotUse !
+% \end{macrocode}
+% \phoinline{Maybe this can be avoided by running the \meta{parameter text}
+% and the \meta{replacement text} separately through \tn{scantokens}
+% and then putting everything together at the end.}
+% \end{macro}
+%
+%
+%
+% \begin{macro}{\@@_patch_required_catcodes:}
+% Here are the catcode settings that are \emph{mandatory} when
+% retokenizing commands. These are the minimum necessary settings to
+% perform the definitions: they identify control sequences, which
+% must be escaped with |\|$_0$, delimit the definition with |{|$_1$
+% and |}|$_2$, and mark parameters with |#|$_6$. Everything else may
+% be changed, but not these.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_patch_required_catcodes:
+ {
+ \char_set_catcode_escape:N \\
+ \char_set_catcode_group_begin:N \{
+ \char_set_catcode_group_end:N \}
+ \char_set_catcode_parameter:N \#
+ % \int_set:Nn \tex_endlinechar:D { -1 }
+ % \int_set:Nn \tex_newlinechar:D { -1 }
+ }
+% \end{macrocode}
+% \phoinline{\pkg{etoolbox} sets the \tn{endlinechar} and \tn{newlinechar}
+% when patching, but as far as I tested these didn't make much of
+% a difference, so I left them out for now. Maybe
+% \tn{newlinechar}|=-1| avoids a space token being added after the
+% definition.}
+% \phoinline{If the patching is split by \meta{parameter text} and
+% \meta{replacement text}, then only \# will have to stay in that
+% list.}
+% \phoinline{Actually now that we patch
+% \texttt{\cs{UseHook}\{cmd/foo/before\}}, all the tokens there need
+% to have the right catcodes, so this list now includes all
+% lowercase letters, U and H, the slash, and whatever characters in
+% the command name\ldots sigh\ldots}
+% \end{macro}
+%
+%
+%
+%
+% \begin{macro}[TF]{\@@_cmd_if_scanable:Nn}
+% Here we'll do a quick test if the command being patched can in fact
+% be retokenized with the specific catcode setting without changing
+% in meaning. The test is straightforward:
+% \begin{enumerate}
+% \item apply \tn{meaning} to the command;
+% \item split the \meta{prefixes}, \meta{parameter text} and
+% \meta{replacement text} and arrange them as
+% \begin{quote}
+% \meta{prefixes}\tn{def}\cs{kerneltmpDoNotUse}%^^A
+% \meta{parameter text}|{|\meta{replacement text}|}|
+% \end{quote}
+% \item rescan that with the given catcode settings, and do
+% the definition; then finally
+% \item compare \cs{kerneltmpDoNotUse} with the original command.
+% \end{enumerate}
+% If both are \tn{ifx}-equal, the command can be safely patched.
+% \begin{macrocode}
+\prg_new_protected_conditional:Npnn \@@_cmd_if_scanable:Nn #1 #2 { TF }
+ {
+ \cs_set_eq:NN \kerneltmpDoNotUse \scan_stop:
+ \cs_set_eq:NN \@@_tmp:w \scan_stop:
+ \use:x
+ {
+ \cs_set:Npn \@@_tmp:w
+ ####1 \tl_to_str:n { macro: } ####2 -> ####3 \s_@@_mark
+ { ####1 \def \kerneltmpDoNotUse ####2 {####3} }
+ \tl_set:Nx \exp_not:N \l_@@_tmpa_tl
+ { \exp_not:N \@@_tmp:w \token_to_meaning:N #1 \s_@@_mark }
+ }
+ \tl_rescan:nV { #2 \@@_patch_required_catcodes: } \l_@@_tmpa_tl
+ \token_if_eq_meaning:NNTF #1 \kerneltmpDoNotUse
+ { \prg_return_true: }
+ { \prg_return_false: }
+ }
+% \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \begin{macro}{\@@_patch_retokenize:Nnnn}
+% Then, if \cs{@@_cmd_if_scanable:NnTF} returned true, we can go on
+% and patch the command.
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_patch_retokenize:Nnnn #1 #2 #3 #4
+ {
+% \end{macrocode}
+% Start off by making some things \tn{relax} to avoid lots of
+% \tn{noexpand} below.
+% \begin{macrocode}
+ \cs_set_eq:NN \kerneltmpDoNotUse \scan_stop:
+ \cs_set_eq:NN \@@_tmp:w \scan_stop:
+ \use:x
+ {
+% \end{macrocode}
+% Now we'll define \cs{@@_tmp:w} such that it splits the \tn{meaning}
+% of the macro (|#1|) into its three parts:
+% \begin{enumerate}
+% \def\makelabel#1{\texttt{\#\#\#\##1}}
+% \item \meta{prefixes}
+% \item \meta{parameter text}
+% \item \meta{replacement text}
+% \end{enumerate}
+% and arrange that a complete definition, then place the |before|
+% or |after| hooks around the \meta{replacement text}:
+% accordingly.
+% \begin{macrocode}
+ \cs_set:Npn \@@_tmp:w
+ ####1 \tl_to_str:n { macro: } ####2 -> ####3 \s_@@_mark
+ {
+ ####1 \def \kerneltmpDoNotUse ####2
+ {
+ \str_if_eq:nnT {#3} { before }
+ { \token_to_str:N \UseHook { cmd / #2 / #3 } }
+ ####3
+ \str_if_eq:nnT {#3} { after }
+ { \token_to_str:N \UseHook { cmd / #2 / #3 } }
+ }
+ }
+% \end{macrocode}
+% Now we just have to get the \tn{meaning} of the command being
+% patched and pass it through the meat grinder above.
+% \begin{macrocode}
+ \tl_set:Nx \exp_not:N \l_@@_tmpa_tl
+ { \exp_not:N \@@_tmp:w \token_to_meaning:N #1 \s_@@_mark }
+ }
+% \end{macrocode}
+% Now rescan with the given catcode settings (overridden by the
+% \cs{@@_patch_required_catcodes:}), and implicitly (by using the
+% rescanned token list) carry out the definition from above.
+% \begin{macrocode}
+ \tl_rescan:nV { #4 \@@_patch_required_catcodes: } \l_@@_tmpa_tl
+% \end{macrocode}
+% And to close, copy the newly-defined command into the old name and
+% the patching is finally completed:
+% \begin{macrocode}
+ \cs_set_eq:NN #1 \kerneltmpDoNotUse
+ }
+% \end{macrocode}
+% \end{macro}
+%
+% \subsection{Messages}
+%
+% \begin{macrocode}
+\__kernel_msg_new:nnnn { hooks } { wrong-cmd-hook }
+ {
+ Command~hook~`cmd/#1/#2'~invalid.\\
+ The~hook~should~be~`cmd/#1/before'~or~`cmd/#1/after'.
+ }
+ {
+ You~tried~to~add~a~hook~to~command~\iow_char:N \\#1,~but~`#2'~
+ is~an~invalid~position.~Only~`before'~or~`after'~are~allowed.
+ }
+\__kernel_msg_new:nnnn { hooks } { cant-patch }
+ {
+ Command~`#1'~cannot~have~hooks~because~it~
+ \@@_unpatchable_cases:n {#2} .
+ }
+ {
+ You~tried~to~add~a~hook~to~`#1',~but~LaTeX~was~not~able~to~
+ add~the~hook~to~that~command~because~`#1'~
+ \@@_unpatchable_cases:n {#2} .
+ }
+\cs_new:Npn \@@_unpatchable_cases:n #1
+ {
+ \str_case:nn {#1}
+ {
+ { undef } { doesn't~exist }
+ { macro } { is~not~a~macro }
+ { expl3 } { is~a~private~expl3~macro }
+ { retok } { can't~be~retokenized~cleanly }
+ }
+ }
+% \end{macrocode}
+%
+%
+% \begin{macrocode}
+%<latexrelease>\IncludeInRelease{0000/00/00}{ltcmdhooks}%
+%<latexrelease> {The~hook~management~system~for~commands}
+%<latexrelease>
+% \end{macrocode}
+% The command \cs{@@_cmd_begindocument_code:} is used in an
+% internal hook, so we need to make sure it has a harmless
+% definition after rollback as that will not remove it from the
+% kernel hook.
+% \begin{macrocode}
+%<latexrelease>\cs_set_eq:NN \@@_cmd_begindocument_code: \prg_do_nothing:
+%<latexrelease>
+%<latexrelease>\EndModuleRelease
+\ExplSyntaxOff
+%</2ekernel|latexrelease>
+% \end{macrocode}
+%
+% \begin{macrocode}
+%<@@=>
+% \end{macrocode}
+%
+% \Finale
+%