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