我正在使用xparse
包中的嵌套宏,我的最终目标是拥有一个自定义宏工厂。我在这里简化了我当前的问题。
我想给出一个模式作为参数,如下例所示:
\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% 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}!
\DeclareFoo
应该是一个黑盒子,用户不应该猜测嵌套层数(请不要标记这作为重复)。
有没有办法逃避#
内部#1
争论?我试图\StrSubstitute
从包中将inxstring
的数量加倍,但无法让它工作……#
#1
答案1
在这种特定情况下,有一些“解决方法”:
- 如果你可以容忍一些“全局命名空间污染”,你可以这样做
\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}
只要辅助宏的名称“足够独特”,就不会有问题。
- 作为替代解决方案,您可以通过宏定义将内容存储在宏/令牌寄存器中来“传递”内容:
\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}
有点\exp_args:NnV \use:n
丑陋,但基本上它def
以变量作为其“主体”来执行。
答案2
为了比较,我们需要在 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
结果与 Ulrich 的回答相同。
答案3
根据\DeclareFoo
定义,默认可选参数必须有一对额外的#
标记:O{\emph{####1}}
,才能使默认\emph
情况起作用。尽管如此,即使在这里,在指定单独的可选参数时仍需要使用符号##1
,例如\DeclareFoo[\color{red}\textbf{##1}]
,因为在随后的调用中##1
需要\DeclareFoo
才能像 一样运行。#1
\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}
答案4
广告“以编程方式将 # 的数量加倍”:
讨论
和
你可能会感兴趣
在那里,你可以找到使用以下方法解决问题的方法:l3regex或者利用诸如\write
双重\scantokens
哈希之类的事实。
为了好玩,我使用下面的代码做了自己的事情。
\detokenize
对于最新的具有 ε-TeX 扩展的TeX 引擎,我可以提供一个例程\ReplicateEveryHash
,在某些情况下可以作为一种解决方法。
例程的语法\ReplicateEveryHash
是
\ReplicateEveryHash{⟨number⟩}{⟨balanced text⟩}
该例程\ReplicateEveryHash
通过\romannumeral
-expansion 递归复制⟨数字⟩乘以包含在类别 6(参数)中的每个显式字符标记⟨平衡文本⟩。
检查哈希的要点是:\string#
提供类别 12(其他)的单个哈希字符标记,而\detokenize
发生哈希加倍,因此\detokenize{#}
提供类别 12(其他)的两个哈希字符标记。(类别 6 的显式空格字符的边缘情况需要单独处理。)
如果⟨平衡文本⟩-argument\ReplicateEveryHash
包含类别 1(开始组)和类别 2(结束组)的显式字符标记匹配对,这些对中的每一个都会触发另一级 -expansion \romannumeral
。因此括号内嵌套过多⟨平衡文本⟩-参数\ReplicateEveryHash
将对语义嵌套产生影响。
除此之外\ReplicateEveryHash
确实替换了类别 1(开始组)和类别 2(结束组)的显式字符标记的匹配对⟨平衡文本⟩-通过匹配第 1 类的左花括号和第 2 类的右花括号来参数。
我想在大多数情况下这不会是个问题,因为通常花括号是 1/2 类的唯一字符。
但必须提到这一点,因为这意味着它只\ReplicateEveryHash
适用于用相同类型的显式花括号标记替换显式开始分组字符标记和显式结束分组字符标记的情况。
注意事项/可能的陷阱:
如果你放入\ReplicateEveryHash{2}{...}
,\edef
由于\romannumeral
-expansion哈希加倍将与\edef\macro{\unexpanded\expandafter{\expanded{#1}}}
-方法相反)发生前扩展构成⟨平衡文本⟩的参数\ReplicateEveryHash
。因此\edef\macro{\ReplicateEveryHash{2}{\string#1}}
(或者\ReplicateEveryHash{2}{\edef\macro{\string#1}}
如果您愿意)将产生有关非法参数数字的错误消息,因为在哈希加倍步骤中,您将获得两个哈希,后面跟着数字1
。第一个哈希将被字符串化。第二个哈希,后面跟着数字,将不会被字符串化,因此将在of为空时1
被视为参数。#1
⟨parameter text⟩
\macro
\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}