page-cst.lua /size: 42 Kb    last modification: 2021-10-28 13:50
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        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)",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        cfound, rfound, nc = here(c,r,nr,lastcolumn,nofrows,cells,boxwidth-threshold,spans)
662    end
663    if cfound then
664        local ht = nr*(lineheight+linedepth)
665        local wd = spans[cfound][nc]
666        dataset.reserved_ht = ht
667        dataset.reserved_wd = wd
668        dataset.reserved_c  = cfound
669        dataset.reserved_r  = rfound
670        dataset.reserved_nc = nc
671        dataset.reserved_nr = nr
672        texsetcount("c_page_grd_reserved_state",0)
673        texsetdimen("d_page_grd_reserved_height",ht)
674        texsetdimen("d_page_grd_reserved_width",wd)
675     -- report("using (%i,%i) x (%i,%i) @ (%p,%p)",cfound,rfound,nc,nr,wd,ht)
676    else
677        dataset.reserved_ht = false
678        dataset.reserved_wd = false
679        dataset.reserved_c  = false
680        dataset.reserved_r  = false
681        dataset.reserved_nc = false
682        dataset.reserved_nr = false
683        texsetcount("c_page_grd_reserved_state",4)
684     -- texsetdimen("d_page_grd_reserved_height",0)
685     -- texsetdimen("d_page_grd_reserved_width",0)
686     -- report("no slot found")
687    end
688end
689
690function columnsets.put(t)
691    local dataset    = data[t.name]
692    local cells      = dataset.cells
693    local widths     = dataset.widths
694    local lineheight = dataset.lineheight
695    local linedepth  = dataset.linedepth
696    local boxnumber  = t.box
697    local box        = boxnumber and takebox(boxnumber)
698    --
699    local c = t.c or dataset.reserved_c
700    local r = t.r or dataset.reserved_r
701    if not c or not r then
702     -- report("no reserved slot (%i,%i)",c,r)
703        return
704    end
705    local lastc = c + dataset.reserved_nc - 1
706    local lastr = r + dataset.reserved_nr - 1
707    --
708    for i=c,lastc do
709        local column = cells[i]
710        for j=r,lastr do
711            column[j] = true
712        end
713    end
714    cells[c][r] = box
715    setwhd(box,widths[c],lineheight,linedepth)
716    dataset.reserved_c  = false
717    dataset.reserved_r  = false
718    dataset.reserved_nc = false
719    dataset.reserved_nr = false
720    --
721end
722
723local function findgap(dataset)
724    local cells         = dataset.cells
725    local nofcolumns    = dataset.nofcolumns
726    local nofrows       = dataset.nofrows
727    local currentrow    = dataset.currentrow
728    local currentcolumn = dataset.currentcolumn
729    --
730    local foundc = 0
731    local foundr = 0
732    local foundn = 0
733    for c=currentcolumn,dataset.lastcolumn do
734        local column = cells[c]
735foundn = 0
736        for r=currentrow,nofrows do
737            if not column[r] then
738                if foundc == 0 then
739                    foundc = c
740                    foundr = r
741                end
742                foundn = foundn + 1
743            elseif foundn > 0 then
744                return foundc, foundr, foundn
745            end
746        end
747        if foundn > 0 then
748            return foundc, foundr, foundn
749        end
750        currentrow = 1
751    end
752end
753
754-- we can enforce grid snapping
755
756-- local function checkroom(head,available,row)
757--     if row == 1 then
758--         while head do
759--             local id = getid(head)
760--             if id == glue_code then
761--                 head = getnext(head)
762--             else
763--                 break
764--             end
765--         end
766--     end
767--     local used = 0
768--     local line = false
769--     while head do
770--         local id = getid(head)
771--         if id == hlist_code or id == vlist_code or id == rule_code then -- <= rule_code
772--             local wd, ht, dp = getwhd(head)
773--             used = used + ht + dp
774--             line = true
775--         elseif id == glue_code then
776--             if line then
777--                 break
778--             end
779--             used = used + getwidth(head)
780--         elseif id == kern_code then
781--             used = used +  getkern(head)
782--         elseif id == penalty_code then
783--         end
784--         if used > available then
785--             break
786--         end
787--         head = getnext(head)
788--     end
789--     return line, used
790-- end
791
792local function checkroom(head,available,row)
793    if row == 1 then
794        while head do
795            local id = getid(head)
796            if id == glue_code then
797                head = getnext(head)
798            else
799                break
800            end
801        end
802    end
803    local used = 0
804    local line = false
805    while head do
806        local id = getid(head)
807        if id == hlist_code or id == vlist_code or id == rule_code then -- <= rule_code
808            local wd, ht, dp = getwhd(head)
809            used = used + ht + dp
810            line = true
811            if used > available then
812                break
813            end
814        elseif id == glue_code then
815            if line then
816                break
817            end
818            used = used + getwidth(head)
819            if used > available then
820                break
821            end
822        elseif id == kern_code then
823            used = used + getkern(head)
824            if used > available then
825                break
826            end
827        elseif id == penalty_code then
828            -- not good enough ... we need to look bakck too
829            if getpenalty(head) >= 10000 then
830                line = false
831            else
832                break
833            end
834        end
835        head = getnext(head)
836    end
837    return line, used
838end
839
840-- we could preroll on a cheap copy .. in fact, a split loop normally works on
841-- a copy ... then we could also stepsise make the height smaller .. slow but nice
842
843-- local function findslice(dataset,head,available,column,row)
844--     local used       = 0
845--     local first      = nil
846--     local last       = nil
847--     local line       = false
848--     local lineheight = dataset.lineheight
849--     local linedepth  = dataset.linedepth
850--     if row == 1 then
851--         while head do
852--             local id = getid(head)
853--             if id == glue_code then
854--                 head = removenode(head,head,true)
855--             else
856--                 break
857--             end
858--         end
859--     end
860--     while head do
861--         -- no direction yet, if so use backend code
862--         local id = getid(head)
863--         local hd = 0
864--         if id == hlist_code or id == vlist_code or id == rule_code then -- <= rule_code
865--             local wd, ht, dp = getwhd(head)
866--             hd = ht + dp
867--         elseif id == glue_code then
868--             hd = getwidth(head)
869--         elseif id == kern_code then
870--             hd = getkern(head)
871--         elseif id == penalty_code then
872--         end
873--         if used + hd > available then
874--             if first then
875--                 setnext(last)
876--                 setprev(head)
877--                 return used, first, head
878--             else
879--                 return 0
880--             end
881--         else
882--             if not first then
883--                 first = head
884--             end
885--             used = used + hd
886--             last = head
887--             head = getnext(head)
888--         end
889--     end
890--     return used, first
891-- end
892
893--  todo
894--
895--                 first = takelist(done)
896--                 head = takelist(rest)
897--                 local tail = nuts.tail(first)
898--                 if false then
899--                     local disc = tex.lists.split_discards_head
900--                     if disc then
901--                         disc = tonut(disc)
902--                         setlink(tail,disc)
903--                         tail = nuts.tail(disc)
904--                         tex.lists.split_discards_head = nil
905--                     end
906--                 end
907--                 setlink(tail,head)
908
909-- We work on a copy because we need to keep properties. We can make faster copies
910-- by only doing a one-level deep copy.
911
912local function findslice(dataset,head,available,column,row)
913    local first      = nil
914    local lineheight = dataset.lineheight
915    local linedepth  = dataset.linedepth
916    local linetotal  = lineheight + linedepth
917    local slack      = 65536 -- 1pt
918    local copy       = copylist(head)
919    local attempts   = 0
920    local usedsize   = available
921    while true do
922        attempts = attempts + 1
923        texsetbox("scratchbox",tonode(new_vlist(copy)))
924        local done = splitbox("scratchbox",usedsize,"additional")
925        local used = getheight(done)
926        local rest = takebox("scratchbox")
927        if used > (usedsize+slack) then
928            if trace_details then
929                report("at (%i,%i) available %p, used %p, overflow %p",column,row,usedsize,used,used-usedsize)
930            end
931            -- flush copy
932            flushlist(takelist(done))
933            flushlist(takelist(rest))
934            -- check it we can try again
935            usedsize = usedsize - linetotal
936            if usedsize > linetotal then
937                copy = copylist(head)
938            else
939                return 0, nil, head
940            end
941        else
942            -- flush copied box
943            flushlist(takelist(done))
944            flushlist(takelist(rest))
945            -- deal with real data
946            texsetbox("scratchbox",tonode(new_vlist(head)))
947            done  = splitbox("scratchbox",usedsize,"additional")
948            rest  = takebox("scratchbox")
949            used  = getheight(done)
950            if attempts > 1 then
951                used = available
952            end
953            first = takelist(done)
954            head  = takelist(rest)
955            -- return result
956            return used, first, head
957        end
958    end
959end
960
961local nofcolumngaps = 0
962
963function columnsets.add(name,box)
964    local dataset       = data[name]
965    local cells         = dataset.cells
966    local nofcolumns    = dataset.nofcolumns
967    local nofrows       = dataset.nofrows
968    local currentrow    = dataset.currentrow
969    local currentcolumn = dataset.currentcolumn
970    local lineheight    = dataset.lineheight
971    local linedepth     = dataset.linedepth
972    local widths        = dataset.widths
973    --
974    local b = getbox(box)
975    local l = getlist(b)
976-- dataset.rest = l
977    if l then
978        setlist(b,nil)
979        local hd = lineheight + linedepth
980        while l do
981            local foundc, foundr, foundn = findgap(dataset)
982            if foundc then
983                local available = foundn * hd
984                local used, first, last = findslice(dataset,l,available,foundc,foundr)
985                if first then
986                    local v
987                    if used == available or (foundr+foundn > nofrows) then
988                        v = vpack(first,available,"exactly")
989                    else
990                        v = new_vlist(first)
991                    end
992                    nofcolumngaps = nofcolumngaps + 1
993                    -- getmetatable(v).columngap = nofcolumngaps
994                    properties[v] = { columngap = nofcolumngaps }
995                 -- report("setting gap %a at (%i,%i)",nofcolumngaps,foundc,foundr)
996                    setwhd(v,widths[currentcolumn],lineheight,linedepth)
997                    local column = cells[foundc]
998                    --
999                    column[foundr] = v
1000                    used = used - hd
1001                    if used > 0 then
1002                        for r=foundr+1,foundr+foundn-1 do
1003                            used = used - hd
1004                            foundr = foundr + 1
1005                            column[r] = true
1006                            if used <= 0 then
1007                                break
1008                            end
1009                        end
1010                    end
1011                    currentcolumn = foundc
1012                    currentrow    = foundr
1013                    dataset.currentcolumn = currentcolumn
1014                    dataset.currentrow    = currentrow
1015                    l = last
1016                    dataset.rest = l
1017                else
1018                    local column = cells[foundc]
1019                    for i=foundr,foundr+foundn-1 do
1020                        column[i] = true
1021                    end
1022                    l = last
1023                end
1024            else
1025                dataset.rest = l
1026                return -- save and flush
1027            end
1028        end
1029    end
1030end
1031
1032do
1033
1034    -- A split approach is more efficient than a context(followup) inside
1035    -- followup itself as we need less (internal) housekeeping.
1036
1037    local followup = nil
1038    local splitter = lpeg.splitter("*",tonumber)
1039
1040    columnsets["noto"] = function(t)
1041        return followup()
1042    end
1043
1044    columnsets["goto"] = function(name,target)
1045        local dataset    = data[name]
1046        local nofcolumns = dataset.nofcolumns
1047        if target == v_yes or target == "" then
1048            local currentcolumn = dataset.currentcolumn
1049            followup = function()
1050                context(dataset.currentcolumn == currentcolumn and 1 or 0)
1051            end
1052            return followup()
1053        end
1054        if target == v_first then
1055            if dataset.currentcolumn > 1  then
1056                target = v_page
1057            else
1058                return context(0)
1059            end
1060        end
1061        if target == v_page then
1062            if dataset.currentcolumn == 1 and dataset.currentrow == 1 then
1063                return context(0)
1064            else
1065                local currentpage = dataset.page
1066                followup = function()
1067                    context(dataset.page == currentpage and 1 or 0)
1068                end
1069                return followup()
1070            end
1071        end
1072        if target == v_last then
1073            target = dataset.nofcolumns
1074            if dataset.currentcolumn ~= target then
1075                followup = function()
1076                    context(dataset.currentcolumn ~= target and 1 or 0)
1077                end
1078                return followup()
1079            end
1080            return
1081        end
1082        local targetpage = tonumber(target)
1083        if targetpage then
1084            followup = function()
1085                context(dataset.currentcolumn ~= targetpage and 1 or 0)
1086            end
1087            return followup()
1088        end
1089        local targetcolumn, targetrow = lpeg.match(splitter,target)
1090        if targetcolumn and targetrow then
1091            if dataset.currentcolumn ~= targetcolumn and dataset.currentrow ~= targetrow then
1092                followup = function()
1093                    if dataset.currentcolumn ~= targetcolumn then
1094                        context(1)
1095                        return
1096                    end
1097                    if dataset.currentcolumn == targetcolumn then
1098                        context(dataset.currentrow ~= targetrow and 1 or 0)
1099                    else
1100                        context(0)
1101                    end
1102                end
1103                return followup()
1104            end
1105        end
1106    end
1107
1108end
1109
1110function columnsets.currentcolumn(name)
1111    local dataset = data[name]
1112    context(dataset.currentcolumn)
1113end
1114
1115function columnsets.flushrest(name,box)
1116    local dataset = data[name]
1117    local rest    = dataset.rest
1118    if rest then
1119        dataset.rest = nil
1120        setbox("global",box,new_vlist(rest))
1121    end
1122end
1123
1124function columnsets.setvsize(name)
1125    local dataset = data[name]
1126    local c, r, n = findgap(dataset)
1127    if n then
1128        dataset.currentcolumn = c
1129        dataset.currentrow    = r
1130    else
1131        dataset.currentcolumn = 1
1132        dataset.currentrow    = 1
1133        n = 0
1134    end
1135    local gap = n*(dataset.lineheight+dataset.linedepth)
1136    texsetdimen("d_page_grd_gap_height",gap)
1137    -- can be integrated
1138 -- report("state %a, n %a, column %a, row %a",dataset.state,n,dataset.currentcolumn,dataset.currentrow)
1139end
1140
1141function columnsets.sethsize(name)
1142    local dataset = data[name]
1143    texsetdimen("d_page_grd_column_width",dataset.widths[dataset.currentcolumn])
1144end
1145
1146function columnsets.sethspan(name,span)
1147    -- no checking if there is really space, so we assume it can be
1148    -- placed which makes spans a very explicit feature
1149    local dataset   = data[name]
1150    local column    = dataset.currentcolumn
1151    local available = dataset.lastcolumn - column + 1
1152    if span > available then
1153        span = available
1154    end
1155    local width = dataset.spans[column][span]
1156    texsetdimen("d_page_grd_span_width",width)
1157end
1158
1159function columnsets.setlines(t)
1160    local dataset = data[t.name]
1161    dataset.lines[t.page][t.column] = t.value
1162end
1163
1164function columnsets.setstart(t)
1165    local dataset = data[t.name]
1166    dataset.start[t.page][t.column] = t.value
1167end
1168
1169function columnsets.setproperties(t)
1170    local dataset = data[t.name]
1171    local column  = t.column
1172    dataset.distances[column] = t.distance
1173    dataset.widths[column] = t.width
1174end
1175
1176local areas = { }
1177
1178function columnsets.registerarea(t)
1179    -- maybe metatable with values
1180    areas[#areas+1] = t
1181end
1182
1183-- state : repeat | start
1184
1185local ctx_page_grd_set_area = context.protected.page_grd_set_area
1186
1187function columnsets.flushareas(name)
1188    local nofareas = #areas
1189    if nofareas == 0 then
1190        return
1191    end
1192    local dataset = data[name]
1193    local page    = dataset.page
1194    if odd(page) then
1195     -- report("checking %i areas",#areas)
1196        local kept = { }
1197        for i=1,nofareas do
1198            local area = areas[i]
1199         -- local page = area.page -- maybe use page counter in columnset
1200         -- local type = area.type
1201            local okay = false
1202            --
1203            local nofcolumns = area.nc
1204            local nofrows    = area.nr
1205            local column     = area.c
1206            local row        = area.r
1207            columnsets.block {
1208                name = name,
1209                c    = column,
1210                r    = row,
1211                nc   = nofcolumns,
1212                nr   = nofrows,
1213            }
1214            local left     = 0
1215            local start    = dataset.nofleft + 1
1216            local overflow = (column + nofcolumns - 1) - dataset.nofleft
1217            local height   = nofrows * (dataset.lineheight + dataset.linedepth)
1218            local width    = dataset.spreads[column][nofcolumns]
1219         -- report("span, width %p, overflow %i",width,overflow)
1220            if overflow > 0 then
1221                local used = nofcolumns - overflow
1222                left  = dataset.spreads[column][used] + getdimen("backspace")
1223            end
1224            ctx_page_grd_set_area(name,area.name,column,row,width,height,start,left) -- or via counters / dimens
1225            if area.state ~= v_repeat then
1226                area = nil
1227            end
1228            if area then
1229                kept[#kept+1] = area
1230            end
1231        end
1232        areas = kept
1233    end
1234end
1235
1236function columnsets.setarea(t)
1237    local dataset = data[t.name]
1238    local cells   = dataset.cells
1239    local box     = takebox(t.box)
1240    local column  = t.c
1241    local row     = t.r
1242    if column and row then
1243        setwhd(box,dataset.widths[column],dataset.lineheight,dataset.linedepth)
1244        cells[column][row] = box
1245    end
1246end
1247
1248-- The interface.
1249
1250interfaces.implement {
1251    name      = "definecolumnset",
1252    actions   = columnsets.define,
1253    arguments = { {
1254        { "name", "string" },
1255    } }
1256}
1257
1258interfaces.implement {
1259    name      = "resetcolumnset",
1260    actions   = columnsets.reset,
1261    arguments = { {
1262        { "name", "string" },
1263        { "nofleft", "integer" },
1264        { "nofright", "integer" },
1265        { "nofrows", "integer" },
1266        { "lineheight", "dimension" },
1267        { "linedepth", "dimension" },
1268        { "width", "dimension" },
1269        { "distance", "dimension" },
1270        { "maxwidth", "dimension" },
1271    } }
1272}
1273
1274interfaces.implement {
1275    name      = "preparecolumnsetflush",
1276    actions   = columnsets.prepareflush,
1277    arguments = "string",
1278}
1279
1280interfaces.implement {
1281    name      = "finishcolumnsetflush",
1282    actions   = columnsets.finishflush,
1283    arguments = "string",
1284}
1285
1286interfaces.implement {
1287    name      = "flushcolumnsetcolumn",
1288    actions   = columnsets.flushcolumn,
1289    arguments = { "string" ,"integer" },
1290}
1291
1292interfaces.implement {
1293    name      = "setvsizecolumnset",
1294    actions   = columnsets.setvsize,
1295    arguments = "string",
1296}
1297
1298interfaces.implement {
1299    name      = "sethsizecolumnset",
1300    actions   = columnsets.sethsize,
1301    arguments = "string",
1302}
1303
1304interfaces.implement {
1305    name      = "sethsizecolumnspan",
1306    actions   = columnsets.sethspan,
1307    arguments = { "string" ,"integer" },
1308}
1309
1310interfaces.implement {
1311    name      = "flushcolumnsetrest",
1312    actions   = columnsets.flushrest,
1313    arguments = { "string", "integer" },
1314}
1315
1316interfaces.implement {
1317    name      = "blockcolumnset",
1318    actions   = columnsets.block,
1319    arguments = { {
1320        { "name", "string" },
1321        { "c", "integer" },
1322        { "r", "integer" },
1323        { "nc", "integer" },
1324        { "nr", "integer" },
1325        { "method", "string" },
1326        { "box", "integer" },
1327    } }
1328}
1329
1330interfaces.implement {
1331    name      = "checkcolumnset",
1332    actions   = columnsets.check,
1333    arguments = { {
1334        { "name", "string" },
1335        { "method", "string" },
1336        { "c", "integer" },
1337        { "r", "integer" },
1338        { "method", "string" },
1339        { "box", "integer" },
1340        { "width", "dimension" },
1341        { "height", "dimension" },
1342        { "option", "string" },
1343    } }
1344}
1345
1346interfaces.implement {
1347    name      = "putincolumnset",
1348    actions   = columnsets.put,
1349    arguments = { {
1350        { "name", "string" },
1351        { "c", "integer" },
1352        { "r", "integer" },
1353        { "method", "string" },
1354        { "box", "integer" },
1355    } }
1356}
1357
1358interfaces.implement {
1359    name    = "addtocolumnset",
1360    actions = columnsets.add,
1361    arguments = { "string", "integer" },
1362}
1363
1364interfaces.implement {
1365    name    = "setcolumnsetlines",
1366    actions = columnsets.setlines,
1367    arguments = { {
1368        { "name", "string" },
1369        { "page", "integer" },
1370        { "column", "integer" },
1371        { "value", "integer" },
1372    } }
1373}
1374
1375interfaces.implement {
1376    name    = "setcolumnsetstart",
1377    actions = columnsets.setstart,
1378    arguments = { {
1379        { "name", "string" },
1380        { "page", "integer" },
1381        { "column", "integer" },
1382        { "value", "integer" },
1383    } }
1384}
1385
1386interfaces.implement {
1387    name    = "setcolumnsetproperties",
1388    actions = columnsets.setproperties,
1389    arguments = { {
1390        { "name", "string" },
1391        { "column", "integer" },
1392        { "distance", "dimension" },
1393        { "width", "dimension" },
1394    } }
1395}
1396
1397interfaces.implement {
1398    name      = "registercolumnsetarea",
1399    actions   = columnsets.registerarea,
1400    arguments = { {
1401        { "name", "string" },
1402        { "type", "string" },
1403        { "page", "integer" },
1404        { "state", "string" },
1405        { "c", "integer" },
1406        { "r", "integer" },
1407        { "nc", "integer" },
1408        { "nr", "integer" },
1409    } }
1410}
1411
1412interfaces.implement {
1413    name      = "flushcolumnsetareas",
1414    actions   = columnsets.flushareas,
1415    arguments = "string",
1416}
1417
1418interfaces.implement {
1419    name      = "setcolumnsetarea",
1420    actions   = columnsets.setarea,
1421    arguments = { {
1422        { "name", "string" },
1423        { "c", "integer" },
1424        { "r", "integer" },
1425        { "method", "string" },
1426        { "box", "integer" },
1427    } }
1428}
1429
1430interfaces.implement {
1431    name      = "columnsetgoto",
1432    actions   = columnsets["goto"],
1433    arguments = "2 strings",
1434}
1435
1436interfaces.implement {
1437    name      = "columnsetnoto",
1438    actions   = columnsets["noto"],
1439}
1440
1441interfaces.implement {
1442    name      = "columnsetcurrentcolumn",
1443    actions   = columnsets.currentcolumn,
1444    arguments = "string",
1445}
1446