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