summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/luavlna/luavlna.lua
blob: d1eaa6f00441533cd1b64e996a404bd2166d6179 (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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
-- Module luavlna
-- code originally created by Patrick Gundlach
-- http://tex.stackexchange.com/q/27780/2891
-- The code was adapted for plain TeX and added some more features
-- 1. It is possible to turn this functionality only for some letters
-- 2. Code now works even for single letters after brackets etc.
--
local M = {}
local utf_match = unicode.utf8.match
local utf_char  = unicode.utf8.char
local utf_len   = unicode.utf8.len

local glue_id  = node.id "glue"
local glyph_id = node.id "glyph"
local hlist_id = node.id "hlist"
local vlist_id = node.id "vlist"
local penalty_id = node.id "penalty"
local math_id  = node.id "math"
local period_char = string.byte(".")

local alpha = string.char(37).."a" -- alpha class, entering 
-- percent char directly caused error
local alphas = {}
local match_char = function(x) return utf_match(x,alpha) end
local match_table = function(x, chars)
  local chars=chars or {} 
  return chars[x] 
end 
local singlechars = {} -- {a=true,i=true,z=true, v=true, u=true, o = true} 

local initials = {}

local main_language = nil

-- when main_language is set, we will not use lang info in the nodes, but 
-- main language instead
local get_language = function(lang)
  return main_language or lang
end

local set_main_language = function(lang)
  main_language = lang
end

local debug = false
local tex4ht = false
-- Enable processing only for certain letters
-- must be table in the {char = true, char2=true} form
local set_singlechars= function(lang,c)
  --print("Set single chars lua")
  if type(lang) == "table" then
    for _,l in pairs(lang) do
      singlechars[l] = c
    end
  else
    local lang = tonumber(lang)
    -- for k,_ in pairs(c) do print(k) end
    singlechars[lang] = c
  end
end

local set_initials = function(lang,c)
  if type(lang) == "table" then
    for _,l in pairs(lang) do
      initials[l] = c
    end
  else
    local lang = tonumber(lang)
    initials[lang]=c
  end
end


local debug_tex4ht = function(head,p)
  --[[ local w = node.new("glyph")
  w.lang = tex.lang
  w.font = font.current()
  w.char = 64
  ]]
  --node.remove(head,node.prev(p))
  local w = node.new("whatsit", "special")
  w.data = "t4ht=<span style='background-color:red;width:2pt;'> </span>"
  return w, head
end

local debug_node = function(head,p)
  local w
  if tex4ht then
    w, head = debug_tex4ht(head,p)
  else
    w = node.new("whatsit","pdf_literal")                          
    w.data = "q 1 0 1 RG 1 0 1 rg 0 0 m 0 5 l 2 5 l 2 0 l b Q"           
  end
  node.insert_after(head,head,w)                                       
  node.insert_after(head,w,p)                                          
  -- return w
end


local set_debug= function(x)
  debug = x
end

local set_tex4ht = function()
  tex4ht = true
end

local insert_penalty = function(head)
  local p = node.new("penalty")                                           
  p.penalty = 10000                                                       
  local debug = debug or false
  if debug then
    local w = debug_node(head,p)
  else
    node.insert_after(head,head,p) 
  end
  return head
end

local replace_with_thin_space = function(head)
  local gluenode = node.new(node.id("glue"))
  local gluespec = node.new(node.id("glue_spec"))
  gluenode.width = tex.sp("0.2em")
  -- gluenode.spec = gluespec
  gluenode.next = head.next
  gluenode.prev = head.prev
  gluenode.next.prev = gluenode
  gluenode.prev.next = gluenode
  return gluenode
end

local is_alpha = function(c)
  local status = alphas[c]
  if not status then 
    status = utf_match(c, alpha)
    alphas[c] = status
  end
  return status
end

-- find whether letter is uppercase
local up_table = {}
local is_uppercase= function(c)
  if not is_alpha(c) then return false end
  local status = up_table[c]
  if status ~= nil then
    return status
  end
  status = unicode.utf8.upper(c) == c
  up_table[c] = status
  return status
end

local is_number = function(word)
  return tonumber(string.sub(word, -1)) ~= nil
end

local init_buffer = ""
local is_initial = function(c, lang)
  return is_uppercase(c)
end

local cut_off_end_chars = function(word, dot)
  local last = string.sub(word, -1)
  while word ~= "" and (not dot or last ~= ".") and not is_alpha(last) do
    word = string.sub(word, 1, -2) -- remove last char
    last = string.sub(word, -1)
  end
  return word
end

local part_until_non_alpha = function(word)
  for i = 1, #word do
    local c = word:sub(i,i)
    if not is_alpha(c) then
      word = string.sub(word, 1, i-1)
      break
    end
  end
  return word
end


function Set (list)
  local set = {}
  for _, l in ipairs(list) do set[l] = true end
  return set
end


local presi = (require "luavlna-presi")
local si = Set(require "luavlna-si")

local is_unit = function(word)
  if M.no_unit==true then return false end
  word = part_until_non_alpha(word)
  if si[word] then
    return true
  end
  for _, prefix in pairs(presi) do
    s, e = string.find(word, prefix)
    if s == 1 then
      local unit = string.sub(word, e+1)
      if si[unit] then
        return true
      end
    end
  end
  return false
end

local predegrees = Set (require "luavlna-predegrees")
local sufdegrees = Set (require "luavlna-sufdegrees")

local function prevent_single_letter (head)                                   
  local singlechars = singlechars  -- or {} 
  -- match_char matches all single letters, but this method is abbandoned
  -- in favor of using table with enabled letters. With this method, multiple
  -- languages are supported
  local test_fn = match_table -- singlechars and match_table or match_char
  local space = true
  local init = false
  local anchor = head
  local wasnumber = false
  local word = ""
  local no_predegrees = M.no_predegrees
  local no_sufdegrees = M.no_sufdegrees
  local no_initials   = M.no_initials
  local in_math = false
  while head do
    local id = head.id 
    local nextn = head.next
    -- local skip = node.has_attribute(head, luatexbase.attributes.preventsinglestatus) 
    local skip = node.has_attribute(head, M.preventsingleid)
    if id == math_id then
      if head.subtype == 0 then
        word = ""
        in_math = true
      else
        in_math = false
        if is_number(word) then wasnumber = true end
        word = ""
      end
    end
    if skip ~= 1 and not in_math  then 
      if id == glue_id then
        if wasnumber then
          if word ~= "" then
            wasnumber = false
            word = cut_off_end_chars(word, false)
            if is_unit(word) then
              anchor = replace_with_thin_space(anchor)
              insert_penalty(anchor.prev)
            end
          end
        elseif is_number(word) then
          wasnumber = true
        else
          word = cut_off_end_chars(word, true)
          if no_predegrees ~= true and predegrees[word] then
            insert_penalty(head.prev)
          elseif no_sufdegrees ~= true and sufdegrees[word] then
            insert_penalty(anchor.prev)
          end
        end
        space=true
        anchor = head
        word = ""
        init = is_initial " " -- reset initials
      elseif space==true and id == glyph_id and is_alpha(utf_char(head.char)) then -- a letter 
        local lang = get_language(head.lang)
        local char = utf_char(head.char)
        word = char
        init = is_initial(char,lang)
        local s = singlechars[lang] or {} -- load singlechars for node's lang
        --[[
        for k, n in pairs(singlechars) do
        for c,_ in pairs(n) do
        --print(type(k), c)
        end
        end
        --]]
        if test_fn(char, s) and nextn ~= nil and nextn.id == glue_id then    -- only if we are at a one letter word
          head = insert_penalty(head)
        end                                                                       
        space = false
        -- handle initials
        -- uppercase letter followed by period (code 46)
      elseif no_initials~=true and init and head.id == glyph_id and head.char == period_char and nextn.id == glue_id and utf_len(word) == 1 then 
        head = insert_penalty(head)
      elseif head.id == glyph_id then
        local char = utf_char(head.char)
        word = word .. char
        init = is_initial(char, head.lang)
        -- hlist support
      elseif head.id == hlist_id then
        prevent_single_letter(head.head)
        -- vlist support
      elseif head.id == vlist_id then
        prevent_single_letter(head.head)
      end              
    elseif id == glyph_id and in_math then
      word = word .. utf_char(head.char)
    end
    head = head.next                                                            
  end                                                                             
  return  true
end               

local hyphenate_langs = {}
local hyphenchar = string.byte("-")
-- don't break if the next or previous char is hyphen, in order to support ligaturing of dashes
local is_hyphen_ligature = function(n)
  local next_node = n.next
  -- split_hyphens code
  if next_node and next_node.id == glyph_id and next_node.char == hyphenchar then
    return true
  end
  local prev_node = n.prev
  if prev_node and prev_node.id == glyph_id and prev_node.char == hyphenchar then
    return true
  end
  return false
end

-- detect if the current character is a hyphen
local function is_breakable_hyphen(n)
  return n.id == glyph_id and hyphenate_langs[n.lang] and n.char == hyphenchar 
end

-- don't process hyphens if the next node is penalty
-- necessary to support \nobreakdash
local function is_next_penalty(n)
  local next_node = n.next
  if next_node and next_node.id == penalty_id then
    return true
  end
end

-- Enable hyphenation of words that contain hyphens
-- and repeating of the explicit hyphen on a new line when
-- the hyphen is hyphenated
-- It should be used in the `hyphenate` callback
-- based on ShreewatsaR's code from:
-- https://tex.stackexchange.com/a/417883/2891
local break_hyphens = function(head, tail)
    local glyph_id = node.id("glyph")
    local n = head
    while n do
       local skip = node.has_attribute(n, M.preventsingleid)
       if skip ~= 1 and is_breakable_hyphen(n) and not is_next_penalty(n) and not is_hyphen_ligature(n)  then
          -- Insert an infinite penalty before, and a zero-width glue node after, the hyphen.
          -- Like writing "\nobreak-\hspace{0pt}" or equivalently "\penalty10000-\hskip0pt"
         local p = node.new(node.id('penalty'))
         p.penalty = 10000
         head, p = node.insert_before(head, n, p)
         local g = node.new(node.id('glue'))
         head, g = node.insert_after(head, n, g)
         -- insert the discretionary
         local disc = node.new("disc")
         disc.penalty = tex.hyphenpenalty
         disc.subtype = 2
         disc.pre = node.copy(n)
         disc.post = node.copy(n)
         disc.replace = node.copy(n)
         node.insert_before(head, g, disc)
         -- insert another penalty
         node.insert_before(head, g, node.copy(p))
         node.remove(head,n)
         n = g
       end
       n = n.next
    end
   lang.hyphenate(head, tail)
end

M.preventsingle = prevent_single_letter
M.singlechars = set_singlechars
M.initials    = set_initials
M.set_tex4ht  = set_tex4ht
M.debug = set_debug
M.set_main_language = set_main_language
M.split_hyphen_langs = hyphenate_langs
M.split_hyphens = break_hyphens
return M