3

Using this great answer I managed to create a function \zx{A & B} that can accepts the & char (to give to some sort of tikz matrices, as I would prefer to avoid using \& instead of &... Note that the error arrives even without using tikz at all). It works great... except in the align environment. In that case even \zx{A} (without using any &) breaks. Any idea how I could solve this issue?

MWE

\documentclass[]{article}
\usepackage{amsmath}
\usepackage{tikz-cd}

\newcommand\zx{%
  \begingroup% To avoid ampersand issues https://tex.stackexchange.com/a/611535/116348
  \NewDocumentCommand{\tmpZX}{O{}+m}{%
    \endgroup%
    \begin{tikzcd}[##1] ##2 \end{tikzcd}
    % Problem is not specific to tikzcd, and also appear if you use instead:
    % Hello (##1) ##2 :)
    % But just make sure to remove & and \rar since they only make sense in tikzcd.
  }%
  \catcode`&=13%
  \tmpZX%
}

\begin{document}

\subsection{Level 1: without inside.}

Works:
Hello \zx{A} and \zx{A \rar & B}.

\noindent Works:
\begin{equation}
  \zx{A} = \zx{A \rar & B}
\end{equation}

\noindent Fails:
% \begin{align}
%   \zx{A}
% \end{align}

\end{document}
5
  • 1
    align grabs its body as an argument so catcode changes can not work, you need to make them before you start the align Oct 23 at 12:22
  • Oh, really? That's annoying... And I guess I can't change them document-wise without breaking other features... Also, do you mean that if an environment is based on Environ, it will also not work?
    – tobiasBora
    Oct 23 at 13:03
  • 1
    yes enviorn would be the same, that's why tikz offers the \& alternative. Oct 23 at 13:08
  • Ok I see thanks... Too bad there is no alternative.
    – tobiasBora
    Oct 23 at 13:50
  • there is always an alternative, you could re-scan with\scantokens (but that is a tricky command to get right) or use some custom macro to look for a normal & and replace by an active one or ... but using \& is not so bad and by far the least likely to break everything. Oct 23 at 15:48
4

No need to make indirections:

\documentclass[]{article}
\usepackage{amsmath}
\usepackage{tikz-cd}

\usepackage{lipsum}

\NewDocumentCommand{\zx}{O{}+m}{%
  \begingroup\catcode`\&=\active
  \begin{tikzcd}[#1] \scantokens{#2} \end{tikzcd}
  \endgroup
}

\begin{document}

Hello \zx{A} and \zx{A \rar & B}.

\lipsum[1][1-2]
\begin{equation}
  \zx{A} = \zx{A \rar & B}
\end{equation}
\lipsum[1][4-6]
\begin{align}
  \zx{A} &= \zx{A \rar & B} \\
  x &= y
\end{align}
\lipsum[2][1-3]

\end{document}

enter image description here

As you see from the last example, the & works in either case, both in and out of \zx.

In a slightly different (and more robust) way:

\documentclass[]{article}
\usepackage{amsmath}
\usepackage{tikz-cd}

\usepackage{lipsum}

\ExplSyntaxOn
\NewDocumentCommand{\zx}{O{}+m}{%
  \tl_rescan:nn { \char_set_catcode_active:N \& } { \begin{tikzcd}[#1] #2 \end{tikzcd} }
}
\ExplSyntaxOff

\begin{document}

Hello \zx{A} and \zx{A \rar & B}.

\lipsum[1][1-2]
\begin{equation}
  \zx{A} = \zx{A \rar & B}
\end{equation}
\lipsum[1][4-6]
\begin{align}
  \zx{A} &= \zx{A \rar & B} \\
  x &= y
\end{align}
\lipsum[2][1-3]

\end{document}
3

If you like a complicated way without \scantokens, I can offer a macro-mechanism
\ReplaceAnd{⟨replacement for &4}⁠{⟨tokens where &4 shall be replaced by ⟨replacement for &4⟩⟩}
which is driven by \romannumeral-expansion and which recursively replaces explicit &-character-tokens of category 4 (alignment tab).

As a side-effect any matching pair of explicit character tokens of category 1(begin group) and 2(end group) in ⟨tokens where &4 shall be replaced by ⟨replacement for &4⟩⟩ is replaced by a matching pair of explicit character-tokens {1 and }2.
As usually { and } are the only characters with category code 1 respective 2, this usually doesn't matter.

Active-& can be used as ⟨replacement for &4.
The tokens forming the tikzcd-environment can be used as ⟨tokens where &4 shall be replaced by ⟨replacement for &4⟩⟩.

\errorcontextlines=10000
\makeatletter
%%///////// Code for \ReplaceAnd //////////////////////////////////////////////
%% Syntax:
%% -------
%%
%% \ReplaceAnd{<replacement for &>}%
%%            {<tokens where & shall be replaced by <replacement for &>>}%
%%
%% The result is delivered after two expansion-steps/by two "hits"
%% with \expandafter.
%%
%% As a side-effect any matching pair of explicit character tokens 
%% of category code 1 and 2 is replaced by a matching pair of
%% explicit character-tokens {_1 and }_2.
%%
%%=============================================================================
%% PARAPHERNALIA:
%% \UD@firstoftwo, \UD@secondoftwo, \UD@PassFirstToSecond, \UD@Exchange,
%% \UD@removespace, \UD@stopromannumeral, \UD@CheckWhetherNull,
%% \UD@CheckWhetherBrace, \UD@CheckWhetherLeadingExplicitSpace,
%% \UD@ExtractFirstArg
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\newcommand\UD@Exchange[2]{#2#1}%
\@ifdefinable\UD@removespace{\UD@Exchange{ }{\def\UD@removespace}{}}%
\@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
%%-----------------------------------------------------------------------------
%% 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]{%
  \romannumeral\expandafter\UD@secondoftwo\string{\expandafter
  \UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
  \UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@secondoftwo}{%
  \expandafter\UD@stopromannumeral\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument is blank (empty or only spaces):
%%-----------------------------------------------------------------------------
%% -- Take advantage of the fact that TeX discards space tokens when
%%    "fetching" _un_delimited arguments: --
%% \UD@CheckWhetherBlank{<Argument which is to be checked>}%
%%                      {<Tokens to be delivered in case that
%%                        argument which is to be checked is blank>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked is not blank>}%
\newcommand\UD@CheckWhetherBlank[1]{%
  \romannumeral\expandafter\expandafter\expandafter\UD@secondoftwo
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo#1{}{}}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%.............................................................................
%% \UD@CheckWhetherBrace{<Argument which is to be checked>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked has a leading
%%                        explicit catcode-1-character-token>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked does not have a
%%                        leading explicit catcode-1-character-token>}%
\newcommand\UD@CheckWhetherBrace[1]{%
  \romannumeral\expandafter\UD@secondoftwo\expandafter{\expandafter{%
  \string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@firstoftwo}{%
  \expandafter\UD@stopromannumeral\UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingExplicitSpace{<Argument which is to be checked>}%
%%                                     {<Tokens to be delivered in case <argument
%%                                       which is to be checked> does have a
%%                                       leading explicit space-token>}%
%%                                     {<Tokens to be delivered in case <argument
%%                                       which is to be checked> does not have a
%%                                       a leading explicit space-token>}%
\newcommand\UD@CheckWhetherLeadingExplicitSpace[1]{%
  \romannumeral\UD@CheckWhetherNull{#1}%
  {\expandafter\UD@stopromannumeral\UD@secondoftwo}%
  {%
    % Let's nest things into \UD@firstoftwo{...}{} to make sure they are nested in braces
    % and thus do not disturb when the test is carried out within \halign/\valign/
    % tabular-environment:
    \expandafter\UD@firstoftwo\expandafter{%
      \expandafter\expandafter\expandafter\UD@stopromannumeral
      \romannumeral\expandafter\UD@secondoftwo
      \string{\UD@CheckWhetherLeadingExplicitSpaceB.#1 }{}%
    }{}%
  }%
}%
\@ifdefinable\UD@CheckWhetherLeadingExplicitSpaceB{%
  \long\def\UD@CheckWhetherLeadingExplicitSpaceB#1 {%
    \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
    {\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
    {\expandafter\expandafter\expandafter\UD@stopromannumeral
     \expandafter\expandafter\expandafter}%
     \expandafter\UD@secondoftwo\expandafter{\string}%
  }%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%.............................................................................
%%   \UD@ExtractFirstArg{ABCDE} yields  {A}
%%
%%   \UD@ExtractFirstArg{{AB}CDE} yields  {{AB}}
%%
%% Due to \romannumeral-expansion the result is delivered after two 
%% expansion-steps/after "hitting" \UD@ExtractFirstArg with \expandafter
%% twice.
%%
%% \UD@ExtractFirstArg's argument must not be blank.
%% This case can be cranked out via \UD@CheckWhetherBlank before calling
%% \UD@ExtractFirstArg.
%%
%% Use frozen-\relax as delimiter for speeding things up.
%% Frozen-\relax is chosen because David Carlisle pointed out in
%% <https://tex.stackexchange.com/a/578877>
%% that frozen-\relax cannot be (re)defined in terms of \outer and cannot be
%% affected by \uppercase/\lowercase.
%%
%% \UD@ExtractFirstArg's argument may contain frozen-\relax:
%% The only effect is that internally more iterations are needed for
%% obtaining the result.
%%.............................................................................
\@ifdefinable\UD@RemoveTillFrozenrelax{%
  \expandafter\expandafter\expandafter\UD@Exchange
  \expandafter\expandafter\expandafter{%
  \expandafter\expandafter\ifnum0=0\fi}%
  {\long\def\UD@RemoveTillFrozenrelax#1#2}{{#1}}%
}%
\expandafter\UD@PassFirstToSecond\expandafter{%
  \romannumeral\expandafter
  \UD@PassFirstToSecond\expandafter{\romannumeral
    \expandafter\expandafter\expandafter\UD@Exchange
    \expandafter\expandafter\expandafter{%
    \expandafter\expandafter\ifnum0=0\fi}{\UD@stopromannumeral#1}%
  }{%
    \UD@stopromannumeral\romannumeral\UD@ExtractFirstArgLoop
  }%
}{%
  \newcommand\UD@ExtractFirstArg[1]%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
  {\UD@stopromannumeral#1}%
  {\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillFrozenrelax#1}}%
}%
%====================================================================
\@ifdefinable\UD@AndInstance{\long\def\UD@AndInstance#1&#2#3!{#2}}%
\newcommand\UD@ReplaceAndInstance[2]{%
  % #1 a single token to examine
  % #2 replacement for &
  \UD@AndInstance#1{\UD@stopromannumeral#2}&{\UD@stopromannumeral#1}!%
}%
\newcommand\UD@AndReplaceloop[3]{%
  % #1 replacement for &
  % #2 tokens forming the result gathered so far
  % #3 remaining token list to process
  \UD@CheckWhetherNull{#3}{\UD@stopromannumeral#2}{%
    \expandafter\UD@Exchange\expandafter{%
      \romannumeral
      \UD@CheckWhetherLeadingExplicitSpace{#3}{%
        \expandafter\UD@PassFirstToSecond
        \expandafter{\UD@removespace#3}{\UD@stopromannumeral{#2 }}%
      }{%
        \expandafter\UD@PassFirstToSecond\expandafter{\UD@firstoftwo{}#3}{%
          \expandafter\expandafter\expandafter\UD@stopromannumeral
          \expandafter\expandafter\expandafter{%
            \expandafter\UD@Exchange\expandafter{%
              \romannumeral
              \UD@CheckWhetherBrace{#3}{%
                \expandafter\UD@stopromannumeral\expandafter{\romannumeral
                  \expandafter\expandafter\expandafter\UD@PassFirstToSecond
                  \UD@ExtractFirstArg{#3}{\UD@AndReplaceloop{#1}{}}%
                }%
              }{%
                \expandafter\expandafter\expandafter\UD@ReplaceAndInstance
                \UD@ExtractFirstArg{#3}{#1}%
              }%
            }%
            {#2}%
          }%
        }%
      }%
    }{\UD@AndReplaceloop{#1}}%
  }%
}%
\newcommand\ReplaceAnd[2]{%
  % #1 replacement for &
  % #2 tokens where to replace & by replacement for &
  \romannumeral\UD@AndReplaceloop{#1}{}{#2}%
}%
%%///////// End of code for \ReplaceAnd //////////////////////////////////////
\makeatother

\documentclass[]{article}
\usepackage{amsmath}
\usepackage{tikz-cd}


\begingroup
\makeatletter
\catcode`\&=13 %
\@firstofone{%
  \endgroup
  \NewDocumentCommand{\zx}{o+m}{%
    \ReplaceAnd{&}{\IfNoValueTF{#1}{\begin{tikzcd}}{\begin{tikzcd}[{#1}]}#2\end{tikzcd}}%
  }%
}%

\begin{document}

\subsection{Level 1: without inside.}

Works:
Hello \zx{A} and \zx{A \rar & B}.

\noindent Works:
\begin{equation}
  \zx{A} = \zx{A \rar & B}
\end{equation}

\noindent Works:
\begin{align}
  \zx{A} &= \zx{A \rar & B}\\
  \zx{UV} &= \zx{UV \rar & UW}
\end{align}

\end{document}

enter image description here


In case you like edge cases and weird scenarios:

Unlike the \scantokens-method the \ReplaceAnd-method does not double hashes and does not produce spaces behind control-word-tokens—here is an example where both the replacement-method and the \scantokens-method are combined so that you can see the subtle differences in edge cases:

\errorcontextlines=10000
\makeatletter
%%///////// Code for \ReplaceAND //////////////////////////////////////////////
%% Syntax:
%% -------
%%
%% \ReplaceAnd{<replacement for &>}%
%%            {<tokens where & shall be replaced by <replacement for &>>}%
%%
%% The result is delivered after two expansion-steps/by two "hits"
%% with \expandafter.
%%
%% As a side-effect any matching pair of explicit character tokens 
%% of category code 1 and 2 is replaced by a matching pair of
%% explicit character-tokens {_1 and }_2.
%%
%%=============================================================================
%% PARAPHERNALIA:
%% \UD@firstoftwo, \UD@secondoftwo, \UD@PassFirstToSecond, \UD@Exchange,
%% \UD@removespace, \UD@stopromannumeral, \UD@CheckWhetherNull,
%% \UD@CheckWhetherBrace, \UD@CheckWhetherLeadingExplicitSpace,
%% \UD@ExtractFirstArg
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\newcommand\UD@Exchange[2]{#2#1}%
\@ifdefinable\UD@removespace{\UD@Exchange{ }{\def\UD@removespace}{}}%
\@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
%%-----------------------------------------------------------------------------
%% 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]{%
  \romannumeral\expandafter\UD@secondoftwo\string{\expandafter
  \UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
  \UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@secondoftwo}{%
  \expandafter\UD@stopromannumeral\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument is blank (empty or only spaces):
%%-----------------------------------------------------------------------------
%% -- Take advantage of the fact that TeX discards space tokens when
%%    "fetching" _un_delimited arguments: --
%% \UD@CheckWhetherBlank{<Argument which is to be checked>}%
%%                      {<Tokens to be delivered in case that
%%                        argument which is to be checked is blank>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked is not blank>}%
\newcommand\UD@CheckWhetherBlank[1]{%
  \romannumeral\expandafter\expandafter\expandafter\UD@secondoftwo
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo#1{}{}}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%.............................................................................
%% \UD@CheckWhetherBrace{<Argument which is to be checked>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked has a leading
%%                        explicit catcode-1-character-token>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked does not have a
%%                        leading explicit catcode-1-character-token>}%
\newcommand\UD@CheckWhetherBrace[1]{%
  \romannumeral\expandafter\UD@secondoftwo\expandafter{\expandafter{%
  \string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@firstoftwo}{%
  \expandafter\UD@stopromannumeral\UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingExplicitSpace{<Argument which is to be checked>}%
%%                                     {<Tokens to be delivered in case <argument
%%                                       which is to be checked> does have a
%%                                       leading explicit space-token>}%
%%                                     {<Tokens to be delivered in case <argument
%%                                       which is to be checked> does not have a
%%                                       a leading explicit space-token>}%
\newcommand\UD@CheckWhetherLeadingExplicitSpace[1]{%
  \romannumeral\UD@CheckWhetherNull{#1}%
  {\expandafter\UD@stopromannumeral\UD@secondoftwo}%
  {%
    % Let's nest things into \UD@firstoftwo{...}{} to make sure they are nested in braces
    % and thus do not disturb when the test is carried out within \halign/\valign/
    % tabular-environment:
    \expandafter\UD@firstoftwo\expandafter{%
      \expandafter\expandafter\expandafter\UD@stopromannumeral
      \romannumeral\expandafter\UD@secondoftwo
      \string{\UD@CheckWhetherLeadingExplicitSpaceB.#1 }{}%
    }{}%
  }%
}%
\@ifdefinable\UD@CheckWhetherLeadingExplicitSpaceB{%
  \long\def\UD@CheckWhetherLeadingExplicitSpaceB#1 {%
    \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
    {\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
    {\expandafter\expandafter\expandafter\UD@stopromannumeral
     \expandafter\expandafter\expandafter}%
     \expandafter\UD@secondoftwo\expandafter{\string}%
  }%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%.............................................................................
%%   \UD@ExtractFirstArg{ABCDE} yields  {A}
%%
%%   \UD@ExtractFirstArg{{AB}CDE} yields  {{AB}}
%%
%% Due to \romannumeral-expansion the result is delivered after two 
%% expansion-steps/after "hitting" \UD@ExtractFirstArg with \expandafter
%% twice.
%%
%% \UD@ExtractFirstArg's argument must not be blank.
%% This case can be cranked out via \UD@CheckWhetherBlank before calling
%% \UD@ExtractFirstArg.
%%
%% Use frozen-\relax as delimiter for speeding things up.
%% Frozen-\relax is chosen because David Carlisle pointed out in
%% <https://tex.stackexchange.com/a/578877>
%% that frozen-\relax cannot be (re)defined in terms of \outer and cannot be
%% affected by \uppercase/\lowercase.
%%
%% \UD@ExtractFirstArg's argument may contain frozen-\relax:
%% The only effect is that internally more iterations are needed for
%% obtaining the result.
%%.............................................................................
\@ifdefinable\UD@RemoveTillFrozenrelax{%
  \expandafter\expandafter\expandafter\UD@Exchange
  \expandafter\expandafter\expandafter{%
  \expandafter\expandafter\ifnum0=0\fi}%
  {\long\def\UD@RemoveTillFrozenrelax#1#2}{{#1}}%
}%
\expandafter\UD@PassFirstToSecond\expandafter{%
  \romannumeral\expandafter
  \UD@PassFirstToSecond\expandafter{\romannumeral
    \expandafter\expandafter\expandafter\UD@Exchange
    \expandafter\expandafter\expandafter{%
    \expandafter\expandafter\ifnum0=0\fi}{\UD@stopromannumeral#1}%
  }{%
    \UD@stopromannumeral\romannumeral\UD@ExtractFirstArgLoop
  }%
}{%
  \newcommand\UD@ExtractFirstArg[1]%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
  {\UD@stopromannumeral#1}%
  {\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillFrozenrelax#1}}%
}%
%====================================================================
\@ifdefinable\UD@AndInstance{\long\def\UD@AndInstance#1&#2#3!{#2}}%
\newcommand\UD@ReplaceAndInstance[2]{%
  % #1 a single token to examine
  % #2 replacement for &
  \UD@AndInstance#1{\UD@stopromannumeral#2}&{\UD@stopromannumeral#1}!%
}%
\newcommand\UD@AndReplaceloop[3]{%
  % #1 replacement for &
  % #2 tokens forming the result gathered so far
  % #3 remaining token list to process
  \UD@CheckWhetherNull{#3}{\UD@stopromannumeral#2}{%
    \expandafter\UD@Exchange\expandafter{%
      \romannumeral
      \UD@CheckWhetherLeadingExplicitSpace{#3}{%
        \expandafter\UD@PassFirstToSecond
        \expandafter{\UD@removespace#3}{\UD@stopromannumeral{#2 }}%
      }{%
        \expandafter\UD@PassFirstToSecond\expandafter{\UD@firstoftwo{}#3}{%
          \expandafter\expandafter\expandafter\UD@stopromannumeral
          \expandafter\expandafter\expandafter{%
            \expandafter\UD@Exchange\expandafter{%
              \romannumeral
              \UD@CheckWhetherBrace{#3}{%
                \expandafter\UD@stopromannumeral\expandafter{\romannumeral
                  \expandafter\expandafter\expandafter\UD@PassFirstToSecond
                  \UD@ExtractFirstArg{#3}{\UD@AndReplaceloop{#1}{}}%
                }%
              }{%
                \expandafter\expandafter\expandafter\UD@ReplaceAndInstance
                \UD@ExtractFirstArg{#3}{#1}%
              }%
            }%
            {#2}%
          }%
        }%
      }%
    }{\UD@AndReplaceloop{#1}}%
  }%
}%
\newcommand\ReplaceAnd[2]{%
  % #1 replacement for &
  % #2 tokens where to replace & by replacement for &
  \romannumeral\UD@AndReplaceloop{#1}{}{#2}%
}%
%%///////// End of code for \ReplaceAnd //////////////////////////////////////
\makeatother

\documentclass[]{article}
\usepackage{amsmath}
\usepackage{tikz-cd}


\begingroup
\makeatletter
\catcode`\&=13 %
\@firstofone{%
  \endgroup
  \NewDocumentCommand{\zx}{o+m}{%
    \ReplaceAnd{&}{\IfNoValueTF{#1}{\begin{tikzcd}}{\begin{tikzcd}[{#1}]}#2\end{tikzcd}}%
  }%
}%


\NewDocumentCommand{\zxE}{O{}+m}{%
  \begingroup\catcode`\&=\active
  \begin{tikzcd}[#1]\scantokens{#2}\end{tikzcd}%
  \endgroup
}

\makeatletter
\newcommand\Q{\futurelet\QQ\QQQ}
\newcommand\QQQ{%
  \text{\texttt{\string\Q}(\ifx\QQ\@sptoken\else no \fi space token behind \texttt{\string\Q})}%
}%
\makeatother


\begin{document}

\noindent\verb|\ReplaceAnd|-method:

\begin{verbatim}
\begin{align}
  \zx{\string#A}&=\zx{\string#A \rar & \string#B}\\
  \zx{\catcode`\Q=12 \Q!}&=\zx{UV \rar & UW}
\end{align}
\end{verbatim}

\begin{align}
  \zx{\string#A}&=\zx{\string#A \rar & \string#B}\\
  \zx{\catcode`\Q=12 \Q!}&=\zx{UV \rar & UW}
\end{align}

\noindent\hrule\hfill\bigskip

\noindent\verb|\scantokens|-method:

\begin{verbatim}
\begin{align}
  \zxE{\string#A}&=\zxE{\string#A \rar & \string#B}\\
  \zxE{\catcode`\Q=12 \Q!}&=\zxE{UV \rar & UW}
\end{align}
\end{verbatim}


\begin{align}
  \text{You get: ! You can't use `macro paramete}&\text{r character \string#' in math mode.}\\
  %\zxE{\string#A}&=\zxE{\string#A \rar & \string#B}\\
  \zxE{\catcode`\Q=12 \Q!}&=\zxE{UV \rar & UW}
\end{align}

\end{document}

enter image description here

About the coming-into-being of the rather unexpected effects/results with the \scantokens-method in those edge-cases/weird scenarios:

Section "3.7 Input Handling" of the eTeX-manual says about \scantokens:

The command \scantokens{...} absorbs a list of unexpanded tokens, converts it into a character string that is treated as if it were an external file, and starts to read from this ‘pseudo-file’. [...] In particular every occurrence of the current newline character is interpreted as start of a new line, and input characters will be converted into tokens as usual. [...]

The crucial point hereby is the phrase "converts it into a character string". This conversion is done by faking the unexpanded writing of the list of tokens and treating the resulting characters (not tokens!) as if they would form an external file whose characters are to be read and pre-processed linewise as usual by TeX's eyes (removal of all space-characters that occur at right ends of lines, appending a character whose code point number in TeX's internal character encoding scheme equals the value of the integer-parameter \endlinechar) and where the characters that form the result of that pre-processing get tokenized by TeX's mouth on demand, i.e., whenever more tokens are needed by the gullet (where expansion is done) and/or by the stomach (where assignments take place and boxes/pages are build etc).

TeX doubles explicit character-tokens of category 6(parameter), i.e., hashes(#), when writing them to file unexpanded.

Apart from that "hash-doubling" the unexpanded-writing of a token is like applying \string to that token and writing the resulting sequence of unexpandable character-tokens and, in case the token in question is a control-word-token, appending a space-character. (In case the token in question is a control-symbol-token or an explicit character-token no space-character is appended.)

Applying \string with control-sequence-tokens in turn implies delivering a preceding escape-character whose code-point-number in TeX's internal character-representation-scheme equals the value of the integer-parameter \escapechar. Usually that value is 92, denoting the backslash, but it can be changed! As a special case applying \string to the nameless control-sequence, which comes into being by expanding \csname\endcsname or by placing a backslash at the end of a line of .tex-input while \endlinechar has a non-positive value, delivers ⟨character according to \escapechar⟩⁠csname⟨character according to \escapechar⟩endcsname, no space character being appended.

All these things can bite you when applying \scantokens.

E.g., with \zxE{\string#A} the hash-doubling of \scantokens at the time of "writing the pseudo-file" leads to hashes in the sequence \string#A being doubled and space-characters being appended to things that come from unexpanded-writing control-word-tokens so that the pseudo-file that is to be processed as usual contains the characters
\string⟨space-character⟩##A.

This is taken for a file. Reading and pre-processing the first line of that file yields the characters
\string⟨space-character⟩##A⟨character according to \endlinechar⟩.

Tokenization yields the control-word-token \string. As TeX's reading-apparatus switches to state S(skipping blanks) after tokenizing a control-word-token, the subsequent ⟨space-character⟩ is just skipped and doesn't yield any token at all. Then the first hash is tokenized and yields an explicit #-character-token of category 6(parameter). \string is applied to that character-token, which in turn yields an explicit #-character-token of category 12(other). This goes to the stomach and is typeset as any ordinary character is typeset in math-mode. Then the second hash is tokenized and yields an explicit #-character-token of category 6(parameter). This passes the gullet and reaches the stomach and causes a "! You can't use `macro parameter character #' in math mode."-error.

With \zxE{\catcode`\Q=12 \Q!} \scantokens' creation of the pseudo-file leads to the pseudo-file consisting of the characters:

\catcode⟨space-character⟩`\Q⟨space-character⟩=12 \Q⟨space-character⟩!

Reading and pre-processing the first line of that file yields the characters

\catcode⟨space-character⟩`\Q⟨space-character⟩=12 \Q⟨space-character⟩!⟨character according to \endlinechar⟩

When tokenizing these characters by and by and processing the resulting tokens, the character \Q gets assigned the category-code 12(other). Thus the last \Q is a control-symbol-token, not a control-word-token. As it is not a control-word-token, the reading-apparatus is not switched to state S(skipping blanks) but to state M(middle of line) and the subsequent ⟨space-character⟩ is not skipped but yields an explicit character-token of category code 10(space) and character-code 32. This in turn is detected by the \futurelet-mechanism underlying the definition of \Q.

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.