11

Here is a challenge:

This construction works just fine as the if is contained to a single write statement

\documentclass[a4paper]{book}
\newcommand\test{\noindent test\par}
\let\testA\iffalse
\let\testB\fi


\begin{document}

\addtocontents{toc}{\protect\testA test\par \protect\testB}


\tableofcontents

\chapter{test}

\end{document}

The code bwlow, on the other hand, will not work as \testA or \testB presumably gets executed inside the \@writefile{toc}{...} statement and we end up with and ! Extra }, or forgotten \endgroup error.

\documentclass[a4paper]{book}
\newcommand\test{\noindent test\par}
\let\testA\iffalse
\let\testB\fi


\begin{document}

\addtocontents{toc}{\protect\testA}
\addtocontents{toc}{\protect\test}
\addtocontents{toc}{\protect\testB}


\tableofcontents

\chapter{test}

\end{document}

So is there a good method for smuggling these two incomplete if statements into the toc file?

  • I thought I figured the problem out, but I just did part of it: The first error comes up because the \write of \addtocontents is delayed until shipout, and at that time \protect has a different meaning. To remedy that you need \protect\protect\protect\testA. However, the \@writefile code uses some toks registers to handle end of lines, and then the unmatched conditionals are seen and chaos ensues. I'm not sure if this is doable without a heavy surgery in some LaTeX internals... – Phelype Oleinik Oct 25 '19 at 12:02
  • @campa then why do I keep getting an error on it? – daleif Oct 25 '19 at 12:05
  • Good question :-) Obviously also @PhelypeOleinik gets an error, so the problem (well, the absence thereof...) in on my side. My only clue is that on my work computer I have only TeXLive2017. – campa Oct 25 '19 at 12:42
  • @campa Yes, this issue was introduced with LaTeX 2018/12/01. More specifically, when fixing this bug. – Phelype Oleinik Oct 25 '19 at 12:44
  • @campa check the aux, is the entry written there first or directly to the toc. I think is used to write it directly to the toc, nowadays it goes through the aux – daleif Oct 25 '19 at 12:44
9
\documentclass[a4paper]{book}
\newcommand\test{\noindent test\par}
\DeclareRobustCommand\activateif{%
\let\testA\iffalse
\let\testB\fi}
\DeclareRobustCommand\deactivateif{%
\let\testA\relax
\let\testB\relax}



\begin{document}

\addtocontents{toc}{
 \activateif
 \protect\testA test\par 
 \protect\testB
 \deactivateif}


\tableofcontents

\chapter{test}

\end{document}

This lets the chapter entry in the toc disappear

\documentclass[a4paper]{book}
\newcommand\test{\noindent test\par}
\DeclareRobustCommand\activateif{%
\let\testA\iffalse
\let\testB\fi}
\DeclareRobustCommand\deactivateif{%
\let\testA\relax
\let\testB\relax} 

\begin{document}

\addtocontents{toc}{\activateif}

\tableofcontents

\addtocontents{toc}{\protect\testA}
\chapter{test}
\addtocontents{toc}{\protect\testB}

\addtocontents{toc}{\deactivateif}

\end{document}
| improve this answer | |
  • For the usecase it has to use two separate addcontents lines, the main goal in the end is to be able to selectively show or not show a single contents line, – daleif Oct 25 '19 at 13:00
  • It shouldn't matter how many \addcontentsline you use: the main point is that \testA and \testB are harmless, when the toc is written at the end of the document. – Ulrike Fischer Oct 25 '19 at 13:04
  • Not understod, as far as I can see you cannot break your definition into three parts. Remember we have no control over the test\par part, that is just illustration. Try making the chapter contentsline not be printed in the toc by wrapping an if around it. – daleif Oct 25 '19 at 13:10
  • @daleif I added an edit which suppress the chapter in the toc. – Ulrike Fischer Oct 25 '19 at 13:26
  • Interesting, I was trying \addtocontents{toc}{\activateif\protect\testA}} ... \addtocontents{toc}{\protect\testB}\deactivateif} which failed. No, now they work, no idea what went wrong – daleif Oct 25 '19 at 13:28
11

Edit:

This fix was added to the LaTeX 2ε and is available in the kernel since the 2020-02-02 release, so since TeXLive 2019.


The issue was introduced with the 2018/12/01 release of LaTeX. Before that, the macro \@writefile (which does the writing from the .aux to the .toc essentially did \write\tocfile{\unexpanded{<stuff>}}, so your unbalanced conditional was written without problems.

After the aforementioned release, a fix for this problem was introduced and the problem boils down to:

\let\testB\fi
\iftrue\else
  \toks0{\testB}
\fi

TeX doesn't care about balancing braces when looking for the matching \else or \fi of a conditional. In this case, \testB is seen as a \fi and you're left with just }\fi.

I think the definition of \add@percent@to@temptokena could be changed a bit to allow avoid this sort of problem:

\documentclass[a4paper]{book}
\newcommand\test{\noindent test\par}
\let\testA\iffalse
\let\testB\fi

\makeatletter
\long\def\add@percent@to@temptokena
    #1\protected@file@percent#2\add@percent@to@temptokena
    {\ifx!#2!\expandafter\dont@add@percent@to@temptokena\else
             \expandafter\do@add@percent@to@temptokena\fi{#1}}
\long\def\dont@add@percent@to@temptokena#1{%
  \@temptokena\expandafter{#1}}
\begingroup
\catcode`\%=12
\catcode`\^^A=9
\long\gdef\do@add@percent@to@temptokena#1{%
  \@temptokena\expandafter{#1%^^A
  }}
\endgroup
\makeatother

\begin{document}

\addtocontents{toc}{\protect\testA}
\addtocontents{toc}{\protect\test}
\addtocontents{toc}{\protect\testB}

\tableofcontents

\chapter{test}

\end{document}
| improve this answer | |
  • Interesting, and probably cheating (at least in the case that this requires a recent latex, though not a bad requirement IMO) – daleif Oct 25 '19 at 13:14
  • @daleif Not really, this should probably be changed in the kernel for safety (GitHub issue). And the code above will work with prior releases, as they don't use \add@percent@to@temptokena (try adding \RequirePackage[2018/11/01]{latexrelease} before \documentclass). But as extra safety you can test for the existence of \add@percent@to@temptokena. – Phelype Oleinik Oct 25 '19 at 13:30
  • The fix proposed by you doesn't let users write comments to external files. Besides this the \ifx!#2!-comparison is still there which means that #2 can still erroneously match up the \ifx, e.g., when doing things like \@writefile{toc}{Some text\protected@file@percent My nice comment with \fi}. In any case I'd prefer a routine which replaces each \protected@file@percent. (A minor challenge would be handling \protected@file@percent nested in curly braces...) – Ulrich Diez Oct 26 '19 at 8:47
  • @UlrichDiez “write comments” is odd, but I assume you mean a catcode-12 % character. If that's correct, it's possible to do so, given the correct tricks to have the .aux see the % as catcode-12, not 14. It is possible, but I don't see why someone would need that, though. You are correct, of course: using \protected@file@percent in the argument would break things, but again I don't see why anyone would need that written to the .toc. It's just an “anchor” control sequence to be used by \add@percent@to@temptokena. – Phelype Oleinik Oct 26 '19 at 13:15
  • Not necessarily in the toc-file. But I've seen more than one person using \newwrite\tf@extension..\addtocontents{extension} for creating more or less arbitrary external text files \jobname.extension via the .aux-file, whereof some form, e.g., separate additional LaTeX documents... (That's something that I would probably do by means of docstrip or the like. Nonetheless I've seen people doing this. In such scenarios you might like to keep things behind \protected@file@percent as comments...) – Ulrich Diez Oct 26 '19 at 13:55
3

It seems that in more recent LaTeX2e-releases some \add@percent@to@temptokena-mechanism was added to \@writefile:

> \@writefile=\long macro:
#1#2->\@ifundefined {tf@#1}\relax {\add@percent@to@temptokena \@empty #2\protec
ted@file@percent \add@percent@to@temptokena \immediate \write \csname tf@#1\end
csname {\the \@temptokena }}.
l.19 \show\@writefile


> \add@percent@to@temptokena=\long macro:
#1\protected@file@percent #2\add@percent@to@temptokena ->\ifx !#2!\@temptokena 
\expandafter {#1}\else \@temptokena \expandafter {#1% }\fi .
l.20 \show\add@percent@to@temptokena

I assume this \ifx !#2!... is intended to be an empty-check for detecting whether \@writefile's second argument contains the token \protected@file@percent. (If so, the first such token not nested in curly braces shall be replaced by % and everything behind it shall be dropped.)

  1. I don't reccomend trying to write a percent-char trailed by an exclamation-mark as in:
    \@writefile{toc}{something\protected@file@percent! This is a very important comment.}
  2. In case \add@percent@to@temptokena's first argument contains unbalanced \else/\fi (as is the case with your scenario), these will erroneously match up the \ifx of
    \ifx !#2!\@temptokena\expandafter{#1}.

I foresee that this \add@percent@to@temptokena-thingie breaks a lot of home-brewed code written by users who rely on having the possibility of writing unmatched \if.., \else or \fi by means of \@writefile.

I might suggest something like:

\documentclass[a4paper]{book}

\begingroup
\makeatletter
\catcode`\&=14 %
\catcode`\%=12 &
\@firstofone{&
  \endgroup
  &&-----------------------------------------------------------------------------
  && Change \add@percent@to@temptokena:
  &&.............................................................................
  \long\def\add@percent@to@temptokena#1\protected@file@percent#2\add@percent@to@temptokena{&
    \ifcat A\detokenize{#2}A\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
    {\@temptokena\expandafter{#1}}{\@temptokena\expandafter{#1% }}&
  }&
}%

\newcommand\test{\noindent test\par}
\let\testA\iffalse
\let\testB\fi

\begin{document}

\addtocontents{toc}{\protect\testA}
\addtocontents{toc}{\protect\test}
\addtocontents{toc}{\protect\testB}

\tableofcontents

\chapter{test}

\end{document}

If you don't like the e-TeX-\detokenize-thingie:

\documentclass[a4paper]{book}

\begingroup    
\makeatletter
\catcode`\&=14 %
\catcode`\%=12 &
\@firstofone{&
  \endgroup
  &&-----------------------------------------------------------------------------
  && Check whether argument is empty:
  &&.............................................................................
  && \UD@CheckWhetherNull{<Argument which is to be checked>}
  &&                     {<Tokens to be delivered in case that argument
  &&                       which is to be checked is empty>}
  &&                     {<Tokens to be delivered in case that argument
  &&                       which is to be checked is not empty>}
  &&
  && The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
  && <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
  \newcommand\UD@CheckWhetherNull[1]{&
    \romannumeral0\expandafter\@secondoftwo\string{\expandafter
    \@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
    \@secondoftwo\string}\expandafter\@firstoftwo\expandafter{\expandafter
    \@secondoftwo\string}\@firstoftwo\expandafter{} \@secondoftwo}&
    {\@firstoftwo\expandafter{} \@firstoftwo}&
  }&
  &&-----------------------------------------------------------------------------
  && Change \add@percent@to@temptokena:
  &&.............................................................................
  \long\def\add@percent@to@temptokena#1\protected@file@percent#2\add@percent@to@temptokena{&
    \UD@CheckWhetherNull{#2}&
                        {\@temptokena\expandafter{#1}}&
                        {\@temptokena\expandafter{#1% }}&
  }&
}%

\newcommand\test{\noindent test\par}
\let\testA\iffalse
\let\testB\fi

\begin{document}

\addtocontents{toc}{\protect\testA}
\addtocontents{toc}{\protect\test}
\addtocontents{toc}{\protect\testB}

\tableofcontents

\chapter{test}

\end{document}

By the way:

One could as well redefine \@writefile to only replace the first \protected@file@percent by % while leaving everything else in place:

\documentclass[a4paper]{book}

\makeatletter
%%-----------------------------------------------------------------------------
%% Change \@writefile to replace the first non-brace-nested
%% \@protected@file@percent in #2 by %
%%.............................................................................
\@ifdefinable\RemoveTo@protected@file@percent{%
  \long\def\RemoveTo@protected@file@percent#1\protected@file@percent{}%
}%
\begingroup
\catcode`\&=14 %
\catcode`\%=12 &
\@firstofone{&
  \endgroup
  \@ifdefinable\Replace@protected@file@percent{&
    \long\def\Replace@protected@file@percent#1\protected@file@percent{&
      \@firstoftwo{ }#1%&
    }&
  }&
}%
\long\def\@writefile#1#2{%
  \@ifundefined{tf@#1}\relax{%
    \immediate\write\csname tf@#1\endcsname{%
      \unexpanded\expandafter{\romannumeral0%
        \ifcat A\detokenize\expandafter{%
          \RemoveTo@protected@file@percent#2\protected@file@percent
        }A\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
        { }%
        {\Replace@protected@file@percent.}%
        #2%
      }%
    }%
  }%
}%
\makeatother

\newcommand\test{\noindent test\par}
\let\testA\iffalse
\let\testB\fi

\begin{document}

\makeatletter
\addtocontents{toc}{\protect\testA\protected@file@percent{This is my nice comment.}}
\makeatother
\addtocontents{toc}{\protect\test}
\addtocontents{toc}{\protect\testB}

\tableofcontents

\chapter{test}

\end{document}

Without \detokenize and without \unexpanded:

\documentclass[a4paper]{book}

\makeatletter
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \UD@CheckWhetherNull{<Argument which is to be checked>}%
%%                     {<Tokens to be delivered in case that argument
%%                       which is to be checked is empty>}%
%%                     {<Tokens to be delivered in case that argument
%%                       which is to be checked is not empty>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
\newcommand\UD@CheckWhetherNull[1]{%
  \romannumeral0\expandafter\@secondoftwo\string{\expandafter
  \@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
  \@secondoftwo\string}\expandafter\@firstoftwo\expandafter{\expandafter
  \@secondoftwo\string}\@firstoftwo\expandafter{} \@secondoftwo}%
  {\@firstoftwo\expandafter{} \@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Change \@writefile to replace the first non-brace-nested
%% \@protected@file@percent in #2 by %
%%.............................................................................
\@ifdefinable\RemoveTo@protected@file@percent{%
  \long\def\RemoveTo@protected@file@percent#1\protected@file@percent{}%
}%
\begingroup
\catcode`\&=14 %
\catcode`\%=12 &
\@firstofone{&
  \endgroup
  \@ifdefinable\Replace@protected@file@percent{&
    \long\def\Replace@protected@file@percent#1\protected@file@percent{&
      \@firstoftwo{}#1%&
    }&
  }&
}%
\long\def\@writefile#1#2{%
  \@ifundefined{tf@#1}\relax{%
    \expandafter\UD@CheckWhetherNull\expandafter{\RemoveTo@protected@file@percent#2\protected@file@percent}%
    {%
      \@temptokena{#2}%
    }{%
      \@temptokena\expandafter\expandafter\expandafter{\Replace@protected@file@percent.#2}%
    }%
    \immediate\write\csname tf@#1\endcsname{\the\@temptokena}%
  }%
}%
\makeatother

\newcommand\test{\noindent test\par}
\let\testA\iffalse
\let\testB\fi

\begin{document}

\makeatletter
\addtocontents{toc}{\protect\testA\protected@file@percent{This is my nice comment.}}
\makeatother
\addtocontents{toc}{\protect\test}
\addtocontents{toc}{\protect\testB}

\tableofcontents

\chapter{test}

\end{document}
| improve this answer | |
  • 1
    The !#2! is actually safe enough as \protected@file@percent is an internal kernel command that nobody should have occasion to use anywhere. (it is defined as \protected\def\protected@file@percent{} so it doesn't do anything useful as such) The case of the unbalanced conditional was an oversight, but otherwise the code obviously didn't "break a lot of home-brewed code written by user" given that is that way in the kernel for about a year now :-) – Frank Mittelbach Oct 26 '19 at 8:37
  • @FrankMittelbach Most of the people using LaTeX I know don't use most recent releases but use older releases integrated in whatsoever Linux distributions. Therefore they are not yet affected by changes occurring in a kernel which is just a year old. Despite the statement in source2e that kernel macros can change at any time, which might break one's own work based on kernel macros, a number of people in my surrounding use kernel macros like \@writefile/\@newl@bel /\@starttoc/\if@filesw in their own ways in the endeavor of not having to reinvent the whole wheelwork. – Ulrich Diez Oct 26 '19 at 9:06
  • @FrankMittelbach If I got it right, \protected@file@percent is intended to be used within the kernel as the last token of the second argument of \addtocontents in order to have it winding up in the second argument of the corresponding \@writefile-entry in the .aux-file for preventing the coming into being of a space-token at the end of the line in question when processing the external file (.toc/.lot/.lof/whatever) that came into being due to \@starttoc{...} and \@writefile{...}{...}. – Ulrich Diez Oct 26 '19 at 9:20
  • There are clearly many people that don't use a recent version of LaTeX, but there are equally many who use miktex or TL and stay with recent versions and so far this issue was the first and only that showed. As to !#2! the #2 has no payload it is either empty or \protected@file@percent unless somebody would start to use \protected@file@percent in random places. – Frank Mittelbach Oct 26 '19 at 15:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.