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