在 expl3 中,我可以将标记列表转换\l_greet_tl
为带有两个参数的控制序列\greet
,如下所示:
\tl_new:N
\l_greet_tl
\tl_set:Nn
\l_greet_tl
{ Hello,~#1!~How~is~#2~doing? }
\cs_generate_variant:Nn
\cs_generate_from_arg_count:NNnn
{ NNnV }
\cs_generate_from_arg_count:NNnV
\greet
\cs_set:Npn
{ 2 }
\l_greet_tl
然后,我可以写入\greet { world } { favorite~species~(humans,~presumably) }
并接收以下扩展:
你好,世界!你最喜欢的物种(大概是人类)过得怎么样?
但是,我现在想做相反的事情,即将控制序列的替换文本转换\greet
回标记列表,以便我可以在其转换回控制序列之前将其附加到其中。我可以手动执行以下两个参数:
\tl_set:No
\l_greet_tl
{ \greet { #1 } { #2 } }
\tl_put_right:Nn
\l_greet_tl
{ ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
\greet
\cs_set:Npn
{ 3 }
\l_greet_tl
现在,我可以编写\greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }
并接收以下扩展:
你好,世界!你最喜欢的物种(大概是人类)过得怎么样?祝你进化出伟大的飞跃!
但是,我希望能够以一种适用于任意数量的参数(而不仅仅是两个)的方式来做到这一点,例如:
\tl_set_from_cs:NNn
\l_greet_tl % token list (output)
\greet % control sequence (input)
{ 2 } % parameter count
\tl_put_right:Nn
\l_greet_tl
{ ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
\greet
\cs_set:Npn
{ 3 }
\l_greet_tl
与现有功能不同\cs_replacement_spec:N
,新功能\tl_set_from_cs:NNn
将在替换文本中保留类别代码。
编辑:进一步澄清(2024-04-26)
与 Max Chernoff 的回答不同,该答案应该适用于 expl3 支持的所有 TeX 引擎,而不仅仅是 LuaTeX。
与我和 @egreg 的答案不同,#
替换文本中的双 s 应该保留。例如,假设对控制序列进行以下重新定义\greet
:
\cs_new_protected:Npn
\greet
#1#2
{
Hello,~#1!
\group_begin:
\cs_set:Npn
\greet
##1
{ ~How~is~##1~doing? }
\greet { #2 }
\group_end:
}
现在,我可以写入\greet { world } { favorite~species~(humans,~presumably) }
并且仍然收到以下扩展:
你好,世界!你最喜欢的物种(大概是人类)过得怎么样?
但是,将控制序列转换为标记列表(如下所示)不再按预期运行:
\tl_set:No
\l_greet_tl
{ \greet { #1 } { #2 } }
\tl_put_right:Nn
\l_greet_tl
{ ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
\greet
\cs_set:Npn
{ 3 }
\l_greet_tl
这是因为,\greet
现在包含以下替换文本:
Hello,~#1!
\group_begin:
\cs_set:Npn
\greet
#1
{ ~How~is~#1~doing? }
\greet { #2 }
\group_end:
~Have~a~great~#3!
如你所见,#1
和之间的区别##1
已经消失。如果我现在写\greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }
,TeX 将产生以下错误:
\greet 的使用与其定义不符。
保留双重#
s 是挑战的一部分。
答案1
对于具有无界参数的宏:
\documentclass{article}
\newcommand{\test}[2]{-#1-#2-}
\ExplSyntaxOn
\seq_new:N \l__witiko_getreplacement_seq
\tl_new:N \l__witiko_getreplacement_tmp_tl
\seq_set_from_clist:Nn \l__witiko_getreplacement_seq
{
{},{#1},{},{#1}{#2},{},{#1}{#2}{#3},{},{#1}{#2}{#3}{#4},
{},{#1}{#2}{#3}{#4}{#5},{},{#1}{#2}{#3}{#4}{#5}{#6},
{},{#1}{#2}{#3}{#4}{#5}{#6}{#7},
{}.{#1}{#2}{#3}{#4}{#5}{#6}{#7}{#8},
{},{#1}{#2}{#3}{#4}{#5}{#6}{#7}{#8}{#9}
}
\cs_new_protected:Nn \witiko_getreplacement:NN
{
\tl_set:Ne \l__witiko_getreplacement_tmp_tl
{
\seq_item:Nn \l__witiko_getreplacement_seq { \tl_count:e { \cs_parameter_spec:N #1 } }
}
\__witiko_getreplacement:NNV #2 #1 \l__witiko_getreplacement_tmp_tl
}
\cs_new_protected:Nn \__witiko_getreplacement:NNn
{
\tl_set:No #1 { #2 #3 }
}
\cs_generate_variant:Nn \__witiko_getreplacement:NNn { NNV }
\witiko_getreplacement:NN \test \l_tmpa_tl
\tl_show:N \l_tmpa_tl
\tl_set:Nn \l_tmpb_tl { -#1-#2- }
\tl_if_eq:NNTF \l_tmpa_tl \l_tmpb_tl { \typeout{EQUAL} } { \typeout{DIFFERENT} }
\stop
控制台会打印
> \l_tmpa_tl=-##1-##2-.
<recently read> }
l.36 \tl_show:N \l_tmpa_tl
?
EQUAL
\tl_set:No
我将可能的参数文本存储在一个序列中,然后在执行时从中提取合适的项目以传递它。
答案2
如果您能够使用 LuaTeX,也可以对带有分隔参数的宏执行此操作。不过,获取宏定义的 catcode 比您想象的要困难得多,因此我们需要使用许多卑鄙的技巧来实现这一点。
\documentclass{article}
\usepackage{luacode}
\begin{luacode*}
-----------------
--- Constants ---
-----------------
local assign_toks_cmdcode = token.command_id("assign_toks")
local car_ret_cmdcode = token.command_id("car_ret")
local cs_token_flag = 0x1FFFFFFF
local first_digit_chrcode = string.byte("0")
local first_letter_chrcode = string.byte("a")
local hash_chrcode = string.byte("#")
local last_letter_chrcode = string.byte("z")
local let_token = token.create("let")
local mac_param_cmdcode = token.command_id("mac_param")
local other_char_cmdcode = token.command_id("other_char")
local par_end_cmdcode = token.command_id("par_end")
local random_csname_length = 8
local slice = table.sub
local stop_cmdcode = token.command_id("stop")
local tokspre_token = token.create("tokspre")
----------------------------
--- Function Definitions ---
----------------------------
-- Gets a table representing tokens of a macro definition
function get_toktab_from_macro(value_csname)
local value_token = token.create(value_csname)
-- By default, LuaTeX only gives us the contents of a macro as a string.
-- However, it will give us the contents of a mark node as a table of
-- tokens, so we create a fake mark node that points to the macro's
-- definition.
local tmp_nd = node.direct.new("mark")
node.direct.setprev(tmp_nd + 1, value_token.mode)
return node.direct.getfield(tmp_nd, "mark")
end
-- Splits a macro definition token table into its parameters and its
-- replacement text.
local function split_macro_toktab(meaning_toktab)
local stop_index
local args_count = 0
for i, t in ipairs(meaning_toktab) do
-- Separator between parameters and replacement text (represented by
-- "->" inside of \meaning).
if t[1] == stop_cmdcode then
stop_index = i
-- Convert a macro parameter token in the body back into a "#"
-- token.
elseif t[1] == mac_param_cmdcode and t[3] == 0 then
table.insert(
meaning_toktab,
i + 1,
{ mac_param_cmdcode, hash_chrcode, 1 }
)
elseif t[1] == mac_param_cmdcode and t[3] == 1 then
t[3] = 0
-- Convert a macro parameter token in the body back into a <digit>
-- token.
elseif t[1] == car_ret_cmdcode then
table.insert(
meaning_toktab,
i + 1,
{ other_char_cmdcode, first_digit_chrcode + t[2], 0 }
)
t[2] = hash_chrcode
t[1] = mac_param_cmdcode
-- Convert a macro parameter token in the parameters back into a
-- pair of tokens {"#", <digit>}.
elseif t[1] == par_end_cmdcode then
args_count = args_count + 1
t[1] = mac_param_cmdcode
t[2] = hash_chrcode
table.insert(
meaning_toktab,
i + 1,
{ other_char_cmdcode, first_digit_chrcode + args_count, 0 }
)
end
end
-- Split the token table
return slice(meaning_toktab, 2, stop_index - 1),
slice(meaning_toktab, stop_index + 1, nil )
end
-- Generates a random control sequence name.
local function random_csname()
local random_table = {}
for i = 1, random_csname_length do
local random_letter = string.char(
math.random(first_letter_chrcode, last_letter_chrcode)
)
table.insert(random_table, random_letter)
end
return table.concat(random_table)
end
-- Converts a token table into a \toks token (without giving it a name).
local function toktab_to_token(value_toktab)
local tmp_csname = random_csname()
-- First, we create a mark node to store the raw token in.
local tmp_nd = node.direct.new("mark")
node.direct.setfield(tmp_nd, "mark", value_toktab)
-- TeX expects two levels of indirection for a \toks token, so we first
-- point a \chardef token to the token created by the mark node.
token.set_char(tmp_csname, node.direct.getprev(tmp_nd + 1), "global")
-- Then, we create a \toks token that points to the \chardef token.
return token.create(
token.create(tmp_csname).tok - cs_token_flag,
assign_toks_cmdcode
)
end
-- \let's a token to a control sequence name.
local function let_csname_token(name_csname, value_token)
-- We need to create a token with the name first, otherwise we get an
-- "undefined_cs" token which is useless.
token.set_char(name_csname, 0)
local name_token = token.create(name_csname)
-- There's no way to do this directly from Lua, so we start a new TeX
-- run and use \let to assign the token.
tex.runtoks(function()
token.put_next(let_token, name_token, value_token)
end)
return token.create(name_csname)
end
-- Copies a fake \toks token into a real \toks register.
--
-- The token created by "let_csname_token" is a semi-valid \toks register:
-- it behaves like a \toks register with \the and similar, but it gives a
-- (mostly harmless) error with \show and \meaning. To fix this, we copy
-- the token's contents into a real \toks register.
local function token_to_toksreg(toksreg_csname, value_token)
-- Clear the register
tex.toks[toksreg_csname] = ""
local toksreg_token = token.create(toksreg_csname)
local value_toksreg = let_csname_token(random_csname(), value_token)
-- Prepend the fake \toks register onto the empty real one, giving
-- us a real \toks register with the correct value.
tex.runtoks(function()
token.put_next(tokspre_token, toksreg_token, value_toksreg)
end)
end
-- Registers a TeX command that calls the given Lua function.
local function register_tex_cmd(target_csname, func, args)
local scanners = {}
for _, arg in ipairs(args) do
scanners[#scanners+1] = token["scan_" .. arg]
end
local function scanning_func()
local values = {}
for _, scanner in ipairs(scanners) do
values[#values+1] = scanner()
end
func(table.unpack(values))
end
local index = luatexbase.new_luafunction(target_csname)
lua.get_functions_table()[index] = scanning_func
token.set_lua(target_csname, index, "global")
end
--------------------
--- TeX Commands ---
--------------------
register_tex_cmd("__example_macro_to_toks:N", function(value_csname)
-- Get a table representing the macro's definition tokens
local meaning_toktab = get_toktab_from_macro(value_csname)
-- Split the macro into its parameters and replacement text
local params_toktab, replacement_toktab = split_macro_toktab(meaning_toktab)
-- Save the parameters in a \toks register
local params_token = toktab_to_token(params_toktab)
token_to_toksreg("l__example_parameters_toks", params_token)
-- Save the replacement text in a \toks register
local replacement_token = toktab_to_token(replacement_toktab)
token_to_toksreg("l__example_replacement_toks", replacement_token)
end, {"csname"})
\end{luacode*}
\ExplSyntaxOn
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Variable Declarations %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\exp_args_generate:n { NNNV }
\newtoks \l__example_parameters_toks
\newtoks \l__example_replacement_toks
\tl_new:N \l_example_parameters_tl
\tl_new:N \l_example_replacement_tl
\scan_new:N \s_example
%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Macro Definitions %%%
%%%%%%%%%%%%%%%%%%%%%%%%%
% Sets "\l_example_parameters_tl" with the parameters of the provided macro
% and "\l_example_replacement_tl" with the replacement text of the same
% macro.
\cs_new:Nn \example_macro_to_tl:N {
\__example_macro_to_toks:N #1
\tl_set:NV \l_example_parameters_tl \l__example_parameters_toks
\tl_set:NV \l_example_replacement_tl \l__example_replacement_toks
}
% Defines the provided macro with parameters "\l_example_parameters_tl" and
% replacement text "\l_example_replacement_tl".
\cs_new:Nn \example_tl_to_macro:N {
\exp_args:NNNNV \exp_last_unbraced:NNV \cs_set:Npn #1 \l_example_parameters_tl \l_example_replacement_tl
}
\ExplSyntaxOff
%%%%%%%%%%%%%%%%%%%%%
%%% Demonstration %%%
%%%%%%%%%%%%%%%%%%%%%
\pagestyle{empty}
\begin{document}
\ExplSyntaxOn
% Define a test macro with weird parameters and body
\cs_new:Npn \example_test:w #1 #2 \s_example {
A B C~
<#1>
{ #2 }
\par $a^2 + b^\bgroup \pi^2\egroup$
}
% Get the parameters and replacement text of the test macro
\example_macro_to_tl:N \example_test:w
% Inspect the extracted token lists
\tl_analysis_show:N \l_example_parameters_tl
\tl_analysis_show:N \l_example_replacement_tl
% Modify the extracted token lists
\tl_put_right:Nn \l_example_parameters_tl { #3 }
\tl_put_left:Nn \l_example_replacement_tl { \textbf{#3} }
% Assemble a new macro from the modified token lists
\example_tl_to_macro:N \test
% Test the new macro
\test X {\itshape y}\s_example Z
% Compare the meanings of the original and new macros
\par \meaning\example_test:w
\par \meaning\test
\ExplSyntaxOff
\end{document}
要实现这个目标,有两个主要的“技巧”。
为了使它正常工作,我们需要将宏定义为一个标记列表(带有适当的 catcode),而不仅仅是 Lua 字符串。因此,要获取宏的定义,我们首先创建一个空
\mark
节点。接下来,我们使用一些技巧将其内容设置为表示宏定义的整数。然后,当请求节点的值时\mark
,我们实际上会获取宏的内容,以 Lua 表的形式表示。然后,为了用
\toks
Lua 表中的标记设置寄存器,我们使用相同的\mark
技巧,但方向相反。然后,我们创建一个指向\chardef
指向的相同标记列表的标记\mark
。接下来,我们小心地溢出寄存器的索引\toks
,使其最终指向该\chardef
标记。由于我们仔细选择了偏移量,这个“假”\toks
寄存器现在表现得好像它具有与 相同的值\mark
。
我们还需要一些代码将内部 TeX 标记代码转换回前端期望的形式,以及一些代码将伪\toks
寄存器移动到真实寄存器,但这相对简单。
答案3
这是一个函数\tl_set_from_cs:NNn
,它接受一个标记列表、一个控制序列和多个参数,并将控制序列的替换文本分配给标记列表:
\cs_new_protected:Nn
\tl_set_from_cs:NNn
{
\tl_set:Nn
\l_tmpa_tl
{ #2 }
\int_step_inline:nn
{ #3 }
{
\tl_put_right:Nn
\l_tmpa_tl
{ { #### ##1 } }
}
\exp_args:NNV
\tl_set:No
#1
\l_tmpa_tl
}
与 egreg 的答案不同,不需要额外的数据结构。这需要额外的扩展步骤,因此如果要频繁调用该函数,egreg 的方法可能更可取。
以下是该函数的使用演示\tl_set_from_cs:NNn
:
\cs_new:Npn
\greet
#1#2
{ Hello,~#1!~How~is~#2~doing? }
\tl_set_from_cs:NNn
\l_greet_tl
\greet
{ 2 }
\tl_put_right:Nn
\l_greet_tl
{ ~Have~a~great~#3! }
\cs_generate_variant:Nn
\cs_generate_from_arg_count:NNnn
{ NNnV }
\cs_generate_from_arg_count:NNnV
\greet
\cs_set:Npn
{ 3 }
\l_greet_tl
现在,我可以编写\greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }
并接收以下扩展:
你好,世界!你最喜欢的物种(大概是人类)过得怎么样?祝你进化出伟大的飞跃!
\tl_set_from_cs:NN
正如 egreg 的回答所证明的那样,我们还可以通过从参数规范中推断出参数的数量来简化函数签名:
\cs_new_protected:Nn
\tl_set_from_cs:NN
{
\tl_set:Ne
\l_tmpa_tl
{ \cs_parameter_spec:N #2 }
\int_set:Nn
\l_tmpa_int
{ \tl_count:N \l_tmpa_tl / 2 }
\tl_set_from_cs:NNV
#1
#2
\l_tmpa_int
}
\cs_generate_variant:Nn
\tl_set_from_cs:NNn
{ NNV }
以下是该函数的使用演示\tl_set_from_cs:NN
:
\cs_new:Npn
\greet
#1#2
{ Hello,~#1!~How~is~#2~doing? }
\tl_set_from_cs:NN
\l_greet_tl
\greet
\tl_put_right:Nn
\l_greet_tl
{ ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
\greet
\cs_set:Npn
{ 3 }
\l_greet_tl
现在,我可以\greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }
再次写入并接收以下扩展:
你好,世界!你最喜欢的物种(大概是人类)过得怎么样?祝你进化出伟大的飞跃!
如果要将该函数\tl_set_from_cs:NN
添加到 expl3 的 l3tl 模块,还应添加其他变体,例如\tl_gset_from_cs:NN
。
此外,应该清楚地记录,只有具有无界参数的控制序列才能通过这种方式转换为标记列表。
编辑:进一步澄清(2024-04-27)
我们可以#
在替换文本中保留重复的 s,如下所示:
- 使用不太可能出现在替换文本中的参数控制序列(如
\witiko_parameter_1
、\witiko_parameter_2
、等)来扩展控制序列。\witiko_parameter_3
#
将扩展结果中的 s加倍。- 将扩展结果中的、、等替换为
\witiko_parameter_1
、、等。\witiko_parameter_2
\witiko_parameter_3
#1
#2
#3
下面是具体的实现:
\cs_new_protected:Nn
\tl_set_from_cs:NNn
{
\tl_set:Nn
\l_tmpa_tl
{ #2 }
\int_step_inline:nn
{ #3 }
{
\exp_args:NNc
\tl_put_right:Nn
\l_tmpa_tl
{ witiko_parameter_ ##1 }
}
\exp_args:NNV
\tl_set:No
\l_tmpb_tl
\l_tmpa_tl
\regex_replace_all:nnN
{ \cP. }
{ \0\0 }
\l_tmpb_tl
\int_step_inline:nn
{ #3 }
{
\regex_replace_all:nnN
{ \c { witiko_parameter_ ##1 } }
{ \cP\# ##1 }
\l_tmpb_tl
}
\tl_set:NV
#1
\l_tmpb_tl
}
下面是实现的演示:
\cs_new_protected:Npn
\greet
#1#2
{
Hello,~#1!
\group_begin:
\cs_set:Npn
\greet
##1
{ ~How~is~##1~doing? }
\greet { #2 }
\group_end:
}
\tl_set_from_cs:NN
\l_greet_tl
\greet
\tl_put_right:Nn
\l_greet_tl
{ ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
\greet
\cs_set:Npn
{ 3 }
\l_greet_tl
现在,我可以编写\greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }
并接收以下扩展:
你好,世界!你最喜欢的物种(大概是人类)过得怎么样?祝你进化出伟大的飞跃!
这正如预期的那样。
答案4
#
我们可以使用双倍参数字符( )、分隔参数以及任何引擎来实现这一点,但有两个限制:
目标宏不能用作
"FF
其宏参数字符。任何分隔符必须有“正常”的 catcode。
我希望 99% 以上的宏都满足这些要求,因此在实践中这应该不会太严格。
\documentclass{article}
\pagestyle{empty}
\parindent=0pt
\ExplSyntaxOn
%%%%%%%%%%%%%%%%%%%
%%% Definitions %%%
%%%%%%%%%%%%%%%%%%%
%% Copies the replacement text of a macro (#1) into a token list (#2).
\cs_new_protected:Nn \example_cs_to_tl:NN {
%% Get the parameters used by the macro
\tl_set:Ne #2 { \cs_parameter_spec:N #1 }
%% Convert the parameters into normal catcodes to handle any delimited
%% arguments.
\tl_set_rescan:NnV #2 {} #2
%% Use <"FF>_6 as the macro parameter character instead of the usual <#>_6.
%% We do this so that we can distinguish between any inner macro parameters
%% and the new ones that we're passing in here.
\regex_replace_all:nnN { \# ( \d ) } { { \cP\xFF \1 } } #2
%% Expand the macro to get at its replacement text.
\tl_set:Nf #2 {
\exp_last_unbraced:NNNo \exp_after:wN \exp_stop_f: #1 #2
}
%% Double all the original parameter characters, ignoring our new ones.
\regex_replace_all:nnN { \cP [^ \xFF ] } { \0 \0 } #2
}
%% I've used inline regexes here to make the code easier to follow, but you
%% should use `\regex_const:Nn` in the real code since it's considerably faster.
\cs_generate_variant:Nn \cs_generate_from_arg_count:NNnn { NNnV }
%%%%%%%%%%%%%%%%%%%%%
%%% Demonstration %%%
%%%%%%%%%%%%%%%%%%%%%
%% A test macro with two normal arguments and some inner doubled #'s.
\cs_new:Npn \original #1 #2 {
Hello,~#1!~
\group_begin:
\cs_set:Npn \original ##1 {
~How~is~##1~doing?
\cs_gset:Npn \inner ####1 {
#1~##1~####1.
}
}
\original { #2 }
\group_end:
}
%% Extract the replacement text and define a new macro with the same body.
\example_cs_to_tl:NN \original \l_tmpa_tl
\cs_generate_from_arg_count:NNnV \new \cs_set:Npn { 2 } \l_tmpa_tl
%% A test macro with some weird delimited arguments.
\cs_new:Npn \weirddelims #1, #2 #3 ABC \relax {
<#2>
\def \inner ##1 #3 {
<#1>
}
}
%% Extract the replacement text and define a new macro with the same body.
\example_cs_to_tl:NN \weirddelims \l_tmpa_tl
\cs_generate_from_arg_count:NNnV \newweirddelims \cs_set:Npn { 3 } \l_tmpa_tl
\ExplSyntaxOff
%% Show the comparison between the original and new macros.
\begin{document}
\texttt{\small \meaning\original}%
\par
\original{AAA}{BBB} \inner{CCC}
\bigskip
\texttt{\small \meaning\original}%
\par
\new{DDD}{EEE} \inner{FFF}
\bigskip
\texttt{\small \meaning\weirddelims}%
\par
\weirddelims{GGG},{HHH}{III}ABC\relax \inner{JJJ}III
\bigskip
\texttt{\small \meaning\newweirddelims}%
\par
\newweirddelims{KKK}{LLL}{MMM} \inner{NNN}MMM
\end{document}
这里的技巧是在扩展目标宏时使用不同的宏参数字符,以便我们可以区分“内部”嵌套参数和最外层的“真实”参数。