page-cst.lmt /size: 42 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ["page-cst"] = {
2    version   = 1.001,
3    comment   = "companion to page-cst.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-- todo: check what is used
10
11local next, type, tonumber, rawget = next, type, tonumber, rawget
12local ceil, odd, round = math.ceil, math.odd, math.round
13local lower = string.lower
14local copy = table.copy
15
16local trace_state   = false  trackers.register("columnsets.trace",   function(v) trace_state  = v end)
17local trace_details = false  trackers.register("columnsets.details", function(v) trace_details = v end)
18local trace_cells   = false  trackers.register("columnsets.cells",   function(v) trace_cells  = v end)
19
20local report       = logs.reporter("column sets")
21
22local setmetatableindex = table.setmetatableindex
23
24local properties        = nodes.properties.data
25
26local nodecodes         = nodes.nodecodes
27
28local hlist_code        = nodecodes.hlist
29local vlist_code        = nodecodes.vlist
30local kern_code         = nodecodes.kern
31local glue_code         = nodecodes.glue
32local penalty_code      = nodecodes.penalty
33local rule_code         = nodecodes.rule
34
35local nuts              = nodes.nuts
36local tonode            = nuts.tonode
37local tonut             = nuts.tonut
38
39local vpack             = nuts.vpack
40local flushlist         = nuts.flushlist
41
42local setlink           = nuts.setlink
43local setlist           = nuts.setlist
44local setnext           = nuts.setnext
45local setprev           = nuts.setprev
46local setsubtype        = nuts.setsubtype
47local setbox            = nuts.setbox
48local getwhd            = nuts.getwhd
49local setwhd            = nuts.setwhd
50local getkern           = nuts.getkern
51local getpenalty        = nuts.getpenalty
52local getwidth          = nuts.getwidth
53local getheight         = nuts.getheight
54
55local getnext           = nuts.getnext
56local getprev           = nuts.getprev
57local getid             = nuts.getid
58local getlist           = nuts.getlist
59local takebox           = nuts.takebox
60local takelist          = nuts.takelist
61local splitbox          = nuts.splitbox
62local getattribute      = nuts.getattribute
63local copylist          = nuts.copylist
64
65local getbox            = nuts.getbox
66local getcount          = tex.getcount
67local getdimen          = tex.getdimen
68
69local texsetbox         = tex.setbox
70local texsetcount       = tex.setcount
71local texsetdimen       = tex.setdimen
72
73local theprop           = nuts.theprop
74
75local nodepool          = nuts.pool
76
77local new_vlist         = nodepool.vlist
78local new_trace_rule    = nodepool.rule
79local new_empty_rule    = nodepool.emptyrule
80
81local context           = context
82local implement         = interfaces.implement
83
84local variables         <const> = interfaces.variables
85local v_here            <const> = variables.here
86local v_fixed           <const> = variables.fixed
87local v_top             <const> = variables.top
88local v_bottom          <const> = variables.bottom
89local v_repeat          <const> = variables["repeat"]
90local v_yes             <const> = variables.yes
91local v_page            <const> = variables.page
92local v_first           <const> = variables.first
93local v_last            <const> = variables.last
94----- v_wide            <const> = variables.wide
95
96pagebuilders            = pagebuilders or { } -- todo: pages.builders
97pagebuilders.columnsets = pagebuilders.columnsets or { }
98local columnsets        = pagebuilders.columnsets
99
100local data = { [""] = { } }
101
102-- todo: use state
103
104local function setstate(t,start)
105    if start or not t.firstcolumn then
106        t.firstcolumn = odd(getcount("realpageno")) and 1 or 2
107    end
108    if t.firstcolumn > 1 then
109        t.firstcolumn = 1
110        t.lastcolumn  = t.nofleft
111        t.state       = "left"
112    else
113        t.firstcolumn = t.nofleft + 1
114        t.lastcolumn  = t.firstcolumn + t.nofright - 1
115        t.state       = "right"
116    end
117    t.currentcolumn = t.firstcolumn
118    t.currentrow    = 1
119end
120
121function columnsets.define(t)
122    local name            = t.name
123    local nofleft         = t.nofleft or 1
124    local nofright        = t.nofright or 1
125    local nofcolumns      = nofleft + nofright
126    local dataset         = data[name] or { }
127    data[name]            = dataset
128    dataset.nofleft       = nofleft
129    dataset.nofright      = nofright
130    dataset.nofcolumns    = nofcolumns
131    dataset.nofrows       = t.nofrows or 1
132    dataset.distance      = t.distance or getdimen("bodyfontsize")
133    dataset.maxwidth      = t.maxwidth or getdimen("makeupwidth")
134    dataset.lineheight    = t.lineheight or getdimen("globalbodyfontstrutheight")
135    dataset.linedepth     = t.linedepth or getdimen("globalbodyfontstrutdepth")
136    --
137    dataset.cells         = { }
138    dataset.currentcolumn = 1
139    dataset.currentrow    = 1
140    --
141    dataset.lines         = dataset.lines or setmetatableindex("table")
142    dataset.start         = dataset.start or setmetatableindex("table")
143    --
144    dataset.page          = 1
145    --
146    local distances = dataset.distances or setmetatableindex(function(t,k)
147        return dataset.distance
148    end)
149    dataset.distances = distances
150    --
151    local widths = dataset.widths or setmetatableindex(function(t,k)
152        return dataset.width
153    end)
154    dataset.widths = widths
155    --
156    local width = t.width
157    if not width or width == 0 then
158        local dl = 0
159        local dr = 0
160        for i=1,nofleft-1 do
161            dl = dl + distances[i]
162        end
163        for i=1,nofright-1 do
164            dr = dr + distances[nofleft+i]
165        end
166        local nl = nofleft
167        local nr = nofright
168        local wl = dataset.maxwidth
169        local wr = wl
170        for i=1,nofleft do
171            local w = rawget(widths,i)
172            if w then
173                nl = nl - 1
174                wl = wl - w
175            end
176        end
177        for i=1,nofright do
178            local w = rawget(widths,nofleft+i)
179            if w then
180                nr = nr - 1
181                wr = wr - w
182            end
183        end
184        dl = (wl - dl) / nl
185        dr = (wr - dr) / nr
186        if dl > dr then
187            report("using %s page column width %p in columnset %a","right",dr,name)
188            width = dr
189        elseif dl < dr then
190            report("using %s page column width %p in columnset %a","left",dl,name)
191            width = dl
192        else
193            width = dl
194        end
195    end
196 -- report("width %p, nleft %i, nright %i",width,nofleft,nofright)
197    width         = round(width)
198    dataset.width = width
199    local spans   = { }
200    dataset.spans = spans
201    for i=1,nofleft do
202        local s = { }
203        local d = 0
204        for j=1,nofleft-i+1 do
205            d = d + width
206            s[j] = round(d)
207            d = d + distances[j]
208        end
209        spans[i]  = s
210    end
211    for i=1,nofright do
212        local s = { }
213        local d = 0
214        for j=1,nofright-i+1 do
215            d = d + width
216            s[j] = round(d)
217            d = d + distances[j]
218        end
219        spans[nofleft+i]  = s
220    end
221    --
222    local spreads   = copy(spans)
223    dataset.spreads = spreads
224    local gap       = 2 * getdimen("backspace")
225    for l=1,nofleft do
226        local s = spreads[l]
227        local n = #s
228        local o = s[n] + gap
229        for r=1,nofright do
230            n = n + 1
231            s[n] = s[r] + o
232        end
233    end
234    --
235    texsetdimen("d_page_grd_column_width",dataset.width)
236    --
237    setstate(dataset,true)
238    --
239    return dataset
240end
241
242local function check(dataset)
243    local cells  = dataset.cells
244    local page   = dataset.page
245    local offset = odd(page) and dataset.nofleft or 0
246    local start  = dataset.start
247    local list   = rawget(start,page)
248    if list then
249        for c, n in next, list do
250            local column = cells[offset + c]
251            if column then
252                for r=1,n do
253                    column[r] = true
254                end
255            end
256        end
257        start[page] = nil
258    end
259    local lines = dataset.lines
260    local list  = rawget(lines,page)
261    local rows  = dataset.nofrows
262    if list then
263        for c, n in next, list do
264            local column = cells[offset + c]
265            if column then
266                if n > 0 then
267                    for r=n+1,rows do
268                        column[r] = true
269                    end
270                elseif n < 0 then
271                    for r=rows,rows+n+1,-1 do
272                        column[r] = true
273                    end
274                end
275            end
276        end
277        lines[page] = nil
278    end
279end
280
281local function erase(dataset,all)
282    local cells   = dataset.cells
283    local nofrows = dataset.nofrows
284    local first   = 1
285    local last    = dataset.nofcolumns
286    --
287    if not all then
288        first = dataset.firstcolumn or first
289        last  = dataset.lastcolumn  or last
290    end
291    for c=first,last do
292        local column = { }
293        for r=1,nofrows do
294            if column[r] then
295                report("slot (%i,%i) is not empty",c,r)
296            end
297            column[r] = false -- not used
298        end
299        cells[c] = column
300    end
301end
302
303function columnsets.reset(t)
304    local dataset = columnsets.define(t)
305    erase(dataset,true)
306    check(dataset)
307end
308
309function columnsets.prepareflush(name)
310    local dataset     = data[name]
311    local cells       = dataset.cells
312    local firstcolumn = dataset.firstcolumn
313    local lastcolumn  = dataset.lastcolumn
314    local nofrows     = dataset.nofrows
315    local lineheight  = dataset.lineheight
316    local linedepth   = dataset.linedepth
317    local widths      = dataset.widths
318    local height      = (lineheight+linedepth)*nofrows -- - linedepth
319    --
320    local columns     = { }
321    dataset.columns   = columns
322    --
323    for c=firstcolumn,lastcolumn do
324        local column = cells[c]
325        for r=1,nofrows do
326            local cell = column[r]
327            if (cell == false) or (cell == true) then
328                if trace_cells then
329                    column[r] = new_trace_rule(65536*2,lineheight,linedepth)
330                else
331                    column[r] = new_empty_rule(0,lineheight,linedepth)
332                end
333            end
334        end
335        for r=1,nofrows-1 do
336            setlink(column[r],column[r+1])
337        end
338        columns[c] = new_vlist(column[1],widths[c],height,0) -- linedepth
339    end
340    --
341    texsetcount("c_page_grd_first_column",firstcolumn)
342    texsetcount("c_page_grd_last_column",lastcolumn)
343end
344
345function columnsets.flushcolumn(name,column)
346    local dataset = data[name]
347    local columns = dataset.columns
348    local packed  = columns[column]
349    setbox("b_page_grd_column",packed)
350    columns[column] = nil
351end
352
353function columnsets.finishflush(name)
354    local dataset     = data[name]
355    local cells       = dataset.cells
356    local firstcolumn = dataset.firstcolumn
357    local lastcolumn  = dataset.lastcolumn
358    local nofrows     = dataset.nofrows
359    for c=firstcolumn,lastcolumn do
360        local column = { }
361        for r=1,nofrows do
362            column[r] = false -- not used
363        end
364        cells[c] = column
365    end
366    dataset.page = dataset.page + 1
367    check(dataset)
368    setstate(dataset)
369end
370
371function columnsets.block(t)
372    local dataset    = data[t.name]
373    local cells      = dataset.cells
374    local nofcolumns = dataset.nofcolumns
375    local nofrows    = dataset.nofrows
376    --
377    local c = t.c or 0
378    local r = t.r or 0
379    if c == 0 or r == 0 or c > nofcolumns or r > nofrows then
380        return
381    end
382    local nc = t.nc or 0
383    local nr = t.nr or 0
384    if nc == 0 then
385        return
386    end
387    if nr == 0 then
388        return
389    end
390    local rr = r + nr - 1
391    local cc = c + nc - 1
392    if rr > nofrows then
393        rr = nofrows
394    end
395    if cc > nofcolumns then
396        cc = nofcolumns
397    end
398    for i=c,cc do
399        local column = cells[i]
400        for j=r,rr do
401            column[j] = true
402        end
403    end
404end
405
406local function here(c,r,nr,nofcolumns,nofrows,cells,width,spans)
407    local rr = r + nr - 1
408    if rr > nofrows then
409        report("%i rows needed, %i rows available, no slots free at (%i,%i), discarding",rr,nofrows,c,r)
410        return false
411    end
412    local cc = 0
413    local wd = spans[c]
414    local wc = 0
415    local nc = 0
416    for i=c,nofcolumns do
417        nc = nc + 1
418        wc = wd[nc]
419        if not wc then
420            break
421        elseif wc >= width then
422            cc = i
423            break
424        end
425    end
426    if cc == 0 or cc > nofcolumns then
427        report("needed %p, no slot free at (%i,%i), discarding",width,c,r)
428        return false
429    end
430    for i=c,cc do
431        local column = cells[i]
432        for j=r,rr do
433            if column[j] then
434                report("width %p, needed %p, checking (%i,%i) x (%i,%i), %s",width,wc,c,r,nc,nr,"quit")
435                return false
436            end
437        end
438    end
439 -- report("width %p, needed %p, checking (%i,%i) x (%i,%i), %s",width,wc,c,r,nc,nr,"match")
440    return c, r, nc
441end
442
443-- we use c/r as range limiters
444
445local methods = {
446    [v_here] = here,
447    [v_fixed] = here,
448    tblr = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
449        for j=r,nofrows-nr+1 do
450            for i=c,nofcolumns do
451                if not cells[i][j] then
452                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
453                    if c then
454                        return c, r, cc
455                    end
456                end
457            end
458        end
459    end,
460    lrtb = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
461        for i=c,nofcolumns do
462            for j=r,nofrows-nr+1 do
463                if not cells[i][j] then
464                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
465                    if c then
466                        return c, r, cc
467                    end
468                end
469            end
470        end
471    end,
472    tbrl = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
473        for j=r,nofrows-nr+1 do
474            for i=nofcolumns,c,-1 do
475                if not cells[i][j] then
476                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
477                    if c then
478                        return c, r, cc
479                    end
480                end
481            end
482        end
483    end,
484    rltb = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
485        for i=nofcolumns,c,-1 do
486            for j=r,nofrows-nr+1 do
487                if not cells[i][j] then
488                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
489                    if c then
490                        return c, r, cc
491                    end
492                end
493            end
494        end
495    end,
496    btlr = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
497     -- for j=nofrows-nr+1,1,-1 do
498        for j=nofrows-nr+1-r+1,1,-1 do
499            for i=c,nofcolumns do
500                if not cells[i][j] then
501                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
502                    if c then
503                        return c, r, cc
504                    end
505                end
506            end
507        end
508    end,
509    lrbt = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
510        for i=c,nofcolumns do
511         -- for j=nofrows-nr+1,1,-1 do
512            for j=nofrows-nr+1-r+1,1,-1 do
513                if not cells[i][j] then
514                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
515                    if c then
516                        return c, r, cc
517                    end
518                end
519            end
520        end
521    end,
522    btrl = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
523     -- for j=nofrows-nr+1,1,-1 do
524        for j=nofrows-nr+1-r+1,1,-1 do
525            for i=nofcolumns,c,-1 do
526                if not cells[i][j] then
527                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
528                    if c then
529                        return c, r, cc
530                    end
531                end
532            end
533        end
534    end,
535    rlbt = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
536        for i=nofcolumns,c,-1 do
537         -- for j=nofrows-nr+1,1,-1 do
538            for j=nofrows-nr+1-r+1,1,-1 do
539                if not cells[i][j] then
540                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
541                    if c then
542                        return c, r, cc
543                    end
544                end
545            end
546        end
547    end,
548    fxtb = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
549        for i=c,nofcolumns do
550            for j=r,nofrows-nr+1 do
551                if not cells[i][j] then
552                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
553                    if c then
554                        return c, r, cc
555                    end
556                end
557                r = 1
558            end
559        end
560    end,
561    fxbt = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
562        for i=c,nofcolumns do
563            for j=nofrows-nr+1,r,-1 do
564                if not cells[i][j] then
565                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
566                    if c then
567                        return c, r, cc
568                    end
569                end
570            end
571            r = 1
572        end
573    end,
574    [v_top] = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
575        for i=c,nofcolumns do
576            for j=1,nofrows-nr+1 do
577                if cells[i][j] then
578                    break
579                else
580                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
581                    if c then
582                        return c, r, cc
583                    end
584                end
585            end
586        end
587    end,
588    [v_bottom] = function(c,r,nr,nofcolumns,nofrows,cells,width,spans)
589        for i=c,nofcolumns do
590            for j=1,nofrows-nr+1 do
591                if cells[i][j] then
592                    break
593                else
594                    local c, r, cc = here(i,j,nr,nofcolumns,nofrows,cells,width,spans)
595                    if c then
596                        return c, r, cc
597                    end
598                end
599            end
600        end
601    end,
602}
603
604local threshold = 50
605
606function columnsets.check(t)
607    local dataset    = data[t.name]
608    local cells      = dataset.cells
609    local nofcolumns = dataset.nofcolumns
610    local nofrows    = dataset.nofrows
611    local widths     = dataset.widths
612    local lineheight = dataset.lineheight
613    local linedepth  = dataset.linedepth
614    local distances  = dataset.distances
615    local spans      = dataset.spans
616    --
617    local method     = lower(t.method or "tblr")
618    local boxwidth   = t.width  or 0
619    local boxheight  = t.height or 0
620    local boxnumber  = t.box
621    local box        = boxnumber and getbox(boxnumber)
622    --
623    if boxwidth > 0 and boxheight > 0 then
624        -- we're ok
625    elseif box then
626        local wd, ht, dp = getwhd(box)
627        boxwidth  = wd
628        boxheight = ht + dp
629    else
630        report("empty box")
631        return
632    end
633    --
634    local c = t.c or 0
635    local r = t.r or 0
636    if c == 0 then
637        c = dataset.currentcolumn
638    end
639    if r == 0 then
640        r = dataset.currentrow
641    end
642    if c == 0 or r == 0 or c > nofcolumns or r > nofrows then
643        texsetcount("c_page_grd_reserved_state",5)
644        return
645    end
646 -- report("checking width %p, height %p, depth %p, slot (%i,%i)",boxwidth,boxheight,boxdepth,c,r)
647    local nr = ceil(boxheight/(lineheight+linedepth))
648    --
649    local action = methods[method]
650    local cfound = false
651    local rfound = false
652    local lastcolumn = dataset.lastcolumn
653 -- if t.option == v_wide then
654 --     lastcolumn = nofcolumns
655 --     spans = dataset.spreads
656 -- end
657    if action then
658        cfound, rfound, nc = action(c,r,nr,lastcolumn,nofrows,cells,boxwidth-threshold,spans)
659    end
660    if not cfound and method ~= v_here then
661--     if not cfound and method == v_here then
662        cfound, rfound, nc = here(c,r,nr,lastcolumn,nofrows,cells,boxwidth-threshold,spans)
663    end
664    if cfound then
665        local ht = nr*(lineheight+linedepth)
666        local wd = spans[cfound][nc]
667        dataset.reserved_ht = ht
668        dataset.reserved_wd = wd
669        dataset.reserved_c  = cfound
670        dataset.reserved_r  = rfound
671        dataset.reserved_nc = nc
672        dataset.reserved_nr = nr
673        texsetcount("c_page_grd_reserved_state",0)
674        texsetdimen("d_page_grd_reserved_height",ht)
675        texsetdimen("d_page_grd_reserved_width",wd)
676     -- report("using (%i,%i) x (%i,%i) @ (%p,%p)",cfound,rfound,nc,nr,wd,ht)
677    else
678        dataset.reserved_ht = false
679        dataset.reserved_wd = false
680        dataset.reserved_c  = false
681        dataset.reserved_r  = false
682        dataset.reserved_nc = false
683        dataset.reserved_nr = false
684        texsetcount("c_page_grd_reserved_state",4)
685     -- texsetdimen("d_page_grd_reserved_height",0)
686     -- texsetdimen("d_page_grd_reserved_width",0)
687     -- report("no slot found")
688    end
689end
690
691function columnsets.put(t)
692    local dataset    = data[t.name]
693    local cells      = dataset.cells
694    local widths     = dataset.widths
695    local lineheight = dataset.lineheight
696    local linedepth  = dataset.linedepth
697    local boxnumber  = t.box
698    local box        = boxnumber and takebox(boxnumber)
699    --
700    local c = t.c or dataset.reserved_c
701    local r = t.r or dataset.reserved_r
702    if not c or not r then
703     -- report("no reserved slot (%i,%i)",c,r)
704        return
705    end
706    local lastc = c + dataset.reserved_nc - 1
707    local lastr = r + dataset.reserved_nr - 1
708    --
709    for i=c,lastc do
710        local column = cells[i]
711        for j=r,lastr do
712            column[j] = true
713        end
714    end
715    cells[c][r] = box
716    setwhd(box,widths[c],lineheight,linedepth)
717    dataset.reserved_c  = false
718    dataset.reserved_r  = false
719    dataset.reserved_nc = false
720    dataset.reserved_nr = false
721    --
722end
723
724local function findgap(dataset)
725    local cells         = dataset.cells
726    local nofcolumns    = dataset.nofcolumns
727    local nofrows       = dataset.nofrows
728    local currentrow    = dataset.currentrow
729    local currentcolumn = dataset.currentcolumn
730    --
731    local foundc = 0
732    local foundr = 0
733    local foundn = 0
734    for c=currentcolumn,dataset.lastcolumn do
735        local column = cells[c]
736foundn = 0
737        for r=currentrow,nofrows do
738            if not column[r] then
739                if foundc == 0 then
740                    foundc = c
741                    foundr = r
742                end
743                foundn = foundn + 1
744            elseif foundn > 0 then
745                return foundc, foundr, foundn
746            end
747        end
748        if foundn > 0 then
749            return foundc, foundr, foundn
750        end
751        currentrow = 1
752    end
753end
754
755-- we can enforce grid snapping
756
757-- local function checkroom(head,available,row)
758--     if row == 1 then
759--         while head do
760--             local id = getid(head)
761--             if id == glue_code then
762--                 head = getnext(head)
763--             else
764--                 break
765--             end
766--         end
767--     end
768--     local used = 0
769--     local line = false
770--     while head do
771--         local id = getid(head)
772--         if id == hlist_code or id == vlist_code or id == rule_code then -- <= rule_code
773--             local wd, ht, dp = getwhd(head)
774--             used = used + ht + dp
775--             line = true
776--         elseif id == glue_code then
777--             if line then
778--                 break
779--             end
780--             used = used + getwidth(head)
781--         elseif id == kern_code then
782--             used = used +  getkern(head)
783--         elseif id == penalty_code then
784--         end
785--         if used > available then
786--             break
787--         end
788--         head = getnext(head)
789--     end
790--     return line, used
791-- end
792
793local function checkroom(head,available,row)
794    if row == 1 then
795        while head do
796            local id = getid(head)
797            if id == glue_code then
798                head = getnext(head)
799            else
800                break
801            end
802        end
803    end
804    local used = 0
805    local line = false
806    while head do
807        local id = getid(head)
808        if id == hlist_code or id == vlist_code or id == rule_code then -- <= rule_code
809            local wd, ht, dp = getwhd(head)
810            used = used + ht + dp
811            line = true
812            if used > available then
813                break
814            end
815        elseif id == glue_code then
816            if line then
817                break
818            end
819            used = used + getwidth(head)
820            if used > available then
821                break
822            end
823        elseif id == kern_code then
824            used = used + getkern(head)
825            if used > available then
826                break
827            end
828        elseif id == penalty_code then
829            -- not good enough ... we need to look bakck too
830            if getpenalty(head) >= 10000 then
831                line = false
832            else
833                break
834            end
835        end
836        head = getnext(head)
837    end
838    return line, used
839end
840
841-- we could preroll on a cheap copy .. in fact, a split loop normally works on
842-- a copy ... then we could also stepsise make the height smaller .. slow but nice
843
844-- local function findslice(dataset,head,available,column,row)
845--     local used       = 0
846--     local first      = nil
847--     local last       = nil
848--     local line       = false
849--     local lineheight = dataset.lineheight
850--     local linedepth  = dataset.linedepth
851--     if row == 1 then
852--         while head do
853--             local id = getid(head)
854--             if id == glue_code then
855--                 head = removenode(head,head,true)
856--             else
857--                 break
858--             end
859--         end
860--     end
861--     while head do
862--         -- no direction yet, if so use backend code
863--         local id = getid(head)
864--         local hd = 0
865--         if id == hlist_code or id == vlist_code or id == rule_code then -- <= rule_code
866--             local wd, ht, dp = getwhd(head)
867--             hd = ht + dp
868--         elseif id == glue_code then
869--             hd = getwidth(head)
870--         elseif id == kern_code then
871--             hd = getkern(head)
872--         elseif id == penalty_code then
873--         end
874--         if used + hd > available then
875--             if first then
876--                 setnext(last)
877--                 setprev(head)
878--                 return used, first, head
879--             else
880--                 return 0
881--             end
882--         else
883--             if not first then
884--                 first = head
885--             end
886--             used = used + hd
887--             last = head
888--             head = getnext(head)
889--         end
890--     end
891--     return used, first
892-- end
893
894--  todo
895--
896--                 first = takelist(done)
897--                 head = takelist(rest)
898--                 local tail = nuts.tail(first)
899--                 if false then
900--                     local disc = tex.lists.split_discards_head
901--                     if disc then
902--                         disc = tonut(disc)
903--                         setlink(tail,disc)
904--                         tail = nuts.tail(disc)
905--                         tex.lists.split_discards_head = nil
906--                     end
907--                 end
908--                 setlink(tail,head)
909
910-- We work on a copy because we need to keep properties. We can make faster copies
911-- by only doing a one-level deep copy.
912
913local function findslice(dataset,head,available,column,row)
914    local first      = nil
915    local lineheight = dataset.lineheight
916    local linedepth  = dataset.linedepth
917    local linetotal  = lineheight + linedepth
918    local slack      = 65536 -- 1pt
919    local copy       = copylist(head)
920    local attempts   = 0
921    local usedsize   = available
922    while true do
923        attempts = attempts + 1
924        texsetbox("scratchbox",tonode(new_vlist(copy)))
925        local done = splitbox("scratchbox",usedsize,"additional")
926        local used = getheight(done)
927        local rest = takebox("scratchbox")
928        if used > (usedsize+slack) then
929            if trace_details then
930                report("at (%i,%i) available %p, used %p, overflow %p",column,row,usedsize,used,used-usedsize)
931            end
932            -- flush copy
933            flushlist(takelist(done))
934            flushlist(takelist(rest))
935            -- check it we can try again
936            usedsize = usedsize - linetotal
937            if usedsize > linetotal then
938                copy = copylist(head)
939            else
940                return 0, nil, head
941            end
942        else
943            -- flush copied box
944            flushlist(takelist(done))
945            flushlist(takelist(rest))
946            -- deal with real data
947            texsetbox("scratchbox",tonode(new_vlist(head)))
948            done  = splitbox("scratchbox",usedsize,"additional")
949            rest  = takebox("scratchbox")
950            used  = getheight(done)
951            if attempts > 1 then
952                used = available
953            end
954            first = takelist(done)
955            head  = takelist(rest)
956            -- return result
957            return used, first, head
958        end
959    end
960end
961
962local nofcolumngaps = 0
963
964function columnsets.add(name,box)
965    local dataset       = data[name]
966    local cells         = dataset.cells
967    local nofcolumns    = dataset.nofcolumns
968    local nofrows       = dataset.nofrows
969    local currentrow    = dataset.currentrow
970    local currentcolumn = dataset.currentcolumn
971    local lineheight    = dataset.lineheight
972    local linedepth     = dataset.linedepth
973    local widths        = dataset.widths
974    --
975    local b = getbox(box)
976    local l = getlist(b)
977-- dataset.rest = l
978    if l then
979        setlist(b,nil)
980        local hd = lineheight + linedepth
981        while l do
982            local foundc, foundr, foundn = findgap(dataset)
983            if foundc then
984                local available = foundn * hd
985                local used, first, last = findslice(dataset,l,available,foundc,foundr)
986                if first then
987                    local v
988                    if used == available or (foundr+foundn > nofrows) then
989                        v = vpack(first,available,"exactly")
990                    else
991                        v = new_vlist(first)
992                    end
993                    nofcolumngaps = nofcolumngaps + 1
994                    -- getmetatable(v).columngap = nofcolumngaps
995                    properties[v] = { columngap = nofcolumngaps }
996                 -- report("setting gap %a at (%i,%i)",nofcolumngaps,foundc,foundr)
997                    setwhd(v,widths[currentcolumn],lineheight,linedepth)
998                    local column = cells[foundc]
999                    --
1000                    column[foundr] = v
1001                    used = used - hd
1002                    if used > 0 then
1003                        for r=foundr+1,foundr+foundn-1 do
1004                            used = used - hd
1005                            foundr = foundr + 1
1006                            column[r] = true
1007                            if used <= 0 then
1008                                break
1009                            end
1010                        end
1011                    end
1012                    currentcolumn = foundc
1013                    currentrow    = foundr
1014                    dataset.currentcolumn = currentcolumn
1015                    dataset.currentrow    = currentrow
1016                    l = last
1017                    dataset.rest = l
1018                else
1019                    local column = cells[foundc]
1020                    for i=foundr,foundr+foundn-1 do
1021                        column[i] = true
1022                    end
1023                    l = last
1024                end
1025            else
1026                dataset.rest = l
1027                return -- save and flush
1028            end
1029        end
1030    end
1031end
1032
1033do
1034
1035    -- A split approach is more efficient than a context(followup) inside
1036    -- followup itself as we need less (internal) housekeeping.
1037
1038    local followup = nil
1039    local splitter = lpeg.splitter("*",tonumber)
1040
1041    columnsets["noto"] = function(t)
1042        return followup()
1043    end
1044
1045    columnsets["goto"] = function(name,target)
1046        local dataset    = data[name]
1047        local nofcolumns = dataset.nofcolumns
1048        if target == v_yes or target == "" then
1049            local currentcolumn = dataset.currentcolumn
1050            followup = function()
1051                context(dataset.currentcolumn == currentcolumn and 1 or 0)
1052            end
1053            return followup()
1054        end
1055        if target == v_first then
1056            if dataset.currentcolumn > 1  then
1057                target = v_page
1058            else
1059                return context(0)
1060            end
1061        end
1062        if target == v_page then
1063            if dataset.currentcolumn == 1 and dataset.currentrow == 1 then
1064                return context(0)
1065            else
1066                local currentpage = dataset.page
1067                followup = function()
1068                    context(dataset.page == currentpage and 1 or 0)
1069                end
1070                return followup()
1071            end
1072        end
1073        if target == v_last then
1074            target = dataset.nofcolumns
1075            if dataset.currentcolumn ~= target then
1076                followup = function()
1077                    context(dataset.currentcolumn ~= target and 1 or 0)
1078                end
1079                return followup()
1080            end
1081            return
1082        end
1083        local targetpage = tonumber(target)
1084        if targetpage then
1085            followup = function()
1086                context(dataset.currentcolumn ~= targetpage and 1 or 0)
1087            end
1088            return followup()
1089        end
1090        local targetcolumn, targetrow = lpeg.match(splitter,target)
1091        if targetcolumn and targetrow then
1092            if dataset.currentcolumn ~= targetcolumn and dataset.currentrow ~= targetrow then
1093                followup = function()
1094                    if dataset.currentcolumn ~= targetcolumn then
1095                        context(1)
1096                        return
1097                    end
1098                    if dataset.currentcolumn == targetcolumn then
1099                        context(dataset.currentrow ~= targetrow and 1 or 0)
1100                    else
1101                        context(0)
1102                    end
1103                end
1104                return followup()
1105            end
1106        end
1107    end
1108
1109end
1110
1111function columnsets.currentcolumn(name)
1112    local dataset = data[name]
1113    context(dataset.currentcolumn)
1114end
1115
1116function columnsets.flushrest(name,box)
1117    local dataset = data[name]
1118    local rest    = dataset.rest
1119    if rest then
1120        dataset.rest = nil
1121        setbox("global",box,new_vlist(rest))
1122    end
1123end
1124
1125function columnsets.setvsize(name)
1126    local dataset = data[name]
1127    local c, r, n = findgap(dataset)
1128    if n then
1129        dataset.currentcolumn = c
1130        dataset.currentrow    = r
1131    else
1132        dataset.currentcolumn = 1
1133        dataset.currentrow    = 1
1134        n = 0
1135    end
1136    local gap = n*(dataset.lineheight+dataset.linedepth)
1137    texsetdimen("d_page_grd_gap_height",gap)
1138    -- can be integrated
1139 -- report("state %a, n %a, column %a, row %a",dataset.state,n,dataset.currentcolumn,dataset.currentrow)
1140end
1141
1142function columnsets.sethsize(name)
1143    local dataset = data[name]
1144    texsetdimen("d_page_grd_column_width",dataset.widths[dataset.currentcolumn])
1145end
1146
1147function columnsets.sethspan(name,span)
1148    -- no checking if there is really space, so we assume it can be
1149    -- placed which makes spans a very explicit feature
1150    local dataset   = data[name]
1151    local column    = dataset.currentcolumn
1152    local available = dataset.lastcolumn - column + 1
1153    if span > available then
1154        span = available
1155    end
1156    local width = dataset.spans[column][span]
1157    texsetdimen("d_page_grd_span_width",width)
1158end
1159
1160function columnsets.setlines(t)
1161    local dataset = data[t.name]
1162    dataset.lines[t.page][t.column] = t.value
1163end
1164
1165function columnsets.setstart(t)
1166    local dataset = data[t.name]
1167    dataset.start[t.page][t.column] = t.value
1168end
1169
1170function columnsets.setproperties(t)
1171    local dataset = data[t.name]
1172    local column  = t.column
1173    dataset.distances[column] = t.distance
1174    dataset.widths[column] = t.width
1175end
1176
1177local areas = { }
1178
1179function columnsets.registerarea(t)
1180    -- maybe metatable with values
1181    areas[#areas+1] = t
1182end
1183
1184-- state : repeat | start
1185
1186local ctx_page_grd_set_area = context.protected.page_grd_set_area
1187
1188function columnsets.flushareas(name)
1189    local nofareas = #areas
1190    if nofareas == 0 then
1191        return
1192    end
1193    local dataset = data[name]
1194    local page    = dataset.page
1195    if odd(page) then
1196     -- report("checking %i areas",#areas)
1197        local kept = { }
1198        for i=1,nofareas do
1199            local area = areas[i]
1200         -- local page = area.page -- maybe use page counter in columnset
1201         -- local type = area.type
1202            local okay = false
1203            --
1204            local nofcolumns = area.nc
1205            local nofrows    = area.nr
1206            local column     = area.c
1207            local row        = area.r
1208            columnsets.block {
1209                name = name,
1210                c    = column,
1211                r    = row,
1212                nc   = nofcolumns,
1213                nr   = nofrows,
1214            }
1215            local left     = 0
1216            local start    = dataset.nofleft + 1
1217            local overflow = (column + nofcolumns - 1) - dataset.nofleft
1218            local height   = nofrows * (dataset.lineheight + dataset.linedepth)
1219            local width    = dataset.spreads[column][nofcolumns]
1220         -- report("span, width %p, overflow %i",width,overflow)
1221            if overflow > 0 then
1222                local used = nofcolumns - overflow
1223                left  = dataset.spreads[column][used] + getdimen("backspace")
1224            end
1225            ctx_page_grd_set_area(name,area.name,column,row,width,height,start,left) -- or via counters / dimens
1226            if area.state ~= v_repeat then
1227                area = nil
1228            end
1229            if area then
1230                kept[#kept+1] = area
1231            end
1232        end
1233        areas = kept
1234    end
1235end
1236
1237function columnsets.setarea(t)
1238    local dataset = data[t.name]
1239    local cells   = dataset.cells
1240    local box     = takebox(t.box)
1241    local column  = t.c
1242    local row     = t.r
1243    if column and row then
1244        setwhd(box,dataset.widths[column],dataset.lineheight,dataset.linedepth)
1245        cells[column][row] = box
1246    end
1247end
1248
1249-- The interface.
1250
1251implement {
1252    name      = "definecolumnset",
1253    actions   = columnsets.define,
1254    arguments = { {
1255        { "name", "string" },
1256    } }
1257}
1258
1259implement {
1260    name      = "resetcolumnset",
1261    actions   = columnsets.reset,
1262    arguments = { {
1263        { "name", "string" },
1264        { "nofleft", "integer" },
1265        { "nofright", "integer" },
1266        { "nofrows", "integer" },
1267        { "lineheight", "dimension" },
1268        { "linedepth", "dimension" },
1269        { "width", "dimension" },
1270        { "distance", "dimension" },
1271        { "maxwidth", "dimension" },
1272    } }
1273}
1274
1275implement {
1276    name      = "preparecolumnsetflush",
1277    actions   = columnsets.prepareflush,
1278    arguments = "string",
1279}
1280
1281implement {
1282    name      = "finishcolumnsetflush",
1283    actions   = columnsets.finishflush,
1284    arguments = "string",
1285}
1286
1287implement {
1288    name      = "flushcolumnsetcolumn",
1289    actions   = columnsets.flushcolumn,
1290    arguments = { "string" ,"integer" },
1291}
1292
1293implement {
1294    name      = "setvsizecolumnset",
1295    actions   = columnsets.setvsize,
1296    arguments = "string",
1297}
1298
1299implement {
1300    name      = "sethsizecolumnset",
1301    actions   = columnsets.sethsize,
1302    arguments = "string",
1303}
1304
1305implement {
1306    name      = "sethsizecolumnspan",
1307    actions   = columnsets.sethspan,
1308    arguments = { "string" ,"integer" },
1309}
1310
1311implement {
1312    name      = "flushcolumnsetrest",
1313    actions   = columnsets.flushrest,
1314    arguments = { "string", "integer" },
1315}
1316
1317implement {
1318    name      = "blockcolumnset",
1319    actions   = columnsets.block,
1320    arguments = { {
1321        { "name", "string" },
1322        { "c", "integer" },
1323        { "r", "integer" },
1324        { "nc", "integer" },
1325        { "nr", "integer" },
1326        { "method", "string" },
1327        { "box", "integer" },
1328    } }
1329}
1330
1331implement {
1332    name      = "checkcolumnset",
1333    actions   = columnsets.check,
1334    arguments = { {
1335        { "name", "string" },
1336        { "method", "string" },
1337        { "c", "integer" },
1338        { "r", "integer" },
1339        { "method", "string" },
1340        { "box", "integer" },
1341        { "width", "dimension" },
1342        { "height", "dimension" },
1343        { "option", "string" },
1344    } }
1345}
1346
1347implement {
1348    name      = "putincolumnset",
1349    actions   = columnsets.put,
1350    arguments = { {
1351        { "name", "string" },
1352        { "c", "integer" },
1353        { "r", "integer" },
1354        { "method", "string" },
1355        { "box", "integer" },
1356    } }
1357}
1358
1359implement {
1360    name    = "addtocolumnset",
1361    actions = columnsets.add,
1362    arguments = { "string", "integer" },
1363}
1364
1365implement {
1366    name    = "setcolumnsetlines",
1367    actions = columnsets.setlines,
1368    arguments = { {
1369        { "name", "string" },
1370        { "page", "integer" },
1371        { "column", "integer" },
1372        { "value", "integer" },
1373    } }
1374}
1375
1376implement {
1377    name    = "setcolumnsetstart",
1378    actions = columnsets.setstart,
1379    arguments = { {
1380        { "name", "string" },
1381        { "page", "integer" },
1382        { "column", "integer" },
1383        { "value", "integer" },
1384    } }
1385}
1386
1387implement {
1388    name    = "setcolumnsetproperties",
1389    actions = columnsets.setproperties,
1390    arguments = { {
1391        { "name", "string" },
1392        { "column", "integer" },
1393        { "distance", "dimension" },
1394        { "width", "dimension" },
1395    } }
1396}
1397
1398implement {
1399    name      = "registercolumnsetarea",
1400    actions   = columnsets.registerarea,
1401    arguments = { {
1402        { "name", "string" },
1403        { "type", "string" },
1404        { "page", "integer" },
1405        { "state", "string" },
1406        { "c", "integer" },
1407        { "r", "integer" },
1408        { "nc", "integer" },
1409        { "nr", "integer" },
1410    } }
1411}
1412
1413implement {
1414    name      = "flushcolumnsetareas",
1415    actions   = columnsets.flushareas,
1416    arguments = "string",
1417}
1418
1419implement {
1420    name      = "setcolumnsetarea",
1421    actions   = columnsets.setarea,
1422    arguments = { {
1423        { "name", "string" },
1424        { "c", "integer" },
1425        { "r", "integer" },
1426        { "method", "string" },
1427        { "box", "integer" },
1428    } }
1429}
1430
1431implement {
1432    name      = "columnsetgoto",
1433    actions   = columnsets["goto"],
1434    arguments = "2 strings",
1435}
1436
1437implement {
1438    name      = "columnsetnoto",
1439    actions   = columnsets["noto"],
1440}
1441
1442implement {
1443    name      = "columnsetcurrentcolumn",
1444    actions   = columnsets.currentcolumn,
1445    arguments = "string",
1446}
1447