In LuaTeX you can intercept the input and change it before TeX sees it. This can be used to disable certain ligatures. Here is a proof of concept in ConTeXt.
\usemodule[translate]
\translateinput[selfish][self|*|ish]
\translateinput[halflife][half|*|life]
\starttext
A selfish person has a small halflife.
\enableinputtranslation
A selfish person has a small halflife.
\disableinputtranslation
A selfish person has a small halflife.
\stoptext
which gives

See m-translate.mkiv in the ConTeXt distribution for implementation details. Keep in mind that this will change all occurrences of "selfish" to "self|*|fish", including those in csname. For example, if you have a selfish
environment, it will fail! The advantage of this approach is that it will work for all words that contain selfish
; for example, selfishness
, unselfish
, etc.
|*|
disables ligatures and does not affect hyphenation. But it introduces a 0.05em
kern between the two letters. If you do not like that add
\definetextmodediscretionary *
{\prewordbreak\discretionary{-}{}{}\prewordbreak}
Here is my solution to this problem, which also uses the ligaturing callback
(reusing lots of code from the earlier answer).
Instead of attempting to do the actual hyphenation in the processing function, my code one inserts whatsit
nodes at the key spots. Those whatsit
nodes then prohibit ligature building at those spots.
\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}
As you can see, the code does not do any ligaturing at all (!) as that is handled by luaotfload in the pre_linebreak_filter
.
However, this also creates a minor glitch: the added whatsits also prevent kerning at those spots, but they cannot be removed here because that would re-enable the ligatures once luaotfload comes into play. I do not know enough of the internals of lualatex to fix this (minor) problem.
Best Answer
In XeLaTeX (or LuaLaTeX), if you are using an opentype/truetype font, you can just load it with the default ligature features (usually just
liga
) turned off.In standard LaTeX, the only safe solution that I know is to create special tfm files that do not contain ligatures. The new primitive, '
\noligs
' in pdftex 1.30 was created specifically so that you do not have to mess with these tfm files. The modification to the tfm files is not that hard, but I do not know how to make latex make use of the results.To patch a tfm file, say 'cmr10.tfm', first find the file and go to its location, then do this:
The output file
cmr10-noligs.pl
is a 'human readable' representation of the tfm contents. You can open it in any text editor. Close to the top, there is a table that starts like this:within the
LIGTABLE
, delete all lines withLIG
in it (most fonts have onlyLIG
, but there are some variations possible like/LIG
andLIG/>
). When you have done that, you may end up with combinations ofLABEL
andSTOP
on consecutive lines. Whenever that happens, delete both those lines also.Then save the file, and run the shell command
This creates the new metrics file,
cmr10-noligs.tfm
, that can then be used to do typesetting without any automatic ligatures.Before you can actually use this font, you (usually) also have to add a dvips/pdftex map file entry for it, otherwise these programs will believe you have created a completely new metafont font. In this case, my
pdftex.map
contains this line forcmr10
:all that is needed is a copy of that line with the new tfm name
Note: it is actually possible that there is no matching map line for the original font because it was itself a virtual font. In that case, you do not need an extra map line at all, but you do need to copy the
<fontname>.vf
file (usekpsewhich
to find it, it is on your disk somewhere) to<fontname>-noligs.vf
.Someone else will have to explain how to create a LaTeX package from new tfm files, I do not remember how to do that any more.