3

I am using nested macros using the xparse package, my ultimate goal being to have a custom macro factory. I have simplified my current problem here.

I want to give a pattern as an argument, like in this example:

\DeclareDocumentCommand\Foo{O{\emph{##1}} O{dummy} m}{{%
    \def\@style##1{#1}%
    \@style{#3}%
}}
Hello \Foo{world}!
This is \Foo[\color{blue}\textbf{#1}]{nice}!

compiled LaTeX

However, if I add one level of nesting, everything breaks:

\NewDocumentCommand\DeclareFoo{O{\emph{##1}}}{%
    \DeclareDocumentCommand\Foo{O{dummy} m}{{%
        \def\@style####1{#1}%
        \@style{##2}%
    }}%
}
\DeclareFoo% gets the #1 of \Foo instead of \@style
Hello \Foo{world}!
\DeclareFoo[\color{blue}\textbf{#1}]% same
This is \Foo{nice}!
\DeclareFoo[\color{red}\textbf{##1}]% this works but I would rather use only one "#"
This is \Foo{not nice}!

compiled LaTeX

\DeclareFoo is supposed to be a black box, and the user is not supposed to guess the number of nesting levels (please do not mark this as a duplicate).

Is there a way to escape the # inside #1 argument? I tried \StrSubstitute from the xstring package to double the number of # in #1, but couldn't make it work...

User9123
  • 147

4 Answers4

2

For comparison: what we need to do in OpTeX:

\fontfam[lm]

\def\Foodeclared{} \optdef\DeclareFoo[]{\ea\def \ea\Foodeclared \ea{\the\opt}} \optdef\Foo[]#1{{\em \Foodeclared \the\opt #1}}

\DeclareFoo Hello, \Foo{world}!

Hello, \Foo[\bf]{world}!

\DeclareFoo[\Blue\bf]% This is \Foo{nice}!

\DeclareFoo[\Red\bf]% Is this \Foo{nice, too}?

Is this \Foo[\Green\caps\rm]{nice, too}?

Is this \Foo{nice, too}?

\bye

The result is the same as in the Ulrich's answer.

wipet
  • 74,238
  • +1 - With your laconic way of making things clear, you once more made my day. ;-) I really need to take an intensive look at OpTeX. I assume things are implemented more efficiently/better there than I am used to do. – Ulrich Diez Jun 23 '22 at 23:38
  • This answer is "cheating" in that it workarounds the issue of doubling # completely by having suitable macros (in this case \bf instead of \textbf, which applies until the end of the group) in order to make passing # unnecessary (but sure, it works, no problem there.) – user202729 Jun 24 '22 at 01:41
1

With the \DeclareFoo definition, the default optional argument must have an extra pair of # tokens: O{\emph{####1}}, in order to make the default \emph case work. Nonetheless, even here, you still need to use the ##1 notation when specifying a separate optional argument, as in \DeclareFoo[\color{red}\textbf{##1}], because ##1 is required in \DeclareFoo to act as #1 in a subsequent invocation of \Foo.

\documentclass{article}
\usepackage{xcolor}
\begin{document}
\DeclareDocumentCommand\Foo{O{\emph{##1}} O{dummy} m}{{%
    \def\@style##1{#1}%
    \@style{#3}%
}}
Hello \Foo{world}!
This is \Foo[\color{blue}\textbf{#1}]{nice}!

\NewDocumentCommand\DeclareFoo{O{\emph{####1}}}{% \DeclareDocumentCommand\Foo{O{dummy} m}{{% \def@style####1{#1}% @style{##2}% }}% } \DeclareFoo% works Hello \Foo{world}! \DeclareFoo[\color{red}\textbf{##1}]% this works but I would rather use only one "#" This is \Foo{required}!

\DeclareFoo[\color{blue}\textbf{#1}]% gets the #1 of \Foo instead of @style This \Foo{would be nice, but} doesn't work because ##1 is needed in DeclareFoo to produce #1 in Foo! \end{document}

enter image description here

  • Is there absolutely no way of "programmatically" double the number of # inside of DeclareFoo? – User9123 Jan 21 '22 at 05:15
  • @User9123 I hesitate to make an absolute statement, for there may be someone of greater skill who can accomplish it...but, for the moment, it is beyond my ken. – Steven B. Segletes Jan 21 '22 at 05:16
  • I have the impression it should be achievable, some other commands accept arguments with single hashes (\titleformat, or \DeclareDocumentCommand itself, to name a few... are these even defined in pure latex?). But I am lacking the skills to understand how they did it. – User9123 Jan 21 '22 at 10:06
  • @User9123 The one technique I have used (though I don't know how to apply it in your situation) is \def\a{\def\b##1}. This allows one to say \a{Here is #1} with the result being \def\b#1{Here is #1}. In this case a single hash #1 is passed as an argument to \a, which defines a separate macro \b. – Steven B. Segletes Jan 21 '22 at 14:37
  • Yes, I have used that technique in the first example (but I am calling \b directly inside of \a). Which makes me think that in the second example, if I move \def\@style in \DeclareFoo instead of \Foo, the second example is suddenly working. – User9123 Jan 21 '22 at 22:28
  • But then, \@style is not a local macro anymore. This is an issue, especially because my goal is to create a function factory, i.e., \DeclareFoo is supposed to take the name of the declared macro as an argument. If there is only one \@style function, this means that each created macros would have the same style. But I want each created macro to have a distinct style. I might try to create a \@style@Foo macro instead, where Foo is replaced by the name of the created function. I am still wondering if it is possible to declare \@style as a local macro... – User9123 Jan 21 '22 at 22:34
  • 1
    @User9123 Commands like \titleformat and \DeclareDocumentCommand don't need to perform hash-doubling with their arguments: At the time when these commands are carried out and their arguments are grabbed and delivered in a mix with the replacement-text, the stuff delivered does not form a second-level-definition/definition nested inside another definition (whose hashes would need to be doubled) but a first-level-definition/a definition not nested within another definition. – Ulrich Diez Jan 24 '22 at 11:27
1

In this specific case there are some "workarounds":

  • In case you can tolerate some "global namespace pollution" you can do this
\documentclass{article}
\usepackage{xcolor}
\begin{document}

\makeatletter \NewDocumentCommand\DeclareFoo{O{\emph{##1}}}{% \def@foo@helper@style##1{#1}% \DeclareDocumentCommand\Foo{O{dummy} m}{% @foo@helper@style{##2}% }% }%

\DeclareFoo% gets the #1 of \Foo instead of @style Hello \Foo{world}! \DeclareFoo[\color{blue}\textbf{#1}]% This is \Foo{nice}! \DeclareFoo[\color{red}\textbf{#1}]% This is \Foo{not nice}!

\end{document}

As long as the helper macro's name is "sufficiently unique" it should not be a problem.

  • As a alternative solution you can "pass" the content through the macro definition by storing it in an macro/token register:
\documentclass{article}
\usepackage{xcolor}
\begin{document}

\makeatletter \ExplSyntaxOn \NewDocumentCommand\DeclareFoo{O{\emph{##1}}}{ \tl_set:Nn @foo@helper@body {#1} \DeclareDocumentCommand\Foo{O{dummy} m}{ \group_begin: \exp_args:NnV \use:n {\def@foo@helper@style####1} @foo@helper@body @foo@helper@style{##2} \group_end: } } \ExplSyntaxOff

\DeclareFoo% gets the #1 of \Foo instead of @style Hello \Foo{world}! \DeclareFoo[\color{blue}\textbf{#1}]% This is \Foo{nice}! \DeclareFoo[\color{red}\textbf{#1}]% This is \Foo{not nice}!

\end{document}

The \exp_args:NnV \use:n is a bit ugly, but basically it executes the def with the variable as its "body".

user202729
  • 7,143
0

Ad "programmatically double the number of #":

The discussions

How to have hashes doubled and things expanded?

and

Double hashes inside macro definition?

might be of interest to you.

There you find approaches to the matter using l3regex or taking advantage of the fact that things like \write and \scantokens double hashes.

For the sake of having fun I did my own thing with the code below.


For recent TeX engines where \detokenize of the ε-TeX extensions is available I can offer a routine \ReplicateEveryHash which can serve as a workaround in some situations.

Syntax of the routine \ReplicateEveryHash is

\ReplicateEveryHash{⟨number⟩}{⟨balanced text⟩}

The routine \ReplicateEveryHash by means of \romannumeral-expansion recursively replicates ⟨number⟩ times every explicit character token of category 6(parameter) that is contained in the ⟨balanced text⟩.

The gist of the check for a hash is: \string# delivers a single hash-character-token of category 12(other) while with \detokenize hash-doubling takes place and therefore \detokenize{#} delivers two hash-character-tokens of category 12(other). (The edge case of an explicit space character of category 6 needs separate treatment.)

In case the ⟨balanced text⟩-argument of \ReplicateEveryHash contains matching pairs of explicit character tokens of category 1(begin group) and 2(end group), each of these pairs triggers another level of \romannumeral-expansion. Therefore excessive nesting of braces within the ⟨balanced text⟩-argument of \ReplicateEveryHash will take its toll on the semantic nest.

Besides this \ReplicateEveryHash does replace matching pairs of explicit character tokens of category 1(begin group) and 2(end group) of the ⟨balanced text⟩-argument by matching pairs of opening curly braces of category 1 and closing curly braces of category 2.
I suppose this won't be a problem in most situations as usually the curly braces are the only characters of category 1/2.
But this must be mentioned because this means that \ReplicateEveryHash is suitable only for situations where replacing explicit begin-grouping-character-tokens and explicit end-grouping-character-tokens by explicit curly-brace-tokens of the same kind doesn't matter.

Caveats/possible pitfalls:

If you place \ReplicateEveryHash{2}{...} into an \edef, due to \romannumeral-expansion hash-doubling will—in contrast with the \edef\macro{\unexpanded\expandafter{\expanded{#1}}}-approach—take place before expanding the tokens that form the ⟨balanced text⟩-argument of \ReplicateEveryHash. Therefore \edef\macro{\ReplicateEveryHash{2}{\string#1}} (or \ReplicateEveryHash{2}{\edef\macro{\string#1}} if you prefer) will yield an error-message about an illegal parameter number because in the hash-doubling-step you will get two hashes trailed by the digit 1. The first hash will be stringified. The second hash, which is trailed by the digit 1, will not be stringified and therefore will be taken for a parameter #1 while the ⟨parameter text⟩ of \macro is empty.

\makeatletter
%%//////////////  Begin of code for \ReplicateEveryHash  //////////////////////
%%=============================================================================
%% PARAPHERNALIA:
%%    \UD@stopromannumeral, 
%%    \UD@firstoftwo, \UD@secondoftwo,
%%    \UD@PassFirstToSecond, \UD@Exchange, \UD@removespace
%%    \UD@CheckWhetherNull, \UD@CheckWhetherBrace,
%%    \UD@CheckWhetherLeadingExplicitSpace, \UD@replicate, \UD@ExtractFirstArg
%%=============================================================================
\@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
\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@firstoftwo{\def\UD@removespace}{} {}}%
%%-----------------------------------------------------------------------------
%% 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's first token is an explicit character of
%% category 1:
%%.............................................................................
%% \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 an explicit 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:
    \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}%
  }%
}%
%------------------------------------------------------------------------------
% \UD@replicate{<number>}{<tokens>}
%------------------------------------------------------------------------------
\newcommand\UD@replicateloop[3]{%
  \if m#3\expandafter\UD@firstoftwo\else\expandafter\UD@secondoftwo\fi
  {\UD@replicateloop{#1}{#2#1}}{\UD@stopromannumeral#2}%
}%
\newcommand\UD@replicate[2]{%
  \romannumeral
  \expandafter\UD@Exchange\expandafter{\romannumeral\number\number#1 000}%
                                      {\UD@replicateloop{#2}{}}\relax
}%
%%=============================================================================
%% 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.
%% I chose frozen-\relax 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}}%
}%
%%=============================================================================
%% \ReplicateEveryHash{<number>}{<balanced text>}%
%%
%%   Each explicit category-6(parameter)-character-token of the 
%%   <balanced text> is replicated <number> times.
%%
%%   You obtain the result after two expansion-steps, i.e., 
%%   in expansion-contexts you get the result after "hitting" 
%%   \ReplicateEveryHash by two \expandafter.
%%   
%%   As a side-effect, the routine does replace matching pairs of explicit
%%   character tokens of catcode 1 and 2 by matching pairs of curly braces
%%   of catcode 1 and 2.
%%   I suppose this won't be a problem in most situations as usually the
%%   curly braces are the only characters of category code 1 / 2...
%%
%%   This routine needs \detokenize from the eTeX extensions.
%%-----------------------------------------------------------------------------
\newcommand\ReplicateEveryHash[2]{%
   \romannumeral
   \expandafter\UD@PassFirstToSecond\expandafter{%
   \romannumeral\number\number#1 000}{%
     \UD@ReplicateEveryHashLoop{#2}{}%
   }%
}%
\newcommand\UD@ReplicateEveryHashLoop[3]{%
  \UD@CheckWhetherNull{#1}{\UD@stopromannumeral#2}{%
    \UD@CheckWhetherLeadingExplicitSpace{#1}{%
       \expandafter\UD@ReplicateEveryHashLoop
       \expandafter{\UD@removespace#1}{#2 }{#3}%
    }{%
      \UD@CheckWhetherBrace{#1}{%
        \expandafter\expandafter\expandafter\UD@PassFirstToSecond
        \expandafter\expandafter\expandafter{%
        \expandafter\UD@PassFirstToSecond\expandafter{%
            \romannumeral%
            \expandafter\expandafter\expandafter\UD@ReplicateEveryHashLoop
            \UD@ExtractFirstArg{#1}{}{#3}%
        }{#2}}%
        {\expandafter\UD@ReplicateEveryHashLoop
         \expandafter{\UD@firstoftwo{}#1}%
        }{#3}%
      }{%
        \expandafter\expandafter\expandafter\UD@CheckWhetherHash
        \UD@ExtractFirstArg{#1}{#1}{#2}{#3}%
      }%
    }%
  }%
}%
\newcommand\UD@CheckWhetherHash[4]{%
  \expandafter\UD@CheckWhetherLeadingExplicitSpace\expandafter{\string#1}{%
    \expandafter\expandafter\expandafter\UD@CheckWhetherNull
    \expandafter\expandafter\expandafter{%
    \expandafter\UD@removespace\string#1}{%
      \expandafter\expandafter\expandafter\UD@CheckWhetherNull
      \expandafter\expandafter\expandafter{%
      \expandafter\UD@removespace\detokenize{#1}}{%
        % something whose stringification yields a single space
        \UD@secondoftwo
      }{% explicit space of catcode 6
        \UD@firstoftwo
      }%
    }{% something whose stringification has a leading space
      \UD@secondoftwo
    }%
  }{%
    \expandafter\expandafter\expandafter\UD@CheckWhetherNull
    \expandafter\expandafter\expandafter{%
    \expandafter\UD@firstoftwo
    \expandafter{\expandafter}\string#1}{%
      \expandafter\expandafter\expandafter\UD@CheckWhetherNull
      \expandafter\expandafter\expandafter{%
      \expandafter\UD@firstoftwo
      \expandafter{\expandafter}\detokenize{#1}}{%
        % no hash
        \UD@secondoftwo
      }{% hash
        \UD@firstoftwo
      }%
    }{% no hash
      \UD@secondoftwo
    }%
  }%
  {% hash
    \expandafter\UD@PassFirstToSecond
    \expandafter{%
      \romannumeral\expandafter
      \UD@Exchange\expandafter{%
        \romannumeral\UD@replicateloop{#1}{}#4\relax
      }{\UD@stopromannumeral#3}%
    }{%
      \expandafter\UD@ReplicateEveryHashLoop
      \expandafter{\UD@firstoftwo{}#2}%
    }%
  }{% no hash
    \expandafter\UD@ReplicateEveryHashLoop
    \expandafter{\UD@firstoftwo{}#2}{#3#1}%
  }{#4}%
}%
%%=============================================================================
%%//////////////  End of code for \ReplicateEveryHash  ////////////////////////
\makeatother

\documentclass{article} \usepackage[dvipsnames]{xcolor}

\makeatletter \NewDocumentCommand\DeclareFoo{O{\emph{##1}}}{% % \DeclareFoo's #1 denotes the tokens that form % \Foo's default-definition-text of the macro % @style. % At the time of carrying out \DeclareFoo the % definition-text of @style is a second-level- % definition where hashes of macro-parameters need % to be doubled.
% Therefore hashes in \DeclareFoo's #1, which denotes % a possible definition-text of @style, need to be % doubled, otherwise the user has to do so when % specifying a non-default value for \DeclareFoo's % optional argument/a non-default definition-text for % the second-level-definition of @style. \ReplicateEveryHash{2}{\DeclareDocumentCommand\Foo{O{#1} m}}{{% \def@style####1{##1}% @style{##2}% }}% % The following might be better for keeping the % definition of @style local because @style isn't % (re)defined any more when its replacement-text is % carried out: % \ReplicateEveryHash{2}{\DeclareDocumentCommand\Foo{O{#1} m}}{{% % \begingroup % \def@style####1{\endgroup##1}% % @style{##2}% % }}% } \makeatother

\begin{document}

\DeclareFoo Hello, \Foo{world}!

Hello, \Foo[\textbf{#1}]{world}!

\DeclareFoo[\color{blue}\textbf{#1}]% This is \Foo{nice}!

\DeclareFoo[\color{red}\textbf{#1}]% Is this \Foo{nice, too}?

Is this \Foo[\color[named]{ForestGreen}\textsc{#1}]{nice, too}?

Is this \Foo{nice, too}?

\end{document}

enter image description here

Ulrich Diez
  • 28,770