tabl-xtb.lmt /size: 42 Kb    last modification: 2024-01-16 10:22
1if not modules then modules = { } end modules ['tabl-xtb'] = {
2    version   = 1.001,
3    comment   = "companion to tabl-xtb.mkvi",
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--[[
10
11This table mechanism is a combination between TeX and Lua. We do process
12cells at the TeX end and inspect them at the Lua end. After some analysis
13we have a second pass using the calculated widths, and if needed cells
14will go through a third pass to get the heights right. This last pass is
15avoided when possible which is why some code below looks a bit more
16complex than needed. The reason for such optimizations is that each cells
17is actually a framed instance and because tables like this can be hundreds
18of pages we want to keep processing time reasonable.
19
20To a large extend the behaviour is comparable with the way bTABLE/eTABLE
21works and there is a module that maps that one onto this one. Eventually
22this mechamism will be improved so that it can replace its older cousin.
23
24]]--
25
26-- todo: use linked list instead of r/c array
27-- todo: we can use the sum of previously forced widths for column spans
28
29local tonumber, next = tonumber, next
30
31local commands            = commands
32local context             = context
33local ctxnode             = context.nodes.flush
34
35local implement           = interfaces.implement
36
37local tex                 = tex
38local texgetcount         = tex.getcount
39local texsetcount         = tex.setcount
40local texiscount          = tex.iscount
41local texgetdimen         = tex.getdimen
42local texsetdimen         = tex.setdimen
43local texisdimen          = tex.isdimen
44local texget              = tex.get
45
46local format              = string.format
47local concat              = table.concat
48local points              = number.points
49
50local todimen             = string.todimen
51
52local ctx_beginvbox       = context.beginvbox
53local ctx_endvbox         = context.endvbox
54local ctx_blank           = context.blank
55local ctx_nointerlineskip = context.nointerlineskip
56local ctx_dummyxcell      = context.dummyxcell
57
58local variables           = interfaces.variables
59
60local setmetatableindex   = table.setmetatableindex
61local settings_to_hash    = utilities.parsers.settings_to_hash
62
63local nuts                = nodes.nuts -- here nuts gain hardly nothing
64local tonut               = nuts.tonut
65local tonode              = nuts.tonode
66
67local getnext             = nuts.getnext
68local getprev             = nuts.getprev
69local getlist             = nuts.getlist
70local getwidth            = nuts.getwidth
71local getbox              = nuts.getbox
72local getwhd              = nuts.getwhd
73
74local setlink             = nuts.setlink
75local setdirection        = nuts.setdirection
76local setshift            = nuts.setshift
77
78local copynodelist        = nuts.copylist
79local hpacknodelist       = nuts.hpack
80local flushnodelist       = nuts.flushlist
81local takebox             = nuts.takebox
82local migratebox          = nuts.migratebox
83
84local nodepool            = nuts.pool
85
86local new_glue            = nodepool.glue
87local new_kern            = nodepool.kern
88local new_hlist           = nodepool.hlist
89
90local lefttoright_code    = tex.directioncodes.lefttoright
91
92local v_stretch           = variables.stretch
93local v_normal            = variables.normal
94local v_width             = variables.width
95local v_height            = variables.height
96local v_repeat            = variables["repeat"]
97local v_max               = variables.max
98local v_fixed             = variables.fixed
99----- v_auto              = variables.auto
100local v_before            = variables.before
101local v_after             = variables.after
102local v_both              = variables.both
103local v_samepage          = variables.samepage
104local v_tight             = variables.tight
105
106local xtables             = { }
107typesetters.xtables       = xtables
108
109local trace_xtable        = false
110local report_xtable       = logs.reporter("xtable")
111
112trackers.register("xtable.construct", function(v) trace_xtable = v end)
113
114local c_tabl_x_nx            = texiscount("c_tabl_x_nx")
115local c_tabl_x_ny            = texiscount("c_tabl_x_ny")
116local c_tabl_x_state         = texiscount("c_tabl_x_state")
117local c_tabl_x_mode          = texiscount("c_tabl_x_mode")
118local c_tabl_x_skip_mode     = texiscount("c_tabl_x_skip_mode")
119local d_tabl_x_final_width   = texisdimen("d_tabl_x_final_width")
120local d_tabl_x_distance      = texisdimen("d_tabl_x_distance")
121local d_tabl_x_width         = texisdimen("d_tabl_x_width")
122local d_tabl_x_height        = texisdimen("d_tabl_x_height")
123local d_tabl_x_depth         = texisdimen("d_tabl_x_depth")
124local d_lineheight           = texisdimen("lineheight")
125
126local c_frameddimensionstate = texiscount("frameddimensionstate")
127
128local head_mode = 1
129local foot_mode = 2
130local more_mode = 3
131local body_mode = 4
132
133local namedmodes = { [0] =
134    "null",
135    "head",
136    "foot",
137    "next",
138    "body",
139}
140
141local stack, data = { }, nil
142
143function xtables.create(settings)
144    table.insert(stack,data)
145    local rows           = { }
146    local widths         = { }
147    local heights        = { }
148    local depths         = { }
149 -- local spans          = { }
150    local distances      = { }
151    local autowidths     = { }
152    local modes          = { }
153    local fixedrows      = { }
154    local fixedcolumns   = { }
155 -- local fixedcspans    = { }
156    local frozencolumns  = { }
157    local options        = { }
158    local rowproperties  = { }
159    data = {
160        rows           = rows,
161        widths         = widths,
162        heights        = heights,
163        depths         = depths,
164     -- spans          = spans,
165        distances      = distances,
166        modes          = modes,
167        autowidths     = autowidths,
168        fixedrows      = fixedrows,
169        fixedcolumns   = fixedcolumns,
170     -- fixedcspans    = fixedcspans,
171        frozencolumns  = frozencolumns,
172        options        = options,
173        nofrows        = 0,
174        nofcolumns     = 0,
175        currentrow     = 0,
176        currentcolumn  = 0,
177        settings       = settings or { },
178        rowproperties  = rowproperties,
179    }
180    local function add_zero(t,k)
181        t[k] = 0
182        return 0
183    end
184    local function add_table(t,k)
185        local v = { }
186        t[k] = v
187        return v
188    end
189    local function add_cell(row,c)
190        local cell = {
191            nx     = 0,
192            ny     = 0,
193            list   = false,
194            wd     = 0,
195            ht     = 0,
196            dp     = 0,
197        }
198        row[c] = cell
199        if c > data.nofcolumns then
200            data.nofcolumns = c
201        end
202        return cell
203    end
204    local function add_row(rows,r)
205        local row = { }
206        setmetatableindex(row,add_cell)
207        rows[r] = row
208        if r > data.nofrows then
209            data.nofrows = r
210        end
211        return row
212    end
213    setmetatableindex(rows,add_row)
214    setmetatableindex(widths,add_zero)
215    setmetatableindex(heights,add_zero)
216    setmetatableindex(depths,add_zero)
217    setmetatableindex(distances,add_zero)
218    setmetatableindex(modes,add_zero)
219    setmetatableindex(fixedrows,add_zero)
220    setmetatableindex(fixedcolumns,add_zero)
221    setmetatableindex(options,add_table)
222 -- setmetatableindex(fixedcspans,add_table)
223    --
224    local globaloptions = settings_to_hash(settings.option)
225    --
226    settings.columndistance      = tonumber(settings.columndistance) or 0
227    settings.rowdistance         = tonumber(settings.rowdistance) or 0
228    settings.leftmargindistance  = tonumber(settings.leftmargindistance) or 0
229    settings.rightmargindistance = tonumber(settings.rightmargindistance) or 0
230    settings.options             = globaloptions
231    settings.textwidth           = tonumber(settings.textwidth) or texget("hsize")
232    settings.lineheight          = tonumber(settings.lineheight) or texgetdimen(d_lineheight)
233    settings.maxwidth            = tonumber(settings.maxwidth) or settings.textwidth/8
234 -- if #stack > 0 then
235 --     settings.textwidth = texget("hsize")
236 -- end
237    data.criterium_v =   2 * data.settings.lineheight
238    data.criterium_h = .75 * data.settings.textwidth
239    --
240    data.tight = globaloptions[v_tight] and true or false
241end
242
243function xtables.initialize_reflow_width(option,width)
244    local r = data.currentrow
245    local c = data.currentcolumn + 1
246    local drc = data.rows[r][c]
247    drc.nx = texgetcount(c_tabl_x_nx)
248    drc.ny = texgetcount(c_tabl_x_ny)
249    local distances = data.distances
250    local distance = texgetdimen(d_tabl_x_distance)
251    if distance > distances[c] then
252        distances[c] = distance
253    end
254    if option and option ~= "" then
255        local options = settings_to_hash(option)
256        data.options[r][c] = options
257        if options[v_fixed] then
258            data.frozencolumns[c] = true
259        end
260    end
261    data.currentcolumn = c
262end
263
264-- todo: we can better set the cell values in one go
265
266function xtables.set_reflow_width()
267    local r = data.currentrow
268    local c = data.currentcolumn
269    local rows = data.rows
270    local row = rows[r]
271    local cold = c
272    while row[c].span do -- can also be previous row ones
273        c = c + 1
274    end
275    -- bah, we can have a span already
276    if c > cold then
277        local ro = row[cold]
278        local rx = ro.nx
279        local ry = ro.ny
280        if rx > 1 or ry > 1 then
281            local rn = row[c]
282            rn.nx = rx
283            rn.ny = ry
284            ro.nx = 1 -- or 0
285            ro.ny = 1 -- or 0
286            -- do we also need to set ro.span and rn.span
287        end
288    end
289    local tb = getbox("b_tabl_x")
290    local drc = row[c]
291    --
292    drc.list = true -- we don't need to keep the content around as we're in trial mode (no: copynodelist(tb))
293    --
294    local width, height, depth = getwhd(tb)
295    --
296    local widths  = data.widths
297    local heights = data.heights
298    local depths  = data.depths
299    local cspan   = drc.nx
300    if cspan < 2 then
301        if width > widths[c] then
302            widths[c] = width
303        end
304    else
305        local options = data.options[r][c]
306        if data.tight then
307            -- no check
308        elseif not options then
309            if width > widths[c] then
310                widths[c] = width
311            end
312        elseif not options[v_tight] then
313            if width > widths[c] then
314                widths[c] = width
315            end
316        end
317    end
318 -- if cspan > 1 then
319 --     local f = data.fixedcspans[c]
320 --     local w = f[cspan] or 0
321 --     if width > w then
322 --         f[cspan] = width -- maybe some day a solution for autospanmax and so
323 --     end
324 -- end
325    if drc.ny < 2 then
326     -- report_xtable("set width, old: ht=%p, dp=%p",heights[r],depths[r])
327     -- report_xtable("set width, new: ht=%p, dp=%p",height,depth)
328        if height > heights[r] then
329            heights[r] = height
330        end
331        if depth > depths[r] then
332            depths[r] = depth
333        end
334    end
335    --
336    drc.wd = width
337    drc.ht = height
338    drc.dp = depth
339    --
340    local dimensionstate = texgetcount(c_frameddimensionstate)
341    local fixedcolumns = data.fixedcolumns
342    local fixedrows = data.fixedrows
343    if dimensionstate == 1 then
344        if cspan > 1 then
345            -- ignore width
346        elseif width > fixedcolumns[c] then -- how about a span here?
347            fixedcolumns[c] = width
348        end
349    elseif dimensionstate == 2 then
350        fixedrows[r]    = height
351    elseif dimensionstate == 3 then
352        fixedrows[r]    = height -- width
353        fixedcolumns[c] = width -- height
354    elseif width <= data.criterium_h and height >= data.criterium_v then
355        -- somewhat tricky branch
356        if width > fixedcolumns[c] then -- how about a span here?
357            -- maybe an image, so let's fix
358             fixedcolumns[c] = width
359        end
360    end
361--
362-- -- this fails so not good enough predictor
363--
364-- -- \startxtable
365-- -- \startxrow
366-- --     \startxcell knuth        \stopxcell
367-- --     \startxcell \input knuth \stopxcell
368-- -- \stopxrow
369--
370--     else
371--         local o = data.options[r][c]
372--         if o and o[v_auto] then -- new per 5/5/2014 - removed per 15/07/2014
373--             data.autowidths[c] = true
374--         else
375--            -- no dimensions are set in the cell
376--            if width <= data.criterium_h and height >= data.criterium_v then
377--                -- somewhat tricky branch
378--                if width > fixedcolumns[c] then -- how about a span here?
379--                    -- maybe an image, so let's fix
380--                     fixedcolumns[c] = width
381--                end
382--             else
383--                 -- safeguard as it could be text that can be recalculated
384--                 -- and the previous branch could have happened in a previous
385--                 -- row and then forces a wrong one-liner in a multiliner
386--                 if width > fixedcolumns[c] then
387--                     data.autowidths[c] = true -- new per 5/5/2014 - removed per 15/07/2014
388--                 end
389--             end
390--         end
391--     end
392--
393    --
394    drc.dimensionstate = dimensionstate
395    --
396    local nx = drc.nx
397    local ny = drc.ny
398    if nx > 1 or ny > 1 then
399     -- local spans = data.spans -- not used
400        local self = true
401        for y=1,ny do
402            for x=1,nx do
403                if self then
404                    self = false
405                else
406                    local ry = r + y - 1
407                    local cx = c + x - 1
408                 -- if y > 1 then
409                 --     spans[ry] = true -- not used
410                 -- end
411                    rows[ry][cx].span = true
412                end
413            end
414        end
415        c = c + nx - 1
416    end
417    if c > data.nofcolumns then
418        data.nofcolumns = c
419    end
420    data.currentcolumn = c
421end
422
423function xtables.initialize_reflow_height()
424    local r = data.currentrow
425    local c = data.currentcolumn + 1
426    local rows = data.rows
427    local row = rows[r]
428    while row[c].span do -- can also be previous row ones
429        c = c + 1
430    end
431    data.currentcolumn = c
432    local widths = data.widths
433    local w = widths[c]
434    local drc = row[c]
435    for x=1,drc.nx-1 do
436        w = w + widths[c+x]
437    end
438    texsetdimen(d_tabl_x_width,w)
439    local dimensionstate = drc.dimensionstate or 0
440    if dimensionstate == 1 or dimensionstate == 3 then
441        -- width was fixed so height is known
442        texsetcount(c_tabl_x_skip_mode,1)
443    elseif dimensionstate == 2 then
444        -- height is enforced
445        texsetcount(c_tabl_x_skip_mode,1)
446    elseif data.autowidths[c] then
447        -- width has changed so we need to recalculate the height
448        texsetcount(c_tabl_x_skip_mode,0)
449    elseif data.fixedcolumns[c] then
450        texsetcount(c_tabl_x_skip_mode,0) -- new
451    else
452        texsetcount(c_tabl_x_skip_mode,1)
453    end
454end
455
456function xtables.set_reflow_height()
457    local r = data.currentrow
458    local c = data.currentcolumn
459    local rows = data.rows
460    local row = rows[r]
461 -- while row[c].span do -- we could adapt drc.nx instead
462 --     c = c + 1
463 -- end
464    local tb  = getbox("b_tabl_x")
465    local drc = row[c]
466    --
467    local width, height, depth = getwhd(tb)
468    --
469    if drc.ny < 2 then
470        if data.fixedrows[r] == 0 then --  and drc.dimensionstate < 2
471            if drc.ht + drc.dp <= height + depth then -- new per 2017-12-15
472                local heights = data.heights
473                local depths  = data.depths
474             -- report_xtable("set height, old: ht=%p, dp=%p",heights[r],depths[r])
475             -- report_xtable("set height, new: ht=%p, dp=%p",height,depth)
476                if height > heights[r] then
477                    heights[r] = height
478                end
479                if depth > depths[r] then
480                    depths[r] = depth
481                end
482            end
483        end
484    end
485    --
486    drc.wd = width
487    drc.ht = height
488    drc.dp = depth
489    --
490 -- c = c + drc.nx - 1
491 -- data.currentcolumn = c
492end
493
494function xtables.initialize_construct()
495    local r = data.currentrow
496    local c = data.currentcolumn + 1
497    local settings = data.settings
498    local rows = data.rows
499    local row = rows[r]
500    while row[c].span do -- can also be previous row ones
501        c = c + 1
502    end
503    data.currentcolumn = c
504    local widths    = data.widths
505    local heights   = data.heights
506    local depths    = data.depths
507    local distances = data.distances
508    --
509    local drc = row[c]
510    local wd  = drc.wd
511    local ht  = drc.ht
512    local dp  = drc.dp
513    local nx  = drc.nx - 1
514    local ny  = drc.ny - 1
515    --
516    local width  = widths[c]
517    local height = heights[r]
518    local depth  = depths[r] -- problem: can be the depth of a one liner
519    --
520    local total  = height + depth
521    --
522    if nx > 0 then
523        for x=1,nx do
524            width = width + widths[c+x] + distances[c+x-1]
525        end
526        local distance = settings.columndistance
527        if distance ~= 0 then
528            width = width + nx * distance
529        end
530    end
531    --
532    if ny > 0 then
533        for y=1,ny do
534            local nxt = r + y
535            total = total + heights[nxt] + depths[nxt]
536        end
537        local distance = settings.rowdistance
538        if distance ~= 0 then
539            total = total + ny * distance
540        end
541    end
542    --
543    texsetdimen(d_tabl_x_width,width)
544    texsetdimen(d_tabl_x_height,total)
545    texsetdimen(d_tabl_x_depth,0) -- for now
546end
547
548function xtables.set_construct()
549    local r = data.currentrow
550    local c = data.currentcolumn
551    local rows = data.rows
552    local row = rows[r]
553 -- while row[c].span do -- can also be previous row ones
554 --     c = c + 1
555 -- end
556    local drc = row[c]
557    -- this will change as soon as in luatex we can reset a box list without freeing
558    drc.list = takebox("b_tabl_x")
559 -- c = c + drc.nx - 1
560 -- data.currentcolumn = c
561end
562
563local function showwidths(where,widths,autowidths)
564    local result = { }
565    for i=1,#widths do
566        result[i] = format("%12s%s",points(widths[i]),autowidths[i] and "*" or " ")
567    end
568    return report_xtable("%s widths: %s",where,concat(result," "))
569end
570
571function xtables.reflow_width()
572    local nofrows = data.nofrows
573    local nofcolumns = data.nofcolumns
574    local rows = data.rows
575    for r=1,nofrows do
576        local row = rows[r]
577        for c=1,nofcolumns do
578            local drc = row[c]
579            if drc.list then
580             -- flushnodelist(drc.list)
581                drc.list = false
582            end
583        end
584    end
585    -- spread
586    local settings = data.settings
587    local options = settings.options
588    local maxwidth = settings.maxwidth
589    -- calculate width
590    local widths = data.widths
591    local heights = data.heights
592    local depths = data.depths
593    local distances = data.distances
594    local autowidths = data.autowidths
595    local fixedcolumns = data.fixedcolumns
596    local frozencolumns = data.frozencolumns
597    local width = 0
598    local distance = 0
599    local nofwide = 0
600    local widetotal = 0
601    local available = settings.textwidth - settings.leftmargindistance - settings.rightmargindistance
602    if trace_xtable then
603        showwidths("stage 1",widths,autowidths)
604    end
605    local noffrozen = 0
606    -- here we can also check spans
607    if options[v_max] then
608        for c=1,nofcolumns do
609            width = width + widths[c]
610            if width > maxwidth then
611                autowidths[c] = true
612                nofwide = nofwide + 1
613                widetotal = widetotal + widths[c]
614            end
615            if c < nofcolumns then
616                distance = distance + distances[c]
617            end
618            if frozencolumns[c] then
619                noffrozen = noffrozen + 1 -- brr, should be nx or so
620            end
621        end
622    else
623        for c=1,nofcolumns do -- also keep track of forced
624            local fixedwidth = fixedcolumns[c]
625            if fixedwidth > 0 then
626                widths[c] = fixedwidth
627                width = width + fixedwidth
628            else
629                local wc = widths[c]
630                width = width + wc
631             -- if width > maxwidth then
632                if wc > maxwidth then -- per 2015-08-09
633                    autowidths[c] = true
634                    nofwide = nofwide + 1
635                    widetotal = widetotal + wc
636                end
637            end
638            if c < nofcolumns then
639                distance = distance + distances[c]
640            end
641            if frozencolumns[c] then
642                noffrozen = noffrozen + 1 -- brr, should be nx or so
643            end
644        end
645    end
646    if trace_xtable then
647        showwidths("stage 2",widths,autowidths)
648    end
649    local delta = available - width - distance - (nofcolumns-1) * settings.columndistance
650    if delta == 0 then
651        -- nothing to be done
652        if trace_xtable then
653            report_xtable("perfect fit")
654        end
655    elseif delta > 0 then
656        -- we can distribute some
657        if not options[v_stretch] then
658            -- not needed
659            if trace_xtable then
660                report_xtable("too wide but no stretch, delta %p",delta)
661            end
662        elseif options[v_width] then
663            local factor = delta / width
664            if trace_xtable then
665                report_xtable("proportional stretch, delta %p, width %p, factor %a",delta,width,factor)
666            end
667            for c=1,nofcolumns do
668                widths[c] = widths[c] + factor * widths[c]
669            end
670        else
671            -- frozen -> a column with option=fixed will not stretch
672            local extra = delta / (nofcolumns - noffrozen)
673            if trace_xtable then
674                report_xtable("normal stretch, delta %p, extra %p",delta,extra)
675            end
676            for c=1,nofcolumns do
677                if not frozencolumns[c] then
678                    widths[c] = widths[c] + extra
679                end
680            end
681        end
682    elseif nofwide > 0 then
683        while true do
684            done = false
685            local available = (widetotal + delta) / nofwide
686            if trace_xtable then
687                report_xtable("shrink check, total %p, delta %p, columns %s, fixed %p",widetotal,delta,nofwide,available)
688            end
689            for c=1,nofcolumns do
690                if autowidths[c] and available >= widths[c] then
691                    autowidths[c] = nil
692                    nofwide = nofwide - 1
693                    widetotal = widetotal - widths[c]
694                    done = true
695                end
696            end
697            if not done then
698                break
699            end
700        end
701        -- maybe also options[v_width] here but tricky as width does not say
702        -- much about amount
703        if options[v_width] then -- not that much (we could have a clever vpack loop balancing .. no fun)
704            local factor = (widetotal + delta) / width
705            if trace_xtable then
706                report_xtable("proportional shrink used, total %p, delta %p, columns %s, factor %s",widetotal,delta,nofwide,factor)
707            end
708            for c=1,nofcolumns do
709                if autowidths[c] then
710                    widths[c] = factor * widths[c]
711                end
712            end
713        else
714            local available = (widetotal + delta) / nofwide
715            if trace_xtable then
716                report_xtable("normal shrink used, total %p, delta %p, columns %s, fixed %p",widetotal,delta,nofwide,available)
717            end
718            for c=1,nofcolumns do
719                if autowidths[c] then
720                    widths[c] = available
721                end
722            end
723        end
724    end
725    if trace_xtable then
726        showwidths("stage 3",widths,autowidths)
727    end
728    --
729    data.currentrow = 0
730    data.currentcolumn = 0
731end
732
733function xtables.reflow_height()
734    data.currentrow = 0
735    data.currentcolumn = 0
736    local settings = data.settings
737    --
738    -- analyze ny
739    --
740    local nofrows    = data.nofrows
741    local nofcolumns = data.nofcolumns
742    local widths     = data.widths
743    local heights    = data.heights
744    local depths     = data.depths
745    --
746    for r=1,nofrows do
747        for c=1,nofcolumns do
748            local drc = data.rows[r][c]
749            if drc then
750                local ny = drc.ny
751                if ny > 1 then
752                    local height = heights[r]
753                    local depth  = depths[r]
754                    local total  = height + depth
755                    local htdp   = drc.ht + drc.dp
756                    for y=1,ny-1 do
757                        local nxt = r + y
758                        total = total + heights[nxt] + depths[nxt]
759                    end
760                    local delta = htdp - total
761                    if delta > 0 then
762                        delta = delta / ny
763                        for y=0,ny-1 do
764                            local nxt = r + y
765                            heights[nxt] = heights[nxt] + delta
766                        end
767                    end
768                end
769            end
770        end
771    end
772    --
773    if settings.options[v_height] then
774        local totalheight = 0
775        local totaldepth = 0
776        for i=1,nofrows do
777            totalheight = totalheight + heights[i]
778            totalheight = totalheight + depths [i]
779        end
780        local total = totalheight + totaldepth
781        local leftover = settings.textheight - total
782        if leftover > 0 then
783            local leftheight = (totalheight / total) * leftover / #heights
784            local leftdepth  = (totaldepth  / total) * leftover / #depths
785            for i=1,nofrows do
786                heights[i] = heights[i] + leftheight
787                depths [i] = depths [i] + leftdepth
788            end
789        end
790    end
791end
792
793local function showspans(data)
794    local rows = data.rows
795    local modes = data.modes
796    local nofcolumns = data.nofcolumns
797    local nofrows = data.nofrows
798    for r=1,nofrows do
799        local line = { }
800        local row = rows[r]
801        for c=1,nofcolumns do
802            local cell =row[c]
803            if cell.list then
804                line[#line+1] = "list"
805            elseif cell.span then
806                line[#line+1] = "span"
807            else
808                line[#line+1] = "none"
809            end
810        end
811        report_xtable("%3d : %s : % t",r,namedmodes[modes[r]] or "----",line)
812    end
813end
814
815function xtables.construct()
816    local rows = data.rows
817    local heights = data.heights
818    local depths = data.depths
819    local widths = data.widths
820 -- local spans = data.spans
821    local distances = data.distances
822    local modes = data.modes
823    local settings = data.settings
824    local nofcolumns = data.nofcolumns
825    local nofrows = data.nofrows
826    local columndistance = settings.columndistance
827    local rowdistance = settings.rowdistance
828    local leftmargindistance = settings.leftmargindistance
829    local rightmargindistance = settings.rightmargindistance
830    local rowproperties = data.rowproperties
831    -- ranges can be mixes so we collect
832
833    if trace_xtable then
834        showspans(data)
835    end
836
837    local ranges = {
838        [head_mode] = { },
839        [foot_mode] = { },
840        [more_mode] = { },
841        [body_mode] = { },
842    }
843    for r=1,nofrows do
844        local m = modes[r]
845        if m == 0 then
846            m = body_mode
847        end
848        local range = ranges[m]
849        range[#range+1] = r
850    end
851    -- todo: hook in the splitter ... the splitter can ask for a chunk of
852    -- a certain size ... no longer a split memory issue then and header
853    -- footer then has to happen here too .. target height
854    local function packaged_column(r)
855        local row = rows[r]
856        local start = nil
857        local stop = nil
858        if leftmargindistance > 0 then
859            start = new_kern(leftmargindistance)
860            stop = start
861        end
862        local hasspan = false
863        for c=1,nofcolumns do
864            local drc = row[c]
865            if not hasspan then
866                hasspan = drc.span
867            end
868            local list = drc.list
869            if list then
870                local w, h, d = getwhd(list)
871                setshift(list,h+d)
872             -- list = hpacknodelist(list) -- is somehow needed
873             -- setwhd(list,0,0,0)
874                -- faster:
875                local h = new_hlist(list)
876                migratebox(list,h)
877                list = h
878                --
879                if start then
880                    setlink(stop,list)
881                else
882                    start = list
883                end
884                stop = list
885            end
886            local step = widths[c]
887            if c < nofcolumns then
888                step = step + columndistance + distances[c]
889            end
890            local kern = new_kern(step)
891            if stop then
892                setlink(stop,kern)
893            else -- can be first spanning next row (ny=...)
894                start = kern
895            end
896            stop = kern
897        end
898        if start then
899            if rightmargindistance > 0 then
900                local kern = new_kern(rightmargindistance)
901                setlink(stop,kern)
902             -- stop = kern
903            end
904            return start, heights[r] + depths[r], hasspan
905        end
906    end
907    local function collect_range(range)
908        local result, nofr = { }, 0
909        local nofrange = #range
910        for i=1,#range do
911            local r = range[i]
912         -- local row = rows[r]
913            local list, size, hasspan = packaged_column(r)
914            if list then
915                if hasspan and nofr > 0 then
916                    result[nofr][4] = true
917                end
918                nofr = nofr + 1
919                local rp = rowproperties[r]
920                -- we have a direction issue here but hpacknodelist(list,0,"exactly") cannot be used
921                -- due to the fact that we need the width
922                local hbox = hpacknodelist(list)
923                setdirection(hbox,lefttoright_code)
924                result[nofr] = {
925                    hbox,
926                    size,
927                    i < nofrange and rowdistance > 0 and rowdistance or false, -- might move
928                    false,
929                    rp or false,
930                }
931            end
932        end
933        if nofr > 0 then
934            -- the [5] slot gets the after break
935            result[1]   [5] = false
936            result[nofr][5] = false
937            for i=2,nofr-1 do
938                local r = result[i][5]
939                if r == v_both or r == v_before then
940                    result[i-1][5] = true
941                elseif r == v_after then
942                    result[i][5] = true
943                end
944            end
945        end
946        return result
947    end
948    local body = collect_range(ranges[body_mode])
949    data.results = {
950        [head_mode] = collect_range(ranges[head_mode]),
951        [foot_mode] = collect_range(ranges[foot_mode]),
952        [more_mode] = collect_range(ranges[more_mode]),
953        [body_mode] = body,
954    }
955    if #body == 0 then
956        texsetcount("global",c_tabl_x_state,0)
957        texsetdimen("global",d_tabl_x_final_width,0)
958    else
959        texsetcount("global",c_tabl_x_state,1)
960        texsetdimen("global",d_tabl_x_final_width,getwidth(body[1][1]))
961    end
962end
963
964-- todo: join as that is as efficient as fushing multiple
965
966local function inject(row,copy,package)
967    local list = row[1]
968    if copy then
969        row[1] = copynodelist(list)
970    end
971    if package then
972        ctx_beginvbox()
973        ctxnode(tonode(list))
974        ctxnode(tonode(new_kern(row[2])))
975        ctx_endvbox()
976        ctx_nointerlineskip() -- figure out a better way
977        if row[4] then
978            -- nothing as we have a span
979        elseif row[5] then
980            if row[3] then
981                ctx_blank { v_samepage, row[3] .. "sp" }
982            else
983                ctx_blank { v_samepage }
984            end
985        elseif row[3] then
986            ctx_blank { row[3] .. "sp" } -- why blank ?
987        else
988            ctxnode(tonode(new_glue(0)))
989        end
990    else
991        ctxnode(tonode(list))
992        ctxnode(tonode(new_kern(row[2])))
993        if row[3] then
994            ctxnode(tonode(new_glue(row[3])))
995        end
996    end
997end
998
999local function total(row,distance)
1000    local n = #row > 0 and rowdistance or 0
1001    for i=1,#row do
1002        local ri = row[i]
1003        n = n + ri[2] + (ri[3] or 0)
1004    end
1005    return n
1006end
1007
1008-- local function append(list,what)
1009--     for i=1,#what do
1010--         local l = what[i]
1011--         list[#list+1] = l[1]
1012--         local k = l[2] + (l[3] or 0)
1013--         if k ~= 0 then
1014--             list[#list+1] = new_kern(k)
1015--         end
1016--     end
1017-- end
1018
1019local function spanheight(body,i)
1020    local height, n = 0, 1
1021    while true do
1022        local bi = body[i]
1023        if bi then
1024            height = height + bi[2] + (bi[3] or 0)
1025            if bi[4] then
1026                n = n + 1
1027                i = i + 1
1028            else
1029                break
1030            end
1031        else
1032            break
1033        end
1034    end
1035    return height, n
1036end
1037
1038function xtables.flush(directives) -- todo split by size / no inbetween then ..  glue list kern blank
1039    local height       = directives.height
1040    local method       = directives.method or v_normal
1041    local settings     = data.settings
1042    local results      = data.results
1043    local rowdistance  = settings.rowdistance
1044    local head         = results[head_mode]
1045    local foot         = results[foot_mode]
1046    local more         = results[more_mode]
1047    local body         = results[body_mode]
1048    local repeatheader = settings.header == v_repeat
1049    local repeatfooter = settings.footer == v_repeat
1050    if height and height > 0 then
1051        ctx_beginvbox()
1052        local bodystart = data.bodystart or 1
1053        local bodystop  = data.bodystop or #body
1054        if bodystart > 0 and bodystart <= bodystop then
1055            local bodysize = height
1056            local footsize = total(foot,rowdistance)
1057            local headsize = total(head,rowdistance)
1058            local moresize = total(more,rowdistance)
1059            local firstsize, firstspans = spanheight(body,bodystart)
1060            if bodystart == 1 then -- first chunk gets head
1061                bodysize = bodysize - headsize - footsize
1062                if headsize > 0 and bodysize >= firstsize then
1063                    for i=1,#head do
1064                        inject(head[i],repeatheader)
1065                    end
1066                    if rowdistance > 0 then
1067                        ctxnode(tonode(new_glue(rowdistance)))
1068                    end
1069                    if not repeatheader then
1070                        results[head_mode] = { }
1071                    end
1072                end
1073            elseif moresize > 0 then -- following chunk gets next
1074                bodysize = bodysize - footsize - moresize
1075                if bodysize >= firstsize then
1076                    for i=1,#more do
1077                        inject(more[i],true)
1078                    end
1079                    if rowdistance > 0 then
1080                        ctxnode(tonode(new_glue(rowdistance)))
1081                    end
1082                end
1083            elseif headsize > 0 and repeatheader then -- following chunk gets head
1084                bodysize = bodysize - footsize - headsize
1085                if bodysize >= firstsize then
1086                    for i=1,#head do
1087                        inject(head[i],true)
1088                    end
1089                    if rowdistance > 0 then
1090                        ctxnode(tonode(new_glue(rowdistance)))
1091                    end
1092                end
1093            else -- following chunk gets nothing
1094                bodysize = bodysize - footsize
1095            end
1096            if bodysize >= firstsize then
1097                local i = bodystart
1098                while i <= bodystop do -- room for improvement
1099                    local total, spans = spanheight(body,i)
1100                    local bs = bodysize - total
1101                    if bs > 0 then
1102                        bodysize = bs
1103                        for s=1,spans do
1104                            inject(body[i])
1105                            body[i] = nil
1106                            i = i + 1
1107                        end
1108                        bodystart = i
1109                    else
1110                        break
1111                    end
1112                end
1113                if bodystart > bodystop then
1114                    -- all is flushed and footer fits
1115                    if footsize > 0 then
1116                        if rowdistance > 0 then
1117                            ctxnode(tonode(new_glue(rowdistance)))
1118                        end
1119                        for i=1,#foot do
1120                            inject(foot[i])
1121                        end
1122                        results[foot_mode] = { }
1123                    end
1124                    results[body_mode] = { }
1125                    texsetcount("global",c_tabl_x_state,0)
1126                else
1127                    -- some is left so footer is delayed
1128                    -- todo: try to flush a few more lines
1129                    if repeatfooter and footsize > 0 then
1130                        if rowdistance > 0 then
1131                            ctxnode(tonode(new_glue(rowdistance)))
1132                        end
1133                        for i=1,#foot do
1134                            inject(foot[i],true)
1135                        end
1136                    else
1137                        -- todo: try to fit more of body
1138                    end
1139                    texsetcount("global",c_tabl_x_state,2)
1140                end
1141            else
1142                if firstsize > height then
1143                    -- get rid of the too large cell
1144                    for s=1,firstspans do
1145                        inject(body[bodystart])
1146                        body[bodystart] = nil
1147                        bodystart = bodystart + 1
1148                    end
1149                end
1150                texsetcount("global",c_tabl_x_state,2) -- 1
1151            end
1152        else
1153            texsetcount("global",c_tabl_x_state,0)
1154        end
1155        data.bodystart = bodystart
1156        data.bodystop = bodystop
1157        ctx_endvbox()
1158    else
1159        if method == variables.split then
1160            -- maybe also a non float mode with header/footer repeat although
1161            -- we can also use a float without caption
1162            for i=1,#head do
1163                inject(head[i],false,true)
1164            end
1165            if #head > 0 and rowdistance > 0 then
1166                ctx_blank { rowdistance .. "sp" }
1167            end
1168            for i=1,#body do
1169                inject(body[i],false,true)
1170            end
1171            if #foot > 0 and rowdistance > 0 then
1172                ctx_blank { rowdistance .. "sp" }
1173            end
1174            for i=1,#foot do
1175                inject(foot[i],false,true)
1176            end
1177        else -- normal
1178            ctx_beginvbox()
1179            for i=1,#head do
1180                inject(head[i])
1181            end
1182            if #head > 0 and rowdistance > 0 then
1183                ctxnode(tonode(new_glue(rowdistance)))
1184            end
1185            for i=1,#body do
1186                inject(body[i])
1187            end
1188            if #foot > 0 and rowdistance > 0 then
1189                ctxnode(tonode(new_glue(rowdistance)))
1190            end
1191            for i=1,#foot do
1192                inject(foot[i])
1193            end
1194            ctx_endvbox()
1195        end
1196        results[head_mode] = { }
1197        results[body_mode] = { }
1198        results[foot_mode] = { }
1199        texsetcount("global",c_tabl_x_state,0)
1200    end
1201end
1202
1203function xtables.cleanup()
1204    for mode, result in next, data.results do
1205        for _, r in next, result do
1206            flushnodelist(r[1])
1207        end
1208    end
1209
1210    -- local rows = data.rows
1211    -- for i=1,#rows do
1212    --     local row = rows[i]
1213    --     for i=1,#row do
1214    --         local cell = row[i]
1215    --         local list = cell.list
1216    --         if list then
1217    --             cell.width, cell.height, cell.depth = getwhd(list)
1218    --             cell.list = true
1219    --         end
1220    --     end
1221    -- end
1222    -- data.result = nil
1223
1224    data = table.remove(stack)
1225end
1226
1227function xtables.next_row(specification)
1228    local r = data.currentrow + 1
1229    data.modes[r] = texgetcount(c_tabl_x_mode)
1230    data.currentrow = r
1231    data.currentcolumn = 0
1232    data.rowproperties[r] = specification
1233end
1234
1235function xtables.finish_row()
1236    local c = data.currentcolumn
1237    local r = data.currentrow
1238    local d = data.rows[r][c]
1239    local n = data.nofcolumns - c
1240    if d then
1241        local nx = d.nx
1242        if nx > 0 then
1243            n = n - nx + 1
1244        end
1245    end
1246    if n > 0 then
1247        for i=1,n do
1248            ctx_dummyxcell()
1249        end
1250    end
1251end
1252
1253-- eventually we might only have commands
1254
1255implement {
1256    name      = "x_table_create",
1257    actions   = xtables.create,
1258    arguments = {
1259        {
1260            { "option" },
1261            { "textwidth", "dimen" },
1262            { "textheight", "dimen" },
1263            { "maxwidth", "dimen" },
1264            { "lineheight", "dimen" },
1265            { "columndistance", "dimen" },
1266            { "leftmargindistance", "dimen" },
1267            { "rightmargindistance", "dimen" },
1268            { "rowdistance", "dimen" },
1269            { "header" },
1270            { "footer" },
1271        }
1272    }
1273}
1274
1275implement {
1276    name      = "x_table_flush",
1277    actions   = xtables.flush,
1278    arguments = {
1279        {
1280            { "method" },
1281            { "height", "dimen" }
1282        }
1283    }
1284}
1285
1286implement { name = "x_table_reflow_width",              actions = xtables.reflow_width }
1287implement { name = "x_table_reflow_height",             actions = xtables.reflow_height }
1288implement { name = "x_table_construct",                 actions = xtables.construct }
1289implement { name = "x_table_cleanup",                   actions = xtables.cleanup }
1290implement { name = "x_table_next_row",                  actions = xtables.next_row }
1291implement { name = "x_table_next_row_option",           actions = xtables.next_row, arguments = "string" }
1292implement { name = "x_table_finish_row",                actions = xtables.finish_row }
1293implement { name = "x_table_init_reflow_width",         actions = xtables.initialize_reflow_width  }
1294implement { name = "x_table_init_reflow_height",        actions = xtables.initialize_reflow_height }
1295implement { name = "x_table_init_reflow_width_option",  actions = xtables.initialize_reflow_width,  arguments = "string" }
1296implement { name = "x_table_init_reflow_height_option", actions = xtables.initialize_reflow_height, arguments = "string" }
1297implement { name = "x_table_init_construct",            actions = xtables.initialize_construct }
1298implement { name = "x_table_set_reflow_width",          actions = xtables.set_reflow_width }
1299implement { name = "x_table_set_reflow_height",         actions = xtables.set_reflow_height }
1300implement { name = "x_table_set_construct",             actions = xtables.set_construct }
1301implement { name = "x_table_r",                         actions = function() context(data.currentrow    or 0) end }
1302implement { name = "x_table_c",                         actions = function() context(data.currentcolumn or 0) end }
1303
1304-- experiment:
1305
1306do
1307
1308    local context         = context
1309    local ctxcore         = context.core
1310
1311    local startxtable     = ctxcore.startxtable
1312    local stopxtable      = ctxcore.stopxtable
1313
1314    local startcollecting = context.startcollecting
1315    local stopcollecting  = context.stopcollecting
1316
1317    function ctxcore.startxtable(...)
1318        startcollecting()
1319        startxtable(...)
1320    end
1321
1322    function ctxcore.stopxtable()
1323        stopxtable()
1324        stopcollecting()
1325    end
1326
1327end
1328