page-mix.lua /size: 35 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ["page-mix"] = {
2    version   = 1.001,
3    comment   = "companion to page-mix.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-- inserts.getname(name)
10
11-- local node, tex = node, tex
12-- local nodes, interfaces, utilities = nodes, interfaces, utilities
13-- local trackers, logs, storage = trackers, logs, storage
14-- local number, table = number, table
15
16-- todo: explore vsplit (for inserts)
17
18local next, type = next, type
19local concat = table.concat
20local ceil = math.ceil
21
22local trace_state   = false  trackers.register("mixedcolumns.trace",   function(v) trace_state   = v end)
23local trace_details = false  trackers.register("mixedcolumns.details", function(v) trace_details = v end)
24
25local report_state = logs.reporter("mixed columns")
26
27local context             = context
28
29local nodecodes           = nodes.nodecodes
30
31local hlist_code          = nodecodes.hlist
32local vlist_code          = nodecodes.vlist
33local kern_code           = nodecodes.kern
34local glue_code           = nodecodes.glue
35local penalty_code        = nodecodes.penalty
36local insert_code         = nodecodes.insert
37local mark_code           = nodecodes.mark
38local rule_code           = nodecodes.rule
39
40local nuts                = nodes.nuts
41local tonode              = nuts.tonode
42local listtoutf           = nodes.listtoutf
43
44local vpack               = nuts.vpack
45local flushnode           = nuts.flush
46local concatnodes         = nuts.concat
47local slidenodes          = nuts.slide -- ok here as we mess with prev links intermediately
48
49local setlink             = nuts.setlink
50local setlist             = nuts.setlist
51local setnext             = nuts.setnext
52local setprev             = nuts.setprev
53local setbox              = nuts.setbox
54local setwhd              = nuts.setwhd
55local setheight           = nuts.setheight
56local setdepth            = nuts.setdepth
57
58local getnext             = nuts.getnext
59local getprev             = nuts.getprev
60local getid               = nuts.getid
61local getlist             = nuts.getlist
62local getindex            = nuts.getindex or nuts.getsubtype -- luatex catch
63local getbox              = nuts.getbox
64local getattr             = nuts.getattr
65local getwhd              = nuts.getwhd
66local getkern             = nuts.getkern
67local getpenalty          = nuts.getpenalty
68local getwidth            = nuts.getwidth
69local getheight           = nuts.getheight
70local getdepth            = nuts.getdepth
71
72local theprop             = nuts.theprop
73
74local nodepool            = nuts.pool
75
76local new_hlist           = nodepool.hlist
77local new_vlist           = nodepool.vlist
78local new_glue            = nodepool.glue
79
80local points              = number.points
81
82local setinsertcontent    = tex.setinsertcontent or tex.setbox
83
84local settings_to_hash    = utilities.parsers.settings_to_hash
85
86local variables           = interfaces.variables
87local v_yes               = variables.yes
88local v_global            = variables["global"]
89local v_local             = variables["local"]
90local v_none              = variables.none
91local v_halfline          = variables.halfline
92
93local context             = context
94local implement           = interfaces.implement
95
96pagebuilders              = pagebuilders or { }
97pagebuilders.mixedcolumns = pagebuilders.mixedcolumns or { }
98local mixedcolumns        = pagebuilders.mixedcolumns
99
100local a_checkedbreak      = attributes.private("checkedbreak")
101local forcedbreak         = -123
102
103-- initializesplitter(specification)
104-- cleanupsplitter()
105
106-- Inserts complicate matters a lot. In order to deal with them well, we need to
107-- distinguish several cases.
108--
109-- (1) full page columns: firstcolumn, columns, lastcolumn, page
110-- (2) mid page columns : firstcolumn, columns, lastcolumn, page
111--
112-- We need to collect them accordingly.
113
114local function collectinserts(result,nxt,nxtid)
115    local inserts, currentskips, nextskips, inserttotal = { }, 0, 0, 0
116    local i = result.i
117    if not i then
118        i = 0
119        result.i = i
120    end
121    while nxt do
122        if nxtid == insert_code then
123            i = i + 1
124            result.i = i
125            inserttotal = inserttotal + getheight(nxt) -- height includes depth (hm, still? needs checking)
126            local s = getindex(nxt)
127            local c = inserts[s]
128            if trace_details then
129                report_state("insert of class %s found",s)
130            end
131            if not c then
132                local width = structures.notes.check_spacing(s,i) -- before
133                c = { }
134                inserts[s] = c
135                if not result.inserts[s] then
136                    currentskips = currentskips + width
137                end
138                nextskips = nextskips + width
139            end
140            c[#c+1] = nxt
141        elseif nxtid == mark_code then
142            if trace_details then
143                report_state("mark found")
144            end
145        else
146            break
147        end
148        nxt = getnext(nxt)
149        if nxt then
150            nxtid = getid(nxt)
151        else
152            break
153        end
154    end
155    return nxt, inserts, currentskips, nextskips, inserttotal
156end
157
158local function appendinserts(ri,inserts)
159    for class, collected in next, inserts do
160        local ric = ri[class]
161        if not ric then
162            -- assign to collected
163            ri[class] = collected
164        else
165            -- append to collected
166            for j=1,#collected do
167                ric[#ric+1] = collected[j]
168            end
169        end
170    end
171end
172
173local function discardtopglue(current,discarded)
174    local size = 0
175    while current do
176        local id = getid(current)
177        if id == glue_code then
178            size = size + getwidth(current)
179            discarded[#discarded+1] = current
180            current = getnext(current)
181        elseif id == penalty_code then
182            if getpenalty(current) == forcedbreak then
183                discarded[#discarded+1] = current
184                current = getnext(current)
185                while current and getid(current) == glue_code do
186                    size = size + getwidth(current)
187                    discarded[#discarded+1] = current
188                    current = getnext(current)
189                end
190            else
191                discarded[#discarded+1] = current
192                current = getnext(current)
193            end
194        else
195            break
196        end
197    end
198    if current then
199        setprev(current) -- prevent look back
200    end
201    return current, size
202end
203
204local function stripbottomglue(results,discarded)
205    local height = 0
206    for i=1,#results do
207        local r = results[i]
208        local t = r.tail
209        while t and t ~= r.head do
210            local prev = getprev(t)
211            if not prev then
212                break
213            end
214            local id = getid(t)
215            if id == penalty_code then
216                if getpenalty(t) == forcedbreak then
217                    break
218                else
219                    discarded[#discarded+1] = t
220                    r.tail = prev
221                    t = prev
222                end
223            elseif id == glue_code then
224                discarded[#discarded+1] = t
225                local width = getwidth(t)
226                if trace_state then
227                    report_state("columns %s, discarded bottom glue %p",i,width)
228                end
229                r.height = r.height - width
230                r.tail = prev
231                t = prev
232            else
233                break
234            end
235        end
236        if r.height > height then
237            height = r.height
238        end
239    end
240    return height
241end
242
243local function preparesplit(specification) -- a rather large function
244    local box = specification.box
245    if not box then
246        report_state("fatal error, no box")
247        return
248    end
249    local list = getbox(box)
250    if not list then
251        report_state("fatal error, no list")
252        return
253    end
254    local head = nil
255    if getid(list) == hlist_code then
256        head = list
257    else
258        head = getlist(list) or specification.originalhead
259    end
260    if not head then
261        report_state("fatal error, no head")
262        return
263    end
264
265    slidenodes(head) -- we can have set prev's to nil to prevent backtracking
266
267    local discarded      = { }
268    local originalhead   = head
269    local originalwidth  = specification.originalwidth  or getwidth(list)
270    local originalheight = specification.originalheight or getheight(list)
271    local current        = head
272    local skipped        = 0
273    local height         = 0
274    local depth          = 0
275    local skip           = 0
276    local handlenotes    = specification.notes or false
277    local splitmethod    = specification.splitmethod or false
278    if splitmethod == v_none then
279        splitmethod = false
280    end
281    local options     = settings_to_hash(specification.option or "")
282    local stripbottom = specification.alternative == v_local
283    local cycle       = specification.cycle or 1
284    local nofcolumns  = specification.nofcolumns or 1
285    if nofcolumns == 0 then
286        nofcolumns = 1
287    end
288    local preheight  = specification.preheight or 0
289    local extra      = specification.extra or 0
290    local maxheight  = specification.maxheight
291    local optimal    = originalheight/nofcolumns
292    local noteheight = specification.noteheight or 0
293
294    maxheight = maxheight - noteheight
295
296    if specification.balance ~= v_yes then
297        optimal = maxheight
298    end
299    local topback   = 0
300    local target    = optimal + extra
301    local overflow  = target > maxheight - preheight
302    local threshold = specification.threshold or 0
303    if overflow then
304        target = maxheight - preheight
305    end
306    if trace_state then
307        report_state("cycle %s, maxheight %p, preheight %p, target %p, overflow %a, extra %p",
308            cycle, maxheight, preheight , target, overflow, extra)
309    end
310    local results = { }
311    for i=1,nofcolumns do
312        results[i] = {
313            head    = false,
314            tail    = false,
315            height  = 0,
316            depth   = 0,
317            inserts = { },
318            delta   = 0,
319            back    = 0,
320        }
321    end
322
323    local column      = 1
324    local line        = 0
325    local result      = results[1]
326    local lasthead    = nil
327    local rest        = nil
328    local lastlocked  = nil
329    local lastcurrent = nil
330    local lastcontent = nil
331    local backtracked = false
332
333    if trace_state then
334        report_state("setting collector to column %s",column)
335    end
336
337    local function unlock(case,penalty)
338        if lastlocked then
339            if trace_state then
340                report_state("penalty %s, unlocking in column %s, case %i",penalty or "-",column,case)
341            end
342            lastlocked  = nil
343        else
344            if trace_state then
345                report_state("penalty %s, ignoring in column %s, case %i",penalty or "-",column,case)
346            end
347        end
348        lastcurrent = nil
349        lastcontent = nil
350    end
351
352    local function lock(case,penalty,current)
353        if trace_state then
354            report_state("penalty %s, locking in column %s, case %i",penalty,column,case)
355        end
356        lastlocked  = penalty
357        lastcurrent = current or lastcurrent
358        lastcontent = nil
359    end
360
361    local function backtrack(start)
362        local current = start
363        -- first skip over glue and penalty
364        while current do
365            local id = getid(current)
366            if id == glue_code then
367                if trace_state then
368                    report_state("backtracking over %s in column %s, value %p","glue",column,getwidth(current))
369                end
370                current = getprev(current)
371            elseif id == penalty_code then
372                if trace_state then
373                    report_state("backtracking over %s in column %s, value %i","penalty",column,getpenalty(current))
374                end
375                current = getprev(current)
376            else
377                break
378            end
379        end
380        -- then skip over content
381        while current do
382            local id = getid(current)
383            if id == glue_code then
384                if trace_state then
385                    report_state("quitting at %s in column %s, value %p","glue",column,getwidth(current))
386                end
387                break
388            elseif id == penalty_code then
389                if trace_state then
390                    report_state("quitting at %s in column %s, value %i","penalty",column,getpenalty(current))
391                end
392                break
393            else
394                current = getprev(current)
395            end
396        end
397        if not current then
398            if trace_state then
399                report_state("no effective backtracking in column %s",column)
400            end
401            current = start
402        end
403        return current
404    end
405
406    local function gotonext()
407        if lastcurrent then
408            if current ~= lastcurrent then
409                if trace_state then
410                    report_state("backtracking to preferred break in column %s",column)
411                end
412                -- todo: also remember height/depth
413                if true then -- todo: option to disable this
414                    current = backtrack(lastcurrent) -- not ok yet
415                else
416                    current = lastcurrent
417                end
418                backtracked = true
419            end
420            lastcurrent = nil
421            if lastlocked then
422                if trace_state then
423                    report_state("unlocking in column %s",column)
424                end
425                lastlocked = nil
426            end
427        end
428        if head == lasthead then
429            if trace_state then
430                report_state("empty column %s, needs more work",column)
431            end
432            rest = current
433            return false, 0
434        else
435            lasthead = head
436            result.head = head
437            if current == head then
438                result.tail = head
439            else
440                result.tail = getprev(current)
441            end
442            result.height = height
443            result.depth  = depth
444        end
445        head   = current
446        height = 0
447        depth  = 0
448        if column == nofcolumns then
449            column = 0 -- nicer in trace
450            rest   = head
451-- rest   = nil
452            return false, 0
453        else
454            local skipped
455            column = column + 1
456            result = results[column]
457            if trace_state then
458                report_state("setting collector to column %s",column)
459            end
460            current, skipped = discardtopglue(current,discarded)
461            if trace_details and skipped ~= 0 then
462                report_state("check > column 1, discarded %p",skipped)
463            end
464            head = current
465            return true, skipped
466        end
467    end
468
469    local function checked(advance,where,locked)
470        local total   = skip + height + depth + advance
471        local delta   = total - target
472        local state   = "same"
473        local okay    = false
474        local skipped = 0
475        local curcol  = column
476        if delta > threshold then
477            result.delta = delta
478            okay, skipped = gotonext()
479            if okay then
480                state = "next"
481            else
482                state = "quit"
483            end
484        end
485        if trace_details then
486            report_state("%-8s > column %s, delta %p, threshold %p, advance %p, total %p, target %p => %a (height %p, depth %p, skip %p)",
487                where,curcol,delta,threshold,advance,total,target,state,height,depth,skip)
488        end
489        return state, skipped
490    end
491
492    current, skipped = discardtopglue(current,discarded)
493    if trace_details and skipped ~= 0 then
494        report_state("check > column 1, discarded %p",skipped)
495    end
496
497    -- problem: when we cannot break after a list (and we only can expect same-page situations as we don't
498    -- care too much about weighted breaks here) we should sort of look ahead or otherwise be able to push
499    -- back inserts and so
500    --
501    -- ok, we could use vsplit but we don't have that one opened up yet .. maybe i should look into the c-code
502    -- .. something that i try to avoid so let's experiment more before we entry dirty trick mode
503    --
504    -- what if we can do a preroll in lua, get head and tail and then slice of a bit and push that ahead
505
506    head = current
507
508    local function process_skip(current,nxt)
509        local advance = getwidth(current)
510        if advance ~= 0 then
511            local state, skipped = checked(advance,"glue")
512            if trace_state then
513                report_state("%-8s > column %s, state %a, advance %p, height %p","glue",column,state,advance,height)
514                if skipped ~= 0 then
515                    report_state("%-8s > column %s, discarded %p","glue",column,skipped)
516                end
517            end
518            if state == "quit" then
519                return true
520            end
521            height = height + depth + skip
522            depth  = 0
523            if advance < 0 then
524                height = height + advance
525                skip = 0
526                if height < 0 then
527                    height = 0
528                end
529            else
530                skip = height > 0 and advance or 0
531            end
532            if trace_state then
533                report_state("%-8s > column %s, height %p, depth %p, skip %p","glue",column,height,depth,skip)
534            end
535        else
536            -- what else? ignore? treat as valid as usual?
537        end
538        if lastcontent then
539            unlock(1)
540        end
541    end
542
543    local function process_kern(current,nxt)
544        local advance = getkern(current)
545        if advance ~= 0 then
546            local state, skipped = checked(advance,"kern")
547            if trace_state then
548                report_state("%-8s > column %s, state %a, advance %p, height %p, state %a","kern",column,state,advance,height)
549                if skipped ~= 0 then
550                    report_state("%-8s > column %s, discarded %p","kern",column,skipped)
551                end
552            end
553            if state == "quit" then
554                return true
555            end
556            height = height + depth + skip + advance
557            depth  = 0
558            skip   = 0
559            if trace_state then
560                report_state("%-8s > column %s, height %p, depth %p, skip %p","kern",column,height,depth,skip)
561            end
562        end
563    end
564
565    local function process_rule(current,nxt)
566        -- simple variant of h|vlist
567        local advance = getheight(current) -- + getdepth(current)
568        if advance ~= 0 then
569            local state, skipped = checked(advance,"rule")
570            if trace_state then
571                report_state("%-8s > column %s, state %a, rule, advance %p, height %p","rule",column,state,advance,inserttotal,height)
572                if skipped ~= 0 then
573                    report_state("%-8s > column %s, discarded %p","rule",column,skipped)
574                end
575            end
576            if state == "quit" then
577                return true
578            end
579            height = height + depth + skip + advance
580         -- if state == "next" then
581         --     height = height + nextskips
582         -- else
583         --     height = height + currentskips
584         -- end
585            depth = getdepth(current)
586            skip  = 0
587        end
588        lastcontent = current
589    end
590
591    -- okay, here we could do some badness like magic but we want something
592    -- predictable and even better: strategies .. so eventually this will
593    -- become installable
594    --
595    -- [chapter] [penalty] [section] [penalty] [first line]
596
597    local function process_penalty(current,nxt)
598        local penalty = getpenalty(current)
599        if penalty == 0 then
600            unlock(2,penalty)
601        elseif penalty == forcedbreak then
602            local needed  = getattr(current,a_checkedbreak)
603            local proceed = not needed or needed == 0
604            if not proceed then
605                local available = target - height
606                proceed = needed >= available
607                if trace_state then
608                    report_state("cycle: %s, column %s, available %p, needed %p, %s break",cycle,column,available,needed,proceed and "forcing" or "ignoring")
609                end
610            end
611            if proceed then
612                unlock(3,penalty)
613                local okay, skipped = gotonext()
614                if okay then
615                    if trace_state then
616                        report_state("cycle: %s, forced column break, same page",cycle)
617                        if skipped ~= 0 then
618                            report_state("%-8s > column %s, discarded %p","penalty",column,skipped)
619                        end
620                    end
621                else
622                    if trace_state then
623                        report_state("cycle: %s, forced column break, next page",cycle)
624                        if skipped ~= 0 then
625                            report_state("%-8s > column %s, discarded %p","penalty",column,skipped)
626                        end
627                    end
628                    return true
629                end
630            end
631        elseif penalty < 0 then
632            -- we don't care too much
633            unlock(4,penalty)
634        elseif penalty >= 10000 then
635            if not lastcurrent then
636                lock(1,penalty,current)
637            elseif penalty > lastlocked then
638                lock(2,penalty)
639            elseif trace_state then
640                report_state("penalty %s, ignoring in column %s, case %i",penalty,column,3)
641            end
642        else
643            unlock(5,penalty)
644        end
645    end
646
647    local function process_list(current,nxt)
648        local nxtid = nxt and getid(nxt)
649        line = line + 1
650        local inserts, insertskips, nextskips, inserttotal = nil, 0, 0, 0
651        local wd, ht, dp = getwhd(current)
652        local advance = ht
653        local more = nxt and (nxtid == insert_code or nxtid == mark_code)
654        if trace_state then
655            report_state("%-8s > column %s, content: %s","line (1)",column,listtoutf(getlist(current),true,true))
656        end
657        if more and handlenotes then
658            nxt, inserts, insertskips, nextskips, inserttotal = collectinserts(result,nxt,nxtid)
659        end
660        local state, skipped = checked(advance+inserttotal+insertskips,more and "line (2)" or "line only",lastlocked)
661        if trace_state then
662            report_state("%-8s > column %s, state %a, line %s, advance %p, insert %p, height %p","line (3)",column,state,line,advance,inserttotal,height)
663            if skipped ~= 0 then
664                report_state("%-8s > column %s, discarded %p","line (4)",column,skipped)
665            end
666        end
667        if state == "quit" then
668            return true
669        end
670     -- if state == "next" then -- only when profile
671     --     local unprofiled = theprop(current).unprofiled
672     --     if unprofiled then
673     --         local h = unprofiled.height
674     --         local s = unprofiled.strutht
675     --         local t = s/2
676     -- print("profiled",h,s)
677     -- local snapped = theprop(current).snapped
678     -- if snapped then
679     --     inspect(snapped)
680     -- end
681     --         if h < s + t then
682     --             result.back = - (h - s)
683     --             advance     = s
684     --         end
685     --     end
686     -- end
687        height = height + depth + skip + advance + inserttotal
688        if state == "next" then
689            height = height + nextskips
690        else
691            height = height + insertskips
692        end
693        depth = dp
694        skip  = 0
695        if inserts then
696            -- so we already collect them ... makes backtracking tricky ... alternatively
697            -- we can do that in a separate loop ... no big deal either
698            appendinserts(result.inserts,inserts)
699        end
700        if trace_state then
701            report_state("%-8s > column %s, height %p, depth %p, skip %p","line (5)",column,height,depth,skip)
702        end
703        lastcontent = current
704    end
705
706    while current do
707
708        local id  = getid(current)
709        local nxt = getnext(current)
710
711        if trace_state then
712            report_state("%-8s > column %s, height %p, depth %p, id %s","node",column,height,depth,nodecodes[id])
713        end
714
715        backtracked = false
716
717        if id == hlist_code or id == vlist_code then
718            if process_list(current,nxt) then break end
719        elseif id == glue_code then
720            if process_skip(current,nxt) then break end
721        elseif id == kern_code then
722            if process_kern(current,nxt) then break end
723        elseif id == penalty_code then
724            if process_penalty(current,nxt) then break end
725        elseif id == rule_code then
726            if process_rule(current,nxt) then break end
727        else
728            -- skip inserts and such
729        end
730
731        if backtracked then
732            nxt = current
733        end
734
735        if nxt then
736            current = nxt
737        elseif head == lasthead then
738            -- to be checked but break needed as otherwise we have a loop
739            if trace_state then
740                report_state("quit as head is lasthead")
741            end
742            break
743        else
744            local r = results[column]
745            r.head   = head
746            r.tail   = current
747            r.height = height
748            r.depth  = depth
749            break
750        end
751    end
752
753    if not current then
754        if trace_state then
755            report_state("nothing left")
756        end
757        -- needs well defined case
758     -- rest = nil
759    elseif rest == lasthead then
760        if trace_state then
761            report_state("rest equals lasthead")
762        end
763        -- test case: x\index{AB} \index{AA}x \blank \placeindex
764        -- makes line disappear: rest = nil
765    end
766
767    if stripbottom then
768        local height = stripbottomglue(results,discarded)
769        if height > 0 then
770            target = height
771        end
772    end
773
774    specification.results        = results
775    specification.height         = target
776    specification.originalheight = originalheight
777    specification.originalwidth  = originalwidth
778    specification.originalhead   = originalhead
779    specification.targetheight   = target or 0
780    specification.rest           = rest
781    specification.overflow       = overflow
782    specification.discarded      = discarded
783    setlist(getbox(specification.box))
784
785    return specification
786end
787
788local function finalize(result)
789    if result then
790        local results  = result.results
791        local columns  = result.nofcolumns
792        local maxtotal = 0
793        for i=1,columns do
794            local r = results[i]
795            local h = r.head
796            if h then
797                setprev(h)
798                if r.back then
799                    local k = new_glue(r.back)
800                    setlink(k,h)
801                    h = k
802                    r.head = h
803                end
804                local t = r.tail
805                if t then
806                    setnext(t)
807                else
808                    setnext(h)
809                    r.tail = h
810                end
811                for c, list in next, r.inserts do
812                    local t = { }
813                    for i=1,#list do
814                        local l = list[i]
815                        local h = new_vlist() -- was hlist but that's wrong
816                        local g = getlist(l)
817                        t[i] = h
818                        setlist(h,g)
819                        local ht = getheight(l)
820                        local dp = getdepth(l)
821                        local wd = getwidth(g)
822                        setwhd(h,wd,ht,dp)
823                        setlist(l)
824                    end
825                    setprev(t[1])  -- needs checking
826                    setnext(t[#t]) -- needs checking
827                    r.inserts[c] = t
828                end
829            end
830            local total = r.height + r.depth
831            if total > maxtotal then
832                maxtotal = total
833            end
834            r.total = total
835        end
836        result.maxtotal = maxtotal
837        for i=1,columns do
838            local r = results[i]
839            r.extra = maxtotal - r.total
840        end
841    end
842end
843
844local splitruns = 0
845
846local function report_deltas(result,str)
847    local t = { }
848    for i=1,result.nofcolumns do
849        t[#t+1] = points(result.results[i].delta or 0)
850    end
851    report_state("%s, cycles %s, deltas % | t",str,result.cycle or 1,t)
852end
853
854local function setsplit(specification)
855    splitruns = splitruns + 1
856    if trace_state then
857        report_state("split run %s",splitruns)
858    end
859    local result = preparesplit(specification)
860    if result then
861        if result.overflow then
862            if trace_state then
863                report_deltas(result,"overflow")
864            end
865            -- we might have some rest
866        elseif result.rest and specification.balance == v_yes then
867            local step = specification.step or 65536*2
868            local cycle = 1
869            local cycles = specification.cycles or 100
870            while result.rest and cycle <= cycles do
871                specification.extra = cycle * step
872                result = preparesplit(specification) or result
873                if trace_state then
874                    report_state("cycle: %s.%s, original height %p, total height %p",
875                        splitruns,cycle,result.originalheight,result.nofcolumns*result.targetheight)
876                end
877                cycle = cycle + 1
878                specification.cycle = cycle
879            end
880            if cycle > cycles then
881                report_deltas(result,"too many balancing cycles")
882            elseif trace_state then
883                report_deltas(result,"balanced")
884            end
885        elseif trace_state then
886            report_deltas(result,"done")
887        end
888        return result
889    elseif trace_state then
890        report_state("no result")
891    end
892end
893
894local function getsplit(result,n)
895    if not result then
896        report_state("flush, column %s, %s",n,"no result")
897        return
898    end
899    local r = result.results[n]
900    if not r then
901        report_state("flush, column %s, %s",n,"empty")
902    end
903    local h = r.head
904    if not h then
905        return new_glue(result.originalwidth)
906    end
907
908    setprev(h) -- move up
909
910    local strutht    = result.strutht
911    local strutdp    = result.strutdp
912    local lineheight = strutht + strutdp
913    local isglobal   = result.alternative == v_global
914
915    local v = new_vlist()
916    setlist(v,h)
917
918    -- safeguard ... i need to figure this out some day
919
920    local c = r.head
921    while c do
922        if c == result.rest then
923            report_state("flush, column %s, %s",n,"suspicous rest")
924            result.rest = nil
925            break
926        else
927            c = getnext(c)
928        end
929    end
930
931 -- local v = vpack(h,"exactly",height)
932
933    if isglobal then -- option
934        result.height = result.maxheight
935    end
936
937    local ht = 0
938    local dp = 0
939    local wd = result.originalwidth
940
941    local grid         = result.grid
942    local internalgrid = result.internalgrid
943    local httolerance  = .25
944    local dptolerance  = .50
945    local lineheight   = internalgrid == v_halfline and lineheight/2 or lineheight
946
947    local function amount(r,s,t)
948        local l = ceil((r-t)/lineheight)
949        local a = lineheight * l
950        if a > s then
951            return a - s
952        else
953            return s
954        end
955    end
956    if grid then
957        -- print(n,result.maxtotal,r.total,r.extra)
958        if isglobal then
959            local rh = r.height
960         -- ht = (lineheight * ceil(result.height/lineheight) - strutdp
961            ht = amount(rh,strutdp,0)
962            dp = strutdp
963        else
964            -- natural dimensions
965            local rh = r.height
966            local rd = r.depth
967            if rh > ht then
968                ht = amount(rh,strutdp,httolerance*strutht)
969            end
970            if rd > dp then
971                dp = amount(rd,strutht,dptolerance*strutdp)
972            end
973            -- forced dimensions
974            local rh = result.height or 0
975            local rd = result.depth or 0
976            if rh > ht then
977                ht = amount(rh,strutdp,httolerance*strutht)
978            end
979            if rd > dp then
980                dp = amount(rd,strutht,dptolerance*strutdp)
981            end
982            -- always one line at least
983            if ht < strutht then
984                ht = strutht
985            end
986            if dp < strutdp then
987                dp = strutdp
988            end
989        end
990    else
991        ht = result.height
992        dp = result.depth
993    end
994
995    setwhd(v,wd,ht,dp)
996
997    if trace_state then
998        local id = getid(h)
999        if id == hlist_code then
1000            report_state("flush, column %s, grid %a, width %p, height %p, depth %p, %s: %s",n,grid,wd,ht,dp,"top line",listtoutf(getlist(h)))
1001        else
1002            report_state("flush, column %s, grid %a, width %p, height %p, depth %p, %s: %s",n,grid,wd,ht,dp,"head node",nodecodes[id])
1003        end
1004    end
1005
1006    for c, list in next, r.inserts do
1007        local l = concatnodes(list)
1008        for i=1,#list-1 do
1009            setdepth(list[i],0)
1010        end
1011        local b = vpack(l)            -- multiple arguments, todo: fastvpack
1012        setinsertcontent(c,tonode(b)) -- when we wrap in a box
1013        r.inserts[c] = nil
1014    end
1015
1016    return v
1017end
1018
1019local function getrest(result)
1020    local rest = result and result.rest
1021    result.rest = nil -- to be sure
1022    return rest
1023end
1024
1025local function getlist(result)
1026    local originalhead = result and result.originalhead
1027    result.originalhead = nil -- to be sure
1028    return originalhead
1029end
1030
1031local function cleanup(result)
1032    local discarded = result.discarded
1033    for i=1,#discarded do
1034        flushnode(discarded[i])
1035    end
1036    result.discarded = { }
1037end
1038
1039mixedcolumns.setsplit = setsplit
1040mixedcolumns.getsplit = getsplit
1041mixedcolumns.finalize = finalize
1042mixedcolumns.getrest  = getrest
1043mixedcolumns.getlist  = getlist
1044mixedcolumns.cleanup  = cleanup
1045
1046-- interface --
1047
1048local result
1049
1050implement {
1051    name      = "mixsetsplit",
1052    actions   = function(specification)
1053        if result then
1054            for k, v in next, specification do
1055                result[k] = v
1056            end
1057            result = setsplit(result)
1058        else
1059            result = setsplit(specification)
1060        end
1061    end,
1062    arguments = {
1063        {
1064           { "box", "integer" },
1065           { "nofcolumns", "integer" },
1066           { "maxheight", "dimen" },
1067           { "noteheight", "dimen" },
1068           { "step", "dimen" },
1069           { "cycles", "integer" },
1070           { "preheight", "dimen" },
1071           { "prebox", "integer" },
1072           { "strutht", "dimen" },
1073           { "strutdp", "dimen" },
1074           { "threshold", "dimen" },
1075           { "splitmethod" },
1076           { "balance" },
1077           { "alternative" },
1078           { "internalgrid" },
1079           { "grid", "boolean" },
1080           { "notes", "boolean" },
1081        }
1082    }
1083}
1084
1085implement {
1086    name      = "mixgetsplit",
1087    arguments = "integer",
1088    actions   = function(n)
1089        if result then
1090            local list = getsplit(result,n)
1091            if list then
1092                context(tonode(list))
1093            end
1094        end
1095    end,
1096}
1097
1098implement {
1099    name    = "mixfinalize",
1100    actions = function()
1101        if result then
1102            finalize(result)
1103        end
1104    end
1105}
1106
1107implement {
1108    name    = "mixflushrest",
1109    actions = function()
1110        if result then
1111            local rest = getrest(result)
1112            if rest then
1113                context(tonode(rest))
1114            end
1115        end
1116    end
1117}
1118
1119implement {
1120    name = "mixflushlist",
1121    actions = function()
1122        if result then
1123            local list = getlist(result)
1124            if list then
1125                context(tonode(list))
1126            end
1127        end
1128    end
1129}
1130
1131implement {
1132    name    = "mixstate",
1133    actions = function()
1134        context(result and result.rest and 1 or 0)
1135    end
1136}
1137
1138implement {
1139    name = "mixcleanup",
1140    actions = function()
1141        if result then
1142            cleanup(result)
1143            result = nil
1144        end
1145    end
1146}
1147