如果字符串是宏或标签的一部分,如何抑制 luatex 定义的宏对字符串的操作

如果字符串是宏或标签的一部分,如何抑制 luatex 定义的宏对字符串的操作

我正在致力于创建一个基于 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),这是不可能的。

相关内容