typo-bld.lmt /size: 16 Kb    last modification: 2025-02-21 11:03
1if modules then modules = { } end modules ['typo-bld'] = { -- was node-par
2    version   = 1.001,
3    comment   = "companion to typo-bld.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-- no need for nuts in the one-line demo (that might move anyway)
10
11local next = next
12local insert, remove, sort = table.insert, table.remove, table.sort
13
14builders                 = builders or { }
15local builders           = builders
16
17builders.paragraphs      = builders.paragraphs or { }
18local parbuilders        = builders.paragraphs
19
20parbuilders.constructors = parbuilders.constructors or { }
21local       constructors = parbuilders.constructors
22
23builders.hpack           = builders.hpack or { }
24builders.vpack           = builders.vpack or { }
25
26local hpackbuilders      = builders.hpack
27local vpackbuilders      = builders.vpack
28
29constructors.names       = constructors.names or { }
30local names              = constructors.names
31
32constructors.numbers     = constructors.numbers or { }
33local numbers            = constructors.numbers
34
35constructors.methods     = constructors.methods or { }
36local methods            = constructors.methods
37
38local a_parbuilder       <const> = attributes.numbers['parbuilder'] or 999 -- why 999
39constructors.attribute   = a_parbuilder
40
41local unsetvalue         <const> = attributes.unsetvalue
42
43local texsetattribute    = tex.setattribute
44local texnest            = tex.nest
45local texget             = tex.get
46local texset             = tex.set
47
48local getspeciallist     = nodes.nuts.getspeciallist
49local setspeciallist     = nodes.nuts.setspeciallist
50
51local nodes              = nodes
52local nodeidstostring    = nodes.idstostring
53local nodepool           = nodes.pool
54local new_baselineskip   = nodepool.baselineskip
55local new_lineskip       = nodepool.lineskip
56local insertnodebefore   = nodes.insertbefore
57local hpack_node         = nodes.hpack
58
59local nuts               = nodes.nuts
60local tonode             = nodes.tonode
61local tonut              = nodes.tonut
62local getattr            = nuts.getattr
63local flush              = nuts.flush
64
65local starttiming        = statistics.starttiming
66local stoptiming         = statistics.stoptiming
67
68local registercallback   = callbacks.register
69
70storage.register("builders/paragraphs/constructors/names",   names,   "builders.paragraphs.constructors.names")
71storage.register("builders/paragraphs/constructors/numbers", numbers, "builders.paragraphs.constructors.numbers")
72
73local trace_page_builder = false  trackers.register("builders.page", function(v) trace_page_builder = v end)
74local trace_vbox_builder = false  trackers.register("builders.vbox", function(v) trace_vbox_builder = v end)
75local trace_post_builder = false  trackers.register("builders.post", function(v) trace_post_builder = v end)
76
77local report_page_builder = logs.reporter("builders","page")
78local report_vbox_builder = logs.reporter("builders","vbox")
79local report_par_builder  = logs.reporter("builders","par")
80
81local mainconstructor = nil -- not stored in format
82local nofconstructors = 0
83local stack           = { }
84
85function constructors.define(name)
86    nofconstructors = nofconstructors + 1
87    names[nofconstructors] = name
88    numbers[name] = nofconstructors
89end
90
91function constructors.set(name) --- will go
92    if name then
93        mainconstructor = numbers[name] or unsetvalue
94    else
95        mainconstructor = stack[#stack] or unsetvalue
96    end
97    texsetattribute(a_parbuilder,mainconstructor)
98    if mainconstructor ~= unsetvalue then
99        constructors.enable()
100    end
101end
102
103function constructors.start(name)
104    local number = numbers[name]
105    insert(stack,number)
106    mainconstructor = number or unsetvalue
107    texsetattribute(a_parbuilder,mainconstructor)
108    if mainconstructor ~= unsetvalue then
109        constructors.enable()
110    end
111 -- report_par_builder("start %a",name)
112end
113
114function constructors.stop()
115    remove(stack)
116    mainconstructor = stack[#stack] or unsetvalue
117    texsetattribute(a_parbuilder,mainconstructor)
118    if mainconstructor == unsetvalue then
119        constructors.disable()
120    end
121 -- report_par_builder("stop")
122end
123
124-- return values:
125--
126-- true  : tex will break itself
127-- false : idem but dangerous
128-- head  : list of valid vmode nodes with last being hlist
129
130function constructors.handler(head,followed_by_display)
131    if type(head) == "boolean" then
132        return head
133    else
134        local attribute = getattr(head,a_parbuilder) -- or mainconstructor
135        if attribute then
136            local method = names[attribute]
137            if method then
138                local handler = methods[method]
139                if handler then
140                    return handler(head,followed_by_display)
141                else
142                    report_par_builder("contructor method %a is not defined",tostring(method))
143                    return true -- let tex break
144                end
145            end
146        end
147        return true -- let tex break
148    end
149end
150
151-- just for testing
152
153function constructors.methods.default(head,followed_by_display)
154    return true -- let tex break
155end
156
157-- also for testing (now also surrounding spacing done)
158
159function parbuilders.constructors.methods.oneline(head,followed_by_display)
160    -- when needed we will turn this into a helper
161    local t = texnest[texnest.ptr]
162    local h = hpack_node(head)
163    local d = texget("baselineskip",false) - t.prevdepth - h.height
164    t.prevdepth = h.depth
165    t.prevgraf  = 1
166    if d < texget("lineskiplimit") then
167        return insertnodebefore(h,h,new_lineskip(texget("lineskip",false))) -- no stretch etc
168    else
169        return insertnodebefore(h,h,new_baselineskip(d))
170    end
171end
172
173-- It makes no sense to have a sequence here as we already have
174-- pre and post hooks and only one parbuilder makes sense, so no:
175--
176-- local actions = nodes.tasks.actions("parbuilders")
177--
178-- yet ... maybe some day.
179
180local actions = constructors.handler
181local enabled = false
182
183local function processor(head,followed_by_display)
184    -- todo: not again in otr so we need to flag
185    if enabled then
186        starttiming(parbuilders)
187        head = tonode(actions(tonut(head),followed_by_display))
188        stoptiming(parbuilders)
189        return head
190    else
191        return true -- let tex do the work
192    end
193end
194
195function constructors.enable () enabled = true  end
196function constructors.disable() enabled = false end
197
198registercallback("linebreak_filter", processor, "breaking paragraps into lines")
199
200statistics.register("linebreak processing time", function()
201    return statistics.elapsedseconds(parbuilders)
202end)
203
204-- todo: move from nodes.builders to builders
205
206nodes.builders = nodes.builder or { }
207local builders = nodes.builders
208
209local vpackactions = nodes.tasks.actions("vboxbuilders")
210
211function builders.vpack_filter(head,groupcode,size,packtype,maxdepth,direction)
212    if head then
213        starttiming(builders)
214        head = vpackactions(head,groupcode)
215        stoptiming(builders)
216    end
217    return head
218end
219
220-- This one is special in the sense that it has no head and we operate on the mlv. Also,
221-- we need to do the vspacing last as it removes items from the mvl.
222
223local pageactions = nodes.tasks.actions("mvlbuilders")
224----- lineactions = nodes.tasks.actions("linebuilders")
225
226local function report(pagecontext,head)
227    report_page_builder("trigger: %s at level %i",pagecontext,texnest.ptr)
228    report_page_builder("  vsize    : %p",texget("vsize"))
229    report_page_builder("  pagegoal : %p",texget("pagegoal"))
230    report_page_builder("  pagetotal: %p",texget("pagetotal"))
231    report_page_builder("  list     : %s",head and nodeidstostring(head) or "<empty>")
232end
233
234-- Watch out: contributehead can be any head (kind of) not per se the page one
235-- but that needs to be intercepted when needed by groupcode and level. At some
236-- point this one might be split by group.
237
238function builders.buildpage_filter(pagecontext)
239    local head, tail = getspeciallist("contributehead")
240    if head then
241        if trace_page_builder then
242            report(pagecontext,head)
243        end
244        starttiming(builders)
245        head = pageactions(head,pagecontext) -- todo: tail
246        stoptiming(builders)
247        setspeciallist("contributehead", head)
248    else
249        if trace_page_builder then
250            report(pagecontext)
251        end
252    end
253end
254
255registercallback("vpack_filter",     builders.vpack_filter,     "vertical spacing etc")
256registercallback("buildpage_filter", builders.buildpage_filter, "vertical spacing etc (mvl)")
257
258local vboxactions = nodes.tasks.actions("vboxhandlers")
259
260function builders.vbox_filter(head,groupcode)
261    if head then
262        starttiming(builders)
263        head = vboxactions(head,groupcode)
264        stoptiming(builders)
265    end
266    return head
267end
268
269registercallback("packed_vbox_filter", builders.vbox_filter, "packed vbox treatments")
270
271statistics.register("v-node processing time", function()
272    return statistics.elapsedseconds(builders)
273end)
274
275local implement = interfaces.implement
276
277implement { name = "defineparbuilder",  actions = constructors.define, arguments = "string" }
278implement { name = "setparbuilder",     actions = constructors.set,    arguments = "string" }
279implement { name = "startparbuilder",   actions = constructors.start,  arguments = "string" }
280implement { name = "stopparbuilder",    actions = constructors.stop    }
281implement { name = "enableparbuilder",  actions = constructors.enable  }
282implement { name = "disableparbuilder", actions = constructors.disable }
283
284-- Here are some tracers:
285
286local nuts         = nodes.nuts
287local tonut        = nodes.tonut
288local setcolor     = nodes.tracers.colors.set
289local listtoutf    = nodes.listtoutf
290local new_kern     = nuts.pool.kern
291local new_rule     = nuts.pool.rule
292local hpack        = nuts.hpack
293local getheight    = nuts.getheight
294local getdepth     = nuts.getdepth
295local getdirection = nuts.getdirection
296local getlist      = nuts.getlist
297local setwidth     = nuts.setwidth
298local setdirection = nuts.setdirection
299local setlink      = nuts.setlink
300local tonode       = nuts.tonode
301
302local list         = { }
303
304local report_quality = logs.reporter("pack quality")
305
306-- overflow|badness w h d dir
307
308local report  = true   trackers.register("builders.vpack.quality", function(v) report  = v end)
309local collect = false  trackers.register("builders.vpack.collect", function(v) collect = v end)
310----  show    = false  trackers.register("builders.vpack.overflow",function(v) show    = v end)
311
312function vpackbuilders.report(how,detail,n,first,last,filename) -- no return value
313    if report then
314        if last <= 0 then
315            report_quality("%s vbox",how)
316        elseif first > 0 and first < last then
317            report_quality("%s vbox at line %i - %i in file %a",how,first,last,filename or "?")
318        else
319            report_quality("%s vbox at line %i in file %a",how,last,filename or "?")
320        end
321    end
322    if collect then
323        list[#list+1] = { "hbox", how, filename, first, last, how, detail }
324    end
325end
326
327-- function builders.vpack.show(how,detail,n,first,last,filename) -- return value
328--     if show then
329--     end
330-- end
331
332local report  = true   trackers.register("builders.hpack.quality", function(v) report  = v end)
333local collect = false  trackers.register("builders.hpack.collect", function(v) collect = v end)
334local show    = false  trackers.register("builders.hpack.overflow",function(v) show    = v end)
335
336function hpackbuilders.report(how,detail,n,first,last,filename) -- no return value
337    local str = (report or collect) and listtoutf(getlist(n),"",true,nil,true)
338    if report then
339        local overfull = how == "overfull"
340        if last <= 0 then
341            report_quality(
342                overfull and "%s hbox: %s (%p)" or "%s hbox: %s (badness %i)",
343                how,str,detail
344            )
345        elseif first > 0 and first < last then
346            report_quality(
347                overfull and "%s hbox at line %i - %i in file %a: %s (%p)" or "%s hbox at line %i - %i in file %a: %s (badness %i)",
348                how,first,last,filename or "?",str,detail
349            )
350        else
351            report_quality(
352                overfull and "%s hbox at line %i in file %a: %s (%p)" or "%s hbox at line %i in file %a: %s (badness %i)",
353                how,last,filename or "?",str,detail
354            )
355        end
356    end
357    if collect then
358        list[#list+1] = { "hbox", how, filename, first, last, str, detail }
359    end
360end
361
362function hpackbuilders.show(how,detail,n,first,last,filename,rule) -- return value
363    if show then
364        local width     = 2*65536
365        local height    = getheight(n)
366        local depth     = getdepth(n)
367        local direction = getdirection(n)
368        if height < 4*65526 then
369            height = 4*65526
370        end
371        if depth < 2*65526 then
372            depth = 2*65526
373        end
374        if rule then
375            flush(rule)
376        end
377        rule = new_rule(width,height,depth)
378        setdirection(rule,direction)
379        if how == "overfull" then
380            setcolor(rule,"red")
381            local kern = new_kern(-detail)
382            setlink(kern,rule)
383            rule = kern
384        elseif how == "underfull" then
385            setcolor(rule,"blue")
386        elseif how == "loose" then
387            setcolor(rule,"magenta")
388        elseif how == "tight" then
389            setcolor(rule,"cyan")
390        end
391        rule = hpack(rule)
392        setwidth(rule,0) -- maybe better whd all zero
393        setdirection(rule,direction)
394    else
395        local width = texget("overfullrule")
396        if width > 0 then
397            rule = new_rule(width)
398        end
399    end
400    return rule
401end
402
403--
404
405local hqualityactions = nodes.tasks.actions("hquality")
406local vqualityactions = nodes.tasks.actions("vquality")
407
408function hpackbuilders.qualityactions(how,detail,n,first,last,filename)
409    local rul = nil
410 -- starttiming(builders)
411    rul = hqualityactions(how,detail,n,first,last,filename)
412 -- stoptiming(builders)
413    return rul
414end
415function vpackbuilders.qualityactions(how,detail,n,first,last,filename)
416    local rul = nil
417 -- starttiming(builders)
418    rul = vqualityactions(how,detail,n,first,last,filename)
419 -- stoptiming(builders)
420    return rul
421end
422
423registercallback("hpack_quality", hpackbuilders.qualityactions, "report horizontal packing quality")
424registercallback("vpack_quality", vpackbuilders.qualityactions, "report vertical packing quality")
425
426statistics.register("quality reports", function()
427    local n = #list
428    if n > 0 then
429        local t = table.setmetatableindex("number")
430        local fw = 0
431        local hw = 0
432        for i=1,n do
433            local f = list[i][1]
434            local h = list[i][2]
435            if #f > fw then
436                fw = #f
437            end
438            if #h > hw then
439                hw = #h
440            end
441            t[h] = t[h] + 1
442        end
443        logs.startfilelogging(report_quality)
444        for i=1,n do
445            local l   = list[i]
446            local how = l[2]
447            report_quality(
448                how == "overfull"
449                    and "%-" .. fw .. "s [%04i - %04i] : %-" .. hw .. "s %s : %s (%p)"
450                     or "%-" .. fw .. "s [%04i - %04i] : %-" .. hw .. "s %s : %s (badness %i)",
451                file.basename(l[3]),l[4],l[5],how,l[1],l[6],l[7]
452            )
453        end
454        logs.stopfilelogging()
455        report_quality()
456        report_quality("%i entries added to the log file : %s",n,table.sequenced(t))
457        report_quality()
458    end
459end)
460
461do
462
463    local loners = { }
464    local report = logs.reporter("loners")
465
466    local texgetcount = tex.getcount
467
468    trackers.register("pages.loners",function(v)
469        callback.register("show_loners",
470            v and function(options,penalty)
471                loners[#loners+1] = { texgetcount("realpageno"), options, penalty }
472            end or nil
473        )
474    end)
475
476    logs.registerfinalactions(function()
477        local n = #loners
478        if n > 0 then
479            local options = tex.getpenaltyoptionvalues()
480            options[0] = nil
481            logs.startfilelogging(report,"page break penalties (loners)")
482            for i=1,n do
483                local l = loners[i]
484                local t = { }
485                local o = l[2]
486                for k, v in next, options do
487                    if o & k == k then t[#t+1] = v end
488                end
489                if #t > 0 then
490                    sort(t)
491                    report("  %4i: penalty %5i, % + t,",l[1],l[3],t)
492                end
493            end
494            logs.stopfilelogging()
495        end
496    end)
497
498    statistics.register("page breaks", function()
499        local n = #loners
500        if n > 0 then
501            local pages = { }
502            for i=1,n do
503                pages[#pages+1] = loners[i][1]
504            end
505            return string.formatters("%i pages have loners reported: % t",n,pages)
506        end
507    end)
508
509end
510