4

I want to patch the \thepage in the \addcontentsline to 2*\c@page + 1 if some bool in preamble is true. I simplify my code to this:

\documentclass{article}
\usepackage{etoolbox}
\makeatletter
\ExplSyntaxOn
\AtEndPreamble
{
  \bool_if:NT \c_true_bool
    {\patchcmd{\addcontentsline}{\thepage}{\int_eval:n {2 * \c@page + 1}}{}{\fail}}
}
% \patchcmd{\addcontentsline}{\thepage}{\int_eval:n {2 * \c@page + 1}}{}{\fail}
\ExplSyntaxOff
\makeatother
\begin{document}
  \tableofcontents
  \section{test}
\end{document}

Things in .toc are

\contentsline {section}{\numberline {1}test}{\int _eval:n {2*\c@page +1}}{}%

We can see that \int _eval:n {2*\c@page +1} is broken and wasn't calculated to 3. However if I remove either \AtEndPreamble or \bool_if:NT \c_true_bool, I get

\contentsline {section}{\numberline {1}test}{1}{}%

1 is still 1. This seems that `\patchcmd' only works in group, is there a "global" version?

If I remove both commands and remove the group, 1 changes to 3, which is what I want.

My question is why would this happen and how can I fix it?


EDIT: In this example, @user202729 's method works. However, if I add hyperref, \thepage in \addcontentsline is unpatchable any more.

[debug] tracing \ifpatchable on input line 23
[debug] analyzing '\addcontentsline'
[debug] ++ control sequence is defined
[debug] ++ control sequence is a macro
[debug] -- macro cannot be retokenized cleanly
[debug] -> the macro may have been defined under a category
[debug]    code regime different from the current one
[debug] -> the replacement text may contain special control
[debug]    sequence tokens formed with \csname...\endcsname;
[debug] -> the replacement text may contain carriage return,
[debug]    newline, or similar characters

Similar question: hyperref and \addcontentsline's patch in expl3 block

2
  • It's rather weird that patchcmd doesn't detect the wrong catcode. (short fix, add \ExplSyntaxOn before \patchcmd and \ExplSyntaxOff after it.)
    – user202729
    Jul 14, 2022 at 5:23
  • With a new latex you can use \inteval (in older load the xfp package) instead of \int_eval:n. That removes the need for complicated catcode juggling. Jul 14, 2022 at 7:06

3 Answers 3

5

There are several problems with your code, the main one is due to category codes. What happens?

  1. You need \ExplSyntaxOn in order to write the code, so TeX interprets correctly \int_eval:n.

  2. The patch is performed in a different context, when \ExplSyntaxOn is no longer in force.

This is where \patchcmd fails. We need to know a bit of how it works. There is no way in TeX to access the replacement text of a macro without knowing precisely what its parameter text is and there is no way to know precisely what the parameter text is. We can just examine and save the parameter and replacement texts through \meaning, which however produces them as strings of category code 12 characters (but catcode 10 for spaces).

So \patchcmd does this and first of all it rebuilds a copy of the macro by means of \scantokens in order to compare it with the macro to be patched. If the comparison fails, \patchcmd makes @ of category code 11 and tries again the rebuild-and-compare. If also this attempt fails, \patchcmd gives up and tells you that the macro is not patchable. Otherwise it proceeds to perform the patch. Again, it has to use strings of category code 12 characters and do \scantokens. Here's where your case fails: when the patch is attempted, _ has category code 8, but your code wants it to have category code 11 and \patchcmd has no way at all to know this, unless you tell it.

\documentclass{article}
\usepackage{etoolbox}

\makeatletter
\ExplSyntaxOn
\AtEndPreamble
 {
  \bool_if:NT \c_true_bool
   {
    \ExplSyntaxOn
    \patchcmd{\addcontentsline}{\thepage}{\int_eval:n {2 * \c@page + 1}}{}{\fail}
    \ExplSyntaxOff
   }
 }
\ExplSyntaxOff
\makeatother

\begin{document}

\tableofcontents

\section{test}

\end{document}

This works because the macro to be patched hasn't spaces, _ or : in its parameter or replacement texts. Otherwise the patch would fail for the same reason as before.

A simpler strategy: do the patch in a safe place and avoid “strange” category codes.

\documentclass{article}
\usepackage{etoolbox}
\usepackage{hyperref}

\NewCommandCopy{\patchedaddcontentsline}{\addcontentsline}
\patchcmd{\patchedaddcontentsline}{\thepage}{\makeodd{page}}{}{\fail}

\ExplSyntaxOn
\NewExpandableDocumentCommand{\makeodd}{m}
 {
  \int_eval:n {2*\value{#1}+1}
 }
\AtEndPreamble
 {
  \bool_if:NT \c_true_bool
   {
    \RenewCommandCopy{\addcontentsline}{\patchedaddcontentsline}
   }
 }
\ExplSyntaxOff

\begin{document}

\tableofcontents

\section{test}

\end{document}
2
  • To be fair, there are many special cases you can exactly get the meaning with some programming (e.g. macro with simple parameter text #1 #2...), but \patchcmd doesn't implement that
    – user202729
    Jul 14, 2022 at 9:37
  • @user202729 “without knowing precisely what its parameter text is”. Yes, in some cases you can deduce the parameter text, but not in general.
    – egreg
    Jul 14, 2022 at 9:52
3
  1. As the documentation states

    The assignment is local.

    You can't make it global.

    Although... you can cheat like this

    \patchcmd{\addcontentsline}{\thepage}{\int_eval:n {2 * \c@page + 1}}{}{\fail}
    \global\let\addcontentsline\addcontentsline
    

    which seems to work but I'm not sure what bad effect it has on the save stack.

    Alternatively, copy the definition of \patchcmd and add \global at appropriate places.

  2. For the catcode part, if the documentation has something you don't understand, don't just skip through it. Important information are written there.

    Note that the patching process involves detokenizing the replacement text of the ⟨command⟩ and retokenizing it under the current category code regime after patching. The category code of the @ sign is temporarily set to 11. If the replacement text of the ⟨command⟩ includes any tokens with non-standard category codes, the respective category codes must be adjusted prior to patching.

    In particular

    • here the "replacement text" consist of \int_eval:n {2 * \c@page + 1}.
    • which includes the token \int_eval:n with non-standard category code (\ExplSyntaxOn region).

    So one way is to do it like this.

    \documentclass{article}
    \usepackage{etoolbox}
    \makeatletter
    \ExplSyntaxOn
    \AtEndPreamble
    {
      \bool_if:NT \c_true_bool
        {
        \ExplSyntaxOn
        \patchcmd{\addcontentsline}{\thepage}{\int_eval:n {2 * \c@page + 1}}{}{\fail}
        \ExplSyntaxOff
    }
    }
    % \patchcmd{\addcontentsline}{\thepage}{\int_eval:n {2 * \c@page + 1}}{}{\fail}
    \ExplSyntaxOff
    \makeatother
    \begin{document}
      \tableofcontents
      \section{test}
    \end{document}
    

    For \makeatletter and \makeatother, you happen-to not need because it's automatic, as mentioned in the documentation above. But for expl3 code, you do need it.

    Side note, you still need the outer \ExplSyntaxOn, you can't change catcode inside a command argument, see macros - Why doesn't \makeatletter work inside \newcommand? - TeX - LaTeX Stack Exchange.

    Alternatively, make the ⟨replacement text⟩ safe under normal catcode with a wrapper command like this.

    \documentclass{article}
    \usepackage{etoolbox}
    \makeatletter
    \ExplSyntaxOn
    \cs_new:Npn \myExpandableCommandSafeUnderNormalCatcode {
        \int_eval:n {2 * \c@page + 1}
    }
    \AtEndPreamble
    {
      \bool_if:NT \c_true_bool
        {
        \patchcmd{\addcontentsline}{\thepage}{\myExpandableCommandSafeUnderNormalCatcode}{}{\fail}
    }
    }
    % \patchcmd{\addcontentsline}{\thepage}{\int_eval:n {2 * \c@page + 1}}{}{\fail}
    \ExplSyntaxOff
    \makeatother
    \begin{document}
      \tableofcontents
      \section{test}
    \end{document}
    
5
  • On re-reading the documentation it does say it checks \ifpatchable before patching...
    – user202729
    Jul 14, 2022 at 5:42
  • Thanks for your answer, but after I add hyperref, \addcontentsline isn't patchable under expl3. And I found this tex.stackexchange.com/questions/644131/… . is there any way to fix?
    – Syvshc
    Jul 14, 2022 at 6:00
  • @Syvshc Ah, you didn't state it. As mentioned in the documentation, patchcmd inherently fail to work in several cases. LaTeX has AddToHook nowadays, not sure how well it works. There's workaround though.
    – user202729
    Jul 14, 2022 at 6:02
  • Sorry, I didn't think hyperref will break it... I should have realized it.
    – Syvshc
    Jul 14, 2022 at 6:04
  • or I think I can just copy all of definition of \addcontentsline and change \thepage to whatever I want.
    – Syvshc
    Jul 14, 2022 at 6:05
3

Use \inteval instead of \int_eval:n. You can also replace \c@page by \value{page}:

\documentclass{article}
\usepackage{etoolbox}
\usepackage{hyperref}
\makeatletter
\ExplSyntaxOn
\AtEndPreamble
{
  \bool_if:NT \c_true_bool
    {    
    \patchcmd{\addcontentsline}{\thepage}{\inteval {2 *  \value{page} + 1}}{}{\fail}    
}
}
\ExplSyntaxOff
\makeatother
\begin{document}
  \tableofcontents
  \section{test}
\end{document}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .