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
|
require('polyglossia') -- just in case...
local add_to_callback = luatexbase.add_to_callback
local remove_from_callback = luatexbase.remove_from_callback
local priority_in_callback = luatexbase.priority_in_callback
local new_attribute = luatexbase.new_attribute
local node = node
local insert_node_before = node.insert_before
local insert_node_after = node.insert_after
local remove_node = node.remove
local has_attribute = node.has_attribute
local node_copy = node.copy
local new_node = node.new
local end_of_math = node.end_of_math
local getnext = node.getnext
local getprev = node.getprev
-- node types according to node.types()
local glue_code = node.id"glue"
local glyph_code = node.id"glyph"
local penalty_code = node.id"penalty"
local kern_code = node.id"kern"
local math_code = node.id"math"
-- we need some node subtypes
local userkern = 1
local userskip = 0
local removable_skip = {
[0] = true, -- userskip
[13] = true, -- spaceskip
[14] = true, -- xspaceskip
}
-- we make a new node, so that we can copy it later on
local kern_node = new_node(kern_code)
kern_node.subtype = userkern -- this kern can be removed later on
local function get_kern_node(dim)
local n = node_copy(kern_node)
n.kern = dim
return n
end
local glue_node = new_node(glue_code)
glue_node.subtype = userskip
local function get_glue_node(dim, stretch, shrink)
local n = node_copy(glue_node)
n.width = dim
n.stretch = stretch
n.shrink = shrink
return n
end
local penalty_node = new_node(penalty_code)
penalty_node.penalty = 10000
local function get_penalty_node()
return node_copy(penalty_node)
end
-- all possible space characters according to section 6.2 of the Unicode Standard
-- https://www.unicode.org/versions/Unicode12.0.0/ch06.pdf
local space_chars = {
[0x20] = true, -- space
[0xA0] = true, -- no-break space
[0x1680] = true, -- ogham space mark
[0x2000] = true, -- en quad
[0x2001] = true, -- em quad
[0x2002] = true, -- en space
[0x2003] = true, -- em space
[0x2004] = true, -- three-per-em-space
[0x2005] = true, -- four-per-em space
[0x2006] = true, -- six-per-em space
[0x2007] = true, -- figure space
[0x2008] = true, -- punctuation space
[0x2009] = true, -- thin space
[0x200A] = true, -- hair space
[0x202F] = true, -- narrow no-break space
[0x205F] = true, -- medium mathematical space
[0x3000] = true -- ideographic space
}
-- all left bracket characters, referenced by their Unicode slot
local left_bracket_chars = {
[0x28] = true, -- left parenthesis
[0x5B] = true, -- left square bracket
[0x7B] = true, -- left curly bracket
[0x27E8] = true -- mathematical left angle bracket
}
-- all right bracket characters, referenced by their Unicode slot
local right_bracket_chars = {
[0x29] = true, -- right parenthesis
[0x5D] = true, -- right square bracket
[0x7D] = true, -- right curly bracket
[0x27E9] = true -- mathematical right angle bracket
}
-- question and exclamation marks, referenced by their Unicode slot
local question_exclamation_chars = {
[0x21] = true, -- exclamation mark !
[0x3F] = true, -- question mark ?
[0x203C] = true, -- double exclamation mark ‼
[0x203D] = true, -- interrobang ‽
[0x2047] = true, -- double question mark ⁇
[0x2048] = true, -- question exclamation mark ⁈
[0x2049] = true -- exclamation question mark ⁉
}
-- from nodes-tst.lua, adapted
local function somespace(n)
if n then
local id, subtype = n.id, n.subtype
if id == glue_code then
-- it is dangerous to remove all the type of glue
return removable_skip[subtype]
elseif id == kern_code then
-- remove only user's kern
return subtype == userkern
elseif id == glyph_code then
return space_chars[n.char]
end
end
end
local function someleftbracket(n)
if n then
local id = n.id
if id == glyph_code then
return left_bracket_chars[n.char]
end
end
end
local function somerightbracket(n)
if n then
local id = n.id
if id == glyph_code then
return right_bracket_chars[n.char]
end
end
end
local function question_exclamation_sequence(n1, n2)
if n1 and n2 then
local id1 = n1.id
local id2 = n2.id
if id1 == glyph_code and id2 == glyph_code then
return question_exclamation_chars[n1.char] and question_exclamation_chars[n2.char]
end
end
end
-- idem
local function somepenalty(n, value)
if n then
local id = n.id
if id == penalty_code then
if value then
return n.penalty == value
else
return true
end
end
end
end
local punct_attr = new_attribute("polyglossia_punct")
local lang_id = {}
local lang_counter = 0
local left_space = {}
local right_space = {}
local function ensure_lang_id(lang)
if not lang_id[lang] then
lang_counter = lang_counter + 1
lang_id[lang] = lang_counter
end
return lang_id[lang]
end
local function clear_spaced_characters(lang)
local id = ensure_lang_id(lang)
left_space[id] = {}
right_space[id] = {}
end
local function illegal_unit(unit)
if unit then
texio.write_nl('Illegal spacing unit "'..unit..'".')
else
texio.write_nl('Spacing unit is a nil value.')
end
end
local function add_left_spaced_character(lang, char, kern, unit, rubber)
-- The parameter kern is a number meant as a fraction of the unit.
-- The unit can be "quad" (1em) or "space" (interword space).
-- The parameter rubber is a Boolean value indicating if the inserted space is
-- stretchable and shrinkable (only relevant if the unit is "space").
local id = ensure_lang_id(lang)
if unit == "quad" or unit == "space" then
left_space[id][char] = {}
left_space[id][char]["kern"] = kern
left_space[id][char]["unit"] = unit
left_space[id][char]["rubber"] = rubber
else
illegal_unit(unit)
end
end
local function add_right_spaced_character(lang, char, kern, unit, rubber)
local id = ensure_lang_id(lang)
if unit == "quad" or unit == "space" then
right_space[id][char] = {}
right_space[id][char]["kern"] = kern
right_space[id][char]["unit"] = unit
right_space[id][char]["rubber"] = rubber
else
illegal_unit(unit)
end
end
-- from typo-spa.lua, adapted
local function process(head)
local current = head
while current do
local id = current.id
if id == glyph_code then
local attr = has_attribute(current, punct_attr)
if attr then
local char, leftspace, rightspace
if current.char <= 0x10FFFF then -- greater values may cause problems with utf8.char
char = utf8.char(current.char)
leftspace = left_space[attr][char]
rightspace = right_space[attr][char]
end
if leftspace or rightspace then
local fontparameters = fonts.hashes.parameters[current.font]
local unit, stretch, shrink, spacing_node
if leftspace and fontparameters then
local prev = getprev(current)
local space_exception = false
if prev then
-- do not add space after left (opening) bracket and between question/exclamation marks
space_exception = someleftbracket(prev) or question_exclamation_sequence(prev, current)
-- TODO: there is a question here: do we override a preceding space or not?...
while somespace(prev) do
head = remove_node(head, prev)
prev = getprev(current)
end
if somepenalty(prev, 10000) then
head = remove_node(head, prev)
end
end
if leftspace.unit == "quad" then
unit = fontparameters.quad
spacing_node = get_kern_node(leftspace.kern*unit)
elseif leftspace.unit == "space" then
unit = fontparameters.space
if leftspace.rubber then
stretch = leftspace.kern*fontparameters.space_stretch
shrink = leftspace.kern*fontparameters.space_shrink
spacing_node = get_glue_node(leftspace.kern*unit, stretch, shrink)
head = insert_node_before(head, current, get_penalty_node())
else
spacing_node = get_kern_node(leftspace.kern*unit)
end
end
if not space_exception then
head = insert_node_before(head, current, spacing_node)
end
end
if rightspace and fontparameters then
local next = getnext(current)
local space_exception = false
if next then
-- do not add space before right (closing) bracket
space_exception = somerightbracket(next)
local nextnext = getnext(next)
if somepenalty(next, 10000) and somespace(nextnext) then
head, next = remove_node(head, next)
end
while somespace(next) do
head, next = remove_node(head, next)
end
end
if rightspace.unit == "quad" then
unit = fontparameters.quad
spacing_node = get_kern_node(rightspace.kern*unit)
elseif rightspace.unit == "space" then
unit = fontparameters.space
if rightspace.rubber then
stretch = rightspace.kern*fontparameters.space_stretch
shrink = rightspace.kern*fontparameters.space_shrink
spacing_node = get_glue_node(rightspace.kern*unit, stretch, shrink)
if not space_exception then
head, current = insert_node_after(head, current, get_penalty_node())
end
else
spacing_node = get_kern_node(rightspace.kern*unit)
end
end
if not space_exception then
head, current = insert_node_after(head, current, spacing_node)
end
end
end
end
elseif id == math_code then
-- warning: this is a feature of luatex > 0.76
current = end_of_math(current) -- weird, can return nil .. no math end?
end
current = getnext(current) -- no error even if current is nil
end
return head
end
local function activate(lang)
local id = ensure_lang_id(lang)
-- We set the punctuation attribute to a language id here. This is
-- important to be able to intermix languages with different spacings
-- in one paragraph.
tex.setattribute(punct_attr, id)
for _, callback_name in ipairs{ "pre_linebreak_filter", "hpack_filter" } do
if not priority_in_callback(callback_name, "polyglossia-punct.process") then
add_to_callback(callback_name, process, "polyglossia-punct.process", 1)
end
end
end
local function deactivate()
tex.setattribute(punct_attr, -0x7FFFFFFF) -- this value means "unset"
-- Though it would make compilation slightly faster, it is not possible to
-- safely uncomment the following lines. Imagine the following case: you
-- start a paragraph by some spaced punctuation text, then, in the same
-- paragraph, you change the language to something else, and thus call the
-- following lines. This means that, at the end of the paragraph, the
-- function won't be in the callback, so the beginning of the paragraph
-- won't be processed by it.
-- if priority_in_callback(callback_name, "polyglossia-punct.process") then
-- remove_from_callback(callback_name, "polyglossia-punct.process")
-- end
end
polyglossia.activate_punct = activate
polyglossia.deactivate_punct = deactivate
polyglossia.add_left_spaced_character = add_left_spaced_character
polyglossia.add_right_spaced_character = add_right_spaced_character
polyglossia.clear_spaced_characters = clear_spaced_characters
|