typo-tal.lmt /size: 15 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['typo-tal'] = {
2    version   = 1.001,
3    comment   = "companion to typo-tal.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- I'll make it a bit more efficient and provide named instances too which is needed for
10-- nested tables.
11--
12-- Currently we have two methods: text and number with some downward compatible
13-- defaulting.
14
15-- We can speed up by saving the current fontcharacters[font] + lastfont.
16
17local next, type, tonumber = next, type, tonumber
18local div = math.div
19local utfbyte = utf.byte
20
21local splitmethod          = utilities.parsers.splitmethod
22
23local glyph_code           <const> = nodes.nodecodes.glyph
24local glue_code            <const> = nodes.nodecodes.glue
25
26local fontcharacters       = fonts.hashes.characters
27----- unicodes             = fonts.hashes.unicodes
28local categories           = characters.categories -- nd
29
30local v_text               <const> = interfaces.variables.text
31local v_number             <const> = interfaces.variables.number
32
33local nuts                 = nodes.nuts
34local tonut                = nuts.tonut
35
36local getnext              = nuts.getnext
37local getprev              = nuts.getprev
38local getboth              = nuts.getboth
39local getid                = nuts.getid
40local getfont              = nuts.getfont
41local getchar              = nuts.getchar
42local getattr              = nuts.getattr
43local isglyph              = nuts.isglyph
44
45local setattr              = nuts.setattr
46local setchar              = nuts.setchar
47
48local insertnodebefore     = nuts.insertbefore
49local insertnodeafter      = nuts.insertafter
50local nextglyph            = nuts.traversers.glyph
51local getdimensions        = nuts.dimensions
52
53local setglue              = nuts.setglue
54
55local nodepool             = nuts.pool
56local new_kern             = nodepool.kern
57
58local tracers              = nodes.tracers
59local setcolor             = tracers.colors.set
60local tracedrule           = tracers.pool.nuts.rule
61
62local enableaction         = nodes.tasks.enableaction
63
64local characteralign       = { }
65typesetters.characteralign = characteralign
66
67local trace_split          = false  trackers.register("typesetters.characteralign", function(v) trace_split = true end)
68local report               = logs.reporter("aligning")
69
70local a_characteralign     <const> = attributes.private("characteralign")
71local a_character          <const> = attributes.private("characters")
72
73local enabled              = false
74
75local datasets             = false
76
77local implement            = interfaces.implement
78
79local comma                <const> = 0x002C
80local period               <const> = 0x002E
81local punctuationspace     <const> = 0x2008
82
83local validseparators = {
84    [comma]            = true,
85    [period]           = true,
86    [punctuationspace] = true,
87}
88
89local validsigns = {
90    [0x002B] = 0x002B, -- plus
91    [0x002D] = 0x2212, -- hyphen
92    [0x00B1] = 0x00B1, -- plusminus
93    [0x2212] = 0x2212, -- minus
94    [0x2213] = 0x2213, -- minusplus
95}
96
97-- If needed we can have more modes which then also means a faster simple handler
98-- for non numbers.
99
100local function setcharacteralign(column,separator,before,after)
101    if not enabled then
102        enableaction("processors","typesetters.characteralign.handler")
103        enabled = true
104    end
105    if not datasets then
106        datasets = { }
107    end
108    local dataset = datasets[column] -- we can use a metatable
109    if not dataset then
110        local method, token
111        if separator then
112            method, token = splitmethod(separator)
113            if method and token then
114                separator = utfbyte(token) or comma
115            else
116                separator = utfbyte(separator) or comma
117                method    = validseparators[separator] and v_number or v_text
118            end
119        else
120            separator = comma
121            method    = v_number
122        end
123        local before = tonumber(before) or 0
124        local after  = tonumber(after) or 0
125        dataset = {
126            separator  = separator,
127            list       = { },
128            maxbefore  = before,
129            maxafter   = after,
130            predefined = before > 0 or after > 0,
131            collected  = false,
132            method     = method,
133            separators = validseparators,
134            signs      = validsigns,
135        }
136        datasets[column] = dataset
137        used = true
138    end
139    return dataset
140end
141
142local function resetcharacteralign()
143    datasets = false
144end
145
146characteralign.setcharacteralign   = setcharacteralign
147characteralign.resetcharacteralign = resetcharacteralign
148
149implement {
150    name      = "setcharacteralign",
151    actions   = setcharacteralign,
152    arguments = { "integer", "string" }
153}
154
155implement {
156    name      = "setcharacteraligndetail",
157    actions   = setcharacteralign,
158    arguments = { "integer", "string", "dimension", "dimension" }
159}
160
161implement {
162    name      = "resetcharacteralign",
163    actions   = resetcharacteralign
164}
165
166local function traced_kern(w)
167    return tracedrule(w,nil,nil,"darkgray")
168end
169
170function characteralign.handler(head,where)
171    if not datasets then
172        return head
173    end
174    local first
175    for n in nextglyph, head do
176        first = n
177        break
178    end
179    if not first then
180        return head
181    end
182    local a = getattr(first,a_characteralign)
183    if not a or a == 0 then
184        return head
185    end
186    local column    = div(a,0xFFFF)
187    local row       = a % 0xFFFF
188    local dataset   = datasets and datasets[column] or setcharacteralign(column)
189    local separator = dataset.separator
190    local list      = dataset.list
191    local b_start   = nil
192    local b_stop    = nil
193    local a_start   = nil
194    local a_stop    = nil
195    local c         = nil
196    local current   = first
197    local sign      = nil
198    --
199    local validseparators = dataset.separators
200    local validsigns      = dataset.signs
201    local method          = dataset.method
202    -- we can think of constraints
203    if method == v_number then
204
205--         local function bothdigit(current) -- this could become a helper
206--             local prev, next = getboth(current)
207--             if next and prev and getid(next) == glyph_code and getid(prev) == glyph_code then
208--                 local pchar    = getchar(prev)
209--                 local nchar    = getchar(next)
210--                 local pdata    = fontcharacters[getfont(prev)][pchar]
211--                 local ndata    = fontcharacters[getfont(next)][nchar]
212--                 local punicode = pdata and pdata.unicode or pchar -- we ignore tables
213--                 local nunicode = ndata and ndata.unicode or nchar -- we ignore tables
214--                 if punicode and nunicode and categories[punicode] == "nd" and categories[nunicode] == "nd" then
215--                     return true
216--                 end
217--             end
218--         end
219
220        local function bothdigit(current) -- this could become a helper
221            local prev, next = getboth(current)
222            if next and prev then
223                local char, font = isglyph(prev)
224                if char then
225                    local data = fontcharacters[font][char]
226                    if data and categories[data.unicode or char] == "nd" then -- we ignore tables
227                        char, font = isglyph(next)
228                        if char then
229                            data = fontcharacters[font][char]
230                            if data and categories[data.unicode or char] == "nd" then -- we ignore tables
231                                return true
232                            end
233                        end
234                    end
235                end
236            end
237        end
238
239        while current do
240            local char, id = isglyph(current)
241            if char then
242                local font    = id --- nicer
243                local data    = fontcharacters[font][char]
244                local unicode = data and data.unicode or char -- ignore tables
245                if not unicode then -- type(unicode) ~= "number"
246                    -- no unicode so forget about it
247                elseif unicode == separator then
248                    c = current
249                    if trace_split then
250                        setcolor(current,"darkred")
251                    end
252                    dataset.hasseparator = true
253                elseif categories[unicode] == "nd" or validseparators[unicode] then
254                    if c then
255                        if not a_start then
256                            a_start = current
257                        end
258                        a_stop = current
259                        if trace_split then
260                            setcolor(current,validseparators[unicode] and "darkcyan" or "darkblue")
261                        end
262                    else
263                        if not b_start then
264                            if sign then
265                                b_start = sign
266                                local c, f = isglyph(sign)
267                                local new = validsigns[c]
268                                if char == new or not fontcharacters[f][new] then
269                                    if trace_split then
270                                        setcolor(sign,"darkyellow")
271                                    end
272                                else
273                                    setchar(sign,new)
274                                    if trace_split then
275                                        setcolor(sign,"darkmagenta")
276                                    end
277                                end
278                                sign = nil
279                                b_stop = current
280                            else
281                                b_start = current
282                                b_stop = current
283                            end
284                        else
285                            b_stop = current
286                        end
287                        if trace_split and current ~= sign then
288                            setcolor(current,validseparators[unicode] and "darkcyan" or "darkblue")
289                        end
290                    end
291                elseif not b_start then
292                    sign = validsigns[unicode] and current
293                 -- if trace_split then
294                 --     setcolor(current,"darkgreen")
295                 -- end
296                end
297            elseif (b_start or a_start) and id == glue_code then
298                -- maybe only in number mode
299                -- somewhat inefficient
300                if bothdigit(current) then
301                    local width = fontcharacters[getfont(b_start or a_start)][separator or period].width
302                    setglue(current,width,0,0)
303                    setattr(current,a_character,punctuationspace)
304                    if a_start then
305                        a_stop = current
306                    elseif b_start then
307                        b_stop = current
308                    end
309                end
310            end
311            current = getnext(current)
312        end
313    else
314        while current do
315            local char, id = isglyph(current)
316            if char then
317                local font = id -- nicer
318             -- local unicode = unicodes[font][char]
319                local unicode = fontcharacters[font][char].unicode or char -- ignore tables
320                if not unicode then
321                    -- no unicode so forget about it
322                elseif unicode == separator then
323                    c = current
324                    if trace_split then
325                        setcolor(current,"darkred")
326                    end
327                    dataset.hasseparator = true
328                else
329                    if c then
330                        if not a_start then
331                            a_start = current
332                        end
333                        a_stop = current
334                        if trace_split then
335                            setcolor(current,"darkgreen")
336                        end
337                    else
338                        if not b_start then
339                            b_start = current
340                        end
341                        b_stop = current
342                        if trace_split then
343                            setcolor(current,"darkblue")
344                        end
345                    end
346                end
347            end
348            current = getnext(current)
349        end
350    end
351    local predefined = dataset.predefined
352    local before, after
353    if predefined then
354        before = b_start and getdimensions(b_start,getnext(b_stop)) or 0
355        after  = a_start and getdimensions(a_start,getnext(a_stop)) or 0
356    else
357        local entry = list[row]
358        if entry then
359            before = entry.before or 0
360            after  = entry.after  or 0
361        else
362            before = b_start and getdimensions(b_start,getnext(b_stop)) or 0
363            after  = a_start and getdimensions(a_start,getnext(a_stop)) or 0
364            list[row] = {
365                before = before,
366                after  = after,
367            }
368            return head, true
369        end
370        if not dataset.collected then
371         -- print("[maxbefore] [maxafter]")
372            local maxbefore = 0
373            local maxafter  = 0
374            for k, v in next, list do
375                local before = v.before
376                local after  = v.after
377                if before and before > maxbefore then
378                    maxbefore = before
379                end
380                if after and after > maxafter then
381                    maxafter = after
382                end
383            end
384            dataset.maxbefore = maxbefore
385            dataset.maxafter  = maxafter
386            dataset.collected = true
387        end
388    end
389    local maxbefore = dataset.maxbefore
390    local maxafter  = dataset.maxafter
391    local new_kern  = trace_split and traced_kern or new_kern
392    if b_start then
393        if before < maxbefore then
394            head = insertnodebefore(head,b_start,new_kern(maxbefore-before))
395        end
396        if not c then
397         -- print("[before]")
398            if dataset.hasseparator then
399                local width = fontcharacters[getfont(b_start)][separator].width
400                insertnodeafter(head,b_stop,new_kern(maxafter+width))
401            end
402        elseif a_start then
403         -- print("[before] [separator] [after]")
404            if after < maxafter then
405                insertnodeafter(head,a_stop,new_kern(maxafter-after))
406            end
407        else
408         -- print("[before] [separator]")
409            if maxafter > 0 then
410                insertnodeafter(head,c,new_kern(maxafter))
411            end
412        end
413    elseif a_start then
414        if c then
415         -- print("[separator] [after]")
416            if maxbefore > 0 then
417                head = insertnodebefore(head,c,new_kern(maxbefore))
418            end
419        else
420         -- print("[after]")
421            local width = fontcharacters[getfont(b_stop)][separator].width
422            head = insertnodebefore(head,a_start,new_kern(maxbefore+width))
423        end
424        if after < maxafter then
425            insertnodeafter(head,a_stop,new_kern(maxafter-after))
426        end
427    elseif c then
428     -- print("[separator]")
429        if maxbefore > 0 then
430            head = insertnodebefore(head,c,new_kern(maxbefore))
431        end
432        if maxafter > 0 then
433            insertnodeafter(head,c,new_kern(maxafter))
434        end
435    end
436    return head
437end
438