前段时间,我尝试\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\relax
TeX 的输入流,然后\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}