如何在 LuaTeX 中定义一个带有参数的新 TeX 寄存器?

如何在 LuaTeX 中定义一个带有参数的新 TeX 寄存器?

前段时间,我尝试\pdfadjustinterwordglue使用回调在 LuaTeX 中实现 pdfTeX。让回调工作起来非常简单,但还有一个主要问题,我至今无法解决。罪魁祸首在于间距参数的设置方式,例如

\knbscode\font`r=1100

这是一个常规的 TeX 作业,可以在 Lua 中使用以下方法实现

\def\knbscode{\directlua{
local fid = token.scan_int()
local chr = token.scan_int()
local str = token.scan_string()

knbscode[fid] = knbscode[fid] or {}
knbscode[fid][chr] = knbscode[fid][chr] or {}
local int = string.gsub(str,"=","")
knbscode[fid][chr][field] = tonumber(int)
}\fontid}

也就是说,我先查找一个整数(即字体 ID)、另一个整数(即字符)和“字符串”,根据 LuaTeX 手册,字符串是

返回 之间给出的字符串{},作为\macro或作为 catcode 为 11 或 12 的字符序列

细心的读者可能已经注意到了这里的问题,因为\knbscode\font`r=1100abc应该按照正常的 TeX 规则进行解析,但在这种情况下会失败。幸运的是,寄存器赋值的使用在 TeX 中很常见,这种情况很少发生。

下一个大问题是寄存器不是只写的。它们也可以使用以下方式读取:

\the\knbscode\font`r

当然,在这种情况下,\knbscode仍然应该扫描字体 ID 和字符,但不应扫描分配。我解决这个问题的想法是重载\the以查看下一个标记,如果是\knbscode,则委托给另一个函数。在 Lua 端,这看起来像这样

local t = lua.get_functions_table()
t[1] = function()
    local next = token.get_next()
    if next.csname == "knbscode" then
        token.put_next(token.create("theknbscode"))
    elseif next.csname == "stbscode" then
        token.put_next(token.create("thestbscode"))
    elseif next.csname == "shbscode" then
        token.put_next(token.create("theshbscode"))
    else
        token.put_next{ token.create("normalthe"), next }
    end
end

在 TeX 端我定义\the引用这个 Lua 函数

\luadef\the1

这样它就可以在一步内展开。但事实并非如此。我的 new\the不会在一步内展开,因为我没有将寄存器中的数字放入输入流中,而是放入了一个必须再次展开的标记。

也许这种方法完全是错误的,所以这个问题相对普遍:

如何在 LuaTeX 中定义一个带有参数的新 TeX 寄存器?

事实上,我之前在 LuaTeX 邮件列表中用一个简化的例子问过同样的问题,所以我得到的答案是:“好吧,使用一个普通的寄存器,呵呵”(https://tug.org/pipermail/luatex/2019-January/007040.html)。


下面我有一个完整的 MWE,您可以尝试一下。

\documentclass{article}
\usepackage{luacode}

\newcount\pdfadjustinterwordglue

\begin{luacode}
local subtypes = node.subtypes("glue")

local knbscode = {}

local t = lua.get_functions_table()
t[1] = function()
    local next = token.get_next()
    if next.csname == "knbscode" then
        token.put_next(token.create("theknbscode"))
    elseif next.csname == "stbscode" then
        token.put_next(token.create("thestbscode"))
    elseif next.csname == "shbscode" then
        token.put_next(token.create("theshbscode"))
    else
        token.put_next{ token.create("normalthe"), next }
    end
end

function set_knbscode(field)
    local fid = token.scan_int()
    local chr = token.scan_int()
    local str = token.scan_string()

    knbscode[fid] = knbscode[fid] or {}
    knbscode[fid][chr] = knbscode[fid][chr] or {}
    local int = string.gsub(str,"=","")
    knbscode[fid][chr][field] = tonumber(int)
end

function get_knbscode(field)
    local fid = token.scan_int()
    local chr = token.scan_int()

    knbscode[fid] = knbscode[fid] or {}
    knbscode[fid][chr] = knbscode[fid][chr] or {}
    tex.sprint([[\numexpr]] .. (knbscode[fid][chr][field] or 0) .. [[\relax]])
end

local microtype_spacing = function(head, tail)
    head, tail, success = node.kerning(head, tail)
    if tex.count.pdfadjustinterwordglue > 0 then
        for space in node.traverse_id(node.id("glue"), head) do
            if subtypes[space.subtype] == "spaceskip" then
                local prev = node.prev(space)
                if prev.id == node.id("glyph") then
                    local knbs = knbscode[prev.font]
                    if knbs and knbs[prev.char] then
                        local f = font.getfont(prev.font)
                        local em = f.parameters.quad

                        local width = knbs[prev.char].width or 0
                        local stretch = knbs[prev.char].stretch or 0
                        local shrink = knbs[prev.char].shrink or 0

                        local glue = node.new(node.id("glue"))
                        glue.width = width*em/1000
                        glue.stretch = stretch*em/1000
                        glue.shrink = shrink*em/1000

                        head = node.insert_before(head, space, glue)
                    end
                end
            end
        end
    end
end

luatexbase.add_to_callback("kerning", microtype_spacing, "microtype_spacing")
\end{luacode}

\protected\def\knbscode{\directlua{set_knbscode("width")}\fontid}
\protected\def\stbscode{\directlua{set_knbscode("stretch")}\fontid}
\protected\def\shbscode{\directlua{set_knbscode("shrink")}\fontid}

\let\normalthe\the

\def\theknbscode{\normalthe\directlua{get_knbscode("width")}\fontid}
\def\thestbscode{\normalthe\directlua{get_knbscode("stretch")}\fontid}
\def\theshbscode{\normalthe\directlua{get_knbscode("shrink")}\fontid}

\luadef\the1

\pdfadjustinterwordglue=1

% Bogus values, just for demonstration
\knbscode\font`r=1100
\stbscode\font`r=10
\shbscode\font`r=10

\knbscode\font`r 1100
\stbscode\font`r 10
\shbscode\font`r 10

\begin{document}

\the\knbscode\font`r
\the\stbscode\font`r
\the\shbscode\font`r

\input lorem

\end{document}

答案1

tl;dr:我认为从 Lua 定义类似 TeX 的寄存器是不可能的,但你可以更接近一点。

修复\the很简单:您只需触发 Lua 的一级扩展即可。您可以尝试,token.expand但该函数对要扩展哪个标记有非常规的理解。因此,依靠标准 TeX 工具来获得一级扩展更容易::\expandafter您可以插入\expandafter\relaxTeX 的输入流,然后\relax使用扩展读取后面的内容。

但还有其他问题。假设我们希望“g”使用与“f”相同的值。在 pdfTeX 中,我们可以说

\knbscode\font`g=\knbscode\font`f

这不适用于这样的模拟寄存器。这是因为两个问题:

设置\knbscode仅接受 Lua 可以转换为数字的字符串,而不是一般的 TeX 数字,并且\knbscode与 一起工作\the,但仍然不是可以在 TeX 接受数字的所有地方使用的内部 TeX 数字。第一个问题可以修复:要读取=后跟整数的可选值,请使用

token.scan_keyword'=' -- Scans an optional "="
local int = token.scan_int()

第二个问题更难解决。如果你想让你的类似寄存器的 Lua 值在所有接受 TeX 数字的地方都可用,你需要某种方式从 Lua 确定 TeX 是否尝试读取寄存器。这些信息不可用,所以我认为完美模拟寄存器是不可能的。

当然,你可以说至少相似就好了,但有时 Lua 版本的界面略有不同,比在使用上有微妙的、更难察觉的差异要好。

建议的修改导致了此版本:

\documentclass{article}
\usepackage{luacode}

\newcount\pdfadjustinterwordglue

\begin{luacode}

local subtypes = node.subtypes("glue")

local knbscode = {}

-- Let's make sure that no one messes with our primitives:
local function frozentok(name)
  local tok = token.create(name)
  return token.new(tok.mode, tok.command)
end
local the, expandafter, relax = frozentok'the', frozentok'expandafter', frozentok'relax'
local t = lua.get_functions_table()
t[1] = function()
    local next = token.get_next()
    if next.csname == "knbscode" then
        token.put_next(token.create("theknbscode"))
    elseif next.csname == "stbscode" then
        token.put_next(token.create("thestbscode"))
    elseif next.csname == "shbscode" then
        token.put_next(token.create("theshbscode"))
    else
        token.put_next{ next }
    end
    token.put_next{ expandafter, relax, the }
    token.scan_token() -- The scanned token will always be the relax we just wrote, but expandafter does it's magic in the progress.
end

function set_knbscode(field)
    local fid = token.scan_int()
    local chr = token.scan_int()
    token.scan_keyword'='
    local int = token.scan_int()

    knbscode[fid] = knbscode[fid] or {}
    knbscode[fid][chr] = knbscode[fid][chr] or {}
    knbscode[fid][chr][field] = int
end

function get_knbscode(field)
    local fid = token.scan_int()
    local chr = token.scan_int()

    knbscode[fid] = knbscode[fid] or {}
    knbscode[fid][chr] = knbscode[fid][chr] or {}
    tex.sprint([[\numexpr]] .. (knbscode[fid][chr][field] or 0) .. [[\relax]])
end

local microtype_spacing = function(head, tail)
    head, tail, success = node.kerning(head, tail)
    if tex.count.pdfadjustinterwordglue > 0 then
        for space in node.traverse_id(node.id("glue"), head) do
            if subtypes[space.subtype] == "spaceskip" then
                local prev = node.prev(space)
                if prev.id == node.id("glyph") then
                    local knbs = knbscode[prev.font]
                    if knbs and knbs[prev.char] then
                        local f = font.getfont(prev.font)
                        local em = f.parameters.quad

                        local width = knbs[prev.char].width or 0
                        local stretch = knbs[prev.char].stretch or 0
                        local shrink = knbs[prev.char].shrink or 0

                        local glue = node.new(node.id("glue"))
                        glue.width = width*em/1000
                        glue.stretch = stretch*em/1000
                        glue.shrink = shrink*em/1000

                        head = node.insert_before(head, space, glue)
                    end
                end
            end
        end
    end
end

luatexbase.add_to_callback("kerning", microtype_spacing, "microtype_spacing")

\end{luacode}

\protected\def\knbscode{\directlua{set_knbscode("width")}\fontid}
\protected\def\stbscode{\directlua{set_knbscode("stretch")}\fontid}
\protected\def\shbscode{\directlua{set_knbscode("shrink")}\fontid}

\def\theknbscode{\directlua{get_knbscode("width")}\fontid}
\def\thestbscode{\directlua{get_knbscode("stretch")}\fontid}
\def\theshbscode{\directlua{get_knbscode("shrink")}\fontid}

\luadef\the1

\pdfadjustinterwordglue=1

% Bogus values, just for demonstration
\knbscode\font`r=1100
\stbscode\font`r=10
\shbscode\font`r=10

\knbscode\font`r 1100
\stbscode\font`r 10
\shbscode\font`r 10

\begin{document}

\the\knbscode\font`r
\the\stbscode\font`r
\the\shbscode\font`r

\input lorem

\end{document}

相关内容