-
-
Notifications
You must be signed in to change notification settings - Fork 275
/
Copy pathltcmdhooks.dtx
1817 lines (1816 loc) · 70.7 KB
/
ltcmdhooks.dtx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
% \iffalse meta-comment
%
%% From File: ltcmdhooks.dtx
%% Copyright (C) 2020-2025
%% Frank Mittelbach, Phelype Oleinik, The LaTeX Project
%
% 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
%
%
% \fi
%
% \iffalse
%%% From File: lthooks.dtx
%
%<*driver>
% \fi
\ProvidesFile{ltcmdhooks.dtx}
[2024/12/25 v1.0k LaTeX Kernel (Command hooks)]
% \iffalse
%
\documentclass{l3doc}
\GetFileInfo{ltcmdhooks.dtx}
% \usepackage{ltcmdhooks}
\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
% \fileversion\ dated \filedate, \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},\!\footnote{In this documentation, when something is
% being said about \tn{AddToHook}, the same will be valid for
% \tn{AddToHookWithArguments}, unless that particular paragraph is
% highlighting the differences between both. The same is true for
% the other hook-related functions and their
% \texttt{\ldots WithArguments} counterparts.}
% \tn{AddToHookNext}, \tn{AddToHookWithArguments}, and
% \tn{AddToHookNextWithArguments}.\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.}
%
% However, this is only true \enquote{in theory}. In practice there
% are a number of restrictions that makes it impossible to use such
% generic command hooks in a number of cases, so please read all
% of section~\ref{sec:restrictions} to understand what may prevent
% you from using them successfully.
%
% The generic command hooks are:
% \begin{description}
% \item[\hook{cmd/\meta{name}/before}]
%
% This hook is executed at the very start of the command, right
% after its arguments (if any) are parsed. The hook \meta{code}
% runs in the command inside a call to \cs{UseHookWithArguments}.
% Any code added to this hook using \tn{AddToHookWithArguments}
% or \tn{AddToHookNextWithArguments} can access the command's
% arguments using |#1|, |#2|, etc., up to the number of arguments
% of the command. If \tn{AddToHook} or \tn{AddToHookNext} are
% used, the arguments cannot be accessed (see the \pkg{lthooks}
% documentation\footnote{\texttt{texdoc lthooks-doc}} on hooks
% with arguments).
%
% \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}=\footnote{More specifically, they are
% inserted in the commands after the \hook{begindocument} hook, so
% they are also not present while \LaTeX{} is reading the
% \texttt{.aux} file.} (i.e., using a command in the preamble
% will never execute the hook) 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]{%
% \UseHookWithArguments{cmd/foo/before}{2}{#1}{#2}%
% 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!%
% \UseHookWithArguments{cmd/foo/after}{2}{#1}{#2}}
%\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 (yet) defined, because the
% defining package has not been loaded at this point;
%
% \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.
%
% We can do this in most cases when commands are defined using
% \cs{NewDocumentCommand} or \cs{newcommand} (with a few exceptions).
% For commands defined with \tn{def} the situation is less good.
% Common cases where the command hooks will not work are:
% \begin{itemize}
% \item
%
% Commands that use special catcode settings within their
% definition. In that case it is usually not possible to augment the
% definition (see~\ref{sec:patching}).
%
% \item
%
% If a command is defined while \cs{ExplSyntaxOn} is in force
% \textbf{and} the command contains \verb=~= characters to represent
% spaces, then it can't be patched to include the command hooks. In
% fact in some very special circumstances you might even get a
% low-level error rather than the information that the command can't
% be patched (see, for example,
% \url{https://github.com/latex3/latex2e/issues/1430}.
%
% \item
%
% Commands that have arguments as far as the user is concerned
% (e.g., \cs{section} or \cs{caption}), but are defined in a way that these
% arguments are not read by the user level command but only later
% during the processing. In that case the \texttt{after} hook
% doesn't work at all. The \text{before} hook only works with
% \cs{AddToHook} but not with \cs{AddToHookWithArguments} because the
% arguments haven't been read at that point where the hook is
% patched in. See
% section~\ref{sec:look-ahead}.
%
%
% \item
% Adding a specific generic command hook is only attempted once per
% command, thus after redefining a command such hooks will no longer
% be there and will also not being re-added, see section~\ref{sec:timing}.
%
% \end{itemize}
% All this means that you have to have a good understanding of how
% commands are defined when you attempt to make use of such hooks and
% something goes wrong.
% What can help in that case is to turn on \cs{DebugHooksOn} in which
% case you get much more (low-level) details on why something fails and
% what was tried to enable the hooks.
%
%
% \subsection{Patching}\label{sec: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}\label{sec: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 only with \cs{AddToHook}
% not \cs{AddToHookWithArguments}),
% but if you try to add anything to the \hook{cmd/section/after}
% hook, \tn{section} will no longer work at all. 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.
%
% In such a case, where it is known that a specific generic command
% hook does not work if code is added to it, the package author can
% add a \cs{DisableGenericHook}\footnote{Please use
% \cs{DisableGenericHook} if at all, only on hooks that you
% \enquote{own}, i.e., for commands your package or class defines and
% not second guess whether or not hooks of other packages should get
% disabled!} declaration to prevent this from happening in user
% documents and thereby avoiding obscure errors.
%
%
% \section{Package Author Interface}
% \label{sec:pkg-author}
%
% 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{UseHookWithArguments} calls inside the command in
% the proper positions, and manually define the hooks with
% \cs{NewHookWithArguments} or \cs{NewReversedHookWithArguments}.
% 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{%
% \UseHookWithArguments{cmd/fancybox/before}{2}{#1}{#2}%
% \parbox{#1}{#2}%
% \UseHookWithArguments{cmd/fancybox/after}{2}{#1}{#2}}}
% \NewHookWithArguments{cmd/fancybox/before}{2}
% \NewReversedHookWithArguments{cmd/fancybox/after}{2}
% \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{NewHookWithArguments} or \cs{NewReversedHookWithArguments},
% otherwise the command hook
% code will try to patch the command. Note also that the call to
% |\UseHookWithArguments{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{DisableGenericHook}\footnote{Please use \cs{DisableGenericHook} if
% at all, only on hooks that you \enquote{own}, i.e., for commands
% 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.
%
% \subsection{Arguments and redefining commands}
% \label{sec:redef-warn}
%
% The code in \pkg{ltcmdhooks} does its best to find out how many
% arguments a given command has, and to insert the appropriate call to
% \cs{UseHookWithArguments}, so that the arguments seen by the hook are
% exactly those grabbed by the command (the hook, after all, is a macro
% call, so the arguments have to be placed in the right order, or they
% won't match).
%
% When using the package writer interface, as discussed in
% section~\ref{sec:pkg-author}, to change the position of the hooks in
% your commands, you are also free to change how the hook code in your
% command sees its arguments. When a \hook{cmd} hook is declared with
% \cs{NewHook} (or \cs{NewHookWithArguments} or other variations of
% that), it loses its \enquote{generic} nature and works as a regular
% hook. This means that you may choose to declare it without
% arguments regardless if the command takes arguments or not, or
% declare it with arguments, even if the command takes none.
%
% However, this flexibility should not be abused. When using a
% nonstandard configuration for the hook arguments, think reasonably:
% a user will expect that the argument \verb|#1| in the hook corresponds
% to the argument's first argument, and so on. Any other configuration
% is likely to cause confusion and, if used, will have to be well
% documented.
%
% This flexibility, however, allows you to \enquote{correct} the
% arguments for the hooks. For example, \LaTeX's \cs{refstepcounter}
% has a single argument, the name of the counter. The \pkg{cleveref}
% package adds an optional argument to \cs{refstepcounter}, making the
% name of the counter argument \verb|#2|. If the author of
% \pkg{cleveref} wanted, for whatever reason, to add hooks to
% \cs{refstepcounter}, to preserve compatibility he could write
% something along the lines of:
% \begin{verbatim}
% \NewHookWithArguments{cmd/refstepcounter/before}{1}
% \renewcommand\refstepcounter[2][<default>]{%
% \UseHookWithArguments{cmd/refstepcounter/before}{1}{#2}%
% <code for \refstepcounter>}
% \end{verbatim}
% so that the mandatory argument, which is arg \verb|#2| in the
% definition, would still be seen as \verb|#1| in the hook code.
%
% Another possibility would be to place the optional argument as the
% second argument for the hook, so that people looking for it would be
% able to use it. In either case, it would have to be well documented
% to cause as little confusion as possible.
%
% \MaybeStop{\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}
%
% \changes{v1.0b}{2021/05/24}{Use \cs{msg_...} instead of \cs{__kernel_msg...}}
% \changes{v1.0j}{2024/04/17}{Use \cs{__kernel_cs_parameter_spec:N} instead
% of \cs{cs_argument_spec:N}/\cs{cs_parameter_spec:N}}
%
% \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_@@_param_text_tl}
% \begin{macro}{\l_@@_replace_text_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_@@_param_text_tl
\tl_new:N \l_@@_replace_text_tl
% \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\c_@@_hash_tl,\c_@@_hashes_tl}
% Two constant token lists that contain one and two parameter tokens.
% \changes{v1.0g}{2023/04/06}
% {Rename to \cs{c__hook_hashes_tl} and add \cs{c__hook_hash_tl} (hook-args)}
% \begin{macrocode}
\tl_const:Nn \c_@@_hash_tl { # }
\tl_const:Nn \c_@@_hashes_tl { # # }
% \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_exp_not:NN}
% \begin{macro}{\@@_def_cmd:w}
% Two temporary macros that change depending on the macro being
% patched.
% \begin{macrocode}
\cs_new_eq:NN \@@_exp_not:NN ?
\cs_new_eq:NN \@@_def_cmd:w ?
% \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\q_@@_recursion_tail,\q_@@_recursion_stop}
% Internal quarks for recursion: they can't appear in any macro being
% patched.
% \begin{macrocode}
\quark_new:N \q_@@_recursion_tail
\quark_new:N \q_@@_recursion_stop
% \end{macrocode}
% \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}
%
% Before |\begin{document}| all patching is delayed.
%
% \begin{macro}{\@@_try_put_cmd_hook:n,\@@_try_put_cmd_hook:w}
% This function is called from within \cs{AddToHook}, when code is
% first added to a generic |cmd| hook.
% If it is called within in the preamble, it delays the action
% until |\begin{document}|;
% otherwise it tries to update the hook.
% \changes{v1.0d}{2021/08/25}{Simplify generic hook detection}
% \begin{macrocode}
%<latexrelease>\IncludeInRelease{2024/12/22}{\@@_try_put_cmd_hook:n}%
%<latexrelease> {Don't~define~command}
\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): } }
% \end{macrocode}
% \cs{@@_patch_cmd_or_delay:Nnn} expects the command to be patched
% as its first argument so we need to construct it from its name
% (\texttt{\#2}). However, at this moment it may not exist yet, so
% using \cs{cs:w} would incorrectly turn it from
% \enquote{undefined} into \cs{relax}. We therefore use the
% following curious construction: we start a group and expand out of
% it to call \cs{cs:w}. If the command is now changed to \cs{relax}
% the \cs{group_end:} will undo that change, but the token is
% nevertheless there to be consumed by \cs{@@_patch_cmd_or_delay:Nnn}.
% \changes{v1.0k}{2024/12/22}{Avoid defining command while adding a cmd
% hook (gh/1591)}
% \begin{macrocode}
\group_begin:
\exp_after:wN
\group_end:
\exp_after:wN
\@@_patch_cmd_or_delay:Nnn
\cs:w #2\cs_end:
{#2} {#3}
}
%<latexrelease>\EndIncludeInRelease
% \end{macrocode}
%
% \begin{macrocode}
%<latexrelease>\IncludeInRelease{2021/11/15}{\@@_try_put_cmd_hook:n}%
%<latexrelease> {Standardise~generic~hook~names}
%<latexrelease>\cs_new_protected:Npn \@@_try_put_cmd_hook:n #1
%<latexrelease> { \@@_try_put_cmd_hook:w #1 / / / \s_@@_mark {#1} }
%<latexrelease>\cs_new_protected:Npn \@@_try_put_cmd_hook:w
%<latexrelease> #1 / #2 / #3 / #4 \s_@@_mark #5
%<latexrelease> {
%<latexrelease> \@@_debug:n { \iow_term:n { ->~Adding~cmd~hook~to~'#2'~(#3): } }
%<latexrelease> \exp_args:Nc \__hook_patch_cmd_or_delay:Nnn {#2} {#2} {#3}
%<latexrelease> }
%<latexrelease>\EndIncludeInRelease
% \end{macrocode}
%
% \begin{macrocode}
%<latexrelease>\IncludeInRelease{2021/06/01}{\@@_try_put_cmd_hook:n}%
%<latexrelease> {Standardise~generic~hook~names}
%<latexrelease>\cs_new_protected:Npn \@@_try_put_cmd_hook:n #1
%<latexrelease> { \@@_try_put_cmd_hook:w #1 / / / \s_@@_mark {#1} }
%<latexrelease>\cs_new_protected:Npn \@@_try_put_cmd_hook:w
%<latexrelease> #1 / #2 / #3 / #4 \s_@@_mark #5
%<latexrelease> {
%<latexrelease> \@@_debug:n { \iow_term:n { ->~Adding~cmd~hook~to~'#2'~(#3): } }
%<latexrelease> \str_case:nnTF {#3}
%<latexrelease> { { before } { } { after } { } }
%<latexrelease> { \exp_args:Nc \@@_patch_cmd_or_delay:Nnn {#2} {#2} {#3} }
%<latexrelease> { \msg_error:nnnn { hooks } { wrong-cmd-hook } {#2} {#3} }
%<latexrelease> }
%<latexrelease>\EndIncludeInRelease
% \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}
{
\msg_error:nnxx { hooks } { cant-patch }
{ \token_to_str:N #2 } {#3}
}
}
% \end{macrocode}
% and a conditional \cs{@@_if_public_command:NTF} 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}; also make sure the command doesn't take args by
% calling \cs{robust@command@chk@safe}). If so, we pass the patching action
% to \cs{@@_patch_newcommand:Nnn}, otherwise we call the patching engine
% \cs{@@_patch_expand_redefine:NNnn} with a \cs{c_false_bool} to
% indicate that there is no optional argument.
%
% \changes{v1.0c}{2021/07/20}
% {Use \cs{robust@command@chk@safe} before \cs{@if@newcommand}.}
% \begin{macrocode}
\cs_new_protected:Npn \@@_patch_DeclareRobustCommand:Nnn #1
{
\exp_args:Nc \@@_patch_DeclareRobustCommand_aux:Nnn
{ \cs_to_str:N #1 ~ }
}
\cs_new_protected:Npn \@@_patch_DeclareRobustCommand_aux:Nnn #1
{
\robust@command@chk@safe #1
{ \@if@newcommand #1 }
{ \use_ii:nn }
{ \@@_patch_newcommand:Nnn }
{ \@@_patch_expand_redefine:NNnn \c_false_bool }
#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}{\@@_redefine_with_hooks: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 leading |[|\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|.
%
% \changes{v1.0f}{2021/10/20}
% {Correct patching by expansion+redefinition when the macro
% contains a parameter token (gh/697)}
% Patching with expansion+redefinition is trickier than it looks like
% at first glance. Suppose the simple definition:
% \begin{verbatim}
% \def\foo#1{#1##2}
% \end{verbatim}
% When defined, its \meta{replacement text} will be a token list
% containing:
% \begin{quote}
% \itshape
% out\_param |1|, mac\_param |#|, character |2|
% \end{quote}
%
% Then, after expanding \cs{foo}|{##1}| (here |##| denotes a single
% |#|$_6$) we end up with a token list with \textit{out\_param}~|1|
% replaced:
% \begin{quote}
% \itshape
% mac\_param |#|, character |1|, mac\_param |#|, character |2|
% \end{quote}
% that is, the definition would be:
% \begin{verbatim}
% \def\foo#1{#1#2}
% \end{verbatim}
% which obviously fails, because the original input in the definition
% was |##| but \TeX{} reduced that to a single parameter token |#|$_6$
% when carrying out the definition. That leaves no room for a clever
% solution with (say) \cs{unexpanded}, because anything that would
% double the second |#|$_6$, would also (incorrectly) double the
% first, so there's not much to do other than a manual solution.
%
% There are three cases we can distinguish to make things hopefully
% faster on simpler cases:
% \begin{enumerate}
% \item a macro with no parameters;
% \item a macro with no parameter tokens in its definition;
% \item a macro with parameters \emph{and} parameter tokens.
% \end{enumerate}
%
% The first case is trivial: if the macro has no parameters, we can
% just use \cs{unexpanded} around it, and if there is a parameter
% token in it, it is handled correctly (the macro can be treated as a
% |tl| variable).
%
% The second case requires looking at the \meta{replacement text} of
% the macro to see if it has a parameter token in there. If it does
% not, then there is no worry, and the macro can be redefined normally
% (without \cs{unexpanded}).
%
% The third case, as usual, is the devious one. Here we'll have to
% loop through the definition token by token, and double every
% parameter token, so that this case can be handled like the previous
% one.
% \begin{macrocode}
%<latexrelease>\IncludeInRelease{2023/06/01}{\@@_patch_expand_redefine:NNnn}
%<latexrelease> {cmd~hooks~with~args}
\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_parameter_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 { \__kernel_cs_parameter_spec:N #2 } / 2
\bool_if:NT #1 { -1 }
}
% \end{macrocode}
% Now build two token lists:
% \begin{description}
% \item[\cs{l_@@_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_@@_replace_text_tl}] will contain braced pairs of
% \cs{c_@@_hashes_tl}\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}
%
% The use of \cs{c_@@_hashes_tl} here is to differentiate actual
% parameters in the macro from parameter tokens in the original
% definition of the macro. Later on, \cs{c_@@_hashes_tl} is either
% replaced by actual parameter tokens, or expanded into them.
% \begin{macrocode}
\int_compare:nNnTF { \l_@@_patch_num_args_int } > { \c_zero_int }
{
% \end{macrocode}
% We'll first check if the command has any parameter token in its
% definition (feeding it empty arguments), and set \cs{@@_exp_not:n}
% accordingly. \cs{@@_exp_not:n} will be used later to either leave
% \cs{c_@@_hashes_tl} or expand it, and also to remember the result of
% \cs{@@_if_has_hash:nTF} to avoid testing twice (the test can be
% rather slow).
% \begin{macrocode}
\tl_set:Nx \l_@@_tmpa_tl { \bool_if:NTF #1 { [ ] } { { } } }
\int_step_inline:nnn { 2 } { \l_@@_patch_num_args_int }
{ \tl_put_right:Nn \l_@@_tmpa_tl { { } } }
\exp_args:NNo \exp_args:No \@@_if_has_hash:nTF
{ \exp_after:wN #2 \l_@@_tmpa_tl }
{ \cs_set_eq:NN \@@_exp_not:n \exp_not:n }
{ \cs_set_eq:NN \@@_exp_not:n \use:n }
\cs_set_protected:Npn \@@_tmp:w ##1 ##2
{
##1 \l_@@_param_text_tl { \use:n ##2 }
##1 \l_@@_replace_text_tl { \@@_exp_not:n {##2} }
}
% \end{macrocode}
% Here we'll conditionally add |[|\ldots|]| around the first
% parameter:
% \changes{v1.0g}{2023/04/06}
% {Rename to \cs{c__hook_hashes_tl} (hook-args)}