将具有可变数量无界参数的控制序列转换为标记列表

将具有可变数量无界参数的控制序列转换为标记列表

在 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}

输出

要实现这个目标,有两个主要的“技巧”。

  1. 为了使它正常工作,我们需要将宏定义为一个标记列表(带有适当的 catcode),而不仅仅是 Lua 字符串。因此,要获取宏的定义,我们首先创建一个空\mark节点。接下来,我们使用一些技巧将其内容设置为表示宏定义的整数。然后,当请求节点的值时\mark,我们实际上会获取宏的内容,以 Lua 表的形式表示。

  2. 然后,为了用\toksLua 表中的标记设置寄存器,我们使用相同的\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,如下所示:

  1. 使用不太可能出现在替换文本中的参数控制序列(如\witiko_parameter_1\witiko_parameter_2、等)来扩展控制序列。\witiko_parameter_3
  2. #将扩展结果中的 s加倍。
  3. 将扩展结果中的、、等替换为\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

#我们可以使用双倍参数字符( )、分隔参数以及任何引擎来实现这一点,但有两个限制:

  1. 目标宏不能用作"FF其宏参数字符。

  2. 任何分隔符必须有“正常”的 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}

输出

这里的技巧是在扩展目标宏时使用不同的宏参数字符,以便我们可以区分“内部”嵌套参数和最外层的“真实”参数。

相关内容