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