正如标题所说:是否存在完全可靠、可扩展的方法来区分已等同于字符标记的控制序列和字符标记本身?(假设特定原语的含义没有改变:否则显然所有赌注都是无效的。)例如,考虑以下内容(使用纯 TeX 以将多余的元素保持在最低限度):
\font\ecrm=ecrm1000 \ecrm
\long\def\test#1#2{\hbox{%
\vbox{\hsize=15em\escapechar=`\\ \tt\detokenize{#1}}%
\vbox{#2}\par%
}}
\long\def\testexp#1{\test{#1}{#1}}
\long\def\testif#1{\test{#1}{#1\else not \fi equal}}
\long\def\exec#1{{\escapechar=`\\ \tt\detokenize{#1}}#1\par}
\begingroup
\exec{\let\^=^}
\exec{\escapechar=-1}
\bigskip
\testexp{\meaning^}
\testexp{\meaning\^}
\testexp{\string^}
\testexp{\string\^}
\testexp{\detokenize{^}}
\testexp{\detokenize{\^}}
\bigskip
\testif{\if^\^}
\testif{\if^\noexpand\^}
\testif{\ifcat^\^}
\testif{\ifcat^\noexpand\^}
\testif{\ifx^\^}
\bigskip
\exec{\def\tmpa{^}\def\tmpb{\^}}
\testif{\ifx\tmpa\tmpb}
\endgroup
\bye
(为了方便起见,我在这里使用了 ε-TeX 原语\detokenize
,但这不是主要原因,因此使用了 tex-core 标签。)
我发现没有办法让任何条件语句识别出两个标记之间的区别:实际的字符标记和控制序列。(我知道这些不是应该来区分它们,但以防万一还是去测试了它们。)如果我另外假设\escapechar
不可打印,就像这里的情况一样,那么甚至\meaning
、\string
或\detokenize
都不会对它们区别对待。
尽管如此,TeX 引擎显然做知道区别,因为当它们被用作宏的替换文本时,\ifx
会将它们识别为不同。当它们被设置为宏参数的分隔符时也是如此。但是,如果我们想要对两个标记进行可扩展的比较,比如宏的参数,那么我们就无法做到这一点。我们也不能改变 的值\escapechar
:如果它不可打印,我们就只能使用它。
有没有一种可扩展的方式可以进行比较,这是我以前没有想到的?如果不在 (e)TeX 中,那么也许在 XeTeX 或 LuaTeX 中?
PS 我发现,另一个有点相关的、奇怪的标记极端情况很难区分:具有空名称的控制序列和(假设是正常的转义字符)具有名称的控制序列csname\endcsname
(在这种情况下,我真的无法理解 TeX 会特意改变这种特殊情况的规则,从而搞乱\string
宏的注入性)。我知道 LuaTeX\csstring
确实可以解决这个问题。
答案1
David 的解决方案期望您将某些内容与固定标记进行比较^
。您不能仅在扩展级别将 \testifhatx 重新定义为其他内容。我的解决方案不仅适用于两个任意标记,还适用于^
另一个标记。
\long\def\testeq#1#2{%
\ifx#1#2%
\testsingleletter#1\iftrue
\testsingleletter#2\iftrue TRUE%
\else FALSE%
\fi
\else \testsingleletter#1\iftrue FALSE%
\else TRUE%
\fi
\fi
\else FALSE
\fi
}
\long\def\testsingleletter#1{\expandafter\testsingleletterA\string#1\end}
\long\def\testsingleletterA#1#2\end{\ifx&\else\expandafter\unless\fi}
\let \^=^
\testeq ^ ^ % prints TRUE
\testeq \a \^ % prints FALSE
\testeq ^ \^ % prints FALSE
\bye
编辑我创建了第二个版本,您可以\fi
根据您的评论,通过它处理参数中的类似内容。
\long\def\splitif#1{#1\expandafter\ignoresecond\else\expandafter\usesecond\fi}
\long\def\usesecond#1#2{#2} \long\def\ignoresecond#1#2{#1}
\long\def\isequal#1#2{\splitif {\ifx#1#2}%
{\splitif {\testsinglechar#1\iftrue}%
{\splitif{\testsinglechar#2\iftrue}{1}{0}}%
{\splitif{\testsinglechar#2\iftrue}{0}{1}}}%
{0}%
}
\long\def\testsinglechar#1{\expandafter\testsinglecharA\string#1\end}
\long\def\testsinglecharA#1#2\end{\ifx&\else\expandafter\unless\fi}
\long\def\test #1#2{\ifnum\isequal#1#2>0 TRUE\else FALSE\fi}
\let \^=^
\test ^ ^ % prints TRUE
\test \a \^ % prints FALSE
\test ^ \^ % prints FALSE
\bye
答案2
答案3
显式字符标记具有一个字符代码,该字符代码表示 TeX 引擎内部字符编码方案中对应字符的代码点编号(对于传统 TeX 引擎为 ASCII,对于基于 LuaTeX/XeTeX 的 TeX 引擎为 unicode)和类别 1(开始分组)、2(结束分组)、3(数学移位)、4(对齐制表符)、6(参数)、7(上标)、8(下标)、10(空格)、11(字母)、12(其他)、13(活动)之一。
不同于 13(活动)类别的显式字符标记不是控制序列。
类别 13(主动)的明确字符标记需要特殊考虑:
它们与控制序列标记(有两种形式 - “控制字标记”和“控制符号标记”)一起属于“控制序列”。
[控制符号标记的属性:
- 控制符号标记的名称由一个字符组成,该字符(!!!)当前1类别代码不是 11(字母)。
- 源代码中,在标记化时类别代码为 10(空格)的字符,如果尾随被标记化为控制符号标记的内容,则不会被丢弃,而是被标记化为显式空格标记(类别 10(空格)的显式字符标记,字符代码 32)。
例外:如果构成控制符号标记名称的字符的类别代码为 10(空格),则类别代码为 10(空格)的尾随字符将被丢弃。 - 当将控制符号标记未扩展地写入外部文本文件时,不会附加任何空格。
控制字标记的属性:
- 控制字标记的名称要么由多个字符组成(不一定所有字符的类别代码都是 11(字母)),要么由当前类别代码为 11(字母)的单个字符组成。
但是,对于通过让 TeX 读取和标记 .tex 输入而产生的控制字标记,控制序列标记的名称是在 .tex 输入文件中遇到类别 0(转义)的反斜杠/字符后获得的,这表示要创建一个控制序列标记,方法是从 .tex 输入文件中当前类别代码为 11(字母)的字符行中收集,直到遇到类别代码不同于 11 的字符。
然而,控制序列标记,以及控制字标记,也可以通过其他方式产生。例如,通过让 TeX 扩展 -expression\csname..\endcsname
。这样,就可以产生多字符控制字标记,其中类别代码不同于 11(字母)的字符也可以作为名称的组成部分。 - 源代码中在标记化时类别代码为 10(空格)的字符,如果位于被标记化为控制字标记的内容之后,则会被丢弃并且不会产生任何标记。
- 当将控制字标记未扩展地写入外部文本文件时,会附加一个空格字符。
1为了将未扩展的内容写入文件,可以在 TeX 处理名称由单个字符组成的控制序列标记作为控制字标记(附加一个空格)或控制符号标记(不附加空格)之间进行切换,方法是在为组成控制序列标记名称的字符分配类别代码 11(字母)或不同于 11 的其他代码之间进行切换。]
\let
如果通过/使得控制序列的含义\futurelet
等于类别不是 13(活动)的显式字符标记的含义,则该控制序列称为“隐式字符标记”。
\let
如果通过/使得控制序列的含义\futurelet
等于类别不是 13(active) 的显式字符标记的含义,则该控制序列本身是类别为 13(active) 的显式字符标记,那么该控制序列既是显式字符标记,又是隐式字符标记。
\let
如果通过或使控制序列的含义(该控制序列可以是活动字符、控制字标记或控制符号标记)等于\futurelet
类别不是 13(活动)的显式字符标记的含义,则与该控制序列进行比较或应用于\ifcat
该\if
控制序列的结果与使用显式字符标记代替控制序列进行比较或应用时得到的结果相同。使用和时,即使 添加到所讨论的控制序列之前也是如此——所讨论的控制序列既不可扩展也不是未定义的控制序列,因此应用没有效果。在执行诸如 之类的操作时也是如此。\ifx
\meaning
\meaning
\ifcat
\if
\noexpand
\noexpand
\expandafter\ifx\noexpand⟨character-token that might be explicit or implicit⟩...
(我提到前面的是\noexpand
因为在 TeXbook 的“第 20 章:定义(也称为宏)”中,您可以找到关于在“查看”活动字符时\ifcat
需要使用来抑制扩展的解释:\noexpand
\ifcat⟨token1⟩⟨token2⟩
(测试类别代码是否一致)这就像
\if
,但它测试的是类别代码,而不是字符代码。活动字符有类别 13,但当您使用或查看此类字符时,您必须说 才能抑制扩展。例如, 测试 ' ' 和 '后将为真,但测试 ' ' 将为假。‘\noexpand⟨active character⟩’
\if
\ifcat
\catcode`[=13 \catcode`]=13 \def[{*}
\ifcat\noexpand[\noexpand]
\ifcat[*
\ifcat\noexpand[*
不要对此感到困惑。这指的是活动字符标记可扩展的情况。但如果控制序列(例如活动字符标记)通过\let
/\futurelet
转换为隐式字符标记,则它不可扩展。)
因此,\ifcat
/ \if
/比较并检查应用— 关注标记含义方面的事物 —\ifx
的结果对于区分明确的字符标记与其隐含的悬垂物是没有用的。\meaning
在某种程度上,你可以通过比较应用的结果来区分事物\string
。
但有些极端情况是无法做到这一点的。
在这种情况下,你可能会对这个问题的答案感兴趣 仅使用扩展方法区分活动角色和非活动吊坠?,这个问题我大约在四年前就问过了。
在这个问题的答案中在扩展阶段可能出现不同标记具有相同含义和相同 \string 表示的情况,我大约在一年前问过这个问题。
在 expl3-manuals (source3.pdf, interface.pdf) 中引入了用于区分标记的属性/方面的术语:
区分标记的两个方面很重要:其“形状”(找不到更好的词来形容),它会影响分隔参数的匹配和包含此标记的标记列表的比较;其“含义”,它会影响标记是否扩展或执行什么操作。不同形状的标记可以具有相同的含义,但反过来却不行。
使用这个术语,我们可以说隐式字符标记与其显式吊坠在形状上有所不同,但在意义上没有区别。
正如引文所示,您可以使用分隔参数来扩展区分特定的显式非类别 13(活动)字符标记与其隐式悬垂部分。
但是据我所知,还没有一种通用的可扩展方法可以 100% 可靠地判断任意字符标记是显式字符标记还是隐式字符标记。
理论上可以实现一种机制,其中使用辅助宏通过分隔参数来产生活动字符标记和单字符控制序列标记。
- 一方面,这种机制需要以这样一种方式来定义:
\outer
只要用户自己不提供这些活动字符标记/单字符控制序列标记\outer
作为参数的组成部分,重新定义其中一些活动字符标记/单字符控制序列标记就无关紧要。 - 另一方面,应该定义这种机制,以确保在对齐/表格中使用时不会中断。
- 再者,传统 TeX 引擎的内部字符编码方案是 ASCII,并且在常见的计算机平台上,传统 TeX 引擎假设输入文件以某种 8 位二进制编码/字节编码进行编码,因此对于传统 TeX 引擎,字符的可能代码点数范围是 0..255。这可能可以处理。但趋势是 LuaTeX 和 XeTeX 假设输入文件以 unicode/utf-8 进行编码。因此,对于这些 TeX 引擎,字符的可能代码点数范围是 0..1114111。这很多。
- 还有一件事,如果你希望可扩展地比较任意标记列表,请考虑
- 甚至更多边缘情况,例如区分冻结
\relax
和非冻结\relax
、冻结\font
控制序列和非冻结吊坠,...... - 括号组等嵌套的标记列表。
- 标记列表,其中,
\if
等不平衡。\else
\fi
- ...
- 甚至更多边缘情况,例如区分冻结
例如,检查应用结果\string
通常依赖于应用于\string
产生多个显式字符标记的控制序列。如果所讨论的控制序列是活动字符标记,或者的值\escapechar
超出有效字符代码点数的范围,而控制序列的名称由单个字符组成,则情况并非如此。
如果通过处理未定界参数的宏进行检查,那么这些显式字符标记中的第一个和/或第二个是显式空格标记(类别 10,字符代码 32)也可能是一个问题。(例如,\meaning
etc提供类别 12(其他)的显式字符标记。但有一个例外:由/ / /etc\string
之类的东西提供的字符代码 32 的字符标记始终属于类别 10(空格),因此是显式空格标记,在收集属于未定界宏参数的第一个标记的过程中可能会被丢弃……)\string
\meaning
\detokenize
应用于\string
无名控制序列 [可以通过扩展\csname\endcsname
或以反斜杠(0 类字符(转义))结束 .tex 源代码的一行而产生,同时\endlinechar
具有负值]
- 如果值
\escapechar
不在字符的有效代码点数范围内,则会产生一系列明确的类别 12(其他)字符标记csnameendcsname
, - 如果值为
\escapechar
32 则产生一个显式空格标记(类别 10 和字符代码 32 的显式字符标记),后面跟着一串显式类别 12(其他)字符标记csname
,后面跟着一个显式空格标记,后面跟着一串显式类别 12(其他)字符标记endcsname
, - 如果 的值为
\escapechar
字符的有效代码点编号范围内的任何其他值,则会产生一个类别 12(其他)的明确字符标记,其字符代码等于 的值,\escapechar
后跟一序列明确的类别 12(其他)字符标记csname
,后跟一个类别 12(其他)的明确字符标记,其字符代码等于 的值\escapechar
,后跟一序列明确的类别 12(其他)字符标记endcsname
。
\string
通过应用通过执行而产生的不同令牌,你可以获得相同的结果\csname csname\string\endcsname\endcsname
。
如果无名控制序列标记和通过\let
或的标记\futurelet
相等,例如,等于相同的显式非类别 13 个字符的标记,则通过扩展方法区分它们是很棘手的,并且可能只能通过处理分隔参数的宏来完成。
请注意,基于 LuaTeX 的引擎是另一回事:由于是\directlua
可扩展的,例如,您可以使用token.scan_toks()
它将一组用大括号分隔的标记放入 Lua 标记表中,然后检查存储在该表中的标记的属性。token.get_next()
也可能感兴趣。
在 TUGboat 第 36 卷(2015 年)第 1 期中,您可以找到 Hans Hagen 撰写的有关此内容的文章“静态标记:LuaTeX 扫描仪”。