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