if not modules then modules = { } end modules ['spac-ver'] = {
    version   = 1.001,
    comment   = "companion to spac-ver.mkiv",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

-- we also need to call the spacer for inserts!

-- this code dates from the beginning and is kind of experimental; it
-- will be optimized and improved soon
--
-- the collapser will be redone with user nodes; also, we might get make
-- parskip into an attribute and appy it explicitly thereby getting rid
-- of automated injections; eventually i want to get rid of the currently
-- still needed tex -> lua -> tex > lua chain (needed because we can have
-- expandable settings at the tex end

-- todo: strip baselineskip around display math

local next, type, tonumber = next, type, tonumber
local format, gmatch, concat, match = string.format, string.gmatch, table.concat, string.match
local ceil, floor, max, min, round = math.ceil, math.floor, math.max, math.min, math.round
local texsprint, texlists, texdimen, texbox = tex.sprint, tex.lists, tex.dimen, tex.box
local lpegmatch = lpeg.match
local unpack = unpack or table.unpack

local ctxcatcodes = tex.ctxcatcodes
local variables = interfaces.variables

local starttiming, stoptiming = statistics.starttiming, statistics.stoptiming

-- vertical space handler

local trace_vbox_vspacing    = false  trackers.register("nodes.vbox_vspacing",    function(v) trace_vbox_vspacing    = v end)
local trace_page_vspacing    = false  trackers.register("nodes.page_vspacing",    function(v) trace_page_vspacing    = v end)
local trace_collect_vspacing = false  trackers.register("nodes.collect_vspacing", function(v) trace_collect_vspacing = v end)
local trace_vspacing         = false  trackers.register("nodes.vspacing",         function(v) trace_vspacing         = v end)
local trace_vsnapping        = false  trackers.register("nodes.vsnapping",        function(v) trace_vsnapping        = v end)

local skip_category = attributes.private('skip-category')
local skip_penalty  = attributes.private('skip-penalty')
local skip_order    = attributes.private('skip-order')
local snap_category = attributes.private('snap-category')
local display_math  = attributes.private('display-math')
local snap_method   = attributes.private('snap-method')
local snap_done     = attributes.private('snap-done')

local has_attribute      = node.has_attribute
local unset_attribute    = node.unset_attribute
local set_attribute      = node.set_attribute
local find_node_tail     = node.tail
local free_node          = node.free
local copy_node          = node.copy
local traverse_nodes     = node.traverse
local traverse_nodes_id  = node.traverse_id
local insert_node_before = node.insert_before
local insert_node_after  = node.insert_after
local remove_node        = nodes.remove
local make_penalty_node  = nodes.penalty
local make_kern_node     = nodes.kern
local make_rule_node     = nodes.rule
local count_nodes        = nodes.count
local node_ids_to_string = nodes.ids_to_string
local hpack_node         = node.hpack
local vpack_node         = node.vpack
local writable_spec      = nodes.writable_spec

local glyph   = node.id("glyph")
local penalty = node.id("penalty")
local kern    = node.id("kern")
local glue    = node.id('glue')
local hlist   = node.id('hlist')
local vlist   = node.id('vlist')
local adjust  = node.id('adjust')

vspacing      = vspacing      or { }
vspacing.data = vspacing.data or { }

vspacing.data.snapmethods = vspacing.data.snapmethods or { }

storage.register("vspacing/data/snapmethods", vspacing.data.snapmethods, "vspacing.data.snapmethods")

local snapmethods = vspacing.data.snapmethods --maybe some older code can go

local default = {
    maxheight = true,
    maxdepth  = true,
    strut     = true,
    hfraction = 1,
    dfraction = 1,
}

local fractions = {
    minheight = "hfraction", maxheight = "hfraction",
    mindepth  = "dfraction", maxdepth  = "dfraction",
    top       = "tlines",    bottom    = "blines",
}

local values = {
    offset = "offset"
}

local colonsplitter = lpeg.splitat(":")

local function listtohash(str)
    local t = { }
    for s in gmatch(str,"[^, ]+") do
        local key, detail = lpegmatch(colonsplitter,s)
        local v = variables[key]
        if v then
            t[v] = true
            if detail then
                local k = fractions[key]
                if k then
                    detail = tonumber("0" .. detail)
                    if detail then
                        t[k] = detail
                    end
                else
                    k = values[key]
                    if k then
                        detail = string.todimen(detail)
                        if detail then
                            t[k] = detail
                        end
                    end
                end
            end
        else
            detail = tonumber("0" .. key)
            if detail then
                t.hfraction, t.dfraction = detail, detail
            end
        end
    end
    if next(t) then
        t.hfraction = t.hfraction or 1
        t.dfraction = t.dfraction or 1
        return t
    else
        return default
    end
end

function vspacing.define_snap_method(name,method)
    local n = #snapmethods + 1
    local t = listtohash(method)
    snapmethods[n] = t
    t.name, t.specification = name, method
    tex.write(n)
end

--~ local rule_id  = node.id("rule")
--~ local vlist_id = node.id("vlist")
--~ function nodes.makevtop(n)
--~     if n.id == vlist_id then
--~         local list = n.list
--~         local height = (list and list.id <= rule_id and list.height) or 0
--~         n.depth = n.depth - height + n.height
--~         n.height = height
--~     end
--~ end

local function snap_hlist(current,method,height,depth) -- method.strut is default
    local snapht, snapdp
--~ print(table.serialize(method))
    if method["local"] then
        -- snapping is done immediately here
        snapht, snapdp = texdimen.bodyfontstrutheight, texdimen.bodyfontstrutdepth
    elseif method["global"] then
        snapht, snapdp = texdimen.globalbodyfontstrutheight, texdimen.globalbodyfontstrutdepth
    else
        -- maybe autolocal
        -- snapping might happen later in the otr
        snapht, snapdp = texdimen.globalbodyfontstrutheight, texdimen.globalbodyfontstrutdepth
        local lsnapht, lsnapdp = texdimen.bodyfontstrutheight, texdimen.bodyfontstrutdepth
        if snapht ~= lsnapht and snapdp ~= lsnapdp then
            snapht, snapdp = lsnapht, lsnapdp
        end
    end
    local h, d = height or current.height, depth or current.depth
    local hr, dr, ch, cd = method.hfraction or 1, method.dfraction or 1, h, d
    local tlines, blines = method.tlines or 1, method.blines or 1
    local done, plusht, plusdp = false, snapht, snapdp
    local snaphtdp = snapht + snapdp
    if method.none then
        plusht, plusdp = 0, 0
    end
    if method.halfline then
        plusht, plusdp = plusht + snaphtdp/2, plusdp + snaphtdp/2
    end
    if method.line then
        plusht, plusdp = plusht + snaphtdp, plusdp + snaphtdp
    end

    if method.first then
        if current.id == vlist then
            local list, lh, ld = current.list
            for n in traverse_nodes_id(hlist,list) do
                lh, ld = n.height, n.depth
                break
            end
            if lh then
                local x = max(ceil((lh-hr*snapht)/snaphtdp),0)*snaphtdp + plusht
                local n = make_kern_node(x-lh)
                n.next, list.prev, current.list = list, n, n
                ch = x + snaphtdp
                cd = max(ceil((d+h-lh-dr*snapdp-hr*snapht)/snaphtdp),0)*snaphtdp + plusdp
                done = true
            end
        end
    elseif method.last then
        if current.id == vlist then
            local list, lh, ld = current.list
            for n in traverse_nodes_id(hlist,list) do
                lh, ld = n.height, n.depth
            end
            if lh then
                local baseline_till_top = h + d - ld
                local x = max(ceil((baseline_till_top-hr*snapht)/snaphtdp),0)*snaphtdp + plusht
                local n = make_kern_node(x-baseline_till_top)
                n.next, list.prev, current.list = list, n, n
                ch = x
                cd = max(ceil((ld-dr*snapdp)/snaphtdp),0)*snaphtdp + plusdp
                done = true
            end
        end
    end

    if done then
        -- first or last
    elseif method.minheight then
        ch = max(floor((h-hr*snapht)/snaphtdp),0)*snaphtdp + plusht
    elseif method.maxheight then
        ch = max(ceil((h-hr*snapht)/snaphtdp),0)*snaphtdp + plusht
    else
        ch = plusht
    end
    if done then
        -- first or last
    elseif method.mindepth then
        cd = max(floor((d-dr*snapdp)/snaphtdp),0)*snaphtdp + plusdp
    elseif method.maxdepth then
        cd = max(ceil((d-dr*snapdp)/snaphtdp),0)*snaphtdp + plusdp
    else
        cd = plusdp
    end
    if method.top then
        ch = ch + tlines * snaphtdp
    end
    if method.bottom then
        cd = cd + blines * snaphtdp
    end

    local offset = method.offset
    if offset then
        -- we need to set the attr
        local shifted = vpack_node(current.list)
        shifted.shift = offset
        current.list = shifted
    end
    if not height then
        current.height = ch
    end
    if not depth then
        current.depth = cd
    end
 -- set_attribute(current,snap_method,0)
    return h, d, ch, cd, (ch+cd)/snaphtdp
end

--~ local function snap_topskip(current,method)
--~     local spec = current.spec
--~     local w = spec.width
--~     local wd = w
--~     if spec then
--~         wd = 0
--~         spec = writable_spec(current)
--~         spec.width = wd
--~     end
--~     return w, wd
--~ end

local function snap_topskip(current,method)
    local spec = current.spec
    local w = spec.width
    local wd = w
    if spec.writable then
        spec.width, wd = 0, 0
    end
    return w, wd
end

vspacing.categories = {
     [0] = 'discard',
     [1] = 'largest',
     [2] = 'force'  ,
     [3] = 'penalty',
     [4] = 'add'    ,
     [5] = 'disable',
     [6] = 'nowhite',
     [7] = 'goback',
     [8] = 'together'
}

local categories = vspacing.categories

function vspacing.tocategories(str)
    local t = { }
    for s in gmatch(str,"[^, ]") do
        local n = tonumber(s)
        if n then
            t[categories[n]] = true
        else
            t[b] = true
        end
    end
    return t
end

function vspacing.tocategory(str)
    if type(str) == "string" then
        return set.tonumber(vspacing.tocategories(str))
    else
        return set.tonumber({ [categories[str]] = true })
    end
end

vspacing.data.map  = vspacing.data.map  or { }
vspacing.data.skip = vspacing.data.skip or { }

storage.register("vspacing/data/map", vspacing.data.map, "vspacing.data.map")
storage.register("vspacing/data/skip", vspacing.data.skip, "vspacing.data.skip")

do -- todo: interface.variables

    local function logger(c,...)
        logs.report("vspacing",concat {...})
        texsprint(c,...)
    end

    vspacing.fixed = false

    local map  = vspacing.data.map
    local skip = vspacing.data.skip

    local multiplier = lpeg.C(lpeg.S("+-")^0 * lpeg.R("09")^1) * lpeg.P("*")
    local category   = lpeg.P(":") * lpeg.C(lpeg.P(1)^1)
    local keyword    = lpeg.C((1-category)^1)
    local splitter   = (multiplier + lpeg.Cc(1)) * keyword * (category + lpeg.Cc(false))

    local k_fixed, k_flexible, k_category, k_penalty, k_order = variables.fixed, variables.flexible, "category", "penalty", "order"

    -- This will change: just node.write and we can store the values in skips which
    -- then obeys grouping

    local function analyse(str,oldcategory,texsprint) -- we could use shorter names
        for s in gmatch(str,"([^ ,]+)") do
            local amount, keyword, detail = lpegmatch(splitter,s)
            if not keyword then
                logs.report("vspacing","unknown directive: %s",s)
            else
                local mk = map[keyword]
                if mk then
                    category = analyse(mk,category,texsprint)
                elseif keyword == k_fixed then
                    texsprint(ctxcatcodes,"\\fixedblankskip")
                elseif keyword == k_flexible then
                    texsprint(ctxcatcodes,"\\flexibleblankskip")
                elseif keyword == k_category then
                    local category = tonumber(detail)
                    if category then
                        texsprint(ctxcatcodes,"\\setblankcategory{",category,"}")
                        if category ~= oldcategory then
                            texsprint(ctxcatcodes,"\\flushblankhandling")
                            oldcategory = category
                        end
                    end
                elseif keyword == k_order and detail then
                    local order = tonumber(detail)
                    if order then
                        texsprint(ctxcatcodes,"\\setblankorder{",order,"}")
                    end
                elseif keyword == k_penalty and detail then
                    local penalty = tonumber(detail)
                    if penalty then
                        texsprint(ctxcatcodes,"\\setblankpenalty{",penalty,"}")
                    end
                else
                    amount = tonumber(amount) or 1
                    local sk = skip[keyword]
                    if sk then
                        texsprint(ctxcatcodes,"\\addpredefinedblankskip{",amount,"}{",keyword,"}")
                    else -- no check
                        texsprint(ctxcatcodes,"\\addaskedblankskip{",amount,"}{",keyword,"}")
                    end
                end
            end
        end
        return category
    end

    function vspacing.analyse(str)
        local texsprint = (trace_vspacing and logger) or texsprint
        texsprint(ctxcatcodes,"\\startblankhandling")
        analyse(str,1,texsprint)
        texsprint(ctxcatcodes,"\\stopblankhandling")
    end

    --

    function vspacing.setmap(from,to)
        map[from] = to
    end

    function vspacing.setskip(key,value,grid)
        if value ~= "" then
            if grid == "" then grid = value end
            skip[key] = { value, grid }
        end
    end

end

-- implementation

--~ nodes.snapvalues = { }

--~ function nodes.setsnapvalue(n,ht,dp)
--~     nodes.snapvalues[n] = { ht, dp, ht+dp }
--~ end

local trace_list, tracing_info, before, after = { }, false, "", ""

local function glue_to_string(glue)
    local spec = glue.spec
    local t = { }
    t[#t+1] = aux.strip_zeros(number.topoints(spec.width))
    if spec.stretch_order and spec.stretch_order ~= 0 then
        t[#t+1] = format("plus -%sfi%s",spec.stretch/65536,string.rep("l",math.abs(spec.stretch_order)-1))
    elseif spec.stretch and spec.stretch ~= 0 then
        t[#t+1] = format("plus %s",aux.strip_zeros(number.topoints(spec.stretch)))
    end
    if spec.shrink_order and spec.shrink_order ~= 0 then
        t[#t+1] = format("minus -%sfi%s",spec.shrink/65536,string.rep("l",math.abs(spec.shrink_order)-1))
    elseif spec.shrink and spec.shrink ~= 0 then
        t[#t+1] = format("minus %s",aux.strip_zeros(number.topoints(spec.shrink)))
    end
    return concat(t," ")
end

local function nodes_to_string(head)
    local current, t = head, { }
    while current do
        local id = current.id
        local ty = node.type(id)
        if id == penalty then
            t[#t+1] = format("%s:%s",ty,current.penalty)
        elseif id == glue then
            t[#t+1] = format("%s:%s",ty,aux.strip_zeros(number.topoints(current.spec.width)))
        elseif id == kern then
            t[#t+1] = format("%s:%s",ty,aux.strip_zeros(number.topoints(current.kern)))
        else
            t[#t+1] = ty
        end
        current = current.next
    end
    return concat(t," + ")
end

local function reset_tracing(head)
    trace_list, tracing_info, before, after = { }, false, nodes_to_string(head), ""
end

local function trace_skip(str,sc,so,sp,data)
    trace_list[#trace_list+1] = { "skip", format("%s | %s | category %s | order %s | penalty %s", str, glue_to_string(data), sc or "-", so or "-", sp or "-") }
    tracing_info = true
end

local function trace_natural(str,data)
    trace_list[#trace_list+1] = { "skip", format("%s | %s", str, glue_to_string(data)) }
    tracing_info = true
end

local function trace_info(message, where, what)
    trace_list[#trace_list+1] = { "info", format("%s: %s/%s",message,where,what) }
end

local function trace_node(what)
    local nt = node.type(what.id)
    local tl = trace_list[#trace_list]
    if tl and tl[1] == "node" then
        trace_list[#trace_list] = { "node", tl[2] .. " + " .. nt }
    else
        trace_list[#trace_list+1] = { "node", nt }
    end
end

local function trace_done(str,data)
    if data.id == penalty then
        trace_list[#trace_list+1] = { "penalty", format("%s | %s", str, data.penalty) }
    else
        trace_list[#trace_list+1] = { "glue", format("%s | %s", str, glue_to_string(data)) }
    end
    tracing_info = true
end

local function show_tracing(head)
    if tracing_info then
        after = nodes_to_string(head)
        for i=1,#trace_list do
            local tag, text = unpack(trace_list[i])
            if tag == "info" then
                logs.report("collapse",text)
            else
                logs.report("collapse","  %s: %s",tag,text)
            end
        end
        logs.report("collapse","before: %s",before)
        logs.report("collapse","after : %s",after)
    end
end

-- alignment box begin_of_par vmode_par hmode_par insert penalty before_display after_display

local user_skip                =  0
local line_skip                =  1
local baseline_skip            =  2
local par_skip                 =  3
local above_display_skip       =  4
local below_display_skip       =  5
local above_display_short_skip =  6
local below_display_short_skip =  7
local left_skip_code           =  8
local right_skip_code          =  9
local top_skip_code            = 10
local split_top_skip_code      = 11
local tab_skip_code            = 12
local space_skip_code          = 13
local xspace_skip_code         = 14
local par_fill_skip_code       = 15
local thin_mu_skip_code        = 16
local med_mu_skip_code         = 17
local thick_mu_skip_code       = 18

local skips = {
   [ 0] = "user_skip",
   [ 1] = "line_skip",
   [ 2] = "baseline_skip",
   [ 3] = "par_skip",
   [ 4] = "above_display_skip",
   [ 5] = "below_display_skip",
   [ 6] = "above_display_short_skip",
   [ 7] = "below_display_short_skip",
   [ 8] = "left_skip_code",
   [ 9] = "right_skip_code",
   [10] = "top_skip_code",
   [11] = "split_top_skip_code",
   [12] = "tab_skip_code",
   [13] = "space_skip_code",
   [14] = "xspace_skip_code",
   [15] = "par_fill_skip_code",
   [16] = "thin_mu_skip_code",
   [17] = "med_mu_skip_code",
   [18] = "thick_mu_skip_code",
}

local free_glue_node = free_node
local discard, largest, force, penalty, add, disable, nowhite, goback, together = 0, 1, 2, 3, 4, 5, 6, 7, 8

--~ local function free_glue_node(n) free_node(n.spec) free_node(n) end

function vspacing.snap_box(n,how)
    local sv = snapmethods[how]
    if sv then
        local box = texbox[n]
        local list = box.list
--~         if list and (list.id == hlist or list.id == vlist) then
        if list then
            local s = has_attribute(list,snap_method)
            if s == 0 then
                if trace_vsnapping then
                --  logs.report("snapper", "hlist not snapped, already done")
                end
            else
                local h, d, ch, cd, lines = snap_hlist(box,sv,box.height,box.depth)
                box.height, box.depth = ch, cd
                if trace_vsnapping then
                    logs.report("snapper", "hlist snapped from (%s,%s) to (%s,%s) using method '%s' (%s) for '%s' (%s lines)",h,d,ch,cd,sv.name,sv.specification,"direct",lines)
                end
                set_attribute(list,snap_method,0)
            end
        end
    end
end

local function forced_skip(head,current,width,where,trace)
    if where == "after" then
        head, current = insert_node_after(head,current,make_rule_node(0,0,0))
        head, current = insert_node_after(head,current,make_kern_node(width))
        head, current = insert_node_after(head,current,make_rule_node(0,0,0))
    else
        local c = current
        head, current = insert_node_before(head,current,make_rule_node(0,0,0))
        head, current = insert_node_before(head,current,make_kern_node(width))
        head, current = insert_node_before(head,current,make_rule_node(0,0,0))
        current = c
    end
    if trace then
        logs.report("vspacing", "inserting forced skip of %s",width)
    end
    return head, current
end

local function collapser(head,where,what,trace,snap) -- maybe also pass tail
    if trace then
        reset_tracing(head)
    end
    local current, oldhead = head, head
    local glue_order, glue_data, force_glue = 0, nil, false
    local penalty_order, penalty_data, natural_penalty = 0, nil, nil
    local parskip, ignore_parskip, ignore_following, ignore_whitespace, keep_together = nil, false, false, false, false
    --
    -- todo: keep_together: between headers
    --
    local function flush(why)
        if penalty_data then
            local p = make_penalty_node(penalty_data)
            if trace then trace_done("flushed due to " .. why,p) end
            head, _ = insert_node_before(head,current,p)
        end
        if glue_data then
            if force_glue then
                if trace then trace_done("flushed due to " .. why,glue_data) end
                head, _ = forced_skip(head,current,glue_data.spec.width,"before",trace)
                free_glue_node(glue_data)
            elseif glue_data.spec.writable then
                if trace then trace_done("flushed due to " .. why,glue_data) end
                head, _ = insert_node_before(head,current,glue_data)
            else
                free_glue_node(glue_data)
            end
        end
        if trace then trace_node(current) end
        glue_order, glue_data, force_glue = 0, nil, false
        penalty_order, penalty_data, natural_penalty = 0, nil, nil
        parskip, ignore_parskip, ignore_following, ignore_whitespace = nil, false, false, false
    end
    if trace_vsnapping then
        logs.report("snapper", "global ht/dp = %s/%s, local ht/dp = %s/%s",
            texdimen.globalbodyfontstrutheight, texdimen.globalbodyfontstrutdepth,
            texdimen.bodyfontstrutheight, texdimen.bodyfontstrutdepth)
    end
    if trace then trace_info("start analyzing",where,what) end
    while current do
        local id, subtype = current.id, current.subtype
        if id == hlist or id == vlist then
-- needs checking, why so many calls
            if snap then
                local s = has_attribute(current,snap_method)
                if not s then
                --  if trace_vsnapping then
                --      logs.report("snapper", "hlist not snapped")
                --  end
                elseif s == 0 then
                    if trace_vsnapping then
                    --  logs.report("snapper", "hlist not snapped, already done")
                    end
                else
                    local sv = snapmethods[s]
                    if sv then
                        local h, d, ch, cd, lines = snap_hlist(current,sv)
                        if trace_vsnapping then
                            logs.report("snapper", "hlist snapped from (%s,%s) to (%s,%s) using method '%s' (%s) for '%s' (%s lines)",h,d,ch,cd,sv.name,sv.specification,where,lines)
                        end
                    elseif trace_vsnapping then
                        logs.report("snapper", "hlist not snapped due to unknown snap specification")
                    end
                    set_attribute(current,snap_method,0)
                end
            else
                --
            end
        --  tex.prevdepth = 0
            flush("list")
            current = current.next
        elseif id == penalty then
            --~ natural_penalty = current.penalty
            --~ if trace then trace_done("removed penalty",current) end
            --~ head, current = remove_node(head, current, true)
            current = current.next
        elseif id == kern then
            if snap and trace_vsnapping and current.kern ~= 0 then
            --~ current.kern = 0
                logs.report("snapper", "kern of %s (kept)",current.kern)
            end
            flush("kern")
            current = current.next
        elseif id ~= glue then
            flush("something else")
            current = current.next
        elseif subtype == user_skip then -- todo, other subtypes, like math
            local sc = has_attribute(current,skip_category)      -- has no default, no unset (yet)
            local so = has_attribute(current,skip_order   ) or 1 -- has  1 default, no unset (yet)
            local sp = has_attribute(current,skip_penalty )      -- has no default, no unset (yet)
            if sp and sc == penalty then
                if not penalty_data then
                    penalty_data = sp
                elseif penalty_order < so then
                    penalty_order, penalty_data = so, sp
                elseif penalty_order == so and sp > penalty_data then
                    penalty_data = sp
                end
                if trace then trace_skip('penalty in skip',sc,so,sp,current) end
                head, current = remove_node(head, current, true)
            elseif not sc then  -- if not sc then
                if glue_data then
                    if trace then trace_done("flush",glue_data) end
                    head, current = nodes.before(head,current,glue_data)
                    if trace then trace_natural("natural",current) end
                    current = current.next
                else
                    -- not look back across head
                    local previous = current.prev
                    if previous and previous.id == glue and previous.subtype == 0 then
                        local ps = previous.spec
                        if ps.writable then
                            local cs = current.spec
                            if cs.writable and ps.stretch_order == 0 and ps.shrink_order == 0 and cs.stretch_order == 0 and cs.shrink_order == 0 then
                                local pw, pp, pm = ps.width, ps.stretch, ps.shrink
                                local cw, cp, cm = cs.width, cs.stretch, cs.shrink
                                ps = writable_spec(previous) -- no writable needed here
                                ps.width, ps.stretch, ps.shrink = pw + cw, pp + cp, pm + cm
                                if trace then trace_natural("removed",current) end
                                head, current = remove_node(head, current, true)
                            --  current = previous
                                if trace then trace_natural("collapsed",previous) end
                            --  current = current.next
                            else
                                if trace then trace_natural("filler",current) end
                                current = current.next
                            end
                        else
                            if trace then trace_natural("natural (no prev spec)",current) end
                            current = current.next
                        end
                    else
                        if trace then trace_natural("natural (no prev)",current) end
                        current = current.next
                    end
                end
                glue_order, glue_data = 0, nil
            elseif sc == disable then
                ignore_following = true
                if trace then trace_skip("disable",sc,so,sp,current) end
                head, current = remove_node(head, current, true)
            elseif sc == together then
                keep_together = true
                if trace then trace_skip("together",sc,so,sp,current) end
                head, current = remove_node(head, current, true)
            elseif sc == nowhite then
                ignore_whitespace = true
                head, current = remove_node(head, current, true)
            elseif sc == discard then
                if trace then trace_skip("discard",sc,so,sp,current) end
                head, current = remove_node(head, current, true)
            elseif ignore_following then
                if trace then trace_skip("disabled",sc,so,sp,current) end
                head, current = remove_node(head, current, true)
            elseif not glue_data then
                if trace then trace_skip("assign",sc,so,sp,current) end
                glue_order = so
                head, current, glue_data = remove_node(head, current)
            elseif glue_order < so then
                if trace then trace_skip("force",sc,so,sp,current) end
                glue_order = so
                free_glue_node(glue_data)
                head, current, glue_data = remove_node(head, current)
            elseif glue_order == so then
                -- is now exclusive, maybe support goback as combi, else why a set
                if sc == largest then
                    local cs, gs = current.spec, glue_data.spec
                    local cw, gw = cs.width, gs.width
                    if cw > gw then
                        if trace then trace_skip('largest',sc,so,sp,current) end
                        free_glue_node(glue_data) -- also free spec
                        head, current, glue_data = remove_node(head, current)
                    else
                        if trace then trace_skip('remove smallest',sc,so,sp,current) end
                        head, current = remove_node(head, current, true)
                    end
                elseif sc == goback then
                    if trace then trace_skip('goback',sc,so,sp,current) end
                    free_glue_node(glue_data) -- also free spec
                    head, current, glue_data = remove_node(head, current)
                elseif sc == force then
                    -- last one counts, some day we can provide an accumulator and largest etc
                    -- but not now
                    if trace then trace_skip('force',sc,so,sp,current) end
                    free_glue_node(glue_data) -- also free spec
                    head, current, glue_data = remove_node(head, current)
                elseif sc == penalty then
                    -- ? ? ? ?
                    if trace then trace_skip('penalty',sc,so,sp,current) end
                    free_glue_node(glue_data) -- also free spec
                    glue_data = nil
                    head, current = remove_node(head, current, true)
                elseif sc == add then
                    if trace then trace_skip('add',sc,so,sp,current) end
--~                     local old, new = glue_data.spec, current.spec
                    local old, new = writable_spec(glue_data), current.spec
                    old.width   = old.width   + new.width
                    old.stretch = old.stretch + new.stretch
                    old.shrink  = old.shrink  + new.shrink
                    -- toto: order
                    head, current = remove_node(head, current, true)
                else
                    if trace then trace_skip("unknown",sc,so,sp,current) end
                    head, current = remove_node(head, current, true)
                end
            else
                if trace then trace_skip("unknown",sc,so,sp,current) end
                head, current = remove_node(head, current, true)
            end
            if sc == force then
                force_glue = true
            end
        elseif subtype == line_skip then
            if snap then
                local s = has_attribute(current,snap_method)
                if s and s ~= 0 then
                    set_attribute(current,snap_method,0)
                    if current.spec.writable then
                        local spec = writable_spec(current)
                        spec.width = 0
                        if trace_vsnapping then
                            logs.report("snapper", "lineskip set to zero")
                        end
                    end
                else
                    if trace then trace_skip("lineskip",sc,so,sp,current) end
                    flush("lineskip")
                end
            else
                if trace then trace_skip("lineskip",sc,so,sp,current) end
                flush("lineskip")
            end
            current = current.next
        elseif subtype == baseline_skip then
            if snap then
                local s = has_attribute(current,snap_method)
                if s and s ~= 0 then
                    set_attribute(current,snap_method,0)
                    if current.spec.writable then
                        local spec = writable_spec(current)
                        spec.width = 0
                        if trace_vsnapping then
                            logs.report("snapper", "baselineskip set to zero")
                        end
                    end
                else
                    if trace then trace_skip("baselineskip",sc,so,sp,current) end
                    flush("baselineskip")
                end
            else
                if trace then trace_skip("baselineskip",sc,so,sp,current) end
                flush("baselineskip")
            end
            current = current.next
        elseif subtype == par_skip then
            -- parskip always comes later
            if ignore_whitespace then
                if trace then trace_natural("ignored parskip",current) end
                head, current = remove_node(head, current, true)
            elseif glue_data then
                local ps, gs = current.spec, glue_data.spec
                if ps.writable and gs.writable and ps.width > gs.width then
                    glue_data.spec = copy_node(ps)
                    if trace then trace_natural("taking parskip",current) end
                else
                    if trace then trace_natural("removed parskip",current) end
                end
                head, current = remove_node(head, current, true)
            else
                if trace then trace_natural("honored parskip",current) end
                head, current, glue_data = remove_node(head, current)
            end
        elseif subtype == top_skip_code or subtype == split_top_skip_code then
            if snap then
                local s = has_attribute(current,snap_method)
                if s and s ~= 0 then
                    set_attribute(current,snap_method,0)
                    local sv = snapmethods[s]
                    local w, cw = snap_topskip(current,sv)
                    if trace_vsnapping then
                        logs.report("snapper", "topskip snapped from %s to %s for '%s'",w,cw,where)
                    end
                else
                    if trace then trace_skip("topskip",sc,so,sp,current) end
                    flush("topskip")
                end
            else
                if trace then trace_skip("topskip",sc,so,sp,current) end
                flush("topskip")
            end
            current = current.next
        elseif subtype == above_display_skip then
            --
            if trace then trace_skip("above display skip (normal)",sc,so,sp,current) end
            flush("above display skip (normal)")
            current = current.next
            --
        elseif subtype == below_display_skip then
            --
            if trace then trace_skip("below display skip (normal)",sc,so,sp,current) end
            flush("below display skip (normal)")
            current = current.next
            --
        elseif subtype == above_display_short_skip then
            --
            if trace then trace_skip("above display skip (short)",sc,so,sp,current) end
            flush("above display skip (short)")
            current = current.next
            --
        elseif subtype == below_display_short_skip then
            --
            if trace then trace_skip("below display skip (short)",sc,so,sp,current) end
            flush("below display skip (short)")
            current = current.next
            --
        else -- other glue
            if snap and trace_vsnapping and current.spec.writable and current.spec.width ~= 0 then
                logs.report("snapper", "%s of %s (kept)",skips[subtype],current.spec.width)
            --~ current.spec.width = 0
            end
            if trace then trace_skip(format("some glue (%s)",subtype),sc,so,sp,current) end
            flush("some glue")
            current = current.next
        end
    end
    if trace then trace_info("stop analyzing",where,what) end
    --~ if natural_penalty and (not penalty_data or natural_penalty > penalty_data) then
    --~     penalty_data = natural_penalty
    --~ end
    if trace and (glue_data or penalty_data) then
        trace_info("start flushing",where,what)
    end
    local tail
    if penalty_data then
        tail = find_node_tail(head)
        local p = make_penalty_node(penalty_data)
        if trace then trace_done("result",p) end
        head, tail = insert_node_after(head,tail,p)
    end
    if glue_data then
        if not tail then tail = find_node_tail(head) end
        if trace then trace_done("result",glue_data) end
        if force_glue then
            head, tail = forced_skip(head,tail,glue_data.spec.width,"after",trace)
            free_glue_node(glue_data)
        else
            head, tail = insert_node_after(head,tail,glue_data)
        end
    end
    if trace then
        if glue_data or penalty_data then
            trace_info("stop flushing",where,what)
        end
        show_tracing(head)
        if oldhead ~= head then
            trace_info("head has been changed from '%s' to '%s'",node.type(oldhead.id),node.type(head.id))
        end
    end
    return head, true
end

-- alignment after_output end box new_graf vmode_par hmode_par insert penalty before_display after_display
-- \par -> vmode_par
--
-- status.best_page_break
-- tex.lists.best_page_break
-- tex.lists.best_size (natural size to best_page_break)
-- tex.lists.least_page_cost (badness of best_page_break)
-- tex.lists.page_head
-- tex.lists.contrib_head

local stackhead, stacktail, stackhack = nil, nil, false

local function report(message,lst)
    logs.report("vspacing",message,count_nodes(lst,true),node_ids_to_string(lst))
end

function nodes.handle_page_spacing(newhead,where)
--~     local newhead = texlists.contrib_head
    if newhead then
--~         starttiming(vspacing)
        local newtail = find_node_tail(newhead)
        local flush = false
        stackhack = true -- todo: only when grid snapping once enabled
        for n in traverse_nodes(newhead) do -- we could just look for glue nodes
            local id = n.id
            if id == glue then
                if n.subtype == 0 then
                    if has_attribute(n,skip_category) then
                        stackhack = true
                    else
                        flush = true
                    end
                else
                    -- tricky
                end
            else
                flush = true
            end
        end
        if flush then
            if stackhead then
                if trace_collect_vspacing then report("appending %s nodes to stack (final): %s",newhead) end
                stacktail.next = newhead
                newhead.prev = stacktail
                newhead = stackhead
                stackhead, stacktail = nil, nil
            end
            if stackhack then
                stackhack = false
                if trace_collect_vspacing then report("processing %s nodes: %s",newhead) end
--~                 texlists.contrib_head = collapser(newhead,"page",where,trace_page_vspacing,true)
newhead = collapser(newhead,"page",where,trace_page_vspacing,true)
            else
                if trace_collect_vspacing then report("flushing %s nodes: %s",newhead) end
--~                 texlists.contrib_head = newhead
            end
        else
            if stackhead then
                if trace_collect_vspacing then report("appending %s nodes to stack (intermediate): %s",newhead) end
                stacktail.next = newhead
                newhead.prev = stacktail
            else
                if trace_collect_vspacing then report("storing %s nodes in stack (initial): %s",newhead) end
                stackhead = newhead
            end
            stacktail = newtail
--~             texlists.contrib_head = nil
newhead = nil
        end
--~         stoptiming(vspacing)
    end
return newhead
end

local ignore = table.tohash {
    "split_keep",
    "split_off",
 -- "vbox",
}

function nodes.handle_vbox_spacing(head,where)
    if head and not ignore[where] and head.next then
    --  starttiming(vspacing)
        head = collapser(head,"vbox",where,trace_vbox_vspacing,false)
    --  stoptiming(vspacing)
    end
    return head
end

function nodes.collapse_vbox(n) -- for boxes
    local list = texbox[n].list
    if list then
    --  starttiming(vspacing)
        texbox[n].list = vpack_node(collapser(list,"snapper","vbox",trace_vbox_vspacing,true))
    --  stoptiming(vspacing)
    end
end

-- we will split this module hence the locals

local attribute = attributes.private('graphicvadjust')

local hlist = node.id('hlist')
local vlist = node.id('vlist')

local remove_node   = nodes.remove
local hpack_node    = node.hpack
local vpack_node    = node.vpack
local has_attribute = node.has_attribute

function nodes.repackage_graphicvadjust(head,groupcode) -- we can make an actionchain for mvl only
    if groupcode == "" then -- mvl only
        local h, p, done = head, nil, false
        while h do
            local id = h.id
            if id == hlist or id == vlist then
                local a = has_attribute(h,attribute)
                if a then
                    if p then
                        local n
                        head, h, n = remove_node(head,h)
                        local pl = p.list
                        if n.width ~= 0 then
                            n = hpack_node(n,0,'exactly') -- todo: dir
                        end
                        if pl then
                            pl.prev = n
                            n.next = pl
                        end
                        p.list = n
                        done = true
                    else
                        -- can't happen
                    end
                else
                    p = h
                    h = h.next
                end
            else
                h = h.next
            end
        end
        return head, done
    else
        return head, false
    end
end

--~ function nodes.repackage_graphicvadjust(head,groupcode) -- we can make an actionchain for mvl only
--~     if groupcode == "" then -- mvl only
--~         return head, false
--~     else
--~         return head, false
--~     end
--~ end

--~ tasks.appendaction("finalizers", "lists", "nodes.repackage_graphicvadjust")

nodes.builders = nodes.builder or { }

local builders = nodes.builders

local actions = tasks.actions("vboxbuilders",5)

function nodes.builders.vpack_filter(head,groupcode,size,packtype,maxdepth,direction)
    local done = false
    if head then
        starttiming(builders)
        if trace_callbacks then
            local before = nodes.count(head)
            head, done = actions(head,groupcode,size,packtype,maxdepth,direction)
            local after = nodes.count(head)
            if done then
                tracer("vpack","changed",head,groupcode,before,after,true)
            else
                tracer("vpack","unchanged",head,groupcode,before,after,true)
            end
            stoptiming(builders)
        else
            head, done = actions(head,groupcode)
            stoptiming(builders)
        end
    end
    return head, done
end

-- This one is special in the sense that it has no head
-- and we operate on the mlv. Also, we need to do the
-- vspacing last as it removes items from the mvl.

local actions = tasks.actions("mvlbuilders",1)

function nodes.builders.buildpage_filter(groupcode)
    starttiming(builders)
    local head = texlists.contrib_head
    local head, done = actions(head,groupcode)
    texlists.contrib_head = head
    stoptiming(builders)
    return (done and head) or true
end

callbacks.register('vpack_filter',     nodes.builders.vpack_filter, "vertical spacing etc")
callbacks.register('buildpage_filter', nodes.builders.buildpage_filter, "vertical spacing etc (mvl)")

statistics.register("v-node processing time", function()
    return statistics.elapsedseconds(builders)
end)
