typo-brk.lua /size: 15 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['typo-brk'] = {
2    version   = 1.001,
3    comment   = "companion to typo-brk.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-- this code dates from the beginning and is kind of experimental; it
10-- will be optimized and improved soon
11
12local next, type, tonumber, tostring = next, type, tonumber, tostring
13local utfbyte, utfchar = utf.byte, utf.char
14local format = string.format
15
16local trace_breakpoints = false  trackers.register("typesetters.breakpoints", function(v) trace_breakpoints = v end)
17
18local report_breakpoints = logs.reporter("typesetting","breakpoints")
19
20local nodes, node = nodes, node
21
22local settings_to_array  = utilities.parsers.settings_to_array
23
24local nuts               = nodes.nuts
25local tonut              = nuts.tonut
26
27local getnext            = nuts.getnext
28local getprev            = nuts.getprev
29local getboth            = nuts.getboth
30local getsubtype         = nuts.getsubtype
31local getfont            = nuts.getfont
32local getid              = nuts.getid
33----- getattr            = nuts.getattr
34local getattrlist        = nuts.getattrlist
35local takeattr           = nuts.takeattr
36local getlanguage        = nuts.getlanguage
37local isglyph            = nuts.isglyph
38
39local setattr            = nuts.setattr
40local setattrlist        = nuts.setattrlist
41local setlink            = nuts.setlink
42local setchar            = nuts.setchar
43local setdisc            = nuts.setdisc
44local setnext            = nuts.setnext
45local setprev            = nuts.setprev
46local setboth            = nuts.setboth
47local setsubtype         = nuts.setsubtype
48
49local copy_node          = nuts.copy
50local copy_node_list     = nuts.copylist
51local flushnode          = nuts.flushnode
52local insertnodebefore   = nuts.insertbefore
53local insertnodeafter    = nuts.insertafter
54local remove_node        = nuts.remove
55local endofmath          = nuts.endofmath
56
57local tonodes            = nuts.tonodes
58
59local texsetattribute    = tex.setattribute
60local unsetvalue         = attributes.unsetvalue
61
62local nodepool           = nuts.pool
63local enableaction       = nodes.tasks.enableaction
64
65local v_reset            = interfaces.variables.reset
66local v_yes              = interfaces.variables.yes
67
68local implement          = interfaces.implement
69
70local new_penalty        = nodepool.penalty
71local new_glue           = nodepool.glue
72local new_disc           = nodepool.disc
73local new_wordboundary   = nodepool.wordboundary
74
75local nodecodes          = nodes.nodecodes
76local kerncodes          = nodes.kerncodes
77
78local kern_code          = nodecodes.kern
79local math_code          = nodecodes.math
80
81local fontkern_code      = kerncodes.fontkern
82local italickern_code    = kerncodes.italiccorrection
83
84local is_letter          = characters.is_letter
85
86local typesetters        = typesetters
87
88typesetters.breakpoints  = typesetters.breakpoints or {}
89local breakpoints        = typesetters.breakpoints
90
91breakpoints.mapping      = breakpoints.mapping or { }
92breakpoints.numbers      = breakpoints.numbers or { }
93
94breakpoints.methods      = breakpoints.methods or { }
95local methods            = breakpoints.methods
96
97local a_breakpoints      = attributes.private("breakpoint")
98
99storage.register("typesetters/breakpoints/mapping", breakpoints.mapping, "typesetters.breakpoints.mapping")
100
101local mapping            = breakpoints.mapping
102local numbers            = breakpoints.mapping
103
104for i=1,#mapping do
105    local m = mapping[i]
106    numbers[m.name] = m
107end
108
109-- this needs a cleanup ... maybe make all of them disc nodes
110
111-- todo: use boundaries
112
113local function insert_break(head,start,stop,before,after,kern)
114    if not kern then
115        local p = new_penalty(before)
116        local g = new_glue()
117        setattrlist(p,start)
118        setattrlist(g,start)
119        insertnodebefore(head,start,p)
120        insertnodebefore(head,start,g)
121    end
122    local p = new_penalty(after)
123    local g = new_glue()
124    setattrlist(p,start)
125    setattrlist(g,start)
126    insertnodeafter(head,stop,g)
127    insertnodeafter(head,stop,p)
128end
129
130methods[1] = function(head,start,stop,settings,kern)
131    local p, n = getboth(stop)
132    if p and n then
133        insert_break(head,start,stop,10000,0,kern)
134    end
135    return head, stop
136end
137
138methods[6] = function(head,start,stop,settings,kern)
139    local p = getprev(start)
140    local n = getnext(stop)
141    if p and n then
142        if kern then
143            insert_break(head,start,stop,10000,0,kern)
144        else
145            local l = new_wordboundary()
146            local d = new_disc()
147            local r = new_wordboundary()
148            setattrlist(d,start) -- otherwise basemode is forced and we crash
149            setlink(p,l,d,r,n)
150            if start == stop then
151                setboth(start)
152                setdisc(d,start,nil,copy_node(start))
153            else
154                setprev(start)
155                setnext(stop)
156                setdisc(d,start,nil,copy_node_list(start))
157            end
158            stop = r
159        end
160    end
161    return head, stop
162end
163
164methods[2] = function(head,start) -- ( => (-
165    local p, n = getboth(start)
166    if p and n then
167        local replace
168        head, start, replace = remove_node(head,start)
169        local post   = copy_node(replace)
170        local hyphen = copy_node(post)
171        setchar(hyphen,languages.prehyphenchar(getlanguage(post)))
172        setlink(post,hyphen)
173        head, start = insertnodebefore(head,start,new_disc(nil,post,replace))
174        setattrlist(start,replace)
175        insert_break(head,start,start,10000,10000)
176    end
177    return head, start
178end
179
180methods[3] = function(head,start) -- ) => -)
181    local p, n = getboth(start)
182    if p and n then
183        local replace
184        head, start, replace = remove_node(head,start)
185        local pre    = copy_node(replace)
186        local hyphen = copy_node(pre)
187        setchar(hyphen,languages.prehyphenchar(getlanguage(pre)))
188        setlink(hyphen,pre)
189        head, start = insertnodebefore(head,start,new_disc(hyphen,nil,replace)) -- so not pre !
190        setattrlist(start,tmp)
191        insert_break(head,start,start,10000,10000)
192    end
193    return head, start
194end
195
196methods[4] = function(head,start) -- - => - - -
197    local p, n = getboth(start)
198    if p and n then
199        local tmp
200        head, start, tmp = remove_node(head,start)
201        head, start = insertnodebefore(head,start,new_disc())
202        setattrlist(start,tmp)
203        setdisc(start,copy_node(tmp),copy_node(tmp),tmp)
204        insert_break(head,start,start,10000,10000)
205    end
206    return head, start
207end
208
209methods[5] = function(head,start,stop,settings) -- x => p q r
210    local p, n = getboth(start)
211    if p and n then
212        local tmp
213        head, start, tmp = remove_node(head,start)
214        head, start  = insertnodebefore(head,start,new_disc())
215        local attr   = getattrlist(tmp)
216        local font   = getfont(tmp)
217        local left   = settings.left
218        local right  = settings.right
219        local middle = settings.middle
220        if left then
221             left = tonodes(tostring(left),font,attr)
222        end
223        if right then
224             right = tonodes(tostring(right),font,attr)
225        end
226        if middle then
227            middle = tonodes(tostring(middle),font,attr)
228        end
229        setdisc(start,left,right,middle)
230        setattrlist(start,attr)
231        flushnode(tmp)
232        insert_break(head,start,start,10000,10000)
233    end
234    return head, start
235end
236
237-- we know we have a limited set
238-- what if characters are replaced by the font handler
239-- do we need to go into disc nodes (or do it as first step but then we need a pre/post font handler)
240
241function breakpoints.handler(head)
242    local done    = false
243    local attr    = nil
244    local map     = nil
245    local current = head
246    while current do
247        local char, id = isglyph(current)
248        if char then
249         -- local a = getattr(current,a_breakpoints)
250            local a = takeattr(current,a_breakpoints)
251            if a and a > 0 then
252                if a ~= attr then
253                    local data = mapping[a]
254                    if data then
255                        map = data.characters
256                    else
257                        map = nil
258                    end
259                    attr = a
260                end
261                if map then
262                    local cmap = map[char]
263                    if cmap then
264                     -- setattr(current,a_breakpoints,unsetvalue) -- should not be needed
265                        -- for now we collect but when found ok we can move the handler here
266                        -- although it saves nothing in terms of performance
267                        local lang = getlanguage(current)
268                        local smap = lang and lang >= 0 and lang < 0x7FFF and (cmap[languages.numbers[lang]] or cmap[""])
269                        if smap then
270                            local skip  = smap.skip
271                            local start = current
272                            local stop  = current
273                            current = getnext(current)
274                            if skip then
275                                while current do
276                                    local c = isglyph(current)
277                                    if c == char then
278                                        stop    = current
279                                        current = getnext(current)
280                                    else
281                                        break
282                                    end
283                                end
284                            end
285                            local d = { start, stop, cmap, smap, char }
286                            if done then
287                                done[#done+1] = d
288                            else
289                                done = { d }
290                            end
291                        else
292                            current = getnext(current)
293                        end
294                    else
295                        current = getnext(current)
296                    end
297                else
298                    current = getnext(current)
299                end
300            else
301                current = getnext(current)
302            end
303        elseif id == math_code then
304            attr    = nil
305            current = endofmath(current)
306            if current then
307                current = getnext(current)
308            end
309        else
310            current = getnext(current)
311        end
312    end
313    if not done then
314        return head
315    end
316    -- we have hits
317 -- local numbers = languages.numbers
318    for i=1,#done do
319        local data  = done[i]
320        local start = data[1]
321        local stop  = data[2]
322        local cmap  = data[3]
323        local smap  = data[4]
324        -- we do a sanity check for language
325     -- local lang  = getlanguage(start)
326     -- local smap = lang and lang >= 0 and lang < 0x7FFF and (cmap[numbers[lang]] or cmap[""])
327     -- if smap then
328            local nleft = smap.nleft
329            local cleft = 0
330            local prev  = getprev(start)
331            local kern  = nil
332            while prev and nleft ~= cleft do
333                local char, id = isglyph(prev)
334                if char then
335                    if not is_letter[char] then
336                        cleft = -1
337                        break
338                    end
339                    cleft = cleft + 1
340                    prev  = getprev(prev)
341                elseif id == kern_code then
342                    local s = getsubtype(prev)
343                    if s == fontkern_code or s == italickern_code then
344                        if cleft == 0 then
345                            kern = prev
346                            prev = getprev(prev)
347                        else
348                            break
349                        end
350                    else
351                        break
352                    end
353                else
354                    break
355                end
356            end
357            if nleft == cleft then
358                local nright = smap.nright
359                local cright = 0
360                local next   = getnext(stop) -- getnext(start)
361                while next and nright ~= cright do
362                    local char, id = isglyph(next)
363                    if char then
364                        if not is_letter[char] then
365                            cright = -1
366                            break
367                        end
368                        if cright == 1 and cmap[char] then
369                            -- let's not make it too messy
370                            break
371                        end
372                        cright = cright + 1
373                        next   = getnext(next)
374                    elseif id == kern_code then
375                        local s = getsubtype(next)
376                        if s == fontkern_code or s == italickern_code then
377                            if cleft == 0 then
378                                next = getnext(next)
379                            else
380                                break
381                            end
382                        else
383                            break
384                        end
385                    else
386                        break
387                    end
388                end
389                if nright == cright then
390                    local method = methods[smap.type]
391                    if method then
392                        head, start = method(head,start,stop,smap,kern)
393                    end
394                end
395         -- end
396        end
397    end
398    return head
399end
400
401local enabled = false
402
403function breakpoints.define(name)
404    local data = numbers[name]
405    if data then
406        -- error
407    else
408        local number = #mapping + 1
409        local data = {
410            name       = name,
411            number     = number,
412            characters = { },
413        }
414        mapping[number] = data
415        numbers[name]   = data
416    end
417end
418
419function breakpoints.setreplacement(name,char,language,settings)
420    char = utfbyte(char)
421    local data = numbers[name]
422    if data then
423        local characters = data.characters
424        local cmap = characters[char]
425        if not cmap then
426            cmap = { }
427            characters[char] = cmap
428        end
429        local left, right, middle = settings.left, settings.right, settings.middle
430        cmap[language or ""] = {
431            type   = tonumber(settings.type)   or 1,
432            nleft  = tonumber(settings.nleft)  or 1,
433            nright = tonumber(settings.nright) or 1,
434            left   = left   ~= "" and left     or nil,
435            right  = right  ~= "" and right    or nil,
436            middle = middle ~= "" and middle   or nil,
437            skip   = settings.range == v_yes,
438        } -- was { type or 1, before or 1, after or 1 }
439    end
440end
441
442function breakpoints.set(n)
443    if n == v_reset then
444        n = unsetvalue
445    else
446        n = mapping[n]
447        if not n then
448            n = unsetvalue
449        else
450            if not enabled then
451                if trace_breakpoints then
452                    report_breakpoints("enabling breakpoints handler")
453                end
454                enableaction("processors","typesetters.breakpoints.handler")
455            end
456            n = n.number
457        end
458    end
459    texsetattribute(a_breakpoints,n)
460end
461
462-- interface
463
464implement {
465    name      = "definebreakpoints",
466    actions   = breakpoints.define,
467    arguments = "string"
468}
469
470implement {
471    name      = "definebreakpoint",
472    actions   = breakpoints.setreplacement,
473    arguments = {
474        "string",
475        "string",
476        "string",
477        {
478            { "type", "integer" },
479            { "nleft", "integer" },
480            { "nright", "integer" },
481            { "right" },
482            { "left" },
483            { "middle" },
484            { "range" },
485        }
486    }
487}
488
489implement {
490    name      = "setbreakpoints",
491    actions   = breakpoints.set,
492    arguments = "string"
493}
494