trac-brk.lmt /size: 22 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['trac-brk'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to trac-brk.mkxl",
5    author    = "Hans Hagen & Mikael Sundqvist",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10local next, tonumber, tostring, type = next, tonumber, tostring, type
11local round, abs = math.round, math.abs
12local formatters = string.formatters
13
14local settings_to_array = utilities.parsers.settings_to_array
15local settings_to_hash  = utilities.parsers.settings_to_hash
16
17local nuts              = nodes.nuts
18local tonut             = nodes.tonut
19
20local newrule           = nuts.pool.virtualrule
21local insertbefore      = nuts.insertbefore
22local insertafter       = nuts.insertafter
23local hpack_string      = nuts.typesetters.tohpack
24local getwidth          = nuts.getwidth
25local getid             = nuts.getid
26local getprev           = nuts.getprev
27local getnext           = nuts.getnext
28local setwhd            = nuts.setwhd
29local getglue           = nuts.getglue
30local getlist           = nuts.getlist
31local setoffsets        = nuts.setoffsets
32local setattr           = nuts.setattr
33
34local setcolor          = nodes.tracers.colors.set
35local settransparency   = nodes.tracers.transparencies.set
36
37local texgetnest        = tex.getnest
38
39local nodecodes         = nodes.nodecodes
40----- fitnesscodes      = tex.fitnesscodes
41
42local penalty_code      <const> = nodecodes.penalty
43local glyph_code        <const> = nodecodes.glyph
44local glue_code         <const> = nodecodes.glue
45local disc_code         <const> = nodecodes.disc
46local kern_code         <const> = nodecodes.kern
47local math_code         <const> = nodecodes.math
48
49local a_tagged          <const> = attributes.private('tagged')
50
51local getnormalizedline = nuts.getnormalizedline
52local getdimensions     = nuts.dimensions
53local getnaturalhsize   = nuts.naturalhsize
54local setruledimensions = nuts.setruledimensions
55local getruledimensions = nuts.getruledimensions
56
57local breakpoints   = tracers.breakpoints or { }
58tracers.breakpoints = breakpoints
59
60local width      =      65536 / 2
61local height     =  9 * 65536
62local depth      =  3 * 65536
63local xoffset    =  1 * 65536
64local yoffset    = -3 * 65536
65
66local serials        = false
67local serial         = false
68local results        = false
69local usedfont       = false
70local nestlevel      = false
71local overloads      = false
72local overload       = false
73local breakpass      = 0
74local breaksubpass   = 0
75local breaksubpasses = 0
76local linenumber     = 0
77local decentfit      = 0
78
79local fitnesscodes = {
80    [4] = {
81        "veryloose",
82        "loose",
83        "decent",
84        "tight",
85    },
86    [5] = {
87        "veryloose",
88        "loose",
89        "decent",
90        "tight",
91        "verytight",
92    },
93    [9] = {
94        "veryloose",
95        "loose",
96        "almostloose",
97        "barelyloose",
98        "decent",
99        "barelytight",
100        "almosttight",
101        "tight",
102        "verytight",
103    },
104}
105
106local report      = logs.reporter("linebreaks")
107local breakcodes  = tex.breakcodes
108
109local f_detail  = formatters["[%02i] b=%i d=%p p=%0.3f r=%0.3f (dt=%i) (%p) from s=%i b=%i"]
110local f_simple  = formatters["[%02i] b=%i from s=%i b=%i"]
111local f_badness = formatters["[%02i] b=%i"]
112
113local maxbadness <const> = tex.magicconstants.maxcalculatedbadness
114local marginoffset       = 0
115local showdetails        = false
116local showsimple         = false
117local showrefcount       = true
118
119local showactions = {
120    [breakcodes.initialize] = function(checks,subpasses)
121        if texgetnest("ptr") == nestlevel then
122            report("initialize")
123            usedfont       = nodes.visualizers.getusedfont()
124            breaksubpasses = subpasses
125        end
126    end,
127    [breakcodes.start] = function(checks,pass,subpass,classes,decent)
128        if texgetnest("ptr") == nestlevel then
129            report("start pass %i, subpass %i",pass,subpass)
130            if breakpass ~= pass and breaksubpass ~= subpass then
131                breakpass    = pass
132                breaksubpass = subpass
133                linenumber   = 0
134                decentfit    = decent
135                serials      = { }
136                results      = { }
137            end
138            serial = { }
139            serials[#serials+1] = serial
140            if overloads then
141                overload = overloads[#serials]
142            end
143        end
144    end,
145    [breakcodes.report] = function(checks,pass,subpass,currentserial,previousserial,line,kind,class,classes,badness,demerits,breakpoint,short,glue,width)
146        if texgetnest("ptr") == nestlevel then
147            if breakpoint then
148                local s = {
149                    serial     = currentserial,
150                    previous   = previousserial,
151                    line       = line,
152                    kind       = kind,
153                    class      = class,
154                    classes    = classes,
155                    badness    = badness,
156                    demerits   = demerits,
157                    breakpoint = breakpoint,
158                    short      = short,
159                    width      = width,
160                    spillover  = 0,
161                    refcount   = 0,
162                }
163                if short < 0 then
164                    short = -short
165                    if short > glue then
166                        local spillover = short - glue
167                        report("pass %i, subpass %i, line %i, %s by %p",pass,subpass,line,"overfull",spillover)
168                        s.spillover = splillover
169                    end
170                elseif short > 0 then
171                    if short > glue then
172                        local spillover = short - glue
173                        report("pass %i, subpass %i, line %i, %s by %p",pass,subpass,line,"underfull",spillover)
174                        s.spillover = splillover
175                    end
176                end
177                serial[currentserial] = s
178                local found = overload and overload[currentserial]
179                if found then
180                    report("pass %i, subpass %i, overloading serial %i demerits from %i to %i",pass,subpass,currentserial,demerits,found)
181                    s.overload = found
182                    return found
183                end
184            end
185        end
186        return demerits
187    end,
188    [breakcodes.stop] = function(checks,demerits)
189        if texgetnest("ptr") == nestlevel then
190            report("stop with demerits %i",demerits)
191        end
192    end,
193    [breakcodes.collect] = function()
194        if texgetnest("ptr") == nestlevel then
195            report("collect")
196            for currentserial=1,#serial do
197                local data       = serial[currentserial]
198                local breakpoint = data.breakpoint
199                local current    = tonut(breakpoint)
200                local trigger    = getid(current)
201                while current do
202                    local id = getid(current)
203                    if id == penalty_code or id == glue_code or id == kern_code or id == math_code then
204                        current = getprev(current)
205                    else
206                        break
207                    end
208                end
209                if current then
210                    local rule   = newrule(width,height,depth,currentserial)
211                    data.rule    = rule
212                    data.trigger = trigger
213                    insertafter(current,current,rule)
214                end
215            end
216        end
217    end,
218    [breakcodes.line] = function(checks,line,badness,overshoot,shrink,stretch,linum,ser)
219        if showdetails and texgetnest("ptr") == nestlevel then
220            linenumber = linenumber + 1
221            line = tonut(line)
222            local linedata   = getnormalizedline(line,true) -- get more details
223            local natural    = linedata.size
224            local linewd     = linedata.width
225            local deltawd    = linewd - natural
226            local splillover = linedata.spillover or 0
227            local ratiob     = 0
228            local ratio, order, sign
229                             = getglue(line)
230            local yoffset    = 2*xoffset
231            local xoffset    = 5*xoffset + linedata.right + marginoffset
232            local serialdata = serial[ser] -- what if we have multiple pars
233            if order == 0 then
234                if deltawd ~= 0 and ratio == 0 and sign == 0 then
235                    xoffset = xoffset + deltawd
236                    ratiob = 10000
237                elseif deltawd < 0 and ratio == 1 then
238                    ratiob = 1000000
239                else -- or test for ratio as maxbadness is 8189
240                    ratiob = round(100*abs(ratio)^3)
241                    if ratiob > maxbadness then
242                        ratiob = 10000
243                    end
244                end
245            end
246            -- Here ratio is the value from Digital Typography and a difference
247            -- indicates a sensitive area. It is just a playground for MS and HH.
248            -- For instance, applied expansion can influence the badness.
249            local sbadness = serialdata and serialdata.badness or 0
250            local text     = showsimple == 1 and f_simple(linenumber,badness,ser,sbadness)  or
251                             showsimple == 2 and f_badness(linenumber,sbadness) or
252                                                 f_detail(linenumber,badness,deltawd,100*deltawd/linewd,ratio,ratiob,spillover,ser,sbadness)
253            local location = linedata.last
254            local head     = linedata.head
255            text = hpack_string(text,usedfont,spillover)
256            setattr(text,a_tagged,0)
257            setwhd(text,0,0,0)
258            setoffsets(text,xoffset,yoffset)
259            insertbefore(head,location,text)
260            deltawd = deltawd - linedata.parinitrightskip - linedata.parfillrightskip
261            local rule = newrule(deltawd < 0 and - deltawd or deltawd,0,depth)
262            setoffsets(rule,deltawd > 0 and -deltawd or 0,yoffset+depth)
263            setcolor(rule,"trace:6") -- darkyellow
264            settransparency(rule,"trace:6") -- darkyellow
265            insertbefore(head,location,rule)
266        end
267    end,
268    [breakcodes.list] = function(checks,currentserial,refcount)
269        if texgetnest("ptr") == nestlevel then
270            local s = serial[currentserial]
271            if s then
272                s.final    = true
273--                 s.refcount = refcount -- can be messed up so we set it again
274            end
275        end
276    end,
277    [breakcodes.delete] = function(checks,currentserial,refcount)
278        if texgetnest("ptr") == nestlevel then
279            local s = serial[currentserial]
280            if s then
281                s.refcount = refcount
282            end
283        end
284    end,
285    [breakcodes.wrapup] = function(checks,demerits,looseness)
286        if texgetnest("ptr") == nestlevel then
287            report("wrapup")
288            if serial then
289                local finals = { }
290                local result = {
291                    looseness = looseness,
292                    demerits  = demerits,
293                    finals    = finals,
294                    pass      = breakpass,
295                    subpass   = breaksubpass,
296                    subpasses = breaksubpasses,
297                }
298                results[#results+1] = result
299                for currentserial=1,#serial do
300                    local data = serial[currentserial]
301                    local rule = data.rule
302                    if rule then
303                        local text    = hpack_string(tostring(currentserial),usedfont)
304                        local size    = getwidth(text)
305                        local trigger = data.trigger
306                        local final   = data.final
307                        local class   = data.class
308                        local codes   = fitnesscodes[data.classes]
309                        local color   = "trace:0" -- darkgray
310                        if trigger == penalty_code then
311                            color = "trace:1" -- darkred
312                        elseif trigger == glue_code then
313                            color = "trace:2" -- darkgreen
314                        elseif trigger == disc_code then
315                            color = "trace:3" -- darkblue
316                        end
317                        if final then
318                            local width, height, depth = getruledimensions(rule)
319                            setruledimensions(rule,2*width,height,depth)
320                        end
321                        setattr(text,a_tagged,0)
322                        setcolor(rule,color)
323                        setwhd(text,0,0,0)
324                        setoffsets(text,-size-xoffset,yoffset)
325                        insertafter(rule,rule,text)
326                        result[#result+1] = data
327                        data.trigger = nodecodes[trigger]
328                        data.class   = codes and codes[class] or ("class " .. class)
329                        data.color   = color
330                    end
331                end
332                --
333                if #serial > 0 then
334                    local lastline = serial[#serial].line
335                    for currentserial=#serial,1,-1 do
336                        local data = serial[currentserial]
337                        if data.refcount == 0 then
338                            local list = { }
339                            local done = {
340                                serial = currentserial,
341                                line   = data.line,
342                                list   = list,
343                                final  = data.final,
344                                color  = data.color
345                            }
346                            finals[#finals+1] = done
347                            while true do
348                                local previous = data.previous
349                                if previous == 0 then
350                                    break
351                                else
352                                    list[#list+1] = previous
353                                    data = serial[previous]
354                                end
355                            end
356                        else
357                            break
358                        end
359                    end
360                end
361                table.reverse(finals)
362                --
363                serial = false
364            end
365        end
366    end,
367}
368
369-- -- --
370
371-- local enabled = false
372
373local function check_options(option)
374    if option then
375        option = type(option) == "table" and option or settings_to_hash(option or "")
376        if option.margin then
377            showdetails = true
378        end
379        if option.simple then
380            showsimple = 1
381        elseif option.badness then
382            showsimple = 2
383        end
384        if option.compact then
385            showrefcount = false
386        end
387    end
388end
389
390function breakpoints.start(specification)
391    local list   = specification.list
392    breakpass    = 0
393    linenumber   = 0
394    showdetails  = false
395    showsimple   = false
396    showrefcount = true
397    serials      = { }
398    results      = { }
399    nestlevel    = texgetnest("ptr") + (tonumber(specification.level) or 0) -- hack
400    marginoffset = specification.offset or 0
401    check_options(specification.option)
402    if list then
403        overloads = type(list) == "table" and list or settings_to_array(list or "")
404        for i=1,#overloads do
405            local s = overloads[i]
406            local t = settings_to_hash(s)
407            local o = { }
408            for k, v in next, t do
409                o[tonumber(k)] = tonumber(v) or -1
410            end
411            overloads[i] = o
412        end
413    end
414end
415
416function breakpoints.stop()
417--     enabled = false
418end
419
420nodes.handlers.linebreakchecks[1] = function(what,...)
421    return showactions[what](...)
422end
423
424function breakpoints.reset()
425    serials    = false
426    serial     = false
427    results    = false
428    nestlevel  = false
429    breakpass  = 0
430    linenumber = 0
431end
432
433function breakpoints.getresults()
434   return results or { }
435end
436
437function breakpoints.nofresults()
438   return results and #results or 0
439end
440
441function breakpoints.typesetresult(n,option)
442    local context, ctx_NC, ctx_NR, ctx_EQ, ctx_color = context, context.NC, context.NR, context.EQ, context.color
443
444    check_options(option)
445
446    local function typeset(result)
447        if result then
448            local lastline = 0
449            context.starttabulate { showrefcount and "|r|rS|r|r|r|r|l|l|c|" or "|r|r|r|r|r|l|l|c|" }
450            for i=1,#result do
451                local r        = result[i]
452                local serial   = r.serial
453                local previous = r.previous
454                local line     = r.line
455                local final    = r.final
456                local refcount = r.refcount or 0
457                local overload = r.overload
458                local demerits = overload or r.demerits
459                local badness  = r.badness
460                local trigger  = r.trigger
461                local class    = r.class
462                local short    = r.short
463                local color    = { r.color } -- cache
464                ctx_NC() if line ~= lastline then lastline = line                              context(line)     end
465                if showrefcount then
466                ctx_NC() if refcount > 0     then if final then ctx_color(color,refcount) else context(refcount) end end
467                end
468                ctx_NC()                          if final then ctx_color(color,serial)   else context(serial)   end
469                ctx_NC()                          if final then ctx_color(color,previous) else context(previous) end
470                ctx_NC()                          if final then ctx_color(color,badness)  else context(badness)  end
471                ctx_NC()                          if final then ctx_color(color,demerits) else context(demerits) end
472                ctx_NC()                          if final then ctx_color(color,class)    else context(class)    end
473                ctx_NC()                                        ctx_color(color,trigger)
474                ctx_NC() if overload         then if final then ctx_color(color,"!")      else context("!")      end end
475                ctx_NC() ctx_NR()
476            end
477            context.stoptabulate()
478            --
479            local finals = result.finals
480            if finals then
481                -- We could limit the number ... let's see what MS asks for that feature.
482                context.starttabulate { "|r|lp|" }
483                for i=1,#finals do
484                    local data   = finals[i]
485                    local final  = data.final
486                    local serial = data.serial
487                    local path   = table.concat(data.list," ")
488                    local color  = final and data.color
489                    if color then
490                        color = { color }
491                    end
492                    ctx_NC() if color then ctx_color(color,serial) else context(serial) end
493                    ctx_NC() if color then ctx_color(color,path) else context(path) end
494                    ctx_NC() ctx_NR()
495                end
496                context.stoptabulate()
497                local passname = {
498                    [-2] = "P",
499                    [-1] = "T",
500                    [ 0] = "E",
501                }
502                context.starttabulate { "|l|r|l|r|" }
503                    local pass      = result.pass
504                    local subpasses = result.subpasses
505                    local subpass   = result.subpass
506                    if subpasses == 0 then
507                        subpass = passname[subpass] or subpass
508                    end
509                    ctx_NC() context("pass")      ctx_EQ() context(pass)
510                    ctx_NC() context("demerits")  ctx_EQ() context(result.demerits)  ctx_NC() ctx_NR()
511                    ctx_NC() context("subpass")   ctx_EQ() context(subpass)
512                    ctx_NC() context("looseness") ctx_EQ() context(result.looseness) ctx_NC() ctx_NR()
513                    ctx_NC() context("subpasses") ctx_EQ() context(subpasses)
514                    ctx_NC()                      ctx_NC()                           ctx_NC() ctx_NR()
515                context.stoptabulate()
516            end
517        end
518    end
519
520    local results = breakpoints.getresults()
521    if n then
522        typeset(results[n])
523    else
524        for i=1,#results do
525            typeset(results[i])
526        end
527    end
528end
529
530-- bonus
531
532function mp.show_breakpoints(dx,dy,sx,sy,lw)
533    local results = breakpoints.getresults()[1]
534    local breaks  = { }
535    local colors  = {
536        none    = "black",
537        glue    = "darkgreen",
538        disc    = "darkblue",
539        penalty = "darkred",
540        math    = "darkgray",
541        lines   = "darkgray",
542    }
543    local everything = {
544        formatters["numeric dx ; dx := %N ;"](dx),
545        formatters["numeric dy ; dy := %N ;"](dy),
546        formatters["numeric sx ; sx := %N ;"](sx),
547        formatters["numeric sy ; sy := %N ;"](sy),
548        formatters["numeric lw ; lw := %N ;"](lw),
549        formatters["pickup pencircle scaled lw ;"](),
550    }
551    results[0] = {
552        final   = true,
553        serial  = 0,
554        trigger = "none"
555    }
556    do
557        local line = 0
558        local slot = 0
559        breaks[0] = { 0, 0 }
560        for i=1,#results do
561            local r = results[i]
562            local l = r.line
563            if l ~= line then
564                slot = 0
565                line = l
566            else
567                slot = slot + 1
568            end
569            breaks[i] = { slot, -l }
570        end
571    end
572    do
573        local result = { }
574        for i=1,#results do
575            local r        = results[i]
576            local previous = r.previous
577            local p1       = breaks[i]
578            local p2       = breaks[previous]
579            if p1 and p2 then
580                result[#result+1] = formatters["((%N,%N)--(%N,%N))"](p1[1],p1[2],p2[1],p2[2])
581            end
582        end
583        everything[#everything+1] = formatters[ [[draw (% && t) xyscaled (dx,dy) withcolor "%s" ;]] ](result,colors.lines)
584    end
585    do
586        local result = { }
587        for i=0,#results do
588            local r = results[i]
589            if r.final then
590                local previous = r.previous
591                if previous and results[previous] and results[previous].final then
592                    local p1 = breaks[i]
593                    local p2 = breaks[previous]
594                    if p1 and p2 then
595                        result[#result+1] = formatters["((%N,%N)--(%N,%N))"](p1[1],p1[2],p2[1],p2[2])
596                    end
597                end
598            end
599        end
600        everything[#everything+1] = formatters[ [[draw (% && t) xyscaled (dx,dy) withcolor "white" ;]] ](result)
601        everything[#everything+1] = formatters[ [[draw (% && t) xyscaled (dx,dy) dashed (evenly scaled .5lw) withcolor "black" ;]] ](result)
602    end
603    for i=0,#results do
604        local p = breaks[i]
605        if p then
606            local r = results[i]
607            everything[#everything+1] = formatters[ [[draw (%N*dx,%N*dy) withpen pencircle xyscaled(sx,sy) withcolor "%s" ;]] ](p[1],p[2],colors[r.trigger])
608            everything[#everything+1] = formatters[ [[draw textext("\ttxx%i") shifted (%N*dx,%N*dy) withcolor "white" ;]] ](r.serial,p[1],p[2])
609        end
610    end
611    --
612    return table.concat(everything,"\n")
613end
614