本着我之前的问题定义 \xthinspace:仅当后面没有特定字符时才使用细空格和省略号和正确的空间因子,我正在尝试定义一个通用\xspace
的宏系统。
我倾向于的语法如下:
\makexfollow{xfollow}{(always)}{(followed)}{(not followed)}
\addtoxfollowexceptions{x}
\newcommand*{\foo}{foo\xfollow}
\foo \foo x
屈服
foo(总是)(未关注)foo(总是)(关注)x
通过编辑和包装egreg 的代码,我已经完成了大部分工作;请参阅下面的代码。我甚至弄清楚了如何使用\cs:w … \cs_end:
和:c
说明符来参数化大多数内部 csname。
问题在于命令\addtoxfollowexceptions
& \removefromxfollowexceptions
;我无法弄清楚如何声明它们以xfollow
用通用内容替换名称的一部分。
我怎样才能实现这个目标?
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\makexfollow}{m m m m}
{
% Token list for “exceptions”
\tl_new:c { g_#1_exceptions_tl }
% Add to list
% Should be `\cs:w addto#1exceptions \cs_end:` or something similar
\NewDocumentCommand{\addtoxfollowexceptions}{m}
{
\tl_gput_right:cn { g_#1_exceptions_tl } { ##1 }
}
% Remove from list
% Should be `\cs:w removefrom#1exceptions \cs_end:` or something similar
\NewDocumentCommand{\removefromxfollowexceptions}{m}
{
\tl_gremove_all:cn { g_#1_exceptions_tl } { ##1 }
}
\cs_new_protected:cpn { #1 }
{
% Unconditional part
#2
% Is next token one of the “exceptions”?
\bool_set_false:c { l_#1_followed_bool }
% Peek ahead, ignoring spaces, then hand-off to `\xfollow_check:`
% with `\l_peek_token` set.
%
% This is a clever hack: We’re actually checking whether the next
% non-space token is a space token; if not, call `\xfollow_check:`.
% Of course, by definition this will always test false, achieving
% the result described above.
\peek_catcode_ignore_spaces:NF \c_space_token
{ \cs:w #1_check: \cs_end: }
}
% Compare `\l_peek_token` against `\g_xfollow_exceptions_tl`
\cs_new_protected:cpn { #1_check: }
{
% Loop over exceptions list
\tl_map_inline:cn { g_#1_exceptions_tl }
{
\token_if_eq_charcode:NNT ####1 \l_peek_token
{
% Tokens match; set flag and break from loop
\bool_set_true:c { l_#1_followed_bool }
\prg_map_break:
}
}
% Conditional part
\bool_if:cTF { l_#1_followed_bool }
{#3}
{#4}
}
}
\ExplSyntaxOff
\makexfollow{xfollow}{(always)}{(followed)}{(not followed)}
\addtoxfollowexceptions{x}
\newcommand*{\foo}{foo\xfollow}
\begin{document}
\foo \foo x
\end{document}
答案1
目前,xparse
仅包含记录的函数以控制序列 ( \foo
) 生成命令,而不是通过控制序列名称 ( foo
)。这是故意的,因为目的是保持语法相对清晰:LaTeX-L 列表中对此进行了一些讨论。话虽如此,我们还没有详细探讨以您尝试的方式创建“用户定义”名称的必要性。因此,目前您需要使用
\exp_args:Nc \NewDocumentCommand { addto #1 exceptions }
来实现你想要的。按名称生成是一个非常专业的事情(在整个文档命令集中),所以我们可能仍然只坚持使用这种方法。
但是,我认为对于你来说,这实际上是错误的方法。从你想要实现的目标的描述来看,你无法明确地生成文档命令。在我看来,你想要的是属于序言而不是文档的命令。同样,这是一个目前尚未完全决定的领域,但到目前为止,这些design level
函数通常应该采用强制参数,因此可以使用\cs_new:Npn
或类似方法直接生成。因此,我倾向于使用大小写混合的名称,并\cs_new_protected:cpn
在这里使用
\cs_new_protected:cpn { AddTo \char_uppercase:N #1 Exceptions } ...
(我用来\char_uppercase:N
改变首字母的大小写#1
)。
正如我所说,这两个领域都需要进一步讨论。最好在 LaTeX-L 列表中进行讨论。
附注:我\use:c { ... }
更倾向于\cs:w ... \cs_end:
。一般来说,我们建议在可用的情况下使用“更高级别”的接口,并且任何 都:w
通常只在必要时使用。
答案2
我强烈建议不要addto #1 exceptions
按照你描述的方式或AddTo #1 Exceptions
Joseph 提倡的方式进行定义。最好将其#1
作为参数传递给单个\AddToPeekExceptions
命令。也就是说,我更喜欢语法
\documentclass{article}
\NewPeekCommand {\xfollow} {(always)} {(followed)} {(not followed)}
\AddToPeekExceptions {\xfollow} {x}
\newcommand*{\foo}{foo\xfollow}
\begin{document}
\foo \foo x
\end{document}
其中\NewPeekCommand
仅定义一个控制序列,用于文档中(或定义其他命令)。
我尝试尽可能清晰地写出下面的实现。
\NewPeekCommand
、\AddToPeekExceptions
和的第一个参数\RemoveFromPeekExceptions
以控制序列而不是字符的形式给出,因为它将在文档中以这种方式使用。这可以防止出现诸如\NewPeekCommand{123}
尝试使用 之类的意外情况\123
。因此,我对输入很挑剔:检查这些用户函数的第一个参数是否确实是一个单一的控制序列,否则就会引发错误。
对于三个用户级命令,我提供了一个代码级命令,它不测试其参数,供想要与此包交互的包编写者使用。这些命令的名称以 开头,
\NPC_
并已记录在案。该
\NPC_new_peek_command:Nnnn
命令仅定义异常标记列表和命令本身。它不像您那样定义一堆帮助程序;相反,命令名称(以及(always)
、(followed)
和(not followed)
标记)作为参数提供给单个辅助程序,\__NPC_check:NnTF
负责查看下一个标记。我保留了你使用 的诀窍
\peek_catcode_ignore_spaces:NTF \c_space_token
。\__NPC_check:NnTF
和辅助对象\__NPC_check_ii:NnTF
以及内部变量\l__NPC_bool
的名称包含__NPC
,表明它们不能在该包之外使用。您可以随意使用此代码进行任何操作(尤其是更改命令名称)。用它制作软件包应该不会太难。
% \begin{documentation} % % This package provides \cs{NewPeekCommand}, \cs{AddToPeekExceptions}, % and \cs{RemoveFromPeekExceptions}. % % ^^A Todo: add example of use. % % \section{Design-level commands} % % \begin{function}{\NewPeekCommand} % \begin{syntax} % \cs{NewPeekCommand} \marg{function} \marg{always} \marg{followed} \marg{not followed} % \end{syntax} % Defines the \meta{function} as a new 'peek' command, which looks at % the following token. If this token is part of the exception list % (see \cs{AddToPeekExceptions}), use the \meta{always} and % \meta{followed} tokens; otherwise use the \meta{always} and % \meta{not followed} tokens. % \end{function} % % \begin{function}{\AddToPeekExceptions} % \begin{syntax} % \cs{AddToPeekExceptions} \marg{function} \marg{exceptions} % \end{syntax} % Adds every token in the \meta{exceptions} to the exception list for % the \meta{function}. % \end{function} % % \begin{function}{\RemoveFromPeekExceptions} % \begin{syntax} % \cs{RemoveFromPeekExceptions} \marg{function} \marg{exceptions} % \end{syntax} % Removes every token in the \meta{exceptions} from the exception list % for the \meta{function}. % \end{function} % % \section{Low-level analogs} % % \begin{function}{\NPC_new_peek_command:Nnnn} % This command is used to implement \cs{NewPeekCommand}, without % checking that the arguments take the correct form. % \end{function} % % \begin{function}{\NPC_add_to_peek_exceptions:Nn} % This command is used to implement \cs{AddToPeekExceptions}, without % checking that the arguments take the correct form. % \end{function} % % \begin{function}{\NPC_remove_from_peek_exceptions:Nn} % This command is used to implement \cs{RemoveFromPeekExceptions}, % without checking that the arguments take the correct form. % \end{function} % % \end{documentation} % % \begin{implementation} % % First load \pkg{xparse} and turn on the \texttt{expl} syntax. There % should probably be a \cs{ProvideExplPackage} in there. See % \emph{e.g.}, \pkg{siunitx} for good practice. % \begin{macrocode} \RequirePackage{xparse} \ExplSyntaxOn % \end{macrocode} % % \section{User commands} % % \begin{macro}{\NewPeekCommand} % We check that |#1| is a single control sequence. First check that % |#1| is a token list with exactly one token. Then check that this % token is a control sequence. That test will fail if |#1| is a % single space (but who cares). If the input is correct (T branch), % call \cs{NPC_new_peek_command:Nnnn}, otherwise raise an error and do % nothing. % \begin{macrocode} \NewDocumentCommand { \NewPeekCommand } {m m m m} { \bool_if:nTF { \tl_if_single_token_p:n {#1} && \token_if_cs_p:N {#1} } { \NPC_new_peek_command:Nnnn #1 {#2} {#3} {#4} } { \msg_error:nnnn { NPC } { cs-expected } { \NewPeekCommand } {#1} } } % \end{macrocode} % \end{macro} % % \begin{macro}{\AddToPeekExceptions} % We check that |#1| is a single control sequence. If it isn't, raise % an error. Otherwise, there is still a need to check that the % exception list exists, in other words that the command |#1| was % declared with \cs{NewPeekCommand}. If the input was correct, call % \cs{NPC_add_to_peek_exceptions:Nn}. % \begin{macrocode} \NewDocumentCommand { \AddToPeekExceptions } { m m } { \bool_if:nTF { \tl_if_single_token_p:n {#1} && \token_if_cs_p:N {#1} } { \tl_if_exist:cF { g_ \cs_to_str:N #1 _exceptions_tl } { \msg_error:nnnn { NPC } { undeclared-peek-command } { \AddToPeekExceptions } {#1} \tl_new:c { g_ \cs_to_str:N #1 _exceptions_tl } } \NPC_add_to_peek_exceptions:Nn #1 {#2} } { \msg_error:nnnn { NPC } { cs-expected } { \AddToPeekExceptions } {#1} } } % \end{macrocode} % \end{macro} % % \begin{macro}{\RemoveFromPeekExceptions} % The error checking is the same as for \cs{AddToPeekExceptions}. % Then call \cs{NPC_remove_from_peek_exceptions:Nn}. % \begin{macrocode} \NewDocumentCommand { \RemoveFromPeekExceptions } { m m } { \bool_if:nTF { \tl_if_single_token_p:n {#1} && \token_if_cs_p:N {#1} } { \tl_if_exist:cF { g_ \cs_to_str:N #1 _exceptions_tl } { \msg_error:nnnn { NPC } { undeclared-peek-command } { \RemoveFromPeekExceptions } {#1} \tl_new:c { g_ \cs_to_str:N #1 _exceptions_tl } } \NPC_remove_from_peek_exceptions:Nn #1 {#2} } { \msg_error:nnnn { NPC } { cs-expected } { \RemoveFromPeekExceptions } {#1} } } % \end{macrocode} % \end{macro} % % \section{Messages} % % If the first argument of one of the user functions is not a single % control sequence, complain. % \begin{macrocode} \msg_new:nnnn { NPC } { cs-expected } { The~first~argument~of~#1 must~be~a~control~sequence. } { The~command~#1 received~'#2'~as~its~first~argument,~instead~ of~a~single~control~sequence~such~as~\token_to_str:N \xspace. } % \end{macrocode} % % If the first argument of \cs{AddToPeekExceptions} or % \cs{RemoveFromPeekExceptions} is an unknown control sequence, % complain. % \begin{macrocode} \msg_new:nnn { NPC } { undeclared-peek-command } { The~first~argument~of~#1 must~be~declared~with~ \token_to_str:N \NewPeekCommand. } % \end{macrocode} % % \section{Low-level} % % \begin{macro}{\NPC_add_to_peek_exceptions:Nn} % Adds all the tokens in |#2| to the list of exceptions for the peek % command |#1|. % \begin{macrocode} \cs_new_protected:Npn \NPC_add_to_peek_exceptions:Nn #1#2 { \tl_gput_right:cn { g_ \cs_to_str:N #1 _exceptions_tl } { #2 } } % \end{macrocode} % \end{macro} % % \begin{macro}{\NPC_remove_from_peek_exceptions:Nn} % Removes each the tokens in |#2| from the list of exceptions for the % peek command |#1|. Allowing more than one token in |#2| requires % putting a loop there. % \begin{macrocode} \cs_new_protected:Npn \NPC_remove_from_peek_exceptions:Nn #1#2 { \tl_map_inline:nn {#2} { \tl_gremove_all:cn { g_ \cs_to_str:N #1 _exceptions_tl } { ##1 } } } % \end{macrocode} % \end{macro} % % \begin{macro}{\NPC_new_peek_command:Nnnn} % Declare a token list to hold the exceptions for the command |#1|, % then declare |#1| which simply calls the internal % \cs{__NPC_check:NnTF} with the appropriate arguments. % \begin{macrocode} \cs_new_protected:Npn \NPC_new_peek_command:Nnnn #1#2#3#4 { \tl_new:c { g_ \cs_to_str:N #1 _exceptions_tl } \cs_new_protected_nopar:Npn #1 { \__NPC_check:NnTF #1 {#2} {#3} {#4} } } % \end{macrocode} % \end{macro} % % \begin{macro}[aux]{\__NPC_check:NnTF} % Peek ahead, ignoring spaces. This is a clever hack: we’re actually % checking whether the next non-space token is a space token; if not, % call \cs{__NPC_check_ii:NnTF}. Of course, by definition this will % always test false. % \begin{macrocode} \cs_new_protected:Npn \__NPC_check:NnTF #1#2#3#4 { \peek_catcode_ignore_spaces:NF \c_space_token { \__NPC_check_ii:NnTF #1 {#2} {#3} {#4} } } % \end{macrocode} % \end{macro} % % \begin{macro}[aux]{\__NPC_check_ii:NnTF} % Now \cs{l_peek_token} is set to the following token, and we wish to % compare it with the exceptions for |#1|. Loop over the exception % list, comparing the character code of each exception with % \cs{l_peek_token}, and if there is a match, turn on the boolean and % break the map. After the loop, leave either the \meta{always} and % \meta{followed} tokens, or the \meta{always} and \meta{not followed} % tokens in the input stream. % \begin{macrocode} \cs_new_protected:Npn \__NPC_check_ii:NnTF #1#2#3#4 { \bool_set_false:N \l__NPC_bool \tl_map_inline:cn { g_\cs_to_str:N #1 _exceptions_tl } { \token_if_eq_charcode:NNT ##1 \l_peek_token { \bool_set_true:N \l__NPC_bool \tl_map_break: } } \bool_if:NTF \l__NPC_bool { #2 #3 } { #2 #4 } } % \end{macrocode} % \end{macro} % % \begin{variable}{\l__NPC_bool} % This boolean is used to keep track of whether the \cs{l_peek_token} % appears in the exception list or not. % \begin{macrocode} \bool_new:N \l__NPC_bool % \end{macrocode} % \end{variable} % % \end{implementation} % \ExplSyntaxOff \documentclass{article} \NewPeekCommand {\xfollow} {(always)} {(followed)} {(not followed)} \AddToPeekExceptions {\xfollow} {x} \newcommand*{\foo}{foo\xfollow} \begin{document} \foo \foo x \end{document}
答案3
只需使用\cs_new_protected:cpn
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\makexfollow}{m m m m}
{
% Token list for exceptions
\tl_new:c { g_#1_exceptions_tl }
% Add to list
\cs_new_protected:cpn { addto #1 exceptions } ##1
{
\tl_gput_right:cn { g_#1_exceptions_tl } { ##1 }
}
% Remove from list
\cs_new_protected:cpn { removefrom #1 exceptions} ##1
{
\tl_gremove_all:cn { g_#1_exceptions_tl } { ##1 }
}
\cs_new_protected:cpn { #1 }
{
% Unconditional part
#2
% Is next token one of the exceptions
\bool_set_false:c { l_#1_followed_bool }
% Peek ahead, ignoring spaces, then hand-off to `\xfollow_check:`
% with `\l_peek_token` set.
%
% This is a clever hack: We're actually checking whether the next
% non-space token is a space token; if not, call `\xfollow_check:`.
% Of course, by definition this will always test false, achieving
% the result described above.
\peek_catcode_ignore_spaces:NF \c_space_token
{ \cs:w #1_check: \cs_end: }
}
% Compare `\l_peek_token` against `\g_xfollow_exceptions_tl`
\cs_new_protected:cpn { #1_check: }
{
% Loop over exceptions list
\tl_map_inline:cn { g_#1_exceptions_tl }
{
\token_if_eq_charcode:NNT ####1 \l_peek_token
{
% Tokens match; set flag and break from loop
\bool_set_true:c { l_#1_followed_bool }
\prg_map_break:
}
}
% Conditional part
\bool_if:cTF { l_#1_followed_bool }
{#3}
{#4}
}
}
\ExplSyntaxOff
\makexfollow{xfollow}{(always)}{(followed)}{(not followed)}
\addtoxfollowexceptions{x}
\show\xfollow
\show\removefromxfollowexceptions
\newcommand*{\foo}{foo\xfollow}
\begin{document}
\foo \foo x
\end{document}