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