在 TeX Book(章节肮脏的技巧)中提到了一种左对齐 displaymaths 的方法。
https://arxiv.org/macros/cp-aa.tex
使用\displaysetup
以 分隔参数的宏。因此,它仅适用于以 结尾的 displaymaths ,而不是以 结尾的。$$
$$
\Ustopdisplaymath
所以我的问题是:
是否有可能定义一个宏,其参数由两个可能的标记之一分隔?
我尝试定义一个宏,其参数由另一个标记分隔。我以为可以插入此标记,
\aftergroup
但失败了。
MWE(不起作用):
\def\mydisplay#1\xxx{#1}
\everydisplay{\aftergroup\xxx\mydisplay}
$$ 0 + 0 = 0 $$
\bye
% This leads to an error message: Paragraph ended before \mydisplay was complete.
我希望\mydisplay
可以定义它以便能够找出 displaymath 是否以$$
或结束\Ustopdisplaymath
。
但是下面的例子有效......为什么?:
\def\mydisplay#1\xxx{#1}
\everydisplay{\mydisplay}
$$ 0 + 0 = 0 $$\xxx
\bye
有任何想法吗? :)
答案1
首先解释一下为什么您的方法不起作用。
它\everydisplay
的作用是在扫描显示数学的开头后立即插入以下标记列表,由$$
(或更准确地说,由两个数学移位字符,catcode 3)指示。要处理的标记流现在看起来像
\aftergroup\xxx\mydisplay 0 + 0 = 0 $$
接下来\aftergroup
执行。我认为你对这个原语的误解是,参数标记被插入立即地当命令执行时给出结果
\mydisplay 0 + 0 = 0 $$\xxx
但事实并非如此。标记被放在一个特殊的内部标记列表中,该列表实际上是在\endgroup
找到最后一个括号(在本例中是数学组的结尾)时插入的。但是,永远不会找到本地组的结尾,因为接下来要发生的事情是扩展\mydisplay
。作为带有由 分隔的参数的宏,\xxx
它会向前扫描,直到在标记流中找到控制序列\xxx
。这里没有出现这样的标记,因此最终会用尽输入。
第二个示例并非如此。在这里,您明确地\xxx
在组后添加,以便\mydisplay
能够找到它并重新应用扫描的标记。
正如评论中提到的,您可以通过扫描数学列表中的标记,直到到达您认为是显示数学结尾的标记之一,从而获得所需的结果。基本思想是使用\futurelet
向前查看下一个标记并根据接下来的标记继续处理。实际的标记读取是通过 TeX 的宏参数处理完成的。
下面是一个普通的 LuaTeX 版本的令牌吸收宏,它可以停止处理$$
LuaTeX 原语\Ustopdisplaymath
:
\catcode`\@=11
\newtoks\absorbed
\begingroup
\def~{\global\let\spacetoken= }~ %
\endgroup
\def\save@tokens#1{\global\absorbed\expandafter{\the\absorbed#1}}
\def\fbox#1{\overline{\underline{#1}}}
\def\absorb{\global\absorbed={}\absorb@}
\def\absorb@{\futurelet\@next\absorb@check}
\def\absorb@check{%
\ifx\@next\spacetoken
\let\@next=\absorb@space
\else\ifx\@next\bgroup
\let\@next=\absorb@group
\else\ifx\@next$%
\let\@next=\absorb@finishI
\else\ifx\@next\Ustopdisplaymath
\let\@next=\absorb@finishII
\else
\let\@next=\absorb@gobble
\fi\fi\fi\fi
\@next%
}
\begingroup
\def~ {\save@tokens{ }\absorb@}
\global\let\absorb@space=~
\endgroup
\def\absorb@group#1{\save@tokens{{#1}}\absorb@}
\def\absorb@gobble#1{\save@tokens{#1}\absorb@}
\def\absorb@finishI$${\absorb@result}
\def\absorb@finishII\Ustopdisplaymath{\absorb@result}
\def\absorb@result{\fbox{\the\absorbed}$$}
\catcode`\@=12
$$\fbox{1 + {i \over n+1} \hbox{ is equal to } x_i^{n+1}}$$
\everydisplay{\absorb}
$$1 + {i \over n+1} \hbox{ is equal to } x_i^{n+1}$$
\Ustartdisplaymath 1 + {i \over n+1} \hbox{ is equal to } x_i^{n+1}\Ustopdisplaymath
\bye
这三个方程的输出相同:
快速概览一下定义的宏:
\spacetoken
是将普通空格字符保存为隐式字符,\ifx
稍后进行比较时需要用到。\save@tokens
将令牌列表添加到令牌列表中\absorbed
,我们使用该列表保存扫描的令牌以供日后使用。\fbox
是 LaTeX 同名宏的温和替代品。\absorb
是主入口点。它只是清除令牌列表并移交给\absorb@
。\absorb@
用于\futurelet
向前查看以下标记(存储在中\@next
)。\absorb@check
检查\@next
并决定下一步该做什么。有五种情况需要考虑:\absorb@gobble
:发现了一个普通的令牌,只是将其放在内部保存列表中。\absorb@finishI
:下一个标记是$
。我们假设其后的标记$
也是 (否则 LuaTeX 会报错),因此我们可以完成处理。\absorb@finishII
:类似于\absorb@finishI
,但以下标记是\Ustopdisplaymath
。\absorb@space
:接下来是空格标记,因此该宏会读取它并将其放回到保存列表中。对空格的额外处理是必要的,因为 TeX 的标准参数处理会吞噬所有空格标记以查找下一个“真实”标记。\absorb@group
:当下一个标记是 时{
,由于 TeX 的参数读取方式,对 的调用\absorb@gobble
将消耗整个标记序列,直到结束括号}
,而不仅仅是开始括号。因此,我们需要对括号组进行特殊处理,将消耗的参数周围的括号放回原处。
\absorb@result
是最后一个宏,当找到整个 token 序列时调用。作为演示函数,扫描到的数学列表只是放入替换中\fbox
。
请注意,虽然这种方法似乎适用于大多数常见用途,但它很可能会在几个特殊情况下失效。
$$
还请注意,只有当宏或宏“直观”出现时,即不隐藏在其他宏中时,它才会起作用\Ustopdisplaymath
。在扫描过程中展开宏将需要更多努力。
答案2
正如我在评论中所建议的那样,您可以吸收令牌并根据下一个令牌决定做什么。
另一个答案提出了一个很好的基于 TeX 宏的解决方案。在这个答案中,我将尝试一个 Lua 解决方案。首先我给出代码,然后讨论它的作用。
\def\fbox#1{\overline{\underline{#1}}}
\def\grabdollar#1$${\fbox{#1}$$}
\def\grabUdisplay#1\Ustopdisplaymath{\fbox{#1}\Ustopdisplaymath}
\begingroup
\catcode`\#=12
\gdef\luagrab{\directlua{
local toks = { true }
local check_next = false
while true do
toks[#toks+1] = token.get_next()
if check_next and toks[#toks].cmdname == "math_shift" then
toks[1] = token.create("grabdollar")
break
end
if toks[#toks].csname == "Ustopdisplaymath" then
toks[1] = token.create("grabUdisplay")
break
end
if toks[#toks].cmdname == "math_shift" then
check_next = true
else
check_next = false
end
end
token.put_next(toks)
}}
\endgroup
$$\fbox{1 + {i \over n+1} \hbox{ is equal to } x_i^{n+1}}$$
\everydisplay{\luagrab}
$$1 + {i \over n+1} \hbox{ is equal to } x_i^{n+1}$$
\Ustartdisplaymath 1 + {i \over n+1} \hbox{ is equal to } x_i^{n+1}\Ustopdisplaymath
\bye
\everydisplay
用于决定使用哪个抓取器宏的宏是\luagrab
,它只执行 Lua 代码。
我们首先定义一个新表来保存我们读取的所有标记,但为抓取器宏保留第一个位置,该位置必须位于所有内容的前面。我true
在这里使用它作为标记,但我也可以使用它作为数字或字符串(基本上除了 之外的所有内容nil
)。
local toks = { true }
当我们想通过双数学移位 ( ) 检测显示是否为分隔符时,$$
我们不能在读取第一个后简单地终止$
,因为这可能是嵌套的内联数学。这就是为什么我们必须记住是否必须检查令牌。
local check_next = false
现在我们开始在无限循环中读取标记并将它们附加到表中。
while true do
toks[#toks+1] = token.get_next()
如果check_next
设置了标志和当前 token 是数学移位,这意味着我们遇到了两个连续的数学移位,这意味着我们已经完成了。在这种情况下,我们要使用宏\grabdollar
。
if check_next and toks[#toks].cmdname == "math_shift" then
toks[1] = token.create("grabdollar")
break
end
如果\Ustopdisplaymath
遇到我们也可能会停止并且我们想要使用\grabUdisplay
宏。
if toks[#toks].csname == "Ustopdisplaymath" then
toks[1] = token.create("grabUdisplay")
break
end
如果当前标记是数学移位,我们必须检查下一个标记是否也是数学移位。如果当前标记不是数学移位,我们将重置标志。
if toks[#toks].cmdname == "math_shift" then
check_next = true
else
check_next = false
end
退出循环后,我们将所有标记写回输入流。
end
token.put_next(toks)
注意:我不想知道如果你忘记终止显示数学运算会发生什么。LuaTeX 可能会崩溃而没有有用的错误消息,或者吞噬你的整个文档并要求输入。