summaryrefslogtreecommitdiff
path: root/macros/luatex/latex/selnolig/selnolig.lua
blob: 7bf40d135e3a41394a588a5b2169357f4e29d1e7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
-- Lua code for the selnolig package.
-- To be loaded with an instruction such as 
--    \directlua{  require("selnolig.lua")  }
-- from a (Lua)LaTeX .sty file.
--
-- Author: Mico Loretan (loretan dot mico at gmail dot com)
--    (with crucial contributions from Taco Hoekwater, 
--    Patrick Gundlach, and Steffen Hildebrandt)
--
-- The entire selnolig package is placed under the terms 
-- of the LaTeX Project Public License, version 1.3 or 
-- later. (http://www.latex-project.org/lppl.txt).
-- It has the status "maintained".

selnolig = { }
selnolig.module = {
   name         = "selnolig",
   version      = "0.256",
   date         = "2015/10/26",
   description  = "Selective suppression of typographic ligatures",
   author       = "Mico Loretan",
   copyright    = "Mico Loretan",
   license      = "LPPL 1.3 or later"
}

debug=false -- default: don't output detailed information

-- Define variables corresponding to various text nodes;
-- cf. sections 8.1.2 and 8.1.4 of LuaTeX reference guide
local rule    = node.id('rule')
local glue    = node.id("glue") --
local kern    = node.id('kern')
local glyph   = node.id('glyph') --
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={}
local keepliga={}          -- String -> Boolean

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

local suppression_on = true  -- if false, process_ligatures won't do anything

local prefix_length = function(word, byte)
  return unicode.utf8.len( string.sub(word,0,byte) )
end

  -- Problem: string.find and unicode.utf8.find return 
  -- the byte-position at which the pattern is found 
  -- instead of the character-position. Fix this by
  -- providing a dedicated string search function.

local unicode_find = function(s, pattern, position)
  -- Start by correcting the incoming position
  if position ~= nil then
    -- debug_info("Position: "..position)
    sub = string.sub(s, 1, position)
    position=position+string.len(sub) - unicode.utf8.len(sub)
    -- debug_info("Corrected position: "..position)
  end
  -- Now execute find and fix it accordingly
  byte_pos = unicode.utf8.find(s, pattern, position)
  if byte_pos ~= nil then
    -- "convert" byte_pos to "unicode_pos"
    return unicode.utf8.len( string.sub(s, 1, byte_pos) )
  else
    return nil
  end
end

function process_ligatures(nodes,tail)
  if not suppression_on then
    return -- suppression disabled
  end
  
  local s={}
  local current_node=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= unicode_find(noliga[v[3]],"|")
      local correction=1
      while c~=nil do
         --debug_info("Position "..(v[1]+c))
         p[v[1]+c-correction] = 1
         c = unicode_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("Inserting nolig whatsit before 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]=unicode.utf8.char(t.char)
    end
    if ( t.id==glue or t.next==nil or t.id==kern or t.id==rule ) then 
      local f=string.gsub(table.concat(s,""),"[\\?!,\\.]+","")
      local throwliga={} 
      for k,v in pairs (noliga) do
        local count=1
        local match = string.find(f,k)
        while match do
          count    = match
          keep     = false
          debug_k1 = ""
          for k1,v1 in pairs (keepliga) do
            if v1 and string.find(f,k1) and string.find(k1,k) then
              debug_k1=k1
              keep=true
              break
            end
          end
          if not keep then
            debug_info("pattern match: "..f .." - "..k)
            local n = match + string.len(k) - 1
            table.insert(throwliga,{prefix_length(f,match),n,k})
          else
            debug_info("pattern match nolig and keeplig: "..f .." - "..k.." - "..debug_k1)
          end
          match= string.find(f,k,count+1)
        end
      end
      if #throwliga==0 then
      --  debug_info("No ligature suppression for: "..f)
      else
        debug_info("Do ligature suppression for: "..f)
        local ligabreaks = build_liga_table(f:len(),throwliga)
        apply_ligatures(current_node,ligabreaks)
      end
      s = {}
      current_node = t
    end
  end
end -- end of function process_ligatures(nodes,tail)

function suppress_liga(s,t)
  noliga[s] = t
end

function always_keep_liga(s)
  keepliga[s] = true
end

function enable_suppression(val)
  suppression_on = val
  if val then
    debug_info("Turning ligature suppression back on")
  else
    debug_info("Turning ligature suppression off")
  end
end

function enableselnolig()
  luatexbase.add_to_callback( "ligaturing", 
    process_ligatures, "Suppress ligatures selectively", 1 )
end

function disableselnolig()
  luatexbase.remove_from_callback( "ligaturing", 
    "Suppress ligatures selectively" )
end