В 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
будет сохранять коды категорий в заменяющем тексте.
EDIT: Дополнительные разъяснения (2024-04-26)
В отличие от ответа Макса Черноффа, ответ должен применяться ко всем движкам TeX, поддерживаемым expl3, а не только к 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. Однако получить catcodes макроопределения гораздо сложнее, чем вы могли бы ожидать, поэтому нам нужно использовать множество грязных трюков, чтобы это работало.
\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}
Чтобы это работало, есть два основных «трюка».
Чтобы это работало в общем случае, нам нужно определение макроса как списка токенов (с правильными catcodes), а не просто как строки 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 }
и получить следующее расширение:
Привет, мир! Как поживает твой любимый вид (предположительно, люди)? Желаю тебе большого эволюционного скачка!
Как показано в ответе egreg, мы также можем упростить сигнатуру функции, \tl_set_from_cs:NN
выведя количество параметров из спецификации параметров:
\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
нужно было добавить в модуль l3tl expl3, то следовало бы добавить и другие варианты, например \tl_gset_from_cs:NN
.
Кроме того, следует четко задокументировать, что таким образом в списки токенов можно преобразовать только управляющие последовательности с неразделенными параметрами.
EDIT: Дополнительные разъяснения (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
в качестве макропараметра символ.Любые разделителидолженимеют «нормальные» каткоды.
Я ожидаю, что >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}
Хитрость здесь заключается в использовании другого символа макропараметра при расширении целевого макроса, чтобы мы могли различать «внутренние» вложенные параметры и самые внешние «реальные» параметры.