普通 LuaTeX:检测 displaymath 是否以 $$ 或 \Ustopdisplaymath 结尾

普通 LuaTeX:检测 displaymath 是否以 $$ 或 \Ustopdisplaymath 结尾

在 TeX Book(章节肮脏的技巧)中提到了一种左对齐 displaymaths 的方法。

https://arxiv.org/macros/cp-aa.tex

使用\displaysetup以 分隔参数的宏。因此,它仅适用于以 结尾的 displaymaths ,而不是以 结尾的。$$$$\Ustopdisplaymath

所以我的问题是:

  1. 是否有可能定义一个宏,其参数由两个可能的标记之一分隔?

  2. 我尝试定义一个宏,其参数由另一个标记分隔。我以为可以插入此标记,\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 可能会崩溃而没有有用的错误消息,或者吞噬你的整个文档并要求输入。

相关内容