typo-tal.lua /size: 14 Kb    last modification: 2021-10-28 13:50
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                else
219                    return false
220                end
221            end
222        end
223
224        while current do
225            local char, id = isglyph(current)
226            if char then
227                local font    = id --- nicer
228                local data    = fontcharacters[font][char]
229                local unicode = data and data.unicode or char -- ignore tables
230                if not unicode then -- type(unicode) ~= "number"
231                    -- no unicode so forget about it
232                elseif unicode == separator then
233                    c = current
234                    if trace_split then
235                        setcolor(current,"darkred")
236                    end
237                    dataset.hasseparator = true
238                elseif categories[unicode] == "nd" or validseparators[unicode] then
239                    if c then
240                        if not a_start then
241                            a_start = current
242                        end
243                        a_stop = current
244                        if trace_split then
245                            setcolor(current,validseparators[unicode] and "darkcyan" or "darkblue")
246                        end
247                    else
248                        if not b_start then
249                            if sign then
250                                b_start = sign
251                                local c, f = isglyph(sign)
252                                local new = validsigns[c]
253                                if char == new or not fontcharacters[f][new] then
254                                    if trace_split then
255                                        setcolor(sign,"darkyellow")
256                                    end
257                                else
258                                    setchar(sign,new)
259                                    if trace_split then
260                                        setcolor(sign,"darkmagenta")
261                                    end
262                                end
263                                sign = nil
264                                b_stop = current
265                            else
266                                b_start = current
267                                b_stop = current
268                            end
269                        else
270                            b_stop = current
271                        end
272                        if trace_split and current ~= sign then
273                            setcolor(current,validseparators[unicode] and "darkcyan" or "darkblue")
274                        end
275                    end
276                elseif not b_start then
277                    sign = validsigns[unicode] and current
278                 -- if trace_split then
279                 --     setcolor(current,"darkgreen")
280                 -- end
281                end
282            elseif (b_start or a_start) and id == glue_code then
283                -- maybe only in number mode
284                -- somewhat inefficient
285                if bothdigit(current) then
286                    local width = fontcharacters[getfont(b_start or a_start)][separator or period].width
287                    setglue(current,width,0,0)
288                    setattr(current,a_character,punctuationspace)
289                    if a_start then
290                        a_stop = current
291                    elseif b_start then
292                        b_stop = current
293                    end
294                end
295            end
296            current = getnext(current)
297        end
298    else
299        while current do
300            local char, id = isglyph(current)
301            if char then
302                local font = id -- nicer
303             -- local unicode = unicodes[font][char]
304                local unicode = fontcharacters[font][char].unicode or char -- ignore tables
305                if not unicode then
306                    -- no unicode so forget about it
307                elseif unicode == separator then
308                    c = current
309                    if trace_split then
310                        setcolor(current,"darkred")
311                    end
312                    dataset.hasseparator = true
313                else
314                    if c then
315                        if not a_start then
316                            a_start = current
317                        end
318                        a_stop = current
319                        if trace_split then
320                            setcolor(current,"darkgreen")
321                        end
322                    else
323                        if not b_start then
324                            b_start = current
325                        end
326                        b_stop = current
327                        if trace_split then
328                            setcolor(current,"darkblue")
329                        end
330                    end
331                end
332            end
333            current = getnext(current)
334        end
335    end
336    local predefined = dataset.predefined
337    local before, after
338    if predefined then
339        before = b_start and getdimensions(b_start,getnext(b_stop)) or 0
340        after  = a_start and getdimensions(a_start,getnext(a_stop)) or 0
341    else
342        local entry = list[row]
343        if entry then
344            before = entry.before or 0
345            after  = entry.after  or 0
346        else
347            before = b_start and getdimensions(b_start,getnext(b_stop)) or 0
348            after  = a_start and getdimensions(a_start,getnext(a_stop)) or 0
349            list[row] = {
350                before = before,
351                after  = after,
352            }
353            return head, true
354        end
355        if not dataset.collected then
356         -- print("[maxbefore] [maxafter]")
357            local maxbefore = 0
358            local maxafter  = 0
359            for k, v in next, list do
360                local before = v.before
361                local after  = v.after
362                if before and before > maxbefore then
363                    maxbefore = before
364                end
365                if after and after > maxafter then
366                    maxafter = after
367                end
368            end
369            dataset.maxbefore = maxbefore
370            dataset.maxafter  = maxafter
371            dataset.collected = true
372        end
373    end
374    local maxbefore = dataset.maxbefore
375    local maxafter  = dataset.maxafter
376    local new_kern  = trace_split and traced_kern or new_kern
377    if b_start then
378        if before < maxbefore then
379            head = insertnodebefore(head,b_start,new_kern(maxbefore-before))
380        end
381        if not c then
382         -- print("[before]")
383            if dataset.hasseparator then
384                local width = fontcharacters[getfont(b_start)][separator].width
385                insertnodeafter(head,b_stop,new_kern(maxafter+width))
386            end
387        elseif a_start then
388         -- print("[before] [separator] [after]")
389            if after < maxafter then
390                insertnodeafter(head,a_stop,new_kern(maxafter-after))
391            end
392        else
393         -- print("[before] [separator]")
394            if maxafter > 0 then
395                insertnodeafter(head,c,new_kern(maxafter))
396            end
397        end
398    elseif a_start then
399        if c then
400         -- print("[separator] [after]")
401            if maxbefore > 0 then
402                head = insertnodebefore(head,c,new_kern(maxbefore))
403            end
404        else
405         -- print("[after]")
406            local width = fontcharacters[getfont(b_stop)][separator].width
407            head = insertnodebefore(head,a_start,new_kern(maxbefore+width))
408        end
409        if after < maxafter then
410            insertnodeafter(head,a_stop,new_kern(maxafter-after))
411        end
412    elseif c then
413     -- print("[separator]")
414        if maxbefore > 0 then
415            head = insertnodebefore(head,c,new_kern(maxbefore))
416        end
417        if maxafter > 0 then
418            insertnodeafter(head,c,new_kern(maxafter))
419        end
420    end
421    return head
422end
423