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
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
|
-- 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 getchar = function(n)
local real_char = n.char
-- test if char is valid utf8 value, return dummy value otherwise
if (real_char >= 57344 and real_char <= 63743) or (real_char >= 983040 and real_char <= 1048573) or real_char >= 1048576 then
return "a"
end
return utf_char(real_char)
end
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)
-- space is not initial
if c == " " then
init_buffer = ""
return false
end
-- look in the initals table
if lang then
-- insert the current character to the init_buffer
-- and try to find the string in the initial table
-- for the current language
init_buffer = init_buffer .. c
local tbl = initials[lang] or {}
if tbl[init_buffer] then return true end
end
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(getchar(head)) then -- a letter
local lang = get_language(head.lang)
local char = getchar(head)
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 then
head = insert_penalty(head)
elseif head.id == glyph_id then
local char = getchar(head)
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 .. getchar(head)
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 and n.next
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
-- split hyphens should be active only inside words, there is no need to break
-- standalone hyphens
local function is_next_glyph(n)
local next_node = n.next
return next_node and next_node.id == glyph_id
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 is_next_glyph(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
|