trac-brk.lmt /size: 15 Kb    last modification: 2024-01-16 10:22
1local next, tonumber, tostring, type = next, tonumber, tostring, type
2local round, abs = math.round, math.abs
3
4local settings_to_array = utilities.parsers.settings_to_array
5local settings_to_hash  = utilities.parsers.settings_to_hash
6
7local nuts              = nodes.nuts
8local tonut             = nodes.tonut
9
10local newrule           = nuts.pool.virtualrule
11local insertbefore      = nuts.insertbefore
12local insertafter       = nuts.insertafter
13local hpack_string      = nuts.typesetters.tohpack
14local getwidth          = nuts.getwidth
15local getid             = nuts.getid
16local getprev           = nuts.getprev
17local getnext           = nuts.getnext
18local setwhd            = nuts.setwhd
19local getglue           = nuts.getglue
20local getlist           = nuts.getlist
21local setoffsets        = nuts.setoffsets
22
23local setcolor          = nodes.tracers.colors.set
24local settransparency   = nodes.tracers.transparencies.set
25
26local texgetnest        = tex.getnest
27
28local nodecodes         = nodes.nodecodes
29local fitnesscodes      = tex.fitnesscodes
30
31local penalty_code      = nodecodes.penalty
32local glue_code         = nodecodes.glue
33local disc_code         = nodecodes.disc
34local kern_code         = nodecodes.kern
35local math_code         = nodecodes.math
36
37local getnormalizedline = nuts.getnormalizedline
38local getdimensions     = nuts.dimensions
39local getnaturalhsize   = nuts.naturalhsize
40local setruledimensions = nuts.setruledimensions
41local getruledimensions = nuts.getruledimensions
42
43local breakpoints   = tracers.breakpoints or { }
44tracers.breakpoints = breakpoints
45
46local width      =      65536 / 2
47local height     =  9 * 65536
48local depth      =  3 * 65536
49local xoffset    =  1 * 65536
50local yoffset    = -3 * 65536
51
52local serials     = false
53local serial      = false
54local results     = false
55local usedfont    = false
56local nestlevel   = false
57local overloads   = false
58local overload    = false
59local breakpass   = 0
60local linenumber  = 0
61
62local report      = logs.reporter("linebreaks")
63
64local breakcodes  = tex.breakcodes
65
66local f_status    = string.formatters["[% 2i] b=%i d=%p p=%0.3f r=%0.3f (dt=%i) (%p)"]
67
68local maxbadness <const> = tex.magicconstants.maxcalculatedbadness
69local marginoffset       = 0
70local showdetails        = false
71
72local actions = {
73    [breakcodes.initialize] = function()
74        if texgetnest("ptr") == nestlevel then
75            report("start registering")
76            usedfont = nodes.visualizers.getusedfont()
77        end
78    end,
79    [breakcodes.start] = function(pass)
80        if texgetnest("ptr") == nestlevel then
81            report("tracing pass %i",pass)
82            if breakpass ~= pass then
83                breakpass  = pass
84                linenumber = 0
85                serials    = { }
86                results    = { }
87            end
88            serial  = { }
89            serials[#serials+1] = serial
90            if overloads then
91                overload = overloads[#serials]
92            end
93        end
94    end,
95    [breakcodes.report] = function(pass,currentserial,previousserial,line,kind,class,demerits,breakpoint,short,glue,width)
96        if texgetnest("ptr") == nestlevel then
97            if breakpoint then
98                local s = {
99                    serial     = currentserial,
100                    previous   = previousserial,
101                    line       = line,
102                    kind       = kind,
103                    class      = class,
104                    demerits   = demerits,
105                    breakpoint = breakpoint,
106                    short      = short,
107                    width      = width,
108                    spillover  = 0,
109                }
110                if short < 0 then
111                    short = -short
112                    if short > glue then
113                        local spillover = short - glue
114                        report("pass %i, line %i, %s by %p",pass,line,"overfull",spillover)
115                        s.spillover = splillover
116                    end
117                elseif short > 0 then
118                    if short > glue then
119                        local spillover = short - glue
120                        report("pass %i, line %i, %s by %p",pass,line,"underfull",spillover)
121                        s.spillover = splillover
122                    end
123                end
124                serial[currentserial] = s
125                local found = overload and overload[currentserial]
126                if found then
127                    report("pass %i, overloading serial %i demerits from %i to %i",breakpass,currentserial,demerits,found)
128                    s.overload = found
129                    return found
130                end
131            end
132        end
133        return demerits
134    end,
135    [breakcodes.stop] = function()
136        if texgetnest("ptr") == nestlevel then
137            report("stop registering")
138        end
139    end,
140    [breakcodes.collect] = function()
141        if texgetnest("ptr") == nestlevel then
142            report("collecting")
143            for currentserial=1,#serial do
144                local data       = serial[currentserial]
145                local breakpoint = data.breakpoint
146                local current    = tonut(breakpoint)
147                local trigger    = getid(current)
148                while current do
149                    local id = getid(current)
150                    if id == penalty_code or id == glue_code or id == kern_code or id == math_code then
151                        current = getprev(current)
152                    else
153                        break
154                    end
155                end
156                if current then
157                    local rule   = newrule(width,height,depth,currentserial)
158                    data.rule    = rule
159                    data.trigger = trigger
160                    insertafter(current,current,rule)
161                end
162            end
163        end
164    end,
165    [breakcodes.line] = function(line,badness,overshoot,shrink,stretch)
166        if showdetails and texgetnest("ptr") == nestlevel then
167            linenumber = linenumber + 1
168            line = tonut(line)
169            local linedata   = getnormalizedline(line,true) -- get more details
170            local natural    = linedata.size
171            local linewd     = linedata.width
172            local deltawd    = linewd - natural
173            local splillover = linedata.spillover or 0
174            local ratiob     = 0
175            local ratio, order, sign
176                             = getglue(line)
177            local yoffset    = 2*xoffset
178            local xoffset    = 5*xoffset + linedata.right + marginoffset
179            if order == 0 then
180                if deltawd ~= 0 and ratio == 0 and sign == 0 then
181                    xoffset = xoffset + deltawd
182                    ratiob = 10000
183                elseif deltawd < 0 and ratio == 1 then
184                    ratiob = 1000000
185                else -- or test for ratio as maxbadness is 8189
186                    ratiob = round(100*abs(ratio)^3)
187                    if ratiob > maxbadness then
188                        ratiob = 10000
189                    end
190                end
191            end
192            -- Here ratio is the value from Digital Typography and a difference
193            -- indicates a sensitive area. It is just a playground for MS and HH.
194            local text     = hpack_string(f_status(linenumber,badness,deltawd,100*deltawd/linewd,ratio,ratiob),usedfont,spillover)
195            local location = linedata.last
196            local head     = linedata.head
197            setwhd(text,0,0,0)
198            setoffsets(text,xoffset,yoffset)
199            insertbefore(head,location,text)
200            deltawd = deltawd - linedata.parinitrightskip - linedata.parfillrightskip
201            local rule = newrule(deltawd < 0 and - deltawd or deltawd,0,depth)
202            setoffsets(rule,deltawd > 0 and -deltawd or 0,yoffset+depth)
203            setcolor(rule,"trace:dy")
204            settransparency(rule,"trace:dy")
205            insertbefore(head,location,rule)
206        end
207    end,
208    [breakcodes.list] = function(currentserial)
209        if texgetnest("ptr") == nestlevel then
210            local s = serial[currentserial]
211            if s then
212                s.final = true
213            end
214        end
215    end,
216    [breakcodes.delete] = function(currentserial)
217        -- eventually most are pruned
218        if texgetnest("ptr") == nestlevel then
219            local s = serial[currentserial]
220            if s then
221                s.deleted = true
222            end
223        end
224    end,
225    [breakcodes.wrapup] = function()
226        if texgetnest("ptr") == nestlevel then
227            report("wrapping up")
228            if serial then
229                local result = { }
230                results[#results+1] = result
231                for currentserial=1,#serial do
232                    local data = serial[currentserial]
233                    local rule = data.rule
234                    if rule then
235                        local text    = hpack_string(tostring(currentserial),usedfont)
236                        local size    = getwidth(text)
237                        local trigger = data.trigger
238                        local final   = data.final
239                        local class   = data.class
240                        local color   = "trace:ds"
241                        if trigger == penalty_code then
242                            color = "trace:dr"
243                        elseif trigger == glue_code then
244                            color = "trace:dg"
245                        elseif trigger == disc_code then
246                            color = "trace:db"
247                        end
248                        if final then
249                            local width, height, depth = getruledimensions(rule)
250                            setruledimensions(rule,2*width,height,depth)
251                        end
252                        setcolor(rule,color)
253                        setwhd(text,0,0,0)
254                        setoffsets(text,-size-xoffset,yoffset)
255                        insertafter(rule,rule,text)
256                        result[#result+1] = data
257                        data.trigger = nodecodes[trigger]
258                        data.class   = fitnesscodes[class]
259                        data.color   = color
260                    end
261                end
262                --
263                -- we could limit to the last line only
264                --
265                local finals = { }
266                for currentserial=#serial,1,-1 do
267                    if serial[currentserial].final then
268                        for finalserial=currentserial-1,currentserial+1 do
269                            local data = serial[finalserial]
270                            if data then
271                                local line = data.line
272                                local list = { }
273                                local done = {
274                                    serial = finalserial,
275                                    line   = line,
276                                    list   = list,
277                                    final  = data.final,
278                                    color  = data.color
279                                }
280                                finals[#finals+1] = done
281                                while true do
282                                    local previous = data.previous
283                                    if previous == 0 then
284                                        break
285                                    else
286                                        list[#list+1] = previous
287                                        data = serial[previous]
288                                    end
289                                end
290                            end
291                        end
292                        break
293                    end
294                end
295
296                result.finals = finals
297                --
298                serial = false
299            end
300        end
301    end,
302}
303
304-- -- --
305
306function breakpoints.start(specification)
307    callback.register("show_break", function(what,...)
308        return actions[what](...)
309    end)
310    --
311    local list   = specification.list
312    local option = specification.option
313    --
314    breakpass    = 0
315    linenumber   = 0
316    showdetails  = false
317    serials      = { }
318    results      = { }
319    nestlevel    = texgetnest("ptr")
320    marginoffset = specification.offset or 0
321    --
322    if option then
323        option = type(options) == "table" and option or settings_to_hash(option or "")
324        if option.margin then
325            showdetails = true
326        end
327    end
328    if list then
329        overloads = type(list) == "table" and list or settings_to_array(list or "")
330        for i=1,#overloads do
331            local s = overloads[i]
332            local t = settings_to_hash(s)
333            local o = { }
334            for k, v in next, t do
335                o[tonumber(k)] = tonumber(v) or -1
336            end
337            overloads[i] = o
338        end
339    end
340end
341
342function breakpoints.stop()
343    callback.register("show_break")
344end
345
346function breakpoints.reset()
347    serials    = false
348    serial     = false
349    results    = false
350    nestlevel  = false
351    breakpass  = 0
352    linenumber = 0
353end
354
355function breakpoints.getresults()
356   return results or { }
357end
358
359function breakpoints.nofresults()
360   return results and #results or 0
361end
362
363function breakpoints.typesetresult(n)
364    local context, ctx_NC, ctx_NR, ctx_color = context, context.NC, context.NR, context.color
365
366    local function typeset(result)
367        if result then
368            local lastline = 0
369         -- context.starttabulate { "|r|c|r|r|r|l|l|c|" }
370            context.starttabulate { "|r|r|r|r|l|l|c|" }
371            for i=1,#result do
372                local r        = result[i]
373                local serial   = r.serial
374                local previous = r.previous
375                local line     = r.line
376                local final    = r.final
377             -- local deleted  = r.deleted
378                local overload = r.overload
379                local demerits = overload or r.demerits
380                local trigger  = r.trigger
381                local class    = r.class
382                local short    = r.short
383                local color    = { r.color } -- cache
384                ctx_NC() if line ~= lastline then context(line) lastline = line end
385             -- ctx_NC() if deleted then context.textminus() end
386                ctx_NC() if final then ctx_color(color,serial) else context(serial) end
387                ctx_NC() context(previous)
388                ctx_NC() if final then ctx_color(color,demerits) else context(demerits) end
389                ctx_NC() if final then ctx_color(color,class) else context(class) end
390                ctx_NC() ctx_color(color,trigger)
391                ctx_NC() if overload then if final then ctx_color(color,"!") else context("!") end end
392                ctx_NC() ctx_NR()
393            end
394            context.stoptabulate()
395            --
396            local finals = result.finals
397            if finals then
398                context.starttabulate { "|r|l|" }
399                for i=1,#finals do
400                    local data   = finals[i]
401                    local final  = data.final
402                    local serial = data.serial
403                    local path   = table.concat(data.list," ")
404                    local color  = final and data.color
405                    if color then
406                        color = { color }
407                    end
408                    ctx_NC() if color then ctx_color(color,serial) else context(serial) end
409                    ctx_NC() if color then ctx_color(color,path) else context(path) end
410                    ctx_NC() ctx_NR()
411                end
412                context.stoptabulate()
413            end
414        end
415    end
416
417    local results = breakpoints.getresults()
418    if n then
419        typeset(results[n])
420    else
421        for i=1,#results do
422            typeset(results[i])
423        end
424    end
425end
426