spac-ver.lmt /size: 105 Kb    last modification: 2024-01-16 09:03
1if not modules then modules = { } end modules ['spac-ver'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to spac-ver.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10-- we also need to call the spacer for inserts!
11
12-- somehow lists still don't always have proper prev nodes so i need to
13-- check all of the luatex code some day .. maybe i should replece the
14-- whole mvl handler by lua code .. why not
15
16-- todo: use lua nodes with lua data (>0.79)
17-- see ** can go when 0.79
18
19-- needs to be redone, too many calls and tests now ... still within some
20-- luatex limitations
21
22-- this code dates from the beginning and is kind of experimental; it
23-- will be optimized and improved soon .. it's way too complex now but
24-- dates from less possibilities
25--
26-- the collapser will be redone with user nodes; also, we might get make
27-- parskip into an attribute and appy it explicitly thereby getting rid
28-- of automated injections; eventually i want to get rid of the currently
29-- still needed tex -> lua -> tex > lua chain (needed because we can have
30-- expandable settings at the tex end
31
32-- todo: strip baselineskip around display math
33
34local next, type, tonumber = next, type, tonumber
35local gmatch, concat = string.gmatch, table.concat
36local lpegmatch = lpeg.match
37local unpack = unpack or table.unpack
38local allocate = utilities.storage.allocate
39local todimen = string.todimen
40local formatters = string.formatters
41
42local nodes        =  nodes
43local trackers     =  trackers
44local attributes   =  attributes
45local context      =  context
46local tex          =  tex
47
48local texlists     = tex.lists
49local texget       = tex.get
50local texgetcount  = tex.getcount
51local texgetdimen  = tex.getdimen
52local texgetglue   = tex.getglue
53local texset       = tex.set
54local texsetdimen  = tex.setdimen
55local texsetcount  = tex.setcount
56local texnest      = tex.nest
57local texgetbox    = tex.getbox
58
59local buildpage    = tex.triggerbuildpage
60
61local variables    = interfaces.variables
62
63local v_local      = variables["local"]
64local v_global     = variables["global"]
65local v_box        = variables.box
66----- v_page       = variables.page -- reserved for future use
67local v_split      = variables.split
68local v_min        = variables.min
69local v_max        = variables.max
70local v_none       = variables.none
71local v_first      = variables.first
72local v_last       = variables.last
73local v_top        = variables.top
74local v_bottom     = variables.bottom
75local v_maxheight  = variables.maxheight
76local v_minheight  = variables.minheight
77local v_mindepth   = variables.mindepth
78local v_maxdepth   = variables.maxdepth
79local v_offset     = variables.offset
80local v_strut      = variables.strut
81
82local v_hfraction  = variables.hfraction
83local v_dfraction  = variables.dfraction
84local v_bfraction  = variables.bfraction
85local v_tlines     = variables.tlines
86local v_blines     = variables.blines
87
88-- vertical space handler
89
90local trace_vbox_vspacing    = false  trackers.register("vspacing.vbox",     function(v) trace_vbox_vspacing    = v end)
91local trace_page_vspacing    = false  trackers.register("vspacing.page",     function(v) trace_page_vspacing    = v end)
92local trace_collect_vspacing = false  trackers.register("vspacing.collect",  function(v) trace_collect_vspacing = v end)
93local trace_vspacing         = false  trackers.register("vspacing.spacing",  function(v) trace_vspacing         = v end)
94local trace_vsnapping        = false  trackers.register("vspacing.snapping", function(v) trace_vsnapping        = v end)
95local trace_specials         = false  trackers.register("vspacing.specials", function(v) trace_specials         = v end)
96
97local remove_math_skips      = true   directives.register("vspacing.removemathskips", function(v) remnove_math_skips = v end)
98
99local report_vspacing     = logs.reporter("vspacing","spacing")
100local report_collapser    = logs.reporter("vspacing","collapsing")
101local report_snapper      = logs.reporter("vspacing","snapping")
102local report_specials     = logs.reporter("vspacing","specials")
103
104local a_skipcategory      = attributes.private('skipcategory')
105local a_skippenalty       = attributes.private('skippenalty')
106local a_skiporder         = attributes.private('skiporder')
107local a_snapmethod        = attributes.private('snapmethod')
108local a_snapvbox          = attributes.private('snapvbox')
109
110local d_bodyfontstrutheight       = tex.isdimen("bodyfontstrutheight")
111local d_bodyfontstrutdepth        = tex.isdimen("bodyfontstrutdepth")
112local d_globalbodyfontstrutheight = tex.isdimen("globalbodyfontstrutheight")
113local d_globalbodyfontstrutdepth  = tex.isdimen("globalbodyfontstrutdepth")
114----- d_strutht                   = tex.isdimen("strutht")
115local d_strutdp                   = tex.isdimen("strutdp")
116local d_spac_overlay              = tex.isdimen("d_spac_overlay")
117
118local c_spac_vspacing_ignore_parskip = tex.iscount("c_spac_vspacing_ignore_parskip")
119
120local nuts                = nodes.nuts
121local tonut               = nuts.tonut
122
123local getnext             = nuts.getnext
124local setlink             = nuts.setlink
125local getprev             = nuts.getprev
126local getid               = nuts.getid
127local getlist             = nuts.getlist
128local setlist             = nuts.setlist
129local getattr             = nuts.getattr
130local setattr             = nuts.setattr
131local getsubtype          = nuts.getsubtype
132local getbox              = nuts.getbox
133local getwhd              = nuts.getwhd
134local setwhd              = nuts.setwhd
135local getprop             = nuts.getprop
136local setprop             = nuts.setprop
137local getglue             = nuts.getglue
138local setglue             = nuts.setglue
139local getkern             = nuts.getkern
140local getpenalty          = nuts.getpenalty
141local setshift            = nuts.setshift
142local setwidth            = nuts.setwidth
143local getwidth            = nuts.getwidth
144local setheight           = nuts.setheight
145local getheight           = nuts.getheight
146local setdepth            = nuts.setdepth
147local getdepth            = nuts.getdepth
148local setnext             = nuts.setnext
149local setprev             = nuts.setprev
150
151local find_node_tail      = nuts.tail
152local flushnode           = nuts.flushnode
153local remove_node         = nuts.remove
154local count_nodes         = nuts.countall
155local hpack_node          = nuts.hpack
156local vpack_node          = nuts.vpack
157
158local startofpar          = nuts.startofpar
159
160local write_node          = nuts.write
161
162local nextnode            = nuts.traversers.node
163local nexthlist           = nuts.traversers.hlist
164
165local nodereference       = nuts.reference
166
167local listtoutf           = nodes.listtoutf
168local nodeidstostring     = nodes.idstostring
169
170local nodepool            = nuts.pool
171
172local new_penalty         = nodepool.penalty
173local new_kern            = nodepool.kern
174local new_glue            = nodepool.glue
175local new_rule            = nodepool.rule
176
177local nodecodes           = nodes.nodecodes
178local gluecodes           = nodes.gluecodes
179----- penaltycodes        = nodes.penaltycodes
180----- listcodes           = nodes.listcodes
181
182local penalty_code        = nodecodes.penalty
183local kern_code           = nodecodes.kern
184local glue_code           = nodecodes.glue
185local hlist_code          = nodecodes.hlist
186local vlist_code          = nodecodes.vlist
187local rule_code           = nodecodes.rule
188local par_code            = nodecodes.par
189local boundary_code       = nodecodes.boundary
190
191local userskip_code       = gluecodes.userskip
192local lineskip_code       = gluecodes.lineskip
193local baselineskip_code   = gluecodes.baselineskip
194local parskip_code        = gluecodes.parskip
195local topskip_code        = gluecodes.topskip
196local splittopskip_code   = gluecodes.splittopskip
197
198local linelist_code       = nodes.listcodes.line
199
200local setvisual           = function(...) setvisual = nuts.setvisual return setvisual(...) end
201
202local properties          = nodes.properties.data
203
204local vspacing            = builders.vspacing or { }
205builders.vspacing         = vspacing
206
207local vspacingdata        = vspacing.data or { }
208vspacing.data             = vspacingdata
209
210local snapmethods         = vspacingdata.snapmethods or { }
211vspacingdata.snapmethods  = snapmethods
212
213storage.register("builders/vspacing/data/snapmethods", snapmethods, "builders.vspacing.data.snapmethods")
214
215do
216
217    local default = {
218        [v_maxheight] = true,
219        [v_maxdepth]  = true,
220        [v_strut]     = true,
221        [v_hfraction] = 1,
222        [v_dfraction] = 1,
223        [v_bfraction] = 0.25,
224    }
225
226    local fractions = {
227        [v_minheight] = v_hfraction, [v_maxheight] = v_hfraction,
228        [v_mindepth]  = v_dfraction, [v_maxdepth]  = v_dfraction,
229        [v_box]       = v_bfraction,
230        [v_top]       = v_tlines,    [v_bottom]    = v_blines,
231    }
232
233    local values = {
234        offset = "offset"
235    }
236
237    local colonsplitter = lpeg.splitat(":")
238
239    local function listtohash(str)
240        local t = { }
241        for s in gmatch(str,"[^, ]+") do
242            local key, detail = lpegmatch(colonsplitter,s)
243            local v = variables[key]
244            if v then
245                t[v] = true
246                if detail then
247                    local k = fractions[key]
248                    if k then
249                        detail = tonumber("0" .. detail)
250                        if detail then
251                            t[k] = detail
252                        end
253                    else
254                        k = values[key]
255                        if k then
256                            detail = todimen(detail)
257                            if detail then
258                                t[k] = detail
259                            end
260                        end
261                    end
262                end
263            else
264                detail = tonumber("0" .. key)
265                if detail then
266                    t[v_hfraction] = detail
267                    t[v_dfraction] = detail
268                end
269            end
270        end
271        if next(t) then
272            t[v_hfraction] = t[v_hfraction] or 1
273            t[v_dfraction] = t[v_dfraction] or 1
274            return t
275        else
276            return default
277        end
278    end
279
280    function vspacing.definesnapmethod(name,method)
281        local n = #snapmethods + 1
282        local t = listtohash(method)
283        snapmethods[n] = t
284        t.name          = name   -- not interfaced
285        t.specification = method -- not interfaced
286        context(n)
287    end
288
289end
290
291local function validvbox(parentid,list)
292    if parentid == hlist_code then
293        local id = getid(list)
294        if id == par_code and startofpar(list) then
295            list = getnext(list)
296            if not next then
297                return nil
298            end
299        end
300        local done = nil
301        for n, id in nextnode, list do
302            if id == vlist_code or id == hlist_code then
303                if done then
304                    return nil
305                else
306                    done = n
307                end
308            elseif id == glue_code or id == penalty_code then
309                -- go on
310            else
311                return nil -- whatever
312            end
313        end
314        if done then
315            local id = getid(done)
316            if id == hlist_code then
317                return validvbox(id,getlist(done))
318            end
319        end
320        return done -- only one vbox
321    end
322end
323
324local function already_done(parentid,list,a_snapmethod) -- todo: done when only boxes and all snapped
325    -- problem: any snapped vbox ends up in a line
326    if list and parentid == hlist_code then
327        local id = getid(list)
328        if id == par_code and startofpar(list) then
329            list = getnext(list)
330            if not list then
331                return false
332            end
333        end
334        for n, id in nextnode, list do
335            if id == hlist_code or id == vlist_code then
336             -- local a = getattr(n,a_snapmethod)
337             -- if not a then
338             --  -- return true -- not snapped at all
339             -- elseif a == 0 then
340             --     return true -- already snapped
341             -- end
342                local p = getprop(n,"snapper")
343                if p then
344                    return p
345                end
346            elseif id == glue_code or id == penalty_code then -- or id == kern_code then
347                -- go on
348            else
349                return false -- whatever
350            end
351        end
352    end
353    return false
354end
355
356-- check variables.none etc
357
358local snap_hlist  do
359
360    local v_noheight   = variables.noheight
361    local v_nodepth    = variables.nodepth
362    local v_line       = variables.line
363    local v_halfline   = variables.halfline
364    local v_line_m     = "-" .. v_line
365    local v_halfline_m = "-" .. v_halfline
366
367    local floor = math.floor
368    local ceil  = math.ceil
369
370    local function fixedprofile(current)
371        local profiling = builders.profiling
372        return profiling and profiling.fixedprofile(current)
373    end
374
375    -- quite tricky: ceil(-something) => -0
376
377    local function ceiled(n)
378        if n < 0 or n < 0.01 then
379            return 0
380        else
381            return ceil(n)
382        end
383    end
384
385    local function floored(n)
386        if n < 0 or n < 0.01 then
387            return 0
388        else
389            return floor(n)
390        end
391    end
392
393    snap_hlist = function(where,current,method,height,depth) -- method[v_strut] is default
394        if fixedprofile(current) then
395            return
396        end
397        local list = getlist(current)
398        local t = trace_vsnapping and { }
399        if t then
400            t[#t+1] = formatters["list content: %s"](listtoutf(list))
401            t[#t+1] = formatters["snap method: %s"](method.name) -- not interfaced
402            t[#t+1] = formatters["specification: %s"](method.specification) -- not interfaced
403        end
404        local snapht, snapdp
405        if method[v_local] then
406            -- snapping is done immediately here
407            snapht = texgetdimen(d_bodyfontstrutheight)
408            snapdp = texgetdimen(d_bodyfontstrutdepth)
409            if t then
410                t[#t+1] = formatters["local: snapht %p snapdp %p"](snapht,snapdp)
411            end
412        elseif method[v_global] then
413            snapht = texgetdimen(d_globalbodyfontstrutheight)
414            snapdp = texgetdimen(d_globalbodyfontstrutdepth)
415            if t then
416                t[#t+1] = formatters["global: snapht %p snapdp %p"](snapht,snapdp)
417            end
418        else
419            -- maybe autolocal
420            -- snapping might happen later in the otr
421            snapht = texgetdimen(d_globalbodyfontstrutheight)
422            snapdp = texgetdimen(d_globalbodyfontstrutdepth)
423            local lsnapht = texgetdimen(d_bodyfontstrutheight)
424            local lsnapdp = texgetdimen(d_bodyfontstrutdepth)
425            if snapht ~= lsnapht and snapdp ~= lsnapdp then
426                snapht, snapdp = lsnapht, lsnapdp
427            end
428            if t then
429                t[#t+1] = formatters["auto: snapht %p snapdp %p"](snapht,snapdp)
430            end
431        end
432
433        local wd, ht, dp = getwhd(current)
434
435        local h        = (method[v_noheight] and 0) or height or ht
436        local d        = (method[v_nodepth]  and 0) or depth  or dp
437        local hr       = method[v_hfraction] or 1
438        local dr       = method[v_dfraction] or 1
439        local br       = method[v_bfraction] or 0
440        local ch       = h
441        local cd       = d
442        local tlines   = method[v_tlines] or 1
443        local blines   = method[v_blines] or 1
444        local done     = false
445        local plusht   = snapht
446        local plusdp   = snapdp
447        local snaphtdp = snapht + snapdp
448        local extra    = 0
449
450        if t then
451            t[#t+1] = formatters["hlist: wd %p ht %p (used %p) dp %p (used %p)"](wd,ht,h,dp,d)
452            t[#t+1] = formatters["fractions: hfraction %s dfraction %s bfraction %s tlines %s blines %s"](hr,dr,br,tlines,blines)
453        end
454
455        if method[v_box] then
456            local br = 1 - br
457            if br < 0 then
458                br = 0
459            elseif br > 1 then
460                br = 1
461            end
462            local n = ceiled((h+d-br*snapht-br*snapdp)/snaphtdp)
463            local x = n * snaphtdp - h - d
464            plusht = h + x / 2
465            plusdp = d + x / 2
466            if t then
467                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_box,plusht,plusdp)
468            end
469        elseif method[v_max] then
470            local n = ceiled((h+d)/snaphtdp)
471            local x = n * snaphtdp - h - d
472            plusht = h + x / 2
473            plusdp = d + x / 2
474            if t then
475                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_max,plusht,plusdp)
476            end
477        elseif method[v_min] then
478            -- we catch a lone min
479            if method.specification ~= v_min then
480                local n = floored((h+d)/snaphtdp)
481                local x = n * snaphtdp - h - d
482                plusht = h + x / 2
483                plusdp = d + x / 2
484                if plusht < 0 then
485                    plusht = 0
486                end
487                if plusdp < 0 then
488                    plusdp = 0
489                end
490            end
491            if t then
492                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_min,plusht,plusdp)
493            end
494        elseif method[v_none] then
495            plusht, plusdp = 0, 0
496            if t then
497                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_none,0,0)
498            end
499        end
500        -- for now, we actually need to tag a box and then check at several points if something ended up
501        -- at the top of a page
502        if method[v_halfline] then -- extra halfline
503            extra  = snaphtdp/2
504            plusht = plusht + extra
505            plusdp = plusdp + extra
506            if t then
507                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_halfline,plusht,plusdp)
508            end
509        end
510        if method[v_line] then -- extra line
511            extra  = snaphtdp
512            plusht = plusht + extra
513            plusdp = plusdp + extra
514            if t then
515                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_line,plusht,plusdp)
516            end
517        end
518        if method[v_halfline_m] then -- extra halfline
519            extra  = - snaphtdp/2
520            plusht = plusht + extra
521            plusdp = plusdp + extra
522            if t then
523                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_halfline_m,plusht,plusdp)
524            end
525        end
526        if method[v_line_m] then -- extra line
527            extra  = - snaphtdp
528            plusht = plusht + extra
529            plusdp = plusdp + extra
530            if t then
531                t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_line_m,plusht,plusdp)
532            end
533        end
534        if method[v_first] then
535            local thebox = current
536            local id = getid(thebox)
537            if id == hlist_code then
538                thebox = validvbox(id,getlist(thebox))
539                id = thebox and getid(thebox)
540            end
541            if thebox and id == vlist_code then
542                local list = getlist(thebox)
543                local lw, lh, ld
544                for n in nexthlist, list do
545                    lw, lh, ld = getwhd(n)
546                    break
547                end
548                if lh then
549                    local wd, ht, dp = getwhd(thebox)
550                    if t then
551                        t[#t+1] = formatters["first line: height %p depth %p"](lh,ld)
552                        t[#t+1] = formatters["dimensions: height %p depth %p"](ht,dp)
553                    end
554                    local delta = h - lh
555                    ch, cd = lh, delta + d
556                    h, d = ch, cd
557                    local shifted = hpack_node(getlist(current))
558                    setshift(shifted,delta)
559                    setlist(current,shifted)
560                    done = true
561                    if t then
562                        t[#t+1] = formatters["first: height %p depth %p shift %p"](ch,cd,delta)
563                    end
564                elseif t then
565                    t[#t+1] = "first: not done, no content"
566                end
567            elseif t then
568                t[#t+1] = "first: not done, no vbox"
569            end
570        elseif method[v_last] then
571            local thebox = current
572            local id = getid(thebox)
573            if id == hlist_code then
574                thebox = validvbox(id,getlist(thebox))
575                id = thebox and getid(thebox)
576            end
577            if thebox and id == vlist_code then
578                local list = getlist(thebox)
579                local lw, lh, ld
580                for n in nexthlist, list do
581                    lw, lh, ld = getwhd(n)
582                end
583                if lh then
584                    local wd, ht, dp = getwhd(thebox)
585                    if t then
586                        t[#t+1] = formatters["last line: height %p depth %p" ](lh,ld)
587                        t[#t+1] = formatters["dimensions: height %p depth %p"](ht,dp)
588                    end
589                    local delta = d - ld
590                    cd, ch = ld, delta + h
591                    h, d = ch, cd
592                    local shifted = hpack_node(getlist(current))
593                    setshift(shifted,delta)
594                    setlist(current,shifted)
595                    done = true
596                    if t then
597                        t[#t+1] = formatters["last: height %p depth %p shift %p"](ch,cd,delta)
598                    end
599                elseif t then
600                    t[#t+1] = "last: not done, no content"
601                end
602            elseif t then
603                t[#t+1] = "last: not done, no vbox"
604            end
605        end
606        if method[v_minheight] then
607            ch = floored((h-hr*snapht)/snaphtdp)*snaphtdp + plusht
608            if t then
609                t[#t+1] = formatters["minheight: %p"](ch)
610            end
611        elseif method[v_maxheight] then
612            ch = ceiled((h-hr*snapht)/snaphtdp)*snaphtdp + plusht
613            if t then
614                t[#t+1] = formatters["maxheight: %p"](ch)
615            end
616        else
617            ch = plusht
618            if t then
619                t[#t+1] = formatters["set height: %p"](ch)
620            end
621        end
622        if method[v_mindepth] then
623            cd = floored((d-dr*snapdp)/snaphtdp)*snaphtdp + plusdp
624            if t then
625                t[#t+1] = formatters["mindepth: %p"](cd)
626            end
627        elseif method[v_maxdepth] then
628            cd = ceiled((d-dr*snapdp)/snaphtdp)*snaphtdp + plusdp
629            if t then
630                t[#t+1] = formatters["maxdepth: %p"](cd)
631            end
632        else
633            cd = plusdp
634            if t then
635                t[#t+1] = formatters["set depth: %p"](cd)
636            end
637        end
638        if method[v_top] then
639            ch = ch + tlines * snaphtdp
640            if t then
641                t[#t+1] = formatters["top height: %p"](ch)
642            end
643        end
644        if method[v_bottom] then
645            cd = cd + blines * snaphtdp
646            if t then
647                t[#t+1] = formatters["bottom depth: %p"](cd)
648            end
649        end
650        local offset = method[v_offset]
651        if offset then
652            -- we need to set the attr
653            if t then
654                local wd, ht, dp = getwhd(current)
655                t[#t+1] = formatters["before offset: %p (width %p height %p depth %p)"](offset,wd,ht,dp)
656            end
657            local shifted = hpack_node(getlist(current))
658            setshift(shifted,offset)
659            setlist(current,shifted)
660            if t then
661                local wd, ht, dp = getwhd(current)
662                t[#t+1] = formatters["after offset: %p (width %p height %p depth %p)"](offset,wd,ht,dp)
663            end
664            setattr(shifted,a_snapmethod,0)
665            setattr(current,a_snapmethod,0)
666        end
667        if not height then
668            setheight(current,ch)
669            if t then
670                t[#t+1] = formatters["forced height: %p"](ch)
671            end
672        end
673        if not depth then
674            setdepth(current,cd)
675            if t then
676                t[#t+1] = formatters["forced depth: %p"](cd)
677            end
678        end
679        local lines = (ch+cd)/snaphtdp
680        if t then
681            local original = (h+d)/snaphtdp
682            local whatever = (ch+cd)/(texgetdimen(d_globalbodyfontstrutheight) + texgetdimen(d_globalbodyfontstrutdepth))
683            t[#t+1] = formatters["final lines : %p -> %p (%p)"](original,lines,whatever)
684            t[#t+1] = formatters["final height: %p -> %p"](h,ch)
685            t[#t+1] = formatters["final depth : %p -> %p"](d,cd)
686        end
687    -- todo:
688    --
689    --     if h < 0 or d < 0 then
690    --         h = 0
691    --         d = 0
692    --     end
693        if t then
694            report_snapper("trace: %s type %s\n\t%\n\tt",where,nodecodes[getid(current)],t)
695        end
696        if not method[v_split] then
697            -- so extra will not be compensated at the top of a page
698            extra = 0
699        end
700        return h, d, ch, cd, lines, extra
701    end
702
703end
704
705local categories = { [0] =
706    "discard",
707    "largest",
708    "force",
709    "penalty",
710    "add",
711    "disable",
712    "nowhite",
713    "goback",
714    "packed",
715    "overlay",
716    "enable",
717    "notopskip",
718}
719
720categories          = allocate(table.swapped(categories,categories))
721vspacing.categories = categories
722
723function vspacing.tocategories(str)
724    local t = { }
725    for s in gmatch(str,"[^, ]") do -- use lpeg instead
726        local n = tonumber(s)
727        if n then
728            t[categories[n]] = true
729        else
730            t[b] = true
731        end
732    end
733    return t
734end
735
736function vspacing.tocategory(str) -- can be optimized
737    if type(str) == "string" then
738        return set.tonumber(vspacing.tocategories(str))
739    else
740        return set.tonumber({ [categories[str]] = true })
741    end
742end
743
744vspacingdata.map  = vspacingdata.map  or { } -- allocate ?
745vspacingdata.skip = vspacingdata.skip or { } -- allocate ?
746
747storage.register("builders/vspacing/data/map",  vspacingdata.map,  "builders.vspacing.data.map")
748storage.register("builders/vspacing/data/skip", vspacingdata.skip, "builders.vspacing.data.skip")
749
750local setspecification, getspecification
751
752-- 1 statepool  : 2 : more overhead : a bit slower than properties
753-- 2 attributes : 1 : more overhead : feels faster than properties
754-- 3 properties : 3 : more natural  : feels slower than attributes
755-- 4 data       : 1 : more native   : is little faster than attributes (limited penalty)
756
757-- testfile: t:/bugs/bottomfloats-001.tex
758
759local method = 1 -- better tracing
760-- local method = 2
761-- local method = 3
762-- local method = 4
763
764-- todo: not true but only visual a_visual,tex.getattribute(a_visual)
765
766if method == 1 then
767
768    local registervalue = attributes.registervalue
769    local getvalue      = attributes.getvalue
770    local values        = attributes.values
771
772    setspecification = function(n,category,penalty,order)
773        local detail = { category, penalty, order or 1 }
774        local value  = registervalue(a_skipcategory,detail)
775        setattr(n,a_skipcategory,value)
776    end
777
778    getspecification = function(n)
779        local value = getattr(n,a_skipcategory)
780        if value then
781            local detail = getvalue(a_skipcategory,value)
782         -- local detail = attributes.values[a_skipcategory][value]
783         -- local detail = values[a_skipcategory][value]
784            if detail then
785                return detail[1], detail[2], detail[3]
786            end
787        end
788        return false, false, 1
789    end
790
791elseif method == 2 then
792
793    -- quite okay but more memory due to attributes (not many)
794
795    local setattrs = nuts.setattrs
796    local getattrs = nuts.getattrs
797
798    setspecification = function(n,category,penalty,order)
799        setattrs(n,false,a_skipcategory,category or nil,a_skippenalty,penalty or nil,a_skiporder,order or 1)
800    end
801
802    getspecification = function(n)
803        local c, p, o = getattrs(n,a_skipcategory,a_skippenalty,a_skiporder)
804        return c or false, p or false, o or 1
805    end
806
807elseif method == 3 then
808
809    -- more natural as we stay in lua
810
811    setspecification = function(n,category,penalty,order)
812        -- we know that there are no properties
813        properties[n] = {
814            [a_skipcategory] = category,
815            [a_skippenalty]  = penalty,
816            [a_skiporder]    = order or 1,
817        }
818    end
819
820    getspecification = function(n)
821        local p = properties[n]
822        if p then
823            return p[a_skipcategory], p[a_skippenalty], p[a_skiporder]
824        end
825    end
826
827elseif method == 4 then
828
829    -- quite efficient but needs testing because we limit values
830
831    local getdata = nuts.getdata
832    local setdata = nuts.setdata
833
834    setspecification = function(n,category,penalty,order)
835        if not category or category > 0xF then
836            category = 0xF
837        end
838        if not order or order > 0xFF then
839            order = 0xFF
840        end
841        if not penalty or penalty > 0x7FFFF then
842            penalty = 0x7FFFF
843        elseif penalty < -0x7FFFF then
844            penalty = -0x7FFFF
845        end
846        -- we need overflow checks
847        setdata(n, (penalty << 12) + (order << 4) + category)
848    end
849
850    getspecification = function(n)
851        local data = getdata(n)
852        if data and data ~= 0 then
853            local category =  data        & 0x0F
854            local order    = (data >>  4) & 0xFF
855            local penalty  =  data >> 12
856            if category == 0xF then
857                category = nil
858            end
859            if order == 0xFF then
860                order = nil
861            end
862            if penalty == 0x7FFFF then
863                penalty = nil
864            end
865            return category, penalty, order
866        else
867            return nil, nil, nil
868        end
869    end
870
871end
872
873do
874
875    local P, C, R, S, Cc, Cs = lpeg.P, lpeg.C, lpeg.R, lpeg.S, lpeg.Cc, lpeg.Cs
876
877    vspacing.fixed   = false
878
879    local map        = vspacingdata.map
880    local skip       = vspacingdata.skip
881
882    local sign       = S("+-")^0
883    local multiplier = C(sign * R("09")^1) * P("*")
884    local singlefier = Cs(sign * Cc(1))
885    local separator  = S(", ")
886    local category   = P(":") * C((1-separator)^1)
887    local keyword    = C((1-category-separator)^1)
888    local splitter   = (multiplier + Cc(1)) * keyword * (category + Cc(false))
889
890    local k_fixed    = variables.fixed
891    local k_flexible = variables.flexible
892    local k_limit    = variables.limit
893
894    local k_category <const> = "category"
895    local k_penalty  <const> = "penalty"
896    local k_order    <const> = "order"
897
898    local setoptions = nuts.setoptions
899
900    function vspacing.setmap(from,to)
901        map[from] = to
902    end
903
904    function vspacing.setskip(key,value,grid)
905        if value ~= "" then
906            if grid == "" then grid = value end
907            skip[key] = { value, grid }
908        end
909    end
910
911    local expandmacro = token.expandmacro -- todo
912 -- local runlocal    = tex.runlocal
913 -- local setmacro    = tokens.setters.macro
914 -- local settoks     = tex.settoks
915    local toscaled    = tex.toscaled
916
917    local b_done     = false
918    local b_packed   = false
919
920    local b_amount   = 0
921    local b_stretch  = 0
922    local b_shrink   = 0
923    local b_category = false
924    local b_penalty  = false
925    local b_order    = false
926    local b_fixed    = false
927    local b_grid     = false
928    local b_limit    = false
929
930    local pattern    = nil
931
932    local packed     = categories.packed
933
934    local gluefactor = .25
935
936    local ctx_ignoreparskip = context.core.ignoreparskip
937
938    local function before()
939        b_amount   = 0
940        b_stretch  = 0
941        b_shrink   = 0
942        b_category = 1
943        b_penalty  = false
944        b_order    = false
945        b_fixed    = b_grid
946        b_limit    = false
947    end
948
949    local function after()
950        if b_fixed then
951            b_stretch = 0
952            b_shrink  = 0
953        else
954            b_stretch = gluefactor * b_amount
955            b_shrink  = gluefactor * b_amount
956        end
957    end
958
959    -- use a cache for predefined ones
960
961    local function inject()
962        local n = new_glue(b_amount,b_stretch,b_shrink)
963        setspecification(n,b_category,b_penalty,b_order or 1)
964        setvisual(n)
965        write_node(n)
966        if b_limit then
967            setoptions(n,tex.glueoptioncodes.limit)
968        end
969        -- todo: inject via value
970    end
971
972    local function flush()
973        after()
974        if b_done then
975            inject()
976            b_done = false
977        end
978        before()
979    end
980
981 -- local cmd = token.create("vspacingfromtempstring")
982 -- local cmd = token.create("vspacingpredefinedvalue") -- not yet known
983
984    ----- s_predefined = "s_spac_vspacing_predefined"
985    local s_predefined = tex.isskip("s_spac_vspacing_predefined")
986
987    local function handler(multiplier, keyword, detail)
988        if not keyword then
989            report_vspacing("unknown directive %a",s)
990        else
991            local mk = map[keyword]
992            if mk then
993                lpegmatch(pattern,mk)
994            elseif keyword == k_fixed then
995                b_fixed = true
996            elseif keyword == k_flexible then
997                b_flexible = false
998            elseif keyword == k_category then
999                local category = tonumber(detail)
1000                if category == packed then
1001                    b_packed = true
1002                elseif category then
1003                    b_category = category
1004                    b_done     = true
1005                    flush()
1006                end
1007            elseif keyword == k_order and detail then
1008                local order = tonumber(detail)
1009                if order then
1010                    b_order = order
1011                end
1012            elseif keyword == k_penalty and detail then
1013                local penalty = tonumber(detail)
1014                if penalty then
1015                    flush()
1016                    b_done = true
1017                    b_category = 3
1018                    b_penalty = penalty
1019                    flush()
1020                end
1021            elseif keyword == k_limit then
1022                b_limit = true
1023            else
1024                local amount, stretch, shrink
1025                multiplier = tonumber(multiplier) or 1
1026                local sk = skip[keyword]
1027                if sk then
1028                    -- multiplier, keyword
1029                    -- best, for now, todo: runlocal with arguments
1030                    expandmacro("vspacingpredefinedvalue",true,keyword)
1031                 -- expandmacro(cmd,true,keyword)
1032                 -- setmacro("tempstring",keyword)
1033                 -- runlocal(cmd)
1034                    -- nicest
1035                 -- runlocal(cache[keyword])
1036                    -- fast
1037                 -- settoks("scratchtoks",keyword)
1038                 -- runlocal("vspacingfromscratchtoks")
1039                    -- middleground
1040                 -- setmacro("tempstring",keyword)
1041                 -- runlocal(ctx_vspacingfromtempstring)
1042                    --
1043                    amount, stretch, shrink = texgetglue(s_predefined)
1044                    if not stretch then
1045                        stretch = 0
1046                    end
1047                    if not shrink then
1048                        shrink = 0
1049                    end
1050                    if stretch == 0 and shrink == 0 then
1051                        stretch = gluefactor * amount -- always unless grid
1052                        shrink  = stretch             -- always unless grid
1053                    end
1054                else -- no check, todo: parse plus and minus
1055                    amount  = toscaled(keyword)
1056                    stretch = gluefactor * amount -- always unless grid
1057                    shrink  = stretch             -- always unless grid
1058                end
1059                -- we look at fixed later
1060                b_amount  = b_amount  + multiplier * amount
1061                b_stretch = b_stretch + multiplier * stretch
1062                b_shrink  = b_shrink  + multiplier * shrink
1063                b_done    = true
1064            end
1065        end
1066    end
1067
1068    -- alternatively we can make a table and have a keyword -> split cache but this is probably
1069    -- not really a bottleneck
1070
1071    local splitter = ((multiplier + singlefier) * keyword * (category + Cc(false))) / handler
1072          pattern  = (splitter + separator^1)^0
1073
1074    function vspacing.inject(grid,str)
1075        if trace_vspacing then
1076         -- ctx_pushlogger(report_vspacing)
1077        end
1078        b_done   = false
1079        b_packed = false
1080        b_grid   = grid == true or grid == 1
1081        before()
1082        lpegmatch(pattern,str)
1083        after()
1084        if b_done then
1085            inject()
1086        end
1087        if b_packed then
1088            ctx_ignoreparskip()
1089        end
1090        if trace_vspacing then
1091         -- ctx_poplogger()
1092        end
1093    end
1094
1095    function vspacing.injectpenalty(penalty)
1096        local n = new_glue()
1097     -- setattrs(n,false,a_skipcategory,categories.penalty,a_skippenalty,penalty,a_skiporder,1)
1098        setspecification(n,categories.penalty,penalty,1)
1099        setvisual(k)
1100        write_node(n)
1101    end
1102
1103    function vspacing.injectskip(amount)
1104        local n = new_glue(amount)
1105     -- setattrs(n,false,a_skipcategory,categories.largest,a_skippenalty,false,a_skiporder,1)
1106        setspecification(n,categories.largest,false,1)
1107        setvisual(k)
1108        write_node(n)
1109    end
1110
1111    function vspacing.injectdisable(amount)
1112        local n = new_glue()
1113     -- setattrs(n,false,a_skipcategory,categories.disable,a_skippenalty,false,a_skiporder,1)
1114        setspecification(n,categories.disable,false,1)
1115        setvisual(k)
1116        write_node(n)
1117    end
1118
1119end
1120
1121-- implementation
1122
1123-- alignment box begin_of_par vmodepar hmodepar insert penalty before_display after_display
1124
1125function vspacing.snapbox(n,how)
1126    local sv = snapmethods[how]
1127    if sv then
1128        local box = getbox(n)
1129        local list = getlist(box)
1130        if list then
1131            local s = getattr(list,a_snapmethod)
1132            if s == 0 then
1133                if trace_vsnapping then
1134                --  report_snapper("box list not snapped, already done")
1135                end
1136            else
1137                local wd, ht, dp = getwhd(box)
1138                if false then -- todo: already_done
1139                    -- assume that the box is already snapped
1140                    if trace_vsnapping then
1141                        report_snapper("box list already snapped at (%p,%p): %s",
1142                            ht,dp,listtoutf(list))
1143                    end
1144                else
1145                    local h, d, ch, cd, lines, extra = snap_hlist("box",box,sv,ht,dp)
1146                    setprop(box,"snapper",{
1147                        ht = h,
1148                        dp = d,
1149                        ch = ch,
1150                        cd = cd,
1151                        extra = extra,
1152                        current = current,
1153                    })
1154                    setwhd(box,wd,ch,cd)
1155                    if trace_vsnapping then
1156                        report_snapper("box list snapped from (%p,%p) to (%p,%p) using method %a (%s) for %a (%s lines): %s",
1157                            h,d,ch,cd,sv.name,sv.specification,"direct",lines,listtoutf(list))
1158                    end
1159                    setattr(box,a_snapmethod,0)  --
1160                    setattr(list,a_snapmethod,0) -- yes or no
1161                end
1162            end
1163        end
1164    end
1165end
1166
1167-- I need to figure out how to deal with the prevdepth that crosses pages. In fact,
1168-- prevdepth is often quite interfering (even over a next paragraph) so I need to
1169-- figure out a trick. Maybe use something other than a rule. If we visualize we'll
1170-- see the baselineskip in action:
1171--
1172-- \blank[force,5*big] { \baselineskip1cm xxxxxxxxx \par } \page
1173-- \blank[force,5*big] { \baselineskip1cm xxxxxxxxx \par } \page
1174-- \blank[force,5*big] { \baselineskip5cm xxxxxxxxx \par } \page
1175
1176-- We can register and copy the rule instead.
1177
1178do
1179
1180    local insertnodeafter  = nuts.insertafter
1181    local insertnodebefore = nuts.insertbefore
1182
1183    local abovedisplayskip_code      = gluecodes.abovedisplayskip
1184    local belowdisplayskip_code      = gluecodes.belowdisplayskip
1185    local abovedisplayshortskip_code = gluecodes.abovedisplayshortskip
1186    local belowdisplayshortskip_code = gluecodes.belowdisplayshortskip
1187
1188    local w, h, d = 0, 0, 0
1189    ----- w, h, d = 100*65536, 65536, 65536
1190
1191    local trace_list   = { }
1192    local tracing_info = { }
1193    local before       = ""
1194    local after        = ""
1195
1196    local function nodes_to_string(head)
1197        local current = head
1198        local t       = { }
1199        while current do
1200            local id = getid(current)
1201            local ty = nodecodes[id]
1202            if id == penalty_code then
1203                t[#t+1] = formatters["%s:%s"](ty,getpenalty(current))
1204            elseif id == glue_code then
1205                t[#t+1] = formatters["%s:%s:%p"](ty,gluecodes[getsubtype(current)],getwidth(current))
1206            elseif id == kern_code then
1207                t[#t+1] = formatters["%s:%p"](ty,getkern(current))
1208            else
1209                t[#t+1] = ty
1210            end
1211            current = getnext(current)
1212        end
1213        return concat(t," + ")
1214    end
1215
1216    local function reset_tracing(head)
1217        trace_list, tracing_info, before, after = { }, false, nodes_to_string(head), ""
1218    end
1219
1220    local function trace_skip(str,sc,so,sp,data)
1221        trace_list[#trace_list+1] = { "skip", formatters["%s | %p | category %s | order %s | penalty %s | subtype %s"](str, getwidth(data), sc or "-", so or "-", sp or "-", gluecodes[getsubtype(data)]) }
1222        tracing_info = true
1223    end
1224
1225    local function trace_natural(str,data)
1226        trace_list[#trace_list+1] = { "skip", formatters["%s | %p"](str, getwidth(data)) }
1227        tracing_info = true
1228    end
1229
1230    local function trace_info(message, where, what)
1231        trace_list[#trace_list+1] = { "info", formatters["%s: %s/%s"](message,where,what) }
1232    end
1233
1234    local function trace_node(what)
1235        local nt = nodecodes[getid(what)]
1236        local tl = trace_list[#trace_list]
1237        if tl and tl[1] == "node" then
1238            trace_list[#trace_list] = { "node", formatters["%s + %s"](tl[2],nt) }
1239        else
1240            trace_list[#trace_list+1] = { "node", nt }
1241        end
1242    end
1243
1244    local function show_tracing(head)
1245        if tracing_info then
1246            after = nodes_to_string(head)
1247            for i=1,#trace_list do
1248                local tag, text = unpack(trace_list[i])
1249                if tag == "info" then
1250                    report_collapser(text)
1251                else
1252                    report_collapser("  %s: %s",tag,text)
1253                end
1254            end
1255            report_collapser("before: %s",before)
1256            report_collapser("after : %s",after)
1257        end
1258    end
1259
1260    local function trace_done(str,data)
1261        if getid(data) == penalty_code then
1262            trace_list[#trace_list+1] = { "penalty", formatters["%s | %s"](str,getpenalty(data)) }
1263        else
1264            trace_list[#trace_list+1] = { "glue", formatters["%s | %p"](str,getwidth(data)) }
1265        end
1266        tracing_info = true
1267    end
1268
1269    local function forced_skip(head,current,width,where,trace) -- looks old ... we have other tricks now
1270        if head == current then
1271            if getsubtype(head) == baselineskip_code then
1272                width = width - getwidth(head)
1273            end
1274        end
1275        if width == 0 then
1276            -- do nothing
1277        else
1278            local b = new_rule(w,h,d)
1279            local k = new_kern(width)
1280            local a = new_rule(w,h,d)
1281            setvisual(k)
1282            if where == "after" then
1283                head, current = insertnodeafter(head,current,b)
1284                head, current = insertnodeafter(head,current,k)
1285                head, current = insertnodeafter(head,current,a)
1286            else
1287                local c = current
1288                head, current = insertnodebefore(head,current,b)
1289                head, current = insertnodebefore(head,current,k)
1290                head, current = insertnodebefore(head,current,a)
1291                current = c
1292            end
1293        end
1294        if trace then
1295            report_vspacing("inserting forced skip of %p",width)
1296        end
1297        return head, current
1298    end
1299
1300    -- penalty only works well when before skip
1301
1302    local discard   = categories.discard
1303    local largest   = categories.largest
1304    local force     = categories.force
1305    local penalty   = categories.penalty
1306    local add       = categories.add
1307    local disable   = categories.disable
1308    local nowhite   = categories.nowhite
1309    local goback    = categories.goback
1310    local packed    = categories.packed
1311    local overlay   = categories.overlay
1312    local enable    = categories.enable
1313    local notopskip = categories.notopskip
1314
1315    -- [whatsits][hlist][glue][glue][penalty]
1316
1317    local special_penalty_min = 32250
1318    local special_penalty_max = 35000
1319    local special_penalty_xxx =     0
1320
1321    -- this is rather messy and complex: we want to make sure that successive
1322    -- header don't break but also make sure that we have at least a decent
1323    -- break when we have succesive ones (often when testing)
1324
1325    -- todo: mark headers as such so that we can recognize them
1326
1327    local specialmethods = { }
1328    local specialmethod  = 1
1329
1330    specialmethods[1] = function(pagehead,pagetail,start,penalty)
1331        --
1332        if not pagehead or penalty < special_penalty_min or penalty > special_penalty_max then
1333            return
1334        end
1335        local current  = pagetail
1336        --
1337        -- nodes.showsimplelist(pagehead,0)
1338        --
1339        if trace_specials then
1340            report_specials("checking penalty %a",penalty)
1341        end
1342        while current do
1343            local id = getid(current)
1344            if id == penalty_code then
1345                local p = properties[current]
1346                if p then
1347                    local p = p.special_penalty
1348                    if not p then
1349                        if trace_specials then
1350                            report_specials("  regular penalty, continue")
1351                        end
1352                    elseif p == penalty then
1353                        if trace_specials then
1354                            report_specials("  context penalty %a, same level, overloading",p)
1355                        end
1356                        return special_penalty_xxx
1357                    elseif p > special_penalty_min and p < special_penalty_max then
1358                        if penalty < p then
1359                            if trace_specials then
1360                                report_specials("  context penalty %a, lower level, overloading",p)
1361                            end
1362                            return special_penalty_xxx
1363                        else
1364                            if trace_specials then
1365                                report_specials("  context penalty %a, higher level, quitting",p)
1366                            end
1367                            return
1368                        end
1369                    elseif trace_specials then
1370                        report_specials("  context penalty %a, higher level, continue",p)
1371                    end
1372                else
1373                    local p = getpenalty(current)
1374                    if p < 10000 then
1375                        -- assume some other mechanism kicks in so we seem to have content
1376                        if trace_specials then
1377                            report_specials("  regular penalty %a, quitting",p)
1378                        end
1379                        break
1380                    else
1381                        if trace_specials then
1382                            report_specials("  regular penalty %a, continue",p)
1383                        end
1384                    end
1385                end
1386            end
1387            current = getprev(current)
1388        end
1389        -- none found, so no reson to be special
1390        if trace_specials then
1391            if pagetail then
1392                report_specials("  context penalty, discarding, nothing special")
1393            else
1394                report_specials("  context penalty, discarding, nothing preceding")
1395            end
1396        end
1397        return special_penalty_xxx
1398    end
1399
1400    -- This will be replaced after 0.80+ when we have a more robust look-back and
1401    -- can look at the bigger picture.
1402
1403    -- todo: look back and when a special is there before a list is seen penalty keep ut
1404
1405    -- we now look back a lot, way too often
1406
1407    -- userskip
1408    -- lineskip
1409    -- baselineskip
1410    -- parskip
1411    -- abovedisplayskip
1412    -- belowdisplayskip
1413    -- abovedisplayshortskip
1414    -- belowdisplayshortskip
1415    -- topskip
1416    -- splittopskip
1417
1418    -- we could inject a vadjust to force a recalculation .. a mess
1419    --
1420    -- So, the next is far from robust and okay but for the moment this overlaying
1421    -- has to do. Always test this with the examples in spac-ver.mkvi!
1422
1423    local function snap_topskip(current,method)
1424        local w = getwidth(current)
1425        setwidth(current,0)
1426        return w, 0
1427    end
1428
1429    local function check_experimental_overlay(head,current)
1430        local p = nil
1431        local c = current
1432        local n = nil
1433        local function overlay(p,n,mvl)
1434            local p_wd, p_ht, p_dp = getwhd(p)
1435            local n_wd, n_ht, n_dp = getwhd(n)
1436            local skips = 0
1437            --
1438            -- We deal with this at the tex end .. we don't see spacing .. enabling this code
1439            -- is probably harmless but then we need to test it.
1440            --
1441            -- we could calculate this before we call
1442            --
1443            -- problem: prev list and next list can be unconnected
1444            --
1445            local c = getnext(p)
1446            local l = c
1447            while c and c ~= n do
1448                local id = getid(c)
1449                if id == glue_code then
1450                    local w = getwidth(c)
1451                    skips = skips + w
1452                    setglue(c,w) -- nil stretch
1453                elseif id == kern_code then
1454                    skips = skips + getkern(c)
1455                end
1456                l = c
1457                c = getnext(c)
1458            end
1459            local c = getprev(n)
1460            while c and c ~= n and c ~= l do
1461                local id = getid(c)
1462                if id == glue_code then
1463                    local w = getwidth(c)
1464                    skips = skips + w
1465                    setglue(c,w) -- nil stretch
1466                elseif id == kern_code then
1467                    skips = skips + getkern(c)
1468                end
1469                c = getprev(c)
1470            end
1471            --
1472            local delta = n_ht + skips + p_dp
1473            texsetdimen("global",d_spac_overlay,-delta) -- for tracing
1474            -- we should adapt pagetotal ! (need a hook for that) .. now we have the wrong pagebreak
1475            local k = new_kern(-delta)
1476            setvisual(k)
1477            head = insertnodebefore(head,n,k)
1478            if n_ht > p_ht then
1479                local k = new_kern(n_ht-p_ht)
1480                setvisual(k)
1481                head = insertnodebefore(head,p,k)
1482            end
1483            if trace_vspacing then
1484                report_vspacing("overlaying, prev height: %p, prev depth: %p, next height: %p, skips: %p, move up: %p",p_ht,p_dp,n_ht,skips,delta)
1485            end
1486            return remove_node(head,current,true)
1487        end
1488
1489        -- goto next line
1490        while c do
1491            local id = getid(c)
1492            if id == glue_code or id == penalty_code or id == kern_code then
1493                -- skip (actually, remove)
1494                c = getnext(c)
1495            elseif id == hlist_code then
1496                n = c
1497                break
1498            else
1499                break
1500            end
1501        end
1502        if n then
1503            -- we have a next line, goto prev line
1504            c = current
1505            while c do
1506                local id = getid(c)
1507                if id == glue_code or id == penalty_code then -- kern ?
1508                    c = getprev(c)
1509                elseif id == hlist_code then
1510                    p = c
1511                    break
1512                else
1513                    break
1514                end
1515            end
1516            if not p then
1517                if a_snapmethod == a_snapvbox then
1518                    -- quit, we're not on the mvl
1519                else
1520                    -- inefficient when we're at the end of a page
1521                    local c = tonut(texlists.pagehead)
1522                    while c and c ~= n do
1523                        local id = getid(c)
1524                        if id == hlist_code then
1525                            p = c
1526                        end
1527                        c = getnext(c)
1528                    end
1529                    if p and p ~= n then
1530                        return overlay(p,n,true)
1531                    end
1532                end
1533            elseif p ~= n then
1534                return overlay(p,n,false)
1535            end
1536        end
1537        -- in fact, we could try again later ... so then no remove (a few tries)
1538        return remove_node(head,current,true)
1539    end
1540
1541    -- where -> scope
1542    -- what  -> where (original context)
1543
1544    local checkslide = false
1545
1546    directives.register("vspacing.checkslide", function(v)
1547        if v then
1548            checkslide = function(head,where,what)
1549                nuts.checkslide(head,where .. " : " .. what)
1550            end
1551        else
1552            checkslide = false
1553        end
1554    end)
1555
1556    local function collapser(head,where,what,trace,snap,a_snapmethod) -- maybe also pass tail
1557        if trace then
1558            reset_tracing(head)
1559        end
1560        local current           = head
1561        local oldhead           = head
1562        local glue_order        = 0
1563        local glue_data
1564        local force_glue        = false
1565        local penalty_order     = 0
1566        local penalty_data
1567        local natural_penalty
1568        local special_penalty
1569        local parskip
1570        local ignore_parskip    = false
1571        local ignore_following  = false
1572        local ignore_whitespace = false
1573        local keep_together     = false
1574        local lastsnap
1575        local pagehead
1576        local pagetail
1577        --
1578        -- todo: keep_together: between headers
1579        -- todo: make this nicer in the engine
1580        --
1581        local function getpagelist()
1582            if not pagehead then
1583                pagehead = texlists.pagehead -- pagehead, pagetail = tex.getlist("pagehead")
1584                if pagehead then
1585                    pagehead = tonut(pagehead)
1586                    pagetail = find_node_tail(pagehead) -- no texlists.page_tail yet-- no texlists.page_tail yet
1587                end
1588            end
1589        end
1590        --
1591     -- local function getpagelist()
1592     --     if not pagehead then
1593     --         pagehead, pagetail = texgetlist("pagehead")
1594     --     end
1595     -- end
1596        --
1597        local function compensate(n)
1598            local g = 0
1599            while n and getid(n) == glue_code do
1600                g = g + getwidth(n)
1601                n = getnext(n)
1602            end
1603            if n then
1604                local p = getprop(n,"snapper")
1605                if p then
1606                    local extra = p.extra
1607                    if extra and extra < 0 then -- hm, extra can be unset ... needs checking
1608                        local h = p.ch -- getheight(n)
1609                        -- maybe an extra check
1610                     -- if h - extra < g then
1611                            setheight(n,h-2*extra)
1612                            p.extra = 0
1613                            if trace_vsnapping then
1614                                report_snapper("removed extra space at top: %p",extra)
1615                            end
1616                     -- end
1617                    end
1618                end
1619                return n
1620            end
1621        end
1622        --
1623        local function removetopsnap()
1624            getpagelist()
1625            if pagehead then
1626                local n = pagehead and compensate(pagehead)
1627                if n and n ~= pagetail then
1628                    local p = getprop(pagetail,"snapper")
1629                    if p then
1630                        local e = p.extra
1631                        if e and e < 0 then
1632                            local t = texget("pagetotal")
1633                            if t > 0 then
1634                                local g = texget("pagegoal") -- 1073741823 is signal
1635                                local d = g - t
1636                                if d < -e then
1637                                    local penalty = new_penalty(1000000)
1638                                    setvisual(penalty)
1639                                    setlink(penalty,head)
1640                                    head = penalty
1641                                    report_snapper("force pagebreak due to extra space at bottom: %p",e)
1642                                end
1643                            end
1644                        end
1645                    end
1646                end
1647            elseif head then
1648                compensate(head)
1649            end
1650        end
1651        --
1652        local function getavailable()
1653            getpagelist()
1654            if pagehead then
1655                local t = texget("pagetotal")
1656                if t > 0 then
1657                    local g = texget("pagegoal")
1658                    return g - t
1659                end
1660            end
1661            return false
1662        end
1663        --
1664        local function flush(why)
1665            if penalty_data then
1666                local p = new_penalty(penalty_data)
1667                setvisual(p)
1668                if trace then
1669                    trace_done("flushed due to " .. why,p)
1670                end
1671                if penalty_data >= 10000 then -- or whatever threshold?
1672                    local prev = getprev(current)
1673                    if getid(prev) == glue_code then -- maybe go back more, or maybe even push back before any glue
1674                            -- tricky case: spacing/grid-007.tex: glue penalty glue
1675                        head = insertnodebefore(head,prev,p)
1676                    else
1677                        head = insertnodebefore(head,current,p)
1678                    end
1679                else
1680                    head = insertnodebefore(head,current,p)
1681                end
1682             -- if penalty_data > special_penalty_min and penalty_data < special_penalty_max then
1683                local props = properties[p]
1684                if props then
1685                    props.special_penalty = special_penalty or penalty_data
1686                else
1687                    properties[p] = {
1688                        special_penalty = special_penalty or penalty_data
1689                    }
1690                end
1691             -- end
1692            end
1693            if glue_data then
1694                if force_glue then
1695                    if trace then
1696                        trace_done("flushed due to forced " .. why,glue_data)
1697                    end
1698                    head = forced_skip(head,current,getwidth(glue_data,width),"before",trace)
1699                    flushnode(glue_data)
1700                else
1701                    local width, stretch, shrink = getglue(glue_data)
1702                    if width ~= 0 then
1703                        if trace then
1704                            trace_done("flushed due to non zero " .. why,glue_data)
1705                        end
1706                        head = insertnodebefore(head,current,glue_data)
1707                    elseif stretch ~= 0 or shrink ~= 0 then
1708                        if trace then
1709                            trace_done("flushed due to stretch/shrink in" .. why,glue_data)
1710                        end
1711                        head = insertnodebefore(head,current,glue_data)
1712                    else
1713                     -- report_vspacing("needs checking (%s): %p",gluecodes[getsubtype(glue_data)],w)
1714                        flushnode(glue_data)
1715                    end
1716                end
1717            end
1718
1719            if trace then
1720                trace_node(current)
1721            end
1722            glue_order, glue_data, force_glue = 0, nil, false
1723            penalty_order, penalty_data, natural_penalty = 0, nil, nil
1724            parskip, ignore_parskip, ignore_following, ignore_whitespace = nil, false, false, false
1725        end
1726        --
1727        if trace_vsnapping then
1728            report_snapper("global ht/dp = %p/%p, local ht/dp = %p/%p",
1729                texgetdimen(d_globalbodyfontstrutheight),
1730                texgetdimen(d_globalbodyfontstrutdepth),
1731                texgetdimen(d_bodyfontstrutheight),
1732                texgetdimen(d_bodyfontstrutdepth)
1733            )
1734        end
1735        if trace then
1736            trace_info("start analyzing",where,what)
1737        end
1738        if snap and where == "page" then
1739            removetopsnap()
1740        end
1741        if checkslide then
1742            checkslide(head,where,what)
1743        end
1744        while current do
1745            local id = getid(current)
1746            if id == hlist_code or id == vlist_code then
1747                -- needs checking, why so many calls
1748                if snap then
1749                    lastsnap = nil
1750                    local list = getlist(current)
1751                    local s = getattr(current,a_snapmethod)
1752                    if not s then
1753                    --  if trace_vsnapping then
1754                    --      report_snapper("mvl list not snapped")
1755                    --  end
1756                    elseif s == 0 then
1757                        if trace_vsnapping then
1758                            report_snapper("mvl %a not snapped, already done: %s",nodecodes[id],listtoutf(list))
1759                        end
1760                    else
1761                        local sv = snapmethods[s]
1762                        if sv then
1763                            -- check if already snapped
1764                            local done = list and already_done(id,list,a_snapmethod)
1765                            if done then
1766                                -- assume that the box is already snapped
1767                                if trace_vsnapping then
1768                                    local w, h, d = getwhd(current)
1769                                    report_snapper("mvl list already snapped at (%p,%p): %s",h,d,listtoutf(list))
1770                                end
1771                            else
1772                                local h, d, ch, cd, lines, extra = snap_hlist("mvl",current,sv,false,false)
1773                                lastsnap = {
1774                                    ht = h,
1775                                    dp = d,
1776                                    ch = ch,
1777                                    cd = cd,
1778                                    extra = extra,
1779                                    current = current,
1780                                }
1781                                setprop(current,"snapper",lastsnap)
1782                                if trace_vsnapping then
1783                                    report_snapper("mvl %a snapped from (%p,%p) to (%p,%p) using method %a (%s) for %a (%s lines): %s",
1784                                        nodecodes[id],h,d,ch,cd,sv.name,sv.specification,where,lines,listtoutf(list))
1785                                end
1786                            end
1787                        elseif trace_vsnapping then
1788                            report_snapper("mvl %a not snapped due to unknown snap specification: %s",nodecodes[id],listtoutf(list))
1789                        end
1790                        setattr(current,a_snapmethod,0)
1791                    end
1792                else
1793                    --
1794                end
1795            --  tex.prevdepth = 0
1796                flush("list")
1797                current = getnext(current)
1798            elseif id == penalty_code then
1799             -- natural_penalty = getpenalty(current)
1800             -- if trace then
1801             --     trace_done("removed penalty",current)
1802             -- end
1803             -- head, current = remove_node(head,current,true)
1804                if trace then
1805                    trace_done("kept penalty",current)
1806                end
1807                current = getnext(current)
1808            elseif id == kern_code then
1809                if snap and trace_vsnapping and getkern(current) ~= 0 then
1810                    report_snapper("kern of %p kept",getkern(current))
1811                end
1812                flush("kern")
1813                current = getnext(current)
1814            elseif id == glue_code then
1815                local subtype = getsubtype(current)
1816                if subtype == userskip_code then
1817                 -- local sc, so, sp = getattrs(current,a_skipcategory,a_skiporder,a_skippenalty)
1818                    local sc, sp, so = getspecification(current)
1819                    if not so then
1820                        so = 1 -- the others have no default value
1821                    end
1822                    if sp and sc == penalty then
1823                        if where == "page" then
1824                            getpagelist()
1825                            local p = specialmethods[specialmethod](pagehead,pagetail,current,sp)
1826                            if p then
1827                             -- todo: other tracer
1828                             --
1829                             -- if trace then
1830                             --     trace_skip("previous special penalty %a is changed to %a using method %a",sp,p,specialmethod)
1831                             -- end
1832                                special_penalty = sp
1833                                sp = p
1834                            end
1835                        end
1836                        if not penalty_data then
1837                            penalty_data = sp
1838                        elseif penalty_order < so then
1839                            penalty_order, penalty_data = so, sp
1840                        elseif penalty_order == so and sp > penalty_data then
1841                            penalty_data = sp
1842                        end
1843                        if trace then
1844                            trace_skip("penalty in skip",sc,so,sp,current)
1845                        end
1846                        head, current = remove_node(head,current,true)
1847                    elseif not sc then  -- if not sc then
1848                        if glue_data then
1849                            if trace then
1850                                trace_done("flush",glue_data)
1851                            end
1852                            head = insertnodebefore(head,current,glue_data)
1853                            if trace then
1854                                trace_natural("natural",current)
1855                            end
1856                            current = getnext(current)
1857                            glue_data = nil
1858                        else
1859                            -- not look back across head
1860                            -- todo: prev can be whatsit (latelua)
1861                            local previous = getprev(current)
1862                            if previous and getid(previous) == glue_code and getsubtype(previous) == userskip_code then
1863                                local pwidth, pstretch, pshrink, pstretch_order, pshrink_order = getglue(previous)
1864                                local cwidth, cstretch, cshrink, cstretch_order, cshrink_order = getglue(current)
1865                                if pstretch_order == 0 and pshrink_order == 0 and cstretch_order == 0 and cshrink_order == 0 then
1866                                    setglue(previous,pwidth + cwidth, pstretch + cstretch, pshrink  + cshrink)
1867                                    if trace then
1868                                        trace_natural("removed",current)
1869                                    end
1870                                    head, current = remove_node(head,current,true)
1871                                    if trace then
1872                                        trace_natural("collapsed",previous)
1873                                    end
1874                                else
1875                                    if trace then
1876                                        trace_natural("filler",current)
1877                                    end
1878                                    current = getnext(current)
1879                                end
1880                            else
1881                                if trace then
1882                                    trace_natural("natural (no prev)",current)
1883                                end
1884                                current = getnext(current)
1885                            end
1886                        end
1887                        glue_order = 0
1888                    elseif sc == disable or sc == enable then
1889                        local next = getnext(current)
1890                        if next then
1891                            ignore_following = sc == disable
1892                            if trace then
1893                                trace_skip(sc == disable and "disable" or "enable",sc,so,sp,current)
1894                            end
1895                            head, current = remove_node(head,current,true)
1896                        else
1897                            current = next
1898                        end
1899                    elseif sc == packed then
1900                        if trace then
1901                            trace_skip("packed",sc,so,sp,current)
1902                        end
1903                        -- can't happen !
1904                        head, current = remove_node(head,current,true)
1905                    elseif sc == nowhite then
1906                        local next = getnext(current)
1907                        if next then
1908                            ignore_whitespace = true
1909                            head, current = remove_node(head,current,true)
1910                        else
1911                            current = next
1912                        end
1913                    elseif sc == discard then
1914                        if trace then
1915                            trace_skip("discard",sc,so,sp,current)
1916                        end
1917                        head, current = remove_node(head,current,true)
1918                    elseif sc == overlay then
1919                        -- todo (overlay following line over previous
1920                        if trace then
1921                            trace_skip("overlay",sc,so,sp,current)
1922                        end
1923                            -- beware: head can actually be after the affected nodes as
1924                            -- we look back ... some day head will the real head
1925                        head, current = check_experimental_overlay(head,current,a_snapmethod)
1926                    elseif ignore_following then
1927                        if trace then
1928                            trace_skip("disabled",sc,so,sp,current)
1929                        end
1930                        head, current = remove_node(head,current,true)
1931                    elseif not glue_data then
1932                        if trace then
1933                            trace_skip("assign",sc,so,sp,current)
1934                        end
1935                        glue_order = so
1936                        head, current, glue_data = remove_node(head,current)
1937                    elseif glue_order < so then
1938                        if trace then
1939                            trace_skip("force",sc,so,sp,current)
1940                        end
1941                        glue_order = so
1942                        flushnode(glue_data)
1943                        head, current, glue_data = remove_node(head,current)
1944                    elseif glue_order == so then
1945                        -- is now exclusive, maybe support goback as combi, else why a set
1946                        if sc == largest then
1947                            local cw = getwidth(current)
1948                            local gw = getwidth(glue_data)
1949                            if cw > gw then
1950                                if trace then
1951                                    trace_skip("largest",sc,so,sp,current)
1952                                end
1953                                flushnode(glue_data)
1954                                head, current, glue_data = remove_node(head,current)
1955                            else
1956                                if trace then
1957                                    trace_skip("remove smallest",sc,so,sp,current)
1958                                end
1959                                head, current = remove_node(head,current,true)
1960                            end
1961                        elseif sc == goback then
1962                            if trace then
1963                                trace_skip("goback",sc,so,sp,current)
1964                            end
1965                            flushnode(glue_data)
1966                            head, current, glue_data = remove_node(head,current)
1967                        elseif sc == force then
1968                            -- last one counts, some day we can provide an accumulator and largest etc
1969                            -- but not now
1970                            if trace then
1971                                trace_skip("force",sc,so,sp,current)
1972                            end
1973                            flushnode(glue_data)
1974                            head, current, glue_data = remove_node(head,current)
1975                        elseif sc == penalty then
1976                            if trace then
1977                                trace_skip("penalty",sc,so,sp,current)
1978                            end
1979                            flushnode(glue_data)
1980                            glue_data = nil
1981                            head, current = remove_node(head,current,true)
1982                        elseif sc == add then
1983                            if trace then
1984                                trace_skip("add",sc,so,sp,current)
1985                            end
1986                            local cwidth, cstretch, cshrink = getglue(current)
1987                            local gwidth, gstretch, gshrink = getglue(glue_data)
1988                            setglue(glue_data,gwidth + cwidth, gstretch + cstretch,gshrink + cshrink)
1989                            -- toto: order
1990                            head, current = remove_node(head,current,true)
1991                        else
1992                            if trace then
1993                                trace_skip("unknown",sc,so,sp,current)
1994                            end
1995                            head, current = remove_node(head,current,true)
1996                        end
1997                    else
1998                        if trace then
1999                            trace_skip("unknown",sc,so,sp,current)
2000                        end
2001                        head, current = remove_node(head,current,true)
2002                    end
2003                    if sc == force then
2004                        force_glue = true
2005                    end
2006                elseif subtype == lineskip_code then
2007                    if snap then
2008                        local s = getattr(current,a_snapmethod)
2009                        if s and s ~= 0 then
2010                            setattr(current,a_snapmethod,0)
2011                            setwidth(current,0)
2012                            if trace_vsnapping then
2013                                report_snapper("lineskip set to zero")
2014                            end
2015                        else
2016                            if trace then
2017                                trace_skip("lineskip",sc,so,sp,current)
2018                            end
2019                            flush("lineskip")
2020                        end
2021                    else
2022                        if trace then
2023                            trace_skip("lineskip",sc,so,sp,current)
2024                        end
2025                        flush("lineskip")
2026                    end
2027                    current = getnext(current)
2028                elseif subtype == baselineskip_code then
2029                    if snap then
2030                        local s = getattr(current,a_snapmethod)
2031                        if s and s ~= 0 then
2032                            setattr(current,a_snapmethod,0)
2033                            setwidth(current,0)
2034                            if trace_vsnapping then
2035                                report_snapper("baselineskip set to zero")
2036                            end
2037                        else
2038                            if trace then
2039                                trace_skip("baselineskip",sc,so,sp,current)
2040                            end
2041                            flush("baselineskip")
2042                        end
2043                    else
2044                        if trace then
2045                            trace_skip("baselineskip",sc,so,sp,current)
2046                        end
2047                        flush("baselineskip")
2048                    end
2049                    current = getnext(current)
2050                elseif subtype == parskip_code then
2051                    -- parskip always comes later
2052                    if ignore_whitespace then
2053                        if trace then
2054                            trace_natural("ignored parskip",current)
2055                        end
2056                        head, current = remove_node(head,current,true)
2057                    elseif glue_data then
2058                        local w = getwidth(current)
2059                        if w ~= 0 and w > getwidth(glue_data) then
2060                            flushnode(glue_data)
2061                            glue_data = current
2062                            if trace then
2063                                trace_natural("taking parskip",current)
2064                            end
2065                            head, current = remove_node(head,current)
2066                        else
2067                            if trace then
2068                                trace_natural("removed parskip",current)
2069                            end
2070                            head, current = remove_node(head,current,true)
2071                        end
2072                    else
2073                        if trace then
2074                            trace_natural("honored parskip",current)
2075                        end
2076                        head, current, glue_data = remove_node(head,current)
2077                    end
2078                elseif subtype == topskip_code or subtype == splittopskip_code then
2079                    local next = getnext(current)
2080                 -- if next and getattr(next,a_skipcategory) == notopskip then
2081                    if next and getspecification(next) == notopskip then
2082                        setglue(current) -- zero
2083                    end
2084                    if snap then
2085                        local s = getattr(current,a_snapmethod)
2086                        if s and s ~= 0 then
2087                            setattr(current,a_snapmethod,0)
2088                            local sv = snapmethods[s]
2089                            local w, cw = snap_topskip(current,sv)
2090                            if trace_vsnapping then
2091                                report_snapper("topskip snapped from %p to %p for %a",w,cw,where)
2092                            end
2093                        else
2094                            if trace then
2095                                trace_skip("topskip",sc,so,sp,current)
2096                            end
2097                            flush("topskip")
2098                        end
2099                    else
2100                        if trace then
2101                            trace_skip("topskip",sc,so,sp,current)
2102                        end
2103                        flush("topskip")
2104                    end
2105                    current = getnext(current)
2106                elseif subtype == abovedisplayskip_code and remove_math_skips then
2107                    --
2108                    if trace then
2109                        trace_skip("above display skip (normal)",sc,so,sp,current)
2110                    end
2111                    flush("above display skip (normal)")
2112                    current = getnext(current)
2113                    --
2114                elseif subtype == belowdisplayskip_code and remove_math_skips then
2115                    --
2116                    if trace then
2117                        trace_skip("below display skip (normal)",sc,so,sp,current)
2118                    end
2119                    flush("below display skip (normal)")
2120                    current = getnext(current)
2121                   --
2122                elseif subtype == abovedisplayshortskip_code and remove_math_skips then
2123                    --
2124                    if trace then
2125                        trace_skip("above display skip (short)",sc,so,sp,current)
2126                    end
2127                    flush("above display skip (short)")
2128                    current = getnext(current)
2129                    --
2130                elseif subtype == belowdisplayshortskip_code and remove_math_skips then
2131                    --
2132                    if trace then
2133                        trace_skip("below display skip (short)",sc,so,sp,current)
2134                    end
2135                    flush("below display skip (short)")
2136                    current = getnext(current)
2137                    --
2138                else -- other glue
2139                    if snap and trace_vsnapping then
2140                        local w = getwidth(current)
2141                        if w ~= 0 then
2142                            report_snapper("glue %p of type %a kept",w,gluecodes[subtype])
2143                        end
2144                    end
2145                    if trace then
2146                        trace_skip(formatters["glue of type %a"](subtype),sc,so,sp,current)
2147                    end
2148                    flush("some glue")
2149                    current = getnext(current)
2150                end
2151            else
2152                flush(trace and formatters["node with id %a"](id) or "other node")
2153                current = getnext(current)
2154            end
2155        end
2156        if trace then
2157            trace_info("stop analyzing",where,what)
2158        end
2159     -- if natural_penalty and (not penalty_data or natural_penalty > penalty_data) then
2160     --     penalty_data = natural_penalty
2161     -- end
2162        if trace and (glue_data or penalty_data) then
2163            trace_info("start flushing",where,what)
2164        end
2165        local tail
2166        if penalty_data then
2167            tail = find_node_tail(head)
2168            local p = new_penalty(penalty_data)
2169            setvisual(p)
2170            if trace then
2171                trace_done("result",p)
2172            end
2173            setlink(tail,p)
2174         -- if penalty_data > special_penalty_min and penalty_data < special_penalty_max then
2175                local props = properties[p]
2176                if props then
2177                    props.special_penalty = special_penalty or penalty_data
2178                else
2179                    properties[p] = {
2180                        special_penalty = special_penalty or penalty_data
2181                    }
2182                end
2183         -- end
2184        end
2185        if glue_data then
2186            if not tail then tail = find_node_tail(head) end
2187            if trace then
2188                trace_done("result",glue_data)
2189            end
2190            if force_glue then
2191                head, tail = forced_skip(head,tail,getwidth(glue_data),"after",trace)
2192                flushnode(glue_data)
2193                glue_data = nil
2194            elseif tail then
2195                setlink(tail,glue_data)
2196                setnext(glue_data)
2197            else
2198                head = glue_data
2199            end
2200            -- texsetnest("top","prevdepth",0)
2201            texnest[texnest.ptr].prevdepth = 0 -- appending to the list bypasses tex's prevdepth handler
2202        end
2203        if trace then
2204            if glue_data or penalty_data then
2205                trace_info("stop flushing",where,what)
2206            end
2207            show_tracing(head)
2208            if oldhead ~= head then
2209                trace_info("head has been changed from %a to %a",nodecodes[getid(oldhead)],nodecodes[getid(head)])
2210            end
2211        end
2212        return head
2213    end
2214
2215--     local function collapser(head,...)
2216--         local current = head
2217--         while current do
2218--             local id = getid(current)
2219--             if id == glue_code then
2220--                 if getsubtype(current) == userskip_code then
2221--                     local glue_data
2222--                     head, current, glue_data = remove_node(head,current)
2223--                     head, current = insertnodebefore(head,current,glue_data)
2224--                 end
2225--             end
2226--             current = getnext(current)
2227--         end
2228--         return head
2229--     end
2230
2231    local stackhead, stacktail, stackhack = nil, nil, false
2232
2233    local function report(message,where,lst)
2234        if lst and where then
2235            report_vspacing(message,where,count_nodes(lst,true),nodeidstostring(lst))
2236        else
2237            report_vspacing(message,count_nodes(lst,true),nodeidstostring(lst))
2238        end
2239    end
2240
2241    -- This really need a rework. although, it has now been stable for 15 years so why
2242    -- mess with it. We get sequences like these where penalties come from the par
2243    -- builder:
2244    --
2245    -- glue
2246    -- glue hlist
2247    -- glue hlist penalty glue hlist penalty glue hlist
2248    --
2249    -- and in context we never use penalties other than spacing related
2250
2251    local forceflush = false
2252
2253    function vspacing.pagehandler(newhead,where)
2254        if newhead then
2255            local newtail = find_node_tail(newhead) -- best pass that tail, known anyway
2256            local flush = false
2257            stackhack = true -- todo: only when grid snapping once enabled
2258
2259-- local t = { } for n, id in nextnode, newhead do t[#t+1] = nodecodes[id] end report_vspacing("page handler: % t",t)
2260
2261            for n, id, subtype in nextnode, newhead do
2262                if id ~= glue_code then
2263                    flush = true
2264                elseif subtype == userskip_code then
2265                 -- local sc = getattr(n,a_skipcategory)
2266                    local sc = getspecification(n)
2267                    if sc then
2268                        stackhack = true
2269                    else
2270                        flush = true
2271                    end
2272                elseif subtype == parskip_code then
2273                    -- if where == new_graf then ... end
2274                    if texgetcount(c_spac_vspacing_ignore_parskip) > 0 then
2275                        setglue(n)
2276                     -- maybe removenode
2277                    end
2278                end
2279            end
2280            texsetcount(c_spac_vspacing_ignore_parskip,0)
2281
2282            if forceflush then
2283                forceflush = false
2284                flush      = true
2285            end
2286
2287            if flush then
2288                if stackhead then
2289                    if trace_collect_vspacing then report("%s > appending %s nodes to stack (final): %s",where,newhead) end
2290                    setlink(stacktail,newhead)
2291                    newhead   = stackhead
2292                    stackhead = nil
2293                    stacktail = nil
2294                end
2295                if stackhack then
2296                    stackhack = false
2297                    if trace_collect_vspacing then report("%s > processing %s nodes: %s",where,newhead) end
2298                    newhead = collapser(newhead,"page",where,trace_page_vspacing,true,a_snapmethod)
2299                else
2300                    if trace_collect_vspacing then report("%s > flushing %s nodes: %s",where,newhead) end
2301                end
2302                return newhead
2303            else
2304                if stackhead then
2305                    if trace_collect_vspacing then report("%s > appending %s nodes to stack (intermediate): %s",where,newhead) end
2306                    setlink(stacktail,newhead)
2307                else
2308                    if trace_collect_vspacing then report("%s > storing %s nodes in stack (initial): %s",where,newhead) end
2309                    stackhead = newhead
2310                end
2311                stacktail = newtail
2312            end
2313        end
2314        return nil
2315    end
2316
2317    function vspacing.pageoverflow()
2318        local h = 0
2319        if stackhead then
2320            for n, id in nextnode, stackhead do
2321                if id == glue_code then
2322                    h = h + getwidth(n)
2323                elseif id == kern_code then
2324                    h = h + getkern(n)
2325                end
2326            end
2327        end
2328        return h
2329    end
2330
2331    function vspacing.forcepageflush()
2332        forceflush = true
2333    end
2334
2335    local ignored = table.tohash {
2336        "splitkeep",
2337        "splitoff",
2338--        "insert",
2339    }
2340
2341    function vspacing.vboxhandler(head,where)
2342        if head and not ignored[where] and getnext(head) then
2343-- if getid(head) == glue_code and getsubtype(head) == topskip_code then
2344            head = collapser(head,"vbox",where,trace_vbox_vspacing,true,a_snapvbox) -- todo: local snapper
2345        end
2346        return head
2347    end
2348
2349    function vspacing.collapsevbox(n,aslist) -- for boxes but using global a_snapmethod
2350        local box = getbox(n)
2351        if box then
2352            local list = getlist(box)
2353            if list then
2354                list = collapser(list,"snapper","vbox",trace_vbox_vspacing,true,a_snapmethod)
2355                if aslist then
2356                    setlist(box,list) -- beware, dimensions of box are wrong now
2357                else
2358                    setlist(box,vpack_node(list))
2359                end
2360            end
2361        end
2362    end
2363
2364end
2365
2366-- This one is needed to prevent bleeding of prevdepth to the next page
2367-- which doesn't work well with forced skips. I'm not that sure if the
2368-- following is a good way out.
2369
2370do
2371
2372    local outer   = texnest[0]
2373
2374    local enabled = true
2375    local trace   = false
2376    local report  = logs.reporter("vspacing")
2377
2378    trackers.register("vspacing.synchronizepage",function(v)
2379        trace = v
2380    end)
2381
2382    directives.register("vspacing.synchronizepage",function(v)
2383        enabled = v
2384    end)
2385
2386    local function ignoredepth()
2387        return texgetdimen("ignoredepthcriterion") -- -65536000
2388    end
2389
2390    -- A previous version analyzed the number of lines moved to the next page in
2391    -- synchronizepage because prevgraf is unreliable in that case. However, we cannot
2392    -- tweak that parameter because it is also used in postlinebreak and hangafter, so
2393    -- there is a danger for interference. Therefore we now do it dynamically.
2394
2395    -- We can also support other lists but there prevgraf probably is ok.
2396
2397    function vspacing.getnofpreviouslines(head)
2398        if enabled then
2399            if not thead then
2400                head = texlists.pagehead
2401            end
2402            local noflines = 0
2403            if head then
2404                local tail = find_node_tail(tonut(head))
2405                while tail do
2406                    local id = getid(tail)
2407                    if id == hlist_code then
2408                        if getsubtype(tail) == linelist_code then
2409                            noflines = noflines + 1
2410                        else
2411                            break
2412                        end
2413                    elseif id == vlist_code then
2414                        break
2415                    elseif id == glue_code then
2416                        local subtype = getsubtype(tail)
2417                        if subtype == baselineskip_code or subtype == lineskip_code then
2418                            -- we're ok
2419                        elseif subtype == parskip_code then
2420                            if getwidth(tail) > 0 then
2421                                break
2422                            else
2423                                -- we assume we're ok
2424                            end
2425                        end
2426                    elseif id == penalty_code then
2427                        -- we're probably ok
2428                    elseif id == rule_code or id == kern_code then
2429                        break
2430                    else
2431                        -- ins, mark, boundary, whatsit
2432                    end
2433                    tail = getprev(tail)
2434                end
2435            end
2436            return noflines
2437        end
2438    end
2439
2440    interfaces.implement {
2441        name    = "getnofpreviouslines",
2442        public  = true,
2443        actions = vspacing.getnofpreviouslines,
2444    }
2445
2446    function vspacing.synchronizepage()
2447        if enabled then
2448            if trace then
2449                local newdepth = outer.prevdepth
2450                local olddepth = newdepth
2451                if not texlists.pagehead then
2452                    newdepth = ignoredepth()
2453                    texset("prevdepth",newdepth)
2454                    outer.prevdepth = newdepth
2455                end
2456                report("page %i, prevdepth %p => %p",texgetcount("realpageno"),olddepth,newdepth)
2457             -- report("list %s",nodes.idsandsubtypes(head))
2458            else
2459                if not texlists.pagehead then
2460                    local newdepth = ignoredepth()
2461                    texset("prevdepth",newdepth)
2462                    outer.prevdepth = newdepth
2463                end
2464            end
2465        end
2466    end
2467
2468    local trace       = false
2469    local abs         = math.abs
2470 -- local last        = nil
2471    local vmode_code  = tex.modelevels.vertical
2472    local temp_code   = nodecodes.temp
2473    local texgetnest  = tex.getnest
2474    local texgetlist  = tex.getlist
2475    local getnodetail = nodes.tail
2476
2477 -- trackers.register("vspacing.forcestrutdepth",function(v) trace = v end)
2478
2479    -- abs : negative is inner
2480
2481    function vspacing.checkstrutdepth(depth)
2482        local nest = texgetnest()
2483        if abs(nest.mode) == vmode_code and nest.head then
2484            local tail = nest.tail
2485            local id   = tail.id
2486            if id == hlist_code then
2487                if tail.depth < depth then
2488                    tail.depth = depth
2489                end
2490                nest.prevdepth = depth
2491            elseif id == temp_code and texgetnest("ptr") == 0 then
2492                local head = texgetlist("pagehead")
2493                if head then
2494                    tail = getnodetail(head)
2495                    if tail and tail.id == hlist_code then
2496                        if tail.depth < depth then
2497                            tail.depth = depth
2498                        end
2499                        nest.prevdepth = depth
2500                        -- only works in lmtx
2501                        texset("pagedepth",depth)
2502                    end
2503                end
2504            end
2505        end
2506    end
2507
2508    interfaces.implement {
2509        name      = "checkstrutdepth",
2510        arguments = "dimension",
2511        actions   = vspacing.checkstrutdepth,
2512    }
2513
2514 -- function vspacing.forcestrutdepth(n,depth,trace_mode,plus)
2515 --     local box = texgetbox(n)
2516 --     if box then
2517 --         box = tonut(box)
2518 --         local head = getlist(box)
2519 --         if head then
2520 --             local tail = find_node_tail(head)
2521 --             if tail then
2522 --                 if getid(tail) == hlist_code then
2523 --                     local dp = getdepth(tail)
2524 --                     if dp < depth then
2525 --                         setdepth(tail,depth)
2526 --                         outer.prevdepth = depth
2527 --                         if trace or trace_mode > 0 then
2528 --                             nuts.setvisual(tail,"depth")
2529 --                         end
2530 --                     end
2531 --                 end
2532 --              -- last = nil
2533 --              -- if plus then
2534 --              --     -- penalty / skip ...
2535 --              --     local height = 0
2536 --              --     local sofar  = 0
2537 --              --     local same   = false
2538 --              --     local seen   = false
2539 --              --     local list   = { }
2540 --              --           last   = nil
2541 --              --     while tail do
2542 --              --         local id = getid(tail)
2543 --              --         if id == hlist_code or id == vlist_code then
2544 --              --             local w, h, d = getwhd(tail)
2545 --              --             height = height + h + d + sofar
2546 --              --             sofar  = 0
2547 --              --             last   = tail
2548 --              --         elseif id == kern_code then
2549 --              --             sofar = sofar + getkern(tail)
2550 --              --         elseif id == glue_code then
2551 --              --             if seen then
2552 --              --                 sofar = sofar + getwidth(tail)
2553 --              --                 seen  = false
2554 --              --             else
2555 --              --                 break
2556 --              --             end
2557 --              --         elseif id == penalty_code then
2558 --              --             local p = getpenalty(tail)
2559 --              --             if p >= 10000 then
2560 --              --                 same = true
2561 --              --                 seen = true
2562 --              --             else
2563 --              --                 break
2564 --              --             end
2565 --              --         else
2566 --              --             break
2567 --              --         end
2568 --              --         tail = getprev(tail)
2569 --              --     end
2570 --              --     texsetdimen("global","d_spac_prevcontent",same and height or 0)
2571 --              -- end
2572 --             end
2573 --         end
2574 --     end
2575 -- end
2576
2577    local hlist_code  = nodes.nodecodes.hlist
2578    local insert_code = nodes.nodecodes.insert
2579    local mark_code   = nodes.nodecodes.mark
2580    local line_code   = nodes.listcodes.line
2581
2582 -- local nuts             = nodes.nuts
2583 -- local getid            = nuts.getid
2584 -- local getsubtype       = nuts.getsubtype
2585 -- local getdepth         = nuts.getdepth
2586 -- local setdepth         = nuts.setdepth
2587    local gettotal         = nuts.gettotal
2588    local getspeciallist   = nuts.getspeciallist
2589    local setspeciallist   = nuts.setspeciallist
2590
2591    local triggerbuildpage = tex.triggerbuildpage
2592
2593 -- local texgetdimen = tex.getdimen
2594 -- local texsetdimen = tex.setdimen
2595    local texgetnest  = tex.getnest
2596 -- local texget      = tex.get
2597 -- local texset      = tex.set
2598
2599    local trace = false  trackers.register("otr.forcestrutdepth", function(v)
2600        trace = v and function(n)
2601            setvisual(nuts.tonut(n),nodes.visualizers.modes.depth)
2602        end
2603    end)
2604
2605    local treversenode = nuts.treversers.node
2606
2607    local function flushcontributions()
2608        if texgetnest("ptr") == 0 then
2609            -- this flushes the contributions
2610            local prev  = nil
2611            local cycle = 1
2612            while cycle <= 10 do
2613                local head = getspeciallist("contributehead")
2614                if head == prev then
2615                    -- This can happen .. maybe 10 is already too much ... e.g.
2616                    -- extreme side float case in m4all.
2617                    cycle = cycle + 1
2618                else
2619                    triggerbuildpage()
2620                    prev = head
2621                end
2622            end
2623            return true
2624        else
2625            return false
2626        end
2627    end
2628
2629    vspacing.flushcontributions = flushcontributions
2630
2631    function vspacing.forcestrutdepth()
2632        -- check if in mvl
2633        if flushcontributions() then
2634            -- now we consult the last line (if present)
2635            local head, tail = getspeciallist("pagehead")
2636            if tail then
2637                for n, id, subtype in treversenode, tail do
2638                    if id == hlist_code then
2639                        if subtype == line_code then
2640                            local strutdp = texgetdimen(d_strutdp)
2641                            local delta   = strutdp - getdepth(n)
2642                            if delta > 0 then
2643                                --- also pagelastdepth
2644                                setdepth(n,strutdp)
2645                                texset("pagetotal",texget("pagetotal") + delta)
2646                                texset("pagedepth",strutdp)
2647                                if trace then
2648                                    trace(n)
2649                                end
2650                            end
2651                        end
2652                        break
2653                    elseif id == insert_code or id == mark_code then
2654                        -- prev
2655                    else
2656-- if id == glue_code then
2657--     print(gluecodes[subtype],nuts.getwidth(n))
2658-- else
2659                        break
2660-- end
2661                    end
2662                end
2663            end
2664        else
2665            local nest = texgetnest()
2666         -- if abs(nest.mode) == vmode_code and nest.head then
2667                local tail = nest.tail
2668                if tail.id == hlist_code and tail.subtype == line_code then
2669                    local strutdp = texgetdimen(d_strutdp)
2670                    if tail.depth < strutdp then
2671                        tail.depth = strutdp
2672                    end
2673                    nest.prevdepth = strutdp
2674                    if trace then
2675                        trace(tail)
2676                    end
2677                end
2678         -- end
2679        end
2680    end
2681
2682    -- highly experimental, only for m4all now; todo: tracing
2683
2684    local setbox = nuts.setbox
2685
2686    function vspacing.interceptsamepagecontent(box)
2687        if vspacing.flushcontributions() then
2688            -- now we consult the last line (if present)
2689            local head, tail = getspeciallist("pagehead")
2690            if tail and getid(tail) == glue_code then
2691                local prev = getprev(tail)
2692                if prev and getid(prev) == penalty_code then
2693                    if getpenalty(prev) >= 10000 then
2694                        local state = nil
2695                        local first = nil
2696                        local last  = tail
2697                        local c = getprev(prev)
2698                        while c do
2699                            if getid(c) == glue_code then
2700                                local p = getprev(c)
2701                                if p and getid(p) == penalty_code then
2702                                    if getpenalty(p) < 10000 then
2703                                        state = 1
2704                                    end
2705                                else
2706                                    state = 2
2707                                    break
2708                                end
2709                            end
2710                            first = c
2711                            c = getprev(c)
2712                        end
2713                        if first and first ~= head then
2714                            setnext(getprev(first))
2715                            setprev(first)
2716                            local vbox = vpack_node(first)
2717                            setvisual(vbox)
2718                            setbox(box,vbox)
2719                            report_vspacing("same page intercept, case %i")
2720                        end
2721                    end
2722                end
2723            end
2724        end
2725    end
2726
2727    interfaces.implement {
2728        name      = "interceptsamepagecontent",
2729        arguments = "integer",
2730        actions   = vspacing.interceptsamepagecontent,
2731    }
2732
2733 -- interfaces.implement {
2734 --     name    = "removelastline",
2735 --     actions = function()
2736 --         local h, t = getspeciallist("pagehead")
2737 --         if t and getid(t) == hlist_code and getsubtype(t) == line_code then
2738 --             local total = gettotal(t)
2739 --             h = remove_node(h,t,true)
2740 --             setspeciallist(h)
2741 --             texset("pagetotal",texget("pagetotal") - total)
2742 --             -- set prevdepth
2743 --         end
2744 --     end
2745 -- }
2746
2747    function vspacing.pushatsame()
2748        -- needs better checking !
2749        if last then -- setsplit
2750            nuts.setnext(getprev(last))
2751            nuts.setprev(last)
2752        end
2753    end
2754
2755    function vspacing.popatsame()
2756        -- needs better checking !
2757        nuts.write(last)
2758    end
2759
2760end
2761
2762-- interface
2763
2764do
2765
2766    local implement = interfaces.implement
2767
2768    implement {
2769        name      = "injectvspacing",
2770        actions   = vspacing.inject,
2771        arguments = { "integer", "string" },
2772    }
2773
2774    implement {
2775        name      = "injectvpenalty",
2776        actions   = vspacing.injectpenalty,
2777        arguments = "integer",
2778    }
2779
2780    implement {
2781        name      = "injectvskip",
2782        actions   = vspacing.injectskip,
2783        arguments = "dimension",
2784    }
2785
2786    implement {
2787        name    = "injectdisable",
2788        actions = vspacing.injectdisable,
2789    }
2790
2791    --
2792
2793    implement {
2794        name      = "synchronizepage",
2795        actions   = vspacing.synchronizepage,
2796        scope     = "private"
2797    }
2798
2799 -- implement {
2800 --     name      = "forcestrutdepth",
2801 --     arguments = { "integer", "dimension", "integer" },
2802 --     actions   = vspacing.forcestrutdepth,
2803 --     scope     = "private"
2804 -- }
2805
2806 -- implement {
2807 --     name      = "forcestrutdepthplus",
2808 --     arguments = { "integer", "dimension", "integer", true },
2809 --     actions   = vspacing.forcestrutdepth,
2810 --     scope     = "private"
2811 -- }
2812
2813    implement {
2814        name      = "forcestrutdepth",
2815        public    = true,
2816        protected = true,
2817        actions   = vspacing.forcestrutdepth,
2818    }
2819
2820    implement {
2821        name      = "pushatsame",
2822        actions   = vspacing.pushatsame,
2823        scope     = "private"
2824    }
2825
2826    implement {
2827        name      = "popatsame",
2828        actions   = vspacing.popatsame,
2829        scope     = "private"
2830    }
2831
2832    implement {
2833        name      = "vspacingsetamount",
2834        actions   = vspacing.setskip,
2835        scope     = "private",
2836        arguments = "string",
2837    }
2838
2839    implement {
2840        name      = "vspacingdefine",
2841        actions   = vspacing.setmap,
2842        scope     = "private",
2843        arguments = "2 strings",
2844    }
2845
2846    implement {
2847        name      = "vspacingcollapse",
2848        actions   = vspacing.collapsevbox,
2849        scope     = "private",
2850        arguments = "integer"
2851    }
2852
2853    implement {
2854        name      = "vspacingcollapseonly",
2855        actions   = vspacing.collapsevbox,
2856        scope     = "private",
2857        arguments = { "integer", true }
2858    }
2859
2860    implement {
2861        name      = "vspacingsnap",
2862        actions   = vspacing.snapbox,
2863        scope     = "private",
2864        arguments = "2 integers",
2865    }
2866
2867    implement {
2868        name      = "definesnapmethod",
2869        actions   = vspacing.definesnapmethod,
2870        scope     = "private",
2871        arguments = "2 strings",
2872    }
2873
2874 -- local remove_node    = nodes.remove
2875 -- local find_node_tail = nodes.tail
2876 --
2877 -- interfaces.implement {
2878 --     name    = "fakenextstrutline",
2879 --     actions = function()
2880 --         local head = texlists.pagehead
2881 --         if head then
2882 --             local head = remove_node(head,find_node_tail(head),true)
2883 --             texlists.pagehead = head
2884 --             buildpage()
2885 --         end
2886 --     end
2887 -- }
2888
2889    implement {
2890        name    = "removelastline",
2891        actions = function()
2892            local head = texlists.pagehead
2893            if head then
2894                local tail = find_node_tail(head)
2895                if tail then
2896                    -- maybe check for hlist subtype 1
2897                    local head = remove_node(head,tail,true)
2898                    texlists.pagehead = head
2899                    buildpage()
2900                end
2901            end
2902        end
2903    }
2904
2905    implement {
2906        name    = "showpagelist", -- will improve
2907        actions = function()
2908            local head = texlists.pagehead
2909            if head then
2910                print("start")
2911                while head do
2912                    print("  " .. tostring(head))
2913                    head = head.next
2914                end
2915            end
2916        end
2917    }
2918
2919    implement {
2920        name    = "pageoverflow",
2921        actions = { vspacing.pageoverflow, context }
2922    }
2923
2924    implement {
2925        name    = "forcepageflush",
2926        actions = vspacing.forcepageflush
2927    }
2928
2929    implement {
2930        name      = "injectzerobaselineskip",
2931        protected = true,
2932        public    = true,
2933        actions   = { nodes.pool.baselineskip, context },
2934    }
2935
2936end
2937