Comprender la expansión en \addtocontents

Comprender la expansión en \addtocontents

Estoy confundido acerca de cómo funciona la expansión en \addtocontents, o más precisamente en \protected@write. Según tengo entendido, \addtocontentsesencialmente debería expandir su segundo argumento \protected@edefy escribir el resultado en el archivo auxiliar. Con este entendimiento, esperaría que el comportamiento fuera (hasta a \protected@) el mismo que

\iow_shipout:Ne \@auxout
  {
    \exp_not:N \@writefile { #1 } { #2 }
  }

Pero como muestra el siguiente MWE, se comportan de manera diferente cuando intentan evitar la expansión dentro de su argumento con \exp_not:n.

\documentclass{article}

\makeatletter

\ExplSyntaxOn
\NewDocumentCommand { \naiveaddtocontents } { m m }
  {
    \iow_shipout:Ne \@auxout
      {
        \exp_not:N \@writefile { #1 } { #2 }
      }
  }
\ExplSyntaxOff

\def\abc{some text}

\begin{document}

bla

\ExplSyntaxOn

% this does what I expect
\protected@edef \l_tmpa_tl { \exp_not:n { \abc } }
\tl_show:N \l_tmpa_tl

% so does this
\naiveaddtocontents{ lof }{ \exp_not:n { \abc } }

% this does not
\addtocontents{ lof }{ \exp_not:n { \abc } }

\ExplSyntaxOff

\end{document}

El archivo auxiliar se parece

\relax 
\@writefile {lof}{\abc }
\@writefile{lof}{some text}
\gdef \@abspage@last{1}

También lo \naiveaddtocontentses prevenir la expansión con \exp_not:n, pero \addtocontentsno lo es. Para mi caso de uso en el que no quiero que el argumento se expanda en absoluto, puedo usar esto \naiveaddtocontents. Pero, ¿por qué no funciona \exp_not:ncomo esperaba incorrectamente \addtocontents?

Respuesta1

El comando \addtocontentsusa \protected@write. A continuación se muestra la definición de \protected@writede source2e:

\long\def \protected@write#1#2#3{%
      \begingroup
       \let\thepage\relax
       #2%
       \let\protect\@unexpandable@protect
       \edef\reserved@a{\write#1{#3}}%
       \reserved@a
      \endgroup
      \if@nobreak\ifvmode\nobreak\fi\fi
}

La parte relevante es \edef\reserved@a{\write#1{#3}}%. Si la definición anterior de \protected@writese agregaría en el documento y en la línea anterior, \edefse reemplaza por \defluego \addtocontents{ lof }{ \exp_not:n { \abc } }se proporciona \@writefile{lof}{\abc }en el archivo .aux.

Sin embargo, con \edef, se produce una expansión adicional que \addtocontents{ lof }{ \exp_not:n { \abc } }da \@writefile{lof}{some text}como resultado el archivo .aux.

Por lo tanto, con \edef, el ejemplo funciona si \exp_not:nse agrega un adicional: \addtocontents{ lof }{ \exp_not:n { \exp_not:n { \abc } } }proporciona \@writefile{lof}{\abc }el archivo .aux.

Respuesta2

Veamos la definición de \addtocontents:

% latex.ltx, line 14289:
\long\def\addtocontents#1#2{%
  \protected@write\@auxout
      {\let\label\@gobble \let\index\@gobble \let\glossary\@gobble}%
      {\string\@writefile{#1}{#2}}}

Con su ingenua definición, tendría grandes problemas si tuviera un \labelsegundo argumento para \addtocontents. Pero dejemos esto de lado, porque es un tecnicismo.

Supongamos que quieres \addtocontents{toc}{Hey, this is \textbf{boldface}}.

\documentclass{article}

\makeatletter
\ExplSyntaxOn
\NewDocumentCommand { \naiveaddtocontents } { m m }
  {
    \iow_shipout:Ne \@auxout
      {
        \exp_not:N \@writefile { #1 } { #2 }
      }
  }
\ExplSyntaxOff
\makeatother

\begin{document}

Some text

\addtocontents{toc}{Hey, this is \textbf{boldface}}

\naiveaddtocontents{toc}{Hey, this is \textbf{boldface}}

\end{document}

La consola imprimirá

(\end occurred when \ifx on line 21 was incomplete)
(\end occurred when \ifx on line 21 was incomplete)
(\end occurred when \ifx on line 21 was incomplete)
(\end occurred when \ifmmode on line 21 was incomplete)</usr/local/texlive/2023
/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb>

y el auxarchivo tendrá

\relax
\@writefile{toc}{Hey, this is \textbf  {boldface}}
\@writefile {toc}{Hey, this is \protect \unhbox \voidb@x \bgroup \edef l3backend-pdftex.def{boldface}\let \futurelet \@let@token \let \protect \relax \edef cmr{cmr}\edef cmss{cmss}\edef cmtt{cmtt}\def ##1,b,{}\series@check@toks {,ulm,elm,lm,slm,mm,sbm,bm,ebm,ubm,muc,mec,mc,msc,msx,mx,mex,mux,{}{},b,}\edef {}\edef b{b}\def ##1,m,{}\series@check@toks {,ulm,elm,lm,slm,mm,sbm,bm,ebm,ubm,muc,mec,mc,msc,msx,mx,mex,mux,{}{},m,}\edef {}\edef m{m}\protect \let }
\gdef \@abspage@last{1}

Realmente no es lo que quieres ver, ¿verdad? Arreglemoslo.

\documentclass{article}

\makeatletter
\ExplSyntaxOn
\NewDocumentCommand { \naiveaddtocontents } { m m }
  {
    \iow_shipout:Ne \@auxout
      {
        \exp_not:N \@writefile { #1 } { \text_expand:n { #2 } }
      }
  }
\ExplSyntaxOff
\makeatother

\newcommand{\abc}{some text}

\begin{document}

Some text

\addtocontents{toc}{Hey, this is \textbf{boldface} and \abc}

\naiveaddtocontents{toc}{Hey, this is \textbf{boldface} and \abc}

\end{document}

Ahora el auxarchivo tendrá

\relax
\@writefile{toc}{Hey, this is \textbf  {boldface} and some text}
\@writefile {toc}{Hey, this is \textbf {boldface} and some text}
\gdef \@abspage@last{1}

Básicamente, \text_expand:nhará más o menos lo mismo que \protected@edefhace, pero dejará la lista de tokens resultante envuelta en \unexpanded.

Si lo hace \exp_not:n(es decir, \unexpanded), TeX serviráNoexpansión, por lo que obtienes \abcy no su expansión.

Sin \makeatlettery \makeatother:

\ExplSyntaxOn
\NewDocumentCommand { \naiveaddtocontents } { m m }
  {
    \iow_shipout:ce { @auxout }
      {
        \token_to_str:c { @writefile } { #1 } { \text_expand:n { #2 } }
      }
  }
\ExplSyntaxOff

En lugar de \token_to_str:cpuedes usar\exp_not:c

información relacionada