From 75db33dc347ec9cfbaa2f3ec538a2697351f571b Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Wed, 5 May 2021 03:02:17 +0000 Subject: CTAN sync 202105050302 --- macros/latex-dev/base/ltcmdhooks.dtx | 1093 ++++++++++++++++++++++++++++++++++ 1 file changed, 1093 insertions(+) create mode 100644 macros/latex-dev/base/ltcmdhooks.dtx (limited to 'macros/latex-dev/base/ltcmdhooks.dtx') 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} +% +% +% \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}{} +%\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}{}= +% 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}{} +% \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 +%\NewModuleRelease{2021/06/01}{ltcmdhooks} +% {The~hook~management~system~for~commands} +% \end{macrocode} +% +% \subsection{Variables} +% +% \begin{macro}[int]{\g_hook_patch_action_list_tl} +% Pairs of |\if..\patch| 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} +%\IncludeInRelease{0000/00/00}{ltcmdhooks}% +% {The~hook~management~system~for~commands} +% +% \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} +%\cs_set_eq:NN \@@_cmd_begindocument_code: \prg_do_nothing: +% +%\EndModuleRelease +\ExplSyntaxOff +% +% \end{macrocode} +% +% \begin{macrocode} +%<@@=> +% \end{macrocode} +% +% \Finale +% -- cgit v1.2.3