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