我正在致力于创建一个基于 lualatex 的包,让用户可以自动抑制所选单词的连字符(目前为 ff、fi、fl、ffi、ffl 和 ft)。(有关背景信息,请参阅这个问题。)该软件包设置为同时处理英语和德语单词。下面的 MWE 是该软件包的一个非常精简的版本,它显示了如何抑制四个选定单词(两个英语、两个德语)的连字符插入。(选定单词的正确连字符(无论是在非连字符点还是单词中的其他地方)也得到了处理。输出图像中的小红色虚线是由显示连字符包,指示 LuaLaTeX 认为可以插入连字符的位置。)
以下是我试图解决的问题:该包的主例程(作为对 process_input_buffer 进行操作的 lua 回调函数实现)结果对自己来说太贪婪了:它试图对一切在输入缓冲区中,包括 TeX 宏的名称和参数。为了使该包适合实地工作,我必须找到一种方法来防止主文本翻译宏在
- 字符串片段是 TeX 宏的一部分,并且
- 选择指令的参数,例如
\label
和\ref
。
(可能还存在其他不宜采用替代的情况。)
是否有任何条件——或者如何创建这样的条件?——来检查匹配的字符串是否是已定义的宏的一部分,或者是或\label
(\ref
或\varioref
,\cref
等)宏的参数的一部分?或者,如何才能完全阻止替换宏对(i)任何 TeX 宏和(ii)选定宏的参数进行操作?
以下是这些问题的几个简单例子:
假设在名为 的文档中有一个名为 的宏
\bookshelfful
。(当然,这不太可能,但这只是为了提供一个例子。)对于这样的宏,我不希望我的宏对其执行操作,因为它最终会被转换成\bookshelf \nobreak\hskip0pt \discretionary{\char\hyphenchar\font}{}{\kern\KERN} \nobreak\hskip0pt ful
。啊。如果文档中有一个名为“
thm:cufflinks
”的标签(是的,当然!),则它不能被翻译成“thm:cuff\nobreak\hskip0pt \discretionary{\char\hyphenchar\font}{}{\kern\KERN} \nobreak\hskip0pt links
”。
双重啊。
% !TEX TS-program = lualatex
\documentclass[12pt]{article}
\usepackage[margin=1in]{geometry}
\usepackage[no-math]{fontspec}
%% work around a bug in luaotfload (cf. https://tex.stackexchange.com/q/47031/5001)
\setmainfont[Renderer=Basic]{Latin Modern Roman}
\defaultfontfeatures{Ligatures={TeX,Common}}
\usepackage{showhyphens} % show all hyphenation points
\usepackage{luatexbase,luacode}
\begin{luacode*}
do
local replace = {}
local filter = function ( buf )
for key, val in pairs ( replace ) do
buf = string.gsub ( buf, key, val )
end
return buf
end
function translateinput ( arg1,arg2 ) -- with discretionary hyphen
replace[arg1]=string.gsub(arg2,"|%*|",[[\kernandhyph ]])
end
function enablefilters()
luatexbase.add_to_callback('process_input_buffer', filter, 'filter')
end
end
\end{luacode*}
\newcommand\enableinputtranslation{
\directlua{
enablefilters()
}
}
\newcommand{\kernandhyph}{%
\nobreak\hskip0pt%
\discretionary{\char\hyphenchar\font}{}{\kern\KERN}%
\nobreak\hskip0pt%
}
\newcommand\translateinput[2]{
\directlua{
translateinput ( "\luatexluaescapestring{#1}",
"\luatexluaescapestring{#2}" )
}
}
% some substitution rules
\translateinput{lfful}{lf|*|ful} %% e.g., shelf-ful(s) bookshelf-ful(s)
\translateinput{fflink}{ff|*|link} %% cuff-link(s)
\translateinput{iflich}{if|*|lich} %% reif-lich begreif-lich tarif-lich
\translateinput{uflauf}{uf|*|lauf} %% auf-laufen
\translateinput{ufform}{uf|*|form} %% auf-formen
\newlength\KERN
\setlength\KERN{0.07ex} % trial value for amount of kern to be inserted
\begin{document}
shelfful cufflink unbegreiflich Auflaufform
\quad \emph{versus}
\enableinputtranslation % turn on input translation
shelfful cufflink unbegreiflich Auflaufform
\end{document}
答案1
这是我对此问题的解决方案,它也使用了ligaturing callback
(重复使用早期答案中的大量代码)。
我的代码没有尝试在处理函数中进行实际的连字,而是whatsit
在关键点插入节点。whatsit
然后这些节点禁止在这些点处构建连字符。
\documentclass{article}
\usepackage{fontspec}
\usepackage{luacode}%,luatexbase}
\setmainfont[Renderer=Basic]{Latin Modern Roman}
%\defaultfontfeatures{Ligatures={TeX,NoCommon}}
%\setmainfont{Linux Libertine O}
\usepackage[margin=1cm]{geometry}
\begin{luacode}
local glyph = node.id('glyph')
local glue = node.id("glue")
local whatsit = node.id("whatsit")
local userdefined
for n,v in pairs(node.whatsits()) do
if v == 'user_defined' then userdefined = n end
end
local identifier = 123456 -- any unique identifier
local noliga={}
debug=false
function debug_info(s)
if debug then
texio.write_nl(s)
end
end
local blocknode = node.new(whatsit, userdefined)
blocknode.type = 100
blocknode.user_id = identifier
function process_ligatures(nodes,tail)
local s={}
local current_node=nodes--node.copy(nodes)
local build_liga_table = function(strlen,t)
local p={}
for i = 1, strlen do
p[i]=0
end
for k,v in pairs(t) do
debug_info("Match: "..v[3])
local c= string.find(noliga[v[3]],"|")
local correction=1
while c~=nil do
debug_info("Position "..(v[1]+c))
p[v[1]+c-correction] = 1
c = string.find(noliga[v[3]],"|",c+1)
correction=correction+1
end
end
debug_info("Liga table: "..table.concat(p, ""))
return p
end
local apply_ligatures=function(head,ligatures)
local i=1
local hh=head
local last=node.tail(head)
for curr in node.traverse_id(glyph,head) do
if ligatures[i]==1 then
debug_info("Current glyph: "..unicode.utf8.char(curr.char))
node.insert_before(hh,curr, node.copy(blocknode))
hh=curr
end
last=curr
if i==#ligatures then
debug_info("Leave node list on position: "..i)
break
end
i=i+1
end
if(last~=nil) then
debug_info("Last char: "..unicode.utf8.char(last.char))
end--]]
end
for t in node.traverse(nodes) do
if t.id==glyph then
s[#s+1]=string.lower(unicode.utf8.char(t.char))
elseif t.id== glue then
local f=string.gsub(table.concat(s,""),"[\\?!,\\.]+","") -- add all interpunction
local throwliga={}
for k, v in pairs(noliga) do
local count=1
local match= string.find(f,k)
while match do
count=match
debug_info("pattern match: "..f .." - "..k)
local n = match + string.len(k)-1
table.insert(throwliga,{match,n,k})
match= string.find(f,k,count+1)
end
end
if #throwliga==0 then
debug_info("No ligature substitution for: "..f)
else
debug_info("Do ligature substitution for: "..f)
local ligabreaks=build_liga_table(f:len(),throwliga)
apply_ligatures(current_node,ligabreaks)
end
s={}
current_node=t
end
end
-- node.ligaturing(nodes) -- not needed, luaotfload does ligaturing
end
function suppress_liga(s,t)
noliga[s]=t
end
function drop_special_nodes (nodes,tail)
for t in node.traverse(nodes) do
if t.id == whatsit and t.subtype == userdefined and t.user_id == identifier then
node.remove(nodes,t)
node.free(t)
end
end
end
luatexbase.add_to_callback("ligaturing", process_ligatures,"Filter ligatures", 1)
--luatexbase.add_to_callback("ligaturing", drop_special_nodes,"Drop filter ligatures", 2)
\end{luacode}
\newcommand\suppressligature[2]{
\directlua{
suppress_liga("\luatexluaescapestring{#1}","\luatexluaescapestring{#2}")
}
}
\newcommand\debugon{%
\directlua{
debug=true
}
}
\begin{document}
\suppressligature{fifi}{f|ifi}
\suppressligature{grafi}{graf|i}
\suppressligature{lfful}{lf|ful}
\suppressligature{fflink}{ff|link}
\suppressligature{iflich}{if|lich}
\suppressligature{uflauf}{uf|lauf}
\suppressligature{ufform}{uf|form}
\debugon
shelfful
cufflink
unbegreiflich
Auflaufform
offen
\end{document}
如您所见,代码根本没有进行任何连字符(!),因为这是由中的 luaotfload 处理的pre_linebreak_filter
。
然而,这也造成了一个小问题:添加的 whatsits 也会阻止这些位置的字距调整,但它们无法在这里被移除,因为一旦 luaotfload 开始发挥作用,就会重新启用连字。我对 lualatex 的内部结构了解不够,无法修复这个(小)问题。
答案2
我刚刚看到这个问题是因为一个后续问题。在特殊情况下禁用连字的另一种可能性是更改开放类型功能。例如,testsubs.fea
您可以告诉开放类型引擎忽略某些替换(不要问我所有方括号,我经过大量尝试和错误才找到可行的语法):
# Script and language coverage
# languagesystem DFLT dflt;
# languagesystem latn dflt;
languagesystem DFLT dflt;
languagesystem latn dflt;
feature nolg {
ignore substitute f' l' [a] [u] [f];
ignore substitute [i] f' l' [i] [c] [h];
substitute f' l' by f_l ;
ignore substitute f' f' [o] [r] [m];
substitute f' f' by f_f ;
} nolg;
fea
然后可以像这样使用这个文件:
\documentclass{article}
\usepackage{fontspec}
\setmainfont[FeatureFile=testsubs.fea, RawFeature=+nolg,Ligatures=NoCommon]
{Latin Modern Roman}
\begin{document}
Auflage Pflicht
unbegreiflich Auflaufform
\end{document}
由此得出:
备注:必须禁用标准连字并重新声明fea
(我不知道是否以及如何将“忽略”语句添加到正常liga
声明中)。
如果功能文件已发生改变,则当前luotfload
将重新创建字体的缓存,因此理论上可以动态创建它们。
答案3
luatex 中有ligaturing
回调,可以用于此目的。编辑有新的代码和一些解释
\documentclass{article}
%\usepackage{fontspec}
\usepackage{luacode}%,luatexbase}
%\setmainfont[Renderer=Basic]{Latin Modern Roman}
%\defaultfontfeatures{Ligatures={TeX,NoCommon}}
%\setmainfont{Linux Libertine O}
\usepackage[margin=1cm]{geometry}
\begin{luacode}
local glyph = node.id('glyph')
local glue= node.id("glue")
local noliga={}
debug=false
function debug_info(s)
if debug then
texio.write_nl(s)
end
end
function process_ligatures(nodes,tail)
local s={}
local current_node=nodes--node.copy(nodes)
local build_liga_table = function(strlen,t)
local p={}
for i = 1, strlen do
p[i]=0
end
for k,v in pairs(t) do
debug_info("Match: "..v[3])
local c= string.find(noliga[v[3]],"|")
local correction=1
while c~=nil do
debug_info("Position "..(v[1]+c))
p[v[1]+c-correction] = 1
c = string.find(noliga[v[3]],"|",c+1)
correction=correction+1
end
end
debug_info("Liga table: "..table.concat(p, ""))
return p
end
local apply_ligatures=function(head,ligatures)
local i=1
local hh=head
local last=node.tail(head)
for curr in node.traverse_id(glyph,head) do
if ligatures[i]==1 then
node.ligaturing(hh,node.prev(curr))
debug_info("Current glyph: "..unicode.utf8.char(curr.char))
hh=curr--node.next(curr)
end
last=curr
if i==#ligatures then
debug_info("Leave node list on position: "..i)
break
end
i=i+1
end
if(last~=nil) then
debug_info("Last char: "..unicode.utf8.char(last.char))
node.ligaturing(hh,last)
end--]]
node.slide(head)
end
for t in node.traverse(nodes) do
if t.id==glyph then
s[#s+1]=string.lower(unicode.utf8.char(t.char))
elseif t.id== glue then
local f=string.gsub(table.concat(s,""),"[\\?!,\\.]+","") -- add all interpunction
local throwliga={}
for k, v in pairs(noliga) do
local count=1
local match= string.find(f,k)
while match do
count=match
debug_info("pattern match: "..f .." - "..k)
local n = match + string.len(k)-1
table.insert(throwliga,{match,n,k})
match= string.find(f,k,count+1)
end
end
if #throwliga==0 then
debug_info("No ligature substitution for: "..f)
node.ligaturing(current_node,t)
else
local ligabreaks=build_liga_table(f:len(),throwliga)
apply_ligatures(current_node,ligabreaks)
end
--node.ligaturing(current_node,t)
s={}
current_node=t
end
end
end
function suppress_liga(s,t)
noliga[s]=t
end
luatexbase.add_to_callback("ligaturing", process_ligatures,"Filter ligatures", 1)
--luatexbase.add_to_callback("pre_linebreak_filter", process_ligatures,"Filter ligatures", 1)
\end{luacode}
\newcommand\suppressligature[2]{
\directlua{
suppress_liga("\luatexluaescapestring{#1}","\luatexluaescapestring{#2}")
}
}
\newcommand\debugon{%
\directlua{
debug=true
}
}
\begin{document}
\suppressligature{fifi}{f|ifi}
\suppressligature{grafi}{graf|i}
\suppressligature{lfful}{lf|ful}
\suppressligature{fflink}{ff|link}
\suppressligature{iflich}{if|lich}
\suppressligature{uflauf}{uf|lauf}
\suppressligature{ufform}{uf|form}
\debugon
shelfful
%cufflink
unbegreiflich Auflaufform
\end{document}
总体思路如下:
您当前的解决方案使用process_input_buffer
回调,这可以轻松替换字符串,但缺点是这些字符串包含原始 TeX 代码,很容易破坏所有内容。我认为正确的方法是在应用所有 TeX 宏并构建节点列表后禁用连字符。有一个ligaturing
回调,用于连字符文档。
在代码中,我们遍历回调收到的节点列表并过滤所有字形节点(这些包含文本),当存在粘合节点时,我们可以保存当前单词并应用连字过滤器。
如果单词不匹配任何模式,我们可以使用
node.ligaturing(word_start_node,word_stop_node)
否则,我们构建一个表,其中所有禁用连字的点都标记为 1,例如:
auflaufform
00010001000
然后我们循环遍历单词的节点,并在每次出现 1 时断开连字符。
结果:
这段代码有两个问题,一个是我无法找到的错误,另一个更严重。
这个错误是,如果单词在连字符断开之前包含连字符,例如cufflink
,则该点之后的所有节点都将被丢弃。我找不到这个错误的来源,但我希望可以解决这个问题。
第二个问题更严重,使用时fontspec
,不应用任何过滤,fontspec
自行处理连字。我不知道是否有可能抑制fontspec's
连字机制,因此目前在现实世界中,即使我们解决了另一个错误,此代码也是无用的。
答案4
(评论太长)
我试了又试,但找不到好的解决方案。@michal.h21 尝试过使用连字符回调,但正如他所写,在 LuaTeX 中使用复杂渲染模式时,这种方法会失败。因此,我尝试挂接到已将pre_linebreak_filter
所有连字符放在其位置的,但我无法将连字符分开。例如,假设您有一个连字符菲并希望抑制 f 和 fi 之间的连字。当我在 中时,TeX(或字体子系统)已经创建了连字,pre_linebreak_filter
您可以将它们分开,但只能以预定义的方式:在单词“fluffiest”中,第二个连字由连字“ff”和“i”组成,因此它是“ffi”。连字“ff”显然是由“f”和“f”组成的。现在很容易在“ff|i”处拆分 - 我保留 ff 连字并在“i”前插入一个空格。但要在“f|fi”处分离“ffi”,我必须创建一个新的连字(“fi”),但这几乎是不可能的(从技术上讲,我可以创建必要的数据结构,但创建连字的逻辑在字体子系统中)。因此pre_linebreak_filter
不是进行解连字的好地方。
但是如果我们继续前进(朝着 TeX 的原始输入),就不可能解析输入并获取所有出现的单词:
\documentclass{article}
\begin{document}
\newcommand\auflauf{Auflauf}
\newcommand\form{form}
\auflauf\form
\end{document}
该词仍然是“连字符”,但您将无法使用问题中想要的方法来解析它。
这导致了我的结果:如果不挂接字体子系统(在本例中luaotfload
),这是不可能的。