if not modules then modules = { } end modules ['spac-ver'] = { version = 1.001, optimize = true, 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! -- somehow lists still don't always have proper prev nodes so i need to -- check all of the luatex code some day .. maybe i should replece the -- whole mvl handler by lua code .. why not -- todo: use lua nodes with lua data (>0.79) -- see ** can go when 0.79 -- needs to be redone, too many calls and tests now ... still within some -- luatex limitations -- this code dates from the beginning and is kind of experimental; it -- will be optimized and improved soon .. it's way too complex now but -- dates from less possibilities -- -- 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 gmatch, concat = string.gmatch, table.concat local lpegmatch = lpeg.match local unpack = unpack or table.unpack local allocate = utilities.storage.allocate local todimen = string.todimen local formatters = string.formatters local nodes = nodes local trackers = trackers local attributes = attributes local context = context local tex = tex local texlists = tex.lists local texget = tex.get local texgetcount = tex.getcount local texgetdimen = tex.getdimen local texgetglue = tex.getglue local texset = tex.set local texsetdimen = tex.setdimen local texsetcount = tex.setcount local texnest = tex.nest local texgetbox = tex.getbox local buildpage = tex.triggerbuildpage local variables = interfaces.variables local v_local = variables["local"] local v_global = variables["global"] local v_box = variables.box ----- v_page = variables.page -- reserved for future use local v_split = variables.split local v_min = variables.min local v_max = variables.max local v_none = variables.none local v_first = variables.first local v_last = variables.last local v_top = variables.top local v_bottom = variables.bottom local v_maxheight = variables.maxheight local v_minheight = variables.minheight local v_mindepth = variables.mindepth local v_maxdepth = variables.maxdepth local v_offset = variables.offset local v_strut = variables.strut local v_hfraction = variables.hfraction local v_dfraction = variables.dfraction local v_bfraction = variables.bfraction local v_tlines = variables.tlines local v_blines = variables.blines -- vertical space handler local trace_vbox_vspacing = false trackers.register("vspacing.vbox", function(v) trace_vbox_vspacing = v end) local trace_page_vspacing = false trackers.register("vspacing.page", function(v) trace_page_vspacing = v end) local trace_collect_vspacing = false trackers.register("vspacing.collect", function(v) trace_collect_vspacing = v end) local trace_vspacing = false trackers.register("vspacing.spacing", function(v) trace_vspacing = v end) local trace_vsnapping = false trackers.register("vspacing.snapping", function(v) trace_vsnapping = v end) local trace_specials = false trackers.register("vspacing.specials", function(v) trace_specials = v end) local remove_math_skips = true directives.register("vspacing.removemathskips", function(v) remnove_math_skips = v end) local report_vspacing = logs.reporter("vspacing","spacing") local report_collapser = logs.reporter("vspacing","collapsing") local report_snapper = logs.reporter("vspacing","snapping") local report_specials = logs.reporter("vspacing","specials") local a_skipcategory = attributes.private('skipcategory') local a_skippenalty = attributes.private('skippenalty') local a_skiporder = attributes.private('skiporder') local a_snapmethod = attributes.private('snapmethod') local a_snapvbox = attributes.private('snapvbox') local d_bodyfontstrutheight = tex.isdimen("bodyfontstrutheight") local d_bodyfontstrutdepth = tex.isdimen("bodyfontstrutdepth") local d_globalbodyfontstrutheight = tex.isdimen("globalbodyfontstrutheight") local d_globalbodyfontstrutdepth = tex.isdimen("globalbodyfontstrutdepth") ----- d_strutht = tex.isdimen("strutht") local d_strutdp = tex.isdimen("strutdp") local d_spac_overlay = tex.isdimen("d_spac_overlay") local c_spac_vspacing_ignore_parskip = tex.iscount("c_spac_vspacing_ignore_parskip") local nuts = nodes.nuts local tonut = nuts.tonut local getnext = nuts.getnext local setlink = nuts.setlink local getprev = nuts.getprev local getid = nuts.getid local getlist = nuts.getlist local setlist = nuts.setlist local getattr = nuts.getattr local setattr = nuts.setattr local getsubtype = nuts.getsubtype local getbox = nuts.getbox local getwhd = nuts.getwhd local setwhd = nuts.setwhd local getprop = nuts.getprop local setprop = nuts.setprop local getglue = nuts.getglue local setglue = nuts.setglue local getkern = nuts.getkern local getpenalty = nuts.getpenalty local setshift = nuts.setshift local setwidth = nuts.setwidth local getwidth = nuts.getwidth local setheight = nuts.setheight local getheight = nuts.getheight local setdepth = nuts.setdepth local getdepth = nuts.getdepth local setnext = nuts.setnext local setprev = nuts.setprev local find_node_tail = nuts.tail local flushnode = nuts.flushnode local remove_node = nuts.remove local count_nodes = nuts.countall local hpack_node = nuts.hpack local vpack_node = nuts.vpack local startofpar = nuts.startofpar local write_node = nuts.write local nextnode = nuts.traversers.node local nexthlist = nuts.traversers.hlist local nodereference = nuts.reference local listtoutf = nodes.listtoutf local nodeidstostring = nodes.idstostring local nodepool = nuts.pool local new_penalty = nodepool.penalty local new_kern = nodepool.kern local new_glue = nodepool.glue local new_rule = nodepool.rule local nodecodes = nodes.nodecodes local gluecodes = nodes.gluecodes ----- penaltycodes = nodes.penaltycodes ----- listcodes = nodes.listcodes local penalty_code = nodecodes.penalty local kern_code = nodecodes.kern local glue_code = nodecodes.glue local hlist_code = nodecodes.hlist local vlist_code = nodecodes.vlist local rule_code = nodecodes.rule local par_code = nodecodes.par local boundary_code = nodecodes.boundary local userskip_code = gluecodes.userskip local lineskip_code = gluecodes.lineskip local baselineskip_code = gluecodes.baselineskip local parskip_code = gluecodes.parskip local topskip_code = gluecodes.topskip local splittopskip_code = gluecodes.splittopskip local linelist_code = nodes.listcodes.line local setvisual = function(...) setvisual = nuts.setvisual return setvisual(...) end local properties = nodes.properties.data local vspacing = builders.vspacing or { } builders.vspacing = vspacing local vspacingdata = vspacing.data or { } vspacing.data = vspacingdata local snapmethods = vspacingdata.snapmethods or { } vspacingdata.snapmethods = snapmethods storage.register("builders/vspacing/data/snapmethods", snapmethods, "builders.vspacing.data.snapmethods") do local default = { [v_maxheight] = true, [v_maxdepth] = true, [v_strut] = true, [v_hfraction] = 1, [v_dfraction] = 1, [v_bfraction] = 0.25, } local fractions = { [v_minheight] = v_hfraction, [v_maxheight] = v_hfraction, [v_mindepth] = v_dfraction, [v_maxdepth] = v_dfraction, [v_box] = v_bfraction, [v_top] = v_tlines, [v_bottom] = v_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 = todimen(detail) if detail then t[k] = detail end end end end else detail = tonumber("0" .. key) if detail then t[v_hfraction] = detail t[v_dfraction] = detail end end end if next(t) then t[v_hfraction] = t[v_hfraction] or 1 t[v_dfraction] = t[v_dfraction] or 1 return t else return default end end function vspacing.definesnapmethod(name,method) local n = #snapmethods + 1 local t = listtohash(method) snapmethods[n] = t t.name = name -- not interfaced t.specification = method -- not interfaced context(n) end end local function validvbox(parentid,list) if parentid == hlist_code then local id = getid(list) if id == par_code and startofpar(list) then list = getnext(list) if not next then return nil end end local done = nil for n, id in nextnode, list do if id == vlist_code or id == hlist_code then if done then return nil else done = n end elseif id == glue_code or id == penalty_code then -- go on else return nil -- whatever end end if done then local id = getid(done) if id == hlist_code then return validvbox(id,getlist(done)) end end return done -- only one vbox end end local function already_done(parentid,list,a_snapmethod) -- todo: done when only boxes and all snapped -- problem: any snapped vbox ends up in a line if list and parentid == hlist_code then local id = getid(list) if id == par_code and startofpar(list) then list = getnext(list) if not list then return false end end for n, id in nextnode, list do if id == hlist_code or id == vlist_code then -- local a = getattr(n,a_snapmethod) -- if not a then -- -- return true -- not snapped at all -- elseif a == 0 then -- return true -- already snapped -- end local p = getprop(n,"snapper") if p then return p end elseif id == glue_code or id == penalty_code then -- or id == kern_code then -- go on else return false -- whatever end end end return false end -- check variables.none etc local snap_hlist do local v_noheight = variables.noheight local v_nodepth = variables.nodepth local v_line = variables.line local v_halfline = variables.halfline local v_line_m = "-" .. v_line local v_halfline_m = "-" .. v_halfline local floor = math.floor local ceil = math.ceil local function fixedprofile(current) local profiling = builders.profiling return profiling and profiling.fixedprofile(current) end -- quite tricky: ceil(-something) => -0 local function ceiled(n) if n < 0 or n < 0.01 then return 0 else return ceil(n) end end local function floored(n) if n < 0 or n < 0.01 then return 0 else return floor(n) end end snap_hlist = function(where,current,method,height,depth) -- method[v_strut] is default if fixedprofile(current) then return end local list = getlist(current) local t = trace_vsnapping and { } if t then t[#t+1] = formatters["list content: %s"](listtoutf(list)) t[#t+1] = formatters["snap method: %s"](method.name) -- not interfaced t[#t+1] = formatters["specification: %s"](method.specification) -- not interfaced end local snapht, snapdp if method[v_local] then -- snapping is done immediately here snapht = texgetdimen(d_bodyfontstrutheight) snapdp = texgetdimen(d_bodyfontstrutdepth) if t then t[#t+1] = formatters["local: snapht %p snapdp %p"](snapht,snapdp) end elseif method[v_global] then snapht = texgetdimen(d_globalbodyfontstrutheight) snapdp = texgetdimen(d_globalbodyfontstrutdepth) if t then t[#t+1] = formatters["global: snapht %p snapdp %p"](snapht,snapdp) end else -- maybe autolocal -- snapping might happen later in the otr snapht = texgetdimen(d_globalbodyfontstrutheight) snapdp = texgetdimen(d_globalbodyfontstrutdepth) local lsnapht = texgetdimen(d_bodyfontstrutheight) local lsnapdp = texgetdimen(d_bodyfontstrutdepth) if snapht ~= lsnapht and snapdp ~= lsnapdp then snapht, snapdp = lsnapht, lsnapdp end if t then t[#t+1] = formatters["auto: snapht %p snapdp %p"](snapht,snapdp) end end local wd, ht, dp = getwhd(current) local h = (method[v_noheight] and 0) or height or ht local d = (method[v_nodepth] and 0) or depth or dp local hr = method[v_hfraction] or 1 local dr = method[v_dfraction] or 1 local br = method[v_bfraction] or 0 local ch = h local cd = d local tlines = method[v_tlines] or 1 local blines = method[v_blines] or 1 local done = false local plusht = snapht local plusdp = snapdp local snaphtdp = snapht + snapdp local extra = 0 if t then t[#t+1] = formatters["hlist: wd %p ht %p (used %p) dp %p (used %p)"](wd,ht,h,dp,d) t[#t+1] = formatters["fractions: hfraction %s dfraction %s bfraction %s tlines %s blines %s"](hr,dr,br,tlines,blines) end if method[v_box] then local br = 1 - br if br < 0 then br = 0 elseif br > 1 then br = 1 end local n = ceiled((h+d-br*snapht-br*snapdp)/snaphtdp) local x = n * snaphtdp - h - d plusht = h + x / 2 plusdp = d + x / 2 if t then t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_box,plusht,plusdp) end elseif method[v_max] then local n = ceiled((h+d)/snaphtdp) local x = n * snaphtdp - h - d plusht = h + x / 2 plusdp = d + x / 2 if t then t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_max,plusht,plusdp) end elseif method[v_min] then -- we catch a lone min if method.specification ~= v_min then local n = floored((h+d)/snaphtdp) local x = n * snaphtdp - h - d plusht = h + x / 2 plusdp = d + x / 2 if plusht < 0 then plusht = 0 end if plusdp < 0 then plusdp = 0 end end if t then t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_min,plusht,plusdp) end elseif method[v_none] then plusht, plusdp = 0, 0 if t then t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_none,0,0) end end -- for now, we actually need to tag a box and then check at several points if something ended up -- at the top of a page if method[v_halfline] then -- extra halfline extra = snaphtdp/2 plusht = plusht + extra plusdp = plusdp + extra if t then t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_halfline,plusht,plusdp) end end if method[v_line] then -- extra line extra = snaphtdp plusht = plusht + extra plusdp = plusdp + extra if t then t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_line,plusht,plusdp) end end if method[v_halfline_m] then -- extra halfline extra = - snaphtdp/2 plusht = plusht + extra plusdp = plusdp + extra if t then t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_halfline_m,plusht,plusdp) end end if method[v_line_m] then -- extra line extra = - snaphtdp plusht = plusht + extra plusdp = plusdp + extra if t then t[#t+1] = formatters["%s: plusht %p plusdp %p"](v_line_m,plusht,plusdp) end end if method[v_first] then local thebox = current local id = getid(thebox) if id == hlist_code then thebox = validvbox(id,getlist(thebox)) id = thebox and getid(thebox) end if thebox and id == vlist_code then local list = getlist(thebox) local lw, lh, ld for n in nexthlist, list do lw, lh, ld = getwhd(n) break end if lh then local wd, ht, dp = getwhd(thebox) if t then t[#t+1] = formatters["first line: height %p depth %p"](lh,ld) t[#t+1] = formatters["dimensions: height %p depth %p"](ht,dp) end local delta = h - lh ch, cd = lh, delta + d h, d = ch, cd local shifted = hpack_node(getlist(current)) setshift(shifted,delta) setlist(current,shifted) done = true if t then t[#t+1] = formatters["first: height %p depth %p shift %p"](ch,cd,delta) end elseif t then t[#t+1] = "first: not done, no content" end elseif t then t[#t+1] = "first: not done, no vbox" end elseif method[v_last] then local thebox = current local id = getid(thebox) if id == hlist_code then thebox = validvbox(id,getlist(thebox)) id = thebox and getid(thebox) end if thebox and id == vlist_code then local list = getlist(thebox) local lw, lh, ld for n in nexthlist, list do lw, lh, ld = getwhd(n) end if lh then local wd, ht, dp = getwhd(thebox) if t then t[#t+1] = formatters["last line: height %p depth %p" ](lh,ld) t[#t+1] = formatters["dimensions: height %p depth %p"](ht,dp) end local delta = d - ld cd, ch = ld, delta + h h, d = ch, cd local shifted = hpack_node(getlist(current)) setshift(shifted,delta) setlist(current,shifted) done = true if t then t[#t+1] = formatters["last: height %p depth %p shift %p"](ch,cd,delta) end elseif t then t[#t+1] = "last: not done, no content" end elseif t then t[#t+1] = "last: not done, no vbox" end end if method[v_minheight] then ch = floored((h-hr*snapht)/snaphtdp)*snaphtdp + plusht if t then t[#t+1] = formatters["minheight: %p"](ch) end elseif method[v_maxheight] then ch = ceiled((h-hr*snapht)/snaphtdp)*snaphtdp + plusht if t then t[#t+1] = formatters["maxheight: %p"](ch) end else ch = plusht if t then t[#t+1] = formatters["set height: %p"](ch) end end if method[v_mindepth] then cd = floored((d-dr*snapdp)/snaphtdp)*snaphtdp + plusdp if t then t[#t+1] = formatters["mindepth: %p"](cd) end elseif method[v_maxdepth] then cd = ceiled((d-dr*snapdp)/snaphtdp)*snaphtdp + plusdp if t then t[#t+1] = formatters["maxdepth: %p"](cd) end else cd = plusdp if t then t[#t+1] = formatters["set depth: %p"](cd) end end if method[v_top] then ch = ch + tlines * snaphtdp if t then t[#t+1] = formatters["top height: %p"](ch) end end if method[v_bottom] then cd = cd + blines * snaphtdp if t then t[#t+1] = formatters["bottom depth: %p"](cd) end end local offset = method[v_offset] if offset then -- we need to set the attr if t then local wd, ht, dp = getwhd(current) t[#t+1] = formatters["before offset: %p (width %p height %p depth %p)"](offset,wd,ht,dp) end local shifted = hpack_node(getlist(current)) setshift(shifted,offset) setlist(current,shifted) if t then local wd, ht, dp = getwhd(current) t[#t+1] = formatters["after offset: %p (width %p height %p depth %p)"](offset,wd,ht,dp) end setattr(shifted,a_snapmethod,0) setattr(current,a_snapmethod,0) end if not height then setheight(current,ch) if t then t[#t+1] = formatters["forced height: %p"](ch) end end if not depth then setdepth(current,cd) if t then t[#t+1] = formatters["forced depth: %p"](cd) end end local lines = (ch+cd)/snaphtdp if t then local original = (h+d)/snaphtdp local whatever = (ch+cd)/(texgetdimen(d_globalbodyfontstrutheight) + texgetdimen(d_globalbodyfontstrutdepth)) t[#t+1] = formatters["final lines : %p -> %p (%p)"](original,lines,whatever) t[#t+1] = formatters["final height: %p -> %p"](h,ch) t[#t+1] = formatters["final depth : %p -> %p"](d,cd) end -- todo: -- -- if h < 0 or d < 0 then -- h = 0 -- d = 0 -- end if t then report_snapper("trace: %s type %s\n\t%\n\tt",where,nodecodes[getid(current)],t) end if not method[v_split] then -- so extra will not be compensated at the top of a page extra = 0 end return h, d, ch, cd, lines, extra end end local categories = { [0] = "discard", "largest", "force", "penalty", "add", "disable", "nowhite", "goback", "packed", "overlay", "enable", "notopskip", } categories = allocate(table.swapped(categories,categories)) vspacing.categories = categories function vspacing.tocategories(str) local t = { } for s in gmatch(str,"[^, ]") do -- use lpeg instead local n = tonumber(s) if n then t[categories[n]] = true else t[b] = true end end return t end function vspacing.tocategory(str) -- can be optimized if type(str) == "string" then return set.tonumber(vspacing.tocategories(str)) else return set.tonumber({ [categories[str]] = true }) end end vspacingdata.map = vspacingdata.map or { } -- allocate ? vspacingdata.skip = vspacingdata.skip or { } -- allocate ? storage.register("builders/vspacing/data/map", vspacingdata.map, "builders.vspacing.data.map") storage.register("builders/vspacing/data/skip", vspacingdata.skip, "builders.vspacing.data.skip") local setspecification, getspecification -- 1 statepool : 2 : more overhead : a bit slower than properties -- 2 attributes : 1 : more overhead : feels faster than properties -- 3 properties : 3 : more natural : feels slower than attributes -- 4 data : 1 : more native : is little faster than attributes (limited penalty) -- testfile: t:/bugs/bottomfloats-001.tex local method = 1 -- better tracing -- local method = 2 -- local method = 3 -- local method = 4 -- todo: not true but only visual a_visual,tex.getattribute(a_visual) if method == 1 then local registervalue = attributes.registervalue local getvalue = attributes.getvalue local values = attributes.values setspecification = function(n,category,penalty,order) local detail = { category, penalty, order or 1 } local value = registervalue(a_skipcategory,detail) setattr(n,a_skipcategory,value) end getspecification = function(n) local value = getattr(n,a_skipcategory) if value then local detail = getvalue(a_skipcategory,value) -- local detail = attributes.values[a_skipcategory][value] -- local detail = values[a_skipcategory][value] if detail then return detail[1], detail[2], detail[3] end end return false, false, 1 end elseif method == 2 then -- quite okay but more memory due to attributes (not many) local setattrs = nuts.setattrs local getattrs = nuts.getattrs setspecification = function(n,category,penalty,order) setattrs(n,false,a_skipcategory,category or nil,a_skippenalty,penalty or nil,a_skiporder,order or 1) end getspecification = function(n) local c, p, o = getattrs(n,a_skipcategory,a_skippenalty,a_skiporder) return c or false, p or false, o or 1 end elseif method == 3 then -- more natural as we stay in lua setspecification = function(n,category,penalty,order) -- we know that there are no properties properties[n] = { [a_skipcategory] = category, [a_skippenalty] = penalty, [a_skiporder] = order or 1, } end getspecification = function(n) local p = properties[n] if p then return p[a_skipcategory], p[a_skippenalty], p[a_skiporder] end end elseif method == 4 then -- quite efficient but needs testing because we limit values local getdata = nuts.getdata local setdata = nuts.setdata setspecification = function(n,category,penalty,order) if not category or category > 0xF then category = 0xF end if not order or order > 0xFF then order = 0xFF end if not penalty or penalty > 0x7FFFF then penalty = 0x7FFFF elseif penalty < -0x7FFFF then penalty = -0x7FFFF end -- we need overflow checks setdata(n, (penalty << 12) + (order << 4) + category) end getspecification = function(n) local data = getdata(n) if data and data ~= 0 then local category = data & 0x0F local order = (data >> 4) & 0xFF local penalty = data >> 12 if category == 0xF then category = nil end if order == 0xFF then order = nil end if penalty == 0x7FFFF then penalty = nil end return category, penalty, order else return nil, nil, nil end end end do local P, C, R, S, Cc, Cs = lpeg.P, lpeg.C, lpeg.R, lpeg.S, lpeg.Cc, lpeg.Cs vspacing.fixed = false local map = vspacingdata.map local skip = vspacingdata.skip local sign = S("+-")^0 local multiplier = C(sign * R("09")^1) * P("*") local singlefier = Cs(sign * Cc(1)) local separator = S(", ") local category = P(":") * C((1-separator)^1) local keyword = C((1-category-separator)^1) local splitter = (multiplier + Cc(1)) * keyword * (category + Cc(false)) local k_fixed = variables.fixed local k_flexible = variables.flexible local k_limit = variables.limit local k_category = "category" local k_penalty = "penalty" local k_order = "order" local setoptions = nuts.setoptions 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 local expandmacro = token.expandmacro -- todo -- local runlocal = tex.runlocal -- local setmacro = tokens.setters.macro -- local settoks = tex.settoks local toscaled = tex.toscaled local b_done = false local b_packed = false local b_amount = 0 local b_stretch = 0 local b_shrink = 0 local b_category = false local b_penalty = false local b_order = false local b_fixed = false local b_grid = false local b_limit = false local pattern = nil local packed = categories.packed local gluefactor = .25 local ctx_ignoreparskip = context.core.ignoreparskip local function before() b_amount = 0 b_stretch = 0 b_shrink = 0 b_category = 1 b_penalty = false b_order = false b_fixed = b_grid b_limit = false end local function after() if b_fixed then b_stretch = 0 b_shrink = 0 else b_stretch = gluefactor * b_amount b_shrink = gluefactor * b_amount end end -- use a cache for predefined ones local function inject() local n = new_glue(b_amount,b_stretch,b_shrink) setspecification(n,b_category,b_penalty,b_order or 1) setvisual(n) write_node(n) if b_limit then setoptions(n,tex.glueoptioncodes.limit) end -- todo: inject via value end local function flush() after() if b_done then inject() b_done = false end before() end -- local cmd = token.create("vspacingfromtempstring") -- local cmd = token.create("vspacingpredefinedvalue") -- not yet known ----- s_predefined = "s_spac_vspacing_predefined" local s_predefined = tex.isskip("s_spac_vspacing_predefined") local function handler(multiplier, keyword, detail) if not keyword then report_vspacing("unknown directive %a",s) else local mk = map[keyword] if mk then lpegmatch(pattern,mk) elseif keyword == k_fixed then b_fixed = true elseif keyword == k_flexible then b_flexible = false elseif keyword == k_category then local category = tonumber(detail) if category == packed then b_packed = true elseif category then b_category = category b_done = true flush() end elseif keyword == k_order and detail then local order = tonumber(detail) if order then b_order = order end elseif keyword == k_penalty and detail then local penalty = tonumber(detail) if penalty then flush() b_done = true b_category = 3 b_penalty = penalty flush() end elseif keyword == k_limit then b_limit = true else local amount, stretch, shrink multiplier = tonumber(multiplier) or 1 local sk = skip[keyword] if sk then -- multiplier, keyword -- best, for now, todo: runlocal with arguments expandmacro("vspacingpredefinedvalue",true,keyword) -- expandmacro(cmd,true,keyword) -- setmacro("tempstring",keyword) -- runlocal(cmd) -- nicest -- runlocal(cache[keyword]) -- fast -- settoks("scratchtoks",keyword) -- runlocal("vspacingfromscratchtoks") -- middleground -- setmacro("tempstring",keyword) -- runlocal(ctx_vspacingfromtempstring) -- amount, stretch, shrink = texgetglue(s_predefined) if not stretch then stretch = 0 end if not shrink then shrink = 0 end if stretch == 0 and shrink == 0 then stretch = gluefactor * amount -- always unless grid shrink = stretch -- always unless grid end else -- no check, todo: parse plus and minus amount = toscaled(keyword) stretch = gluefactor * amount -- always unless grid shrink = stretch -- always unless grid end -- we look at fixed later b_amount = b_amount + multiplier * amount b_stretch = b_stretch + multiplier * stretch b_shrink = b_shrink + multiplier * shrink b_done = true end end end -- alternatively we can make a table and have a keyword -> split cache but this is probably -- not really a bottleneck local splitter = ((multiplier + singlefier) * keyword * (category + Cc(false))) / handler pattern = (splitter + separator^1)^0 function vspacing.inject(grid,str) if trace_vspacing then -- ctx_pushlogger(report_vspacing) end b_done = false b_packed = false b_grid = grid == true or grid == 1 before() lpegmatch(pattern,str) after() if b_done then inject() end if b_packed then ctx_ignoreparskip() end if trace_vspacing then -- ctx_poplogger() end end function vspacing.injectpenalty(penalty) local n = new_glue() -- setattrs(n,false,a_skipcategory,categories.penalty,a_skippenalty,penalty,a_skiporder,1) setspecification(n,categories.penalty,penalty,1) setvisual(k) write_node(n) end function vspacing.injectskip(amount) local n = new_glue(amount) -- setattrs(n,false,a_skipcategory,categories.largest,a_skippenalty,false,a_skiporder,1) setspecification(n,categories.largest,false,1) setvisual(k) write_node(n) end function vspacing.injectdisable(amount) local n = new_glue() -- setattrs(n,false,a_skipcategory,categories.disable,a_skippenalty,false,a_skiporder,1) setspecification(n,categories.disable,false,1) setvisual(k) write_node(n) end end -- implementation -- alignment box begin_of_par vmodepar hmodepar insert penalty before_display after_display function vspacing.snapbox(n,how) local sv = snapmethods[how] if sv then local box = getbox(n) local list = getlist(box) if list then local s = getattr(list,a_snapmethod) if s == 0 then if trace_vsnapping then -- report_snapper("box list not snapped, already done") end else local wd, ht, dp = getwhd(box) if false then -- todo: already_done -- assume that the box is already snapped if trace_vsnapping then report_snapper("box list already snapped at (%p,%p): %s", ht,dp,listtoutf(list)) end else local h, d, ch, cd, lines, extra = snap_hlist("box",box,sv,ht,dp) setprop(box,"snapper",{ ht = h, dp = d, ch = ch, cd = cd, extra = extra, current = current, }) setwhd(box,wd,ch,cd) if trace_vsnapping then report_snapper("box list snapped from (%p,%p) to (%p,%p) using method %a (%s) for %a (%s lines): %s", h,d,ch,cd,sv.name,sv.specification,"direct",lines,listtoutf(list)) end setattr(box,a_snapmethod,0) -- setattr(list,a_snapmethod,0) -- yes or no end end end end end -- I need to figure out how to deal with the prevdepth that crosses pages. In fact, -- prevdepth is often quite interfering (even over a next paragraph) so I need to -- figure out a trick. Maybe use something other than a rule. If we visualize we'll -- see the baselineskip in action: -- -- \blank[force,5*big] { \baselineskip1cm xxxxxxxxx \par } \page -- \blank[force,5*big] { \baselineskip1cm xxxxxxxxx \par } \page -- \blank[force,5*big] { \baselineskip5cm xxxxxxxxx \par } \page -- We can register and copy the rule instead. do local insertnodeafter = nuts.insertafter local insertnodebefore = nuts.insertbefore local abovedisplayskip_code = gluecodes.abovedisplayskip local belowdisplayskip_code = gluecodes.belowdisplayskip local abovedisplayshortskip_code = gluecodes.abovedisplayshortskip local belowdisplayshortskip_code = gluecodes.belowdisplayshortskip local w, h, d = 0, 0, 0 ----- w, h, d = 100*65536, 65536, 65536 local trace_list = { } local tracing_info = { } local before = "" local after = "" local function nodes_to_string(head) local current = head local t = { } while current do local id = getid(current) local ty = nodecodes[id] if id == penalty_code then t[#t+1] = formatters["%s:%s"](ty,getpenalty(current)) elseif id == glue_code then t[#t+1] = formatters["%s:%s:%p"](ty,gluecodes[getsubtype(current)],getwidth(current)) elseif id == kern_code then t[#t+1] = formatters["%s:%p"](ty,getkern(current)) else t[#t+1] = ty end current = getnext(current) 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", formatters["%s | %p | category %s | order %s | penalty %s | subtype %s"](str, getwidth(data), sc or "-", so or "-", sp or "-", gluecodes[getsubtype(data)]) } tracing_info = true end local function trace_natural(str,data) trace_list[#trace_list+1] = { "skip", formatters["%s | %p"](str, getwidth(data)) } tracing_info = true end local function trace_info(message, where, what) trace_list[#trace_list+1] = { "info", formatters["%s: %s/%s"](message,where,what) } end local function trace_node(what) local nt = nodecodes[getid(what)] local tl = trace_list[#trace_list] if tl and tl[1] == "node" then trace_list[#trace_list] = { "node", formatters["%s + %s"](tl[2],nt) } else trace_list[#trace_list+1] = { "node", nt } end 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 report_collapser(text) else report_collapser(" %s: %s",tag,text) end end report_collapser("before: %s",before) report_collapser("after : %s",after) end end local function trace_done(str,data) if getid(data) == penalty_code then trace_list[#trace_list+1] = { "penalty", formatters["%s | %s"](str,getpenalty(data)) } else trace_list[#trace_list+1] = { "glue", formatters["%s | %p"](str,getwidth(data)) } end tracing_info = true end local function forced_skip(head,current,width,where,trace) -- looks old ... we have other tricks now if head == current then if getsubtype(head) == baselineskip_code then width = width - getwidth(head) end end if width == 0 then -- do nothing else local b = new_rule(w,h,d) local k = new_kern(width) local a = new_rule(w,h,d) setvisual(k) if where == "after" then head, current = insertnodeafter(head,current,b) head, current = insertnodeafter(head,current,k) head, current = insertnodeafter(head,current,a) else local c = current head, current = insertnodebefore(head,current,b) head, current = insertnodebefore(head,current,k) head, current = insertnodebefore(head,current,a) current = c end end if trace then report_vspacing("inserting forced skip of %p",width) end return head, current end -- penalty only works well when before skip local discard = categories.discard local largest = categories.largest local force = categories.force local penalty = categories.penalty local add = categories.add local disable = categories.disable local nowhite = categories.nowhite local goback = categories.goback local packed = categories.packed local overlay = categories.overlay local enable = categories.enable local notopskip = categories.notopskip -- [whatsits][hlist][glue][glue][penalty] local special_penalty_min = 32250 local special_penalty_max = 35000 local special_penalty_xxx = 0 -- this is rather messy and complex: we want to make sure that successive -- header don't break but also make sure that we have at least a decent -- break when we have succesive ones (often when testing) -- todo: mark headers as such so that we can recognize them local specialmethods = { } local specialmethod = 1 specialmethods[1] = function(pagehead,pagetail,start,penalty) -- if not pagehead or penalty < special_penalty_min or penalty > special_penalty_max then return end local current = pagetail -- -- nodes.showsimplelist(pagehead,0) -- if trace_specials then report_specials("checking penalty %a",penalty) end while current do local id = getid(current) if id == penalty_code then local p = properties[current] if p then local p = p.special_penalty if not p then if trace_specials then report_specials(" regular penalty, continue") end elseif p == penalty then if trace_specials then report_specials(" context penalty %a, same level, overloading",p) end return special_penalty_xxx elseif p > special_penalty_min and p < special_penalty_max then if penalty < p then if trace_specials then report_specials(" context penalty %a, lower level, overloading",p) end return special_penalty_xxx else if trace_specials then report_specials(" context penalty %a, higher level, quitting",p) end return end elseif trace_specials then report_specials(" context penalty %a, higher level, continue",p) end else local p = getpenalty(current) if p < 10000 then -- assume some other mechanism kicks in so we seem to have content if trace_specials then report_specials(" regular penalty %a, quitting",p) end break else if trace_specials then report_specials(" regular penalty %a, continue",p) end end end end current = getprev(current) end -- none found, so no reson to be special if trace_specials then if pagetail then report_specials(" context penalty, discarding, nothing special") else report_specials(" context penalty, discarding, nothing preceding") end end return special_penalty_xxx end -- This will be replaced after 0.80+ when we have a more robust look-back and -- can look at the bigger picture. -- todo: look back and when a special is there before a list is seen penalty keep ut -- we now look back a lot, way too often -- userskip -- lineskip -- baselineskip -- parskip -- abovedisplayskip -- belowdisplayskip -- abovedisplayshortskip -- belowdisplayshortskip -- topskip -- splittopskip -- we could inject a vadjust to force a recalculation .. a mess -- -- So, the next is far from robust and okay but for the moment this overlaying -- has to do. Always test this with the examples in spac-ver.mkvi! local function snap_topskip(current,method) local w = getwidth(current) setwidth(current,0) return w, 0 end local function check_experimental_overlay(head,current) local p = nil local c = current local n = nil local function overlay(p,n,mvl) local p_wd, p_ht, p_dp = getwhd(p) local n_wd, n_ht, n_dp = getwhd(n) local skips = 0 -- -- We deal with this at the tex end .. we don't see spacing .. enabling this code -- is probably harmless but then we need to test it. -- -- we could calculate this before we call -- -- problem: prev list and next list can be unconnected -- local c = getnext(p) local l = c while c and c ~= n do local id = getid(c) if id == glue_code then local w = getwidth(c) skips = skips + w setglue(c,w) -- nil stretch elseif id == kern_code then skips = skips + getkern(c) end l = c c = getnext(c) end local c = getprev(n) while c and c ~= n and c ~= l do local id = getid(c) if id == glue_code then local w = getwidth(c) skips = skips + w setglue(c,w) -- nil stretch elseif id == kern_code then skips = skips + getkern(c) end c = getprev(c) end -- local delta = n_ht + skips + p_dp texsetdimen("global",d_spac_overlay,-delta) -- for tracing -- we should adapt pagetotal ! (need a hook for that) .. now we have the wrong pagebreak local k = new_kern(-delta) setvisual(k) head = insertnodebefore(head,n,k) if n_ht > p_ht then local k = new_kern(n_ht-p_ht) setvisual(k) head = insertnodebefore(head,p,k) end if trace_vspacing then 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) end return remove_node(head,current,true) end -- goto next line while c do local id = getid(c) if id == glue_code or id == penalty_code or id == kern_code then -- skip (actually, remove) c = getnext(c) elseif id == hlist_code then n = c break else break end end if n then -- we have a next line, goto prev line c = current while c do local id = getid(c) if id == glue_code or id == penalty_code then -- kern ? c = getprev(c) elseif id == hlist_code then p = c break else break end end if not p then if a_snapmethod == a_snapvbox then -- quit, we're not on the mvl else -- inefficient when we're at the end of a page local c = tonut(texlists.pagehead) while c and c ~= n do local id = getid(c) if id == hlist_code then p = c end c = getnext(c) end if p and p ~= n then return overlay(p,n,true) end end elseif p ~= n then return overlay(p,n,false) end end -- in fact, we could try again later ... so then no remove (a few tries) return remove_node(head,current,true) end -- where -> scope -- what -> where (original context) local checkslide = false directives.register("vspacing.checkslide", function(v) if v then checkslide = function(head,where,what) nuts.checkslide(head,where .. " : " .. what) end else checkslide = false end end) local function collapser(head,where,what,trace,snap,a_snapmethod) -- maybe also pass tail if trace then reset_tracing(head) end local current = head local oldhead = head local glue_order = 0 local glue_data local force_glue = false local penalty_order = 0 local penalty_data local natural_penalty local special_penalty local parskip local ignore_parskip = false local ignore_following = false local ignore_whitespace = false local keep_together = false local lastsnap local pagehead local pagetail -- -- todo: keep_together: between headers -- todo: make this nicer in the engine -- local function getpagelist() if not pagehead then pagehead = texlists.pagehead -- pagehead, pagetail = tex.getlist("pagehead") if pagehead then pagehead = tonut(pagehead) pagetail = find_node_tail(pagehead) -- no texlists.page_tail yet-- no texlists.page_tail yet end end end -- -- local function getpagelist() -- if not pagehead then -- pagehead, pagetail = texgetlist("pagehead") -- end -- end -- local function compensate(n) local g = 0 while n and getid(n) == glue_code do g = g + getwidth(n) n = getnext(n) end if n then local p = getprop(n,"snapper") if p then local extra = p.extra if extra and extra < 0 then -- hm, extra can be unset ... needs checking local h = p.ch -- getheight(n) -- maybe an extra check -- if h - extra < g then setheight(n,h-2*extra) p.extra = 0 if trace_vsnapping then report_snapper("removed extra space at top: %p",extra) end -- end end end return n end end -- local function removetopsnap() getpagelist() if pagehead then local n = pagehead and compensate(pagehead) if n and n ~= pagetail then local p = getprop(pagetail,"snapper") if p then local e = p.extra if e and e < 0 then local t = texget("pagetotal") if t > 0 then local g = texget("pagegoal") -- 1073741823 is signal local d = g - t if d < -e then local penalty = new_penalty(1000000) setvisual(penalty) setlink(penalty,head) head = penalty report_snapper("force pagebreak due to extra space at bottom: %p",e) end end end end end elseif head then compensate(head) end end -- local function getavailable() getpagelist() if pagehead then local t = texget("pagetotal") if t > 0 then local g = texget("pagegoal") return g - t end end return false end -- local function flush(why) if penalty_data then local p = new_penalty(penalty_data) setvisual(p) if trace then trace_done("flushed due to " .. why,p) end if penalty_data >= 10000 then -- or whatever threshold? local prev = getprev(current) if getid(prev) == glue_code then -- maybe go back more, or maybe even push back before any glue -- tricky case: spacing/grid-007.tex: glue penalty glue head = insertnodebefore(head,prev,p) else head = insertnodebefore(head,current,p) end else head = insertnodebefore(head,current,p) end -- if penalty_data > special_penalty_min and penalty_data < special_penalty_max then local props = properties[p] if props then props.special_penalty = special_penalty or penalty_data else properties[p] = { special_penalty = special_penalty or penalty_data } end -- end end if glue_data then if force_glue then if trace then trace_done("flushed due to forced " .. why,glue_data) end head = forced_skip(head,current,getwidth(glue_data,width),"before",trace) flushnode(glue_data) else local width, stretch, shrink = getglue(glue_data) if width ~= 0 then if trace then trace_done("flushed due to non zero " .. why,glue_data) end head = insertnodebefore(head,current,glue_data) elseif stretch ~= 0 or shrink ~= 0 then if trace then trace_done("flushed due to stretch/shrink in" .. why,glue_data) end head = insertnodebefore(head,current,glue_data) else -- report_vspacing("needs checking (%s): %p",gluecodes[getsubtype(glue_data)],w) flushnode(glue_data) end 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 report_snapper("global ht/dp = %p/%p, local ht/dp = %p/%p", texgetdimen(d_globalbodyfontstrutheight), texgetdimen(d_globalbodyfontstrutdepth), texgetdimen(d_bodyfontstrutheight), texgetdimen(d_bodyfontstrutdepth) ) end if trace then trace_info("start analyzing",where,what) end if snap and where == "page" then removetopsnap() end if checkslide then checkslide(head,where,what) end while current do local id = getid(current) if id == hlist_code or id == vlist_code then -- needs checking, why so many calls if snap then lastsnap = nil local list = getlist(current) local s = getattr(current,a_snapmethod) if not s then -- if trace_vsnapping then -- report_snapper("mvl list not snapped") -- end elseif s == 0 then if trace_vsnapping then report_snapper("mvl %a not snapped, already done: %s",nodecodes[id],listtoutf(list)) end else local sv = snapmethods[s] if sv then -- check if already snapped local done = list and already_done(id,list,a_snapmethod) if done then -- assume that the box is already snapped if trace_vsnapping then local w, h, d = getwhd(current) report_snapper("mvl list already snapped at (%p,%p): %s",h,d,listtoutf(list)) end else local h, d, ch, cd, lines, extra = snap_hlist("mvl",current,sv,false,false) lastsnap = { ht = h, dp = d, ch = ch, cd = cd, extra = extra, current = current, } setprop(current,"snapper",lastsnap) if trace_vsnapping then report_snapper("mvl %a snapped from (%p,%p) to (%p,%p) using method %a (%s) for %a (%s lines): %s", nodecodes[id],h,d,ch,cd,sv.name,sv.specification,where,lines,listtoutf(list)) end end elseif trace_vsnapping then report_snapper("mvl %a not snapped due to unknown snap specification: %s",nodecodes[id],listtoutf(list)) end setattr(current,a_snapmethod,0) end else -- end -- tex.prevdepth = 0 flush("list") current = getnext(current) elseif id == penalty_code then -- natural_penalty = getpenalty(current) -- if trace then -- trace_done("removed penalty",current) -- end -- head, current = remove_node(head,current,true) if trace then trace_done("kept penalty",current) end current = getnext(current) elseif id == kern_code then if snap and trace_vsnapping and getkern(current) ~= 0 then report_snapper("kern of %p kept",getkern(current)) end flush("kern") current = getnext(current) elseif id == glue_code then local subtype = getsubtype(current) if subtype == userskip_code then -- local sc, so, sp = getattrs(current,a_skipcategory,a_skiporder,a_skippenalty) local sc, sp, so = getspecification(current) if not so then so = 1 -- the others have no default value end if sp and sc == penalty then if where == "page" then getpagelist() local p = specialmethods[specialmethod](pagehead,pagetail,current,sp) if p then -- todo: other tracer -- -- if trace then -- trace_skip("previous special penalty %a is changed to %a using method %a",sp,p,specialmethod) -- end special_penalty = sp sp = p end end 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 = insertnodebefore(head,current,glue_data) if trace then trace_natural("natural",current) end current = getnext(current) glue_data = nil else -- not look back across head -- todo: prev can be whatsit (latelua) local previous = getprev(current) if previous and getid(previous) == glue_code and getsubtype(previous) == userskip_code then local pwidth, pstretch, pshrink, pstretch_order, pshrink_order = getglue(previous) local cwidth, cstretch, cshrink, cstretch_order, cshrink_order = getglue(current) if pstretch_order == 0 and pshrink_order == 0 and cstretch_order == 0 and cshrink_order == 0 then setglue(previous,pwidth + cwidth, pstretch + cstretch, pshrink + cshrink) if trace then trace_natural("removed",current) end head, current = remove_node(head,current,true) if trace then trace_natural("collapsed",previous) end else if trace then trace_natural("filler",current) end current = getnext(current) end else if trace then trace_natural("natural (no prev)",current) end current = getnext(current) end end glue_order = 0 elseif sc == disable or sc == enable then local next = getnext(current) if next then ignore_following = sc == disable if trace then trace_skip(sc == disable and "disable" or "enable",sc,so,sp,current) end head, current = remove_node(head,current,true) else current = next end elseif sc == packed then if trace then trace_skip("packed",sc,so,sp,current) end -- can't happen ! head, current = remove_node(head,current,true) elseif sc == nowhite then local next = getnext(current) if next then ignore_whitespace = true head, current = remove_node(head,current,true) else current = next end elseif sc == discard then if trace then trace_skip("discard",sc,so,sp,current) end head, current = remove_node(head,current,true) elseif sc == overlay then -- todo (overlay following line over previous if trace then trace_skip("overlay",sc,so,sp,current) end -- beware: head can actually be after the affected nodes as -- we look back ... some day head will the real head head, current = check_experimental_overlay(head,current,a_snapmethod) 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 flushnode(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 cw = getwidth(current) local gw = getwidth(glue_data) if cw > gw then if trace then trace_skip("largest",sc,so,sp,current) end flushnode(glue_data) 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 flushnode(glue_data) 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 flushnode(glue_data) head, current, glue_data = remove_node(head,current) elseif sc == penalty then if trace then trace_skip("penalty",sc,so,sp,current) end flushnode(glue_data) 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 cwidth, cstretch, cshrink = getglue(current) local gwidth, gstretch, gshrink = getglue(glue_data) setglue(glue_data,gwidth + cwidth, gstretch + cstretch,gshrink + cshrink) -- 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 == lineskip_code then if snap then local s = getattr(current,a_snapmethod) if s and s ~= 0 then setattr(current,a_snapmethod,0) setwidth(current,0) if trace_vsnapping then report_snapper("lineskip set to zero") 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 = getnext(current) elseif subtype == baselineskip_code then if snap then local s = getattr(current,a_snapmethod) if s and s ~= 0 then setattr(current,a_snapmethod,0) setwidth(current,0) if trace_vsnapping then report_snapper("baselineskip set to zero") 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 = getnext(current) elseif subtype == parskip_code 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 w = getwidth(current) if w ~= 0 and w > getwidth(glue_data) then flushnode(glue_data) glue_data = current if trace then trace_natural("taking parskip",current) end head, current = remove_node(head,current) else if trace then trace_natural("removed parskip",current) end head, current = remove_node(head,current,true) end else if trace then trace_natural("honored parskip",current) end head, current, glue_data = remove_node(head,current) end elseif subtype == topskip_code or subtype == splittopskip_code then local next = getnext(current) -- if next and getattr(next,a_skipcategory) == notopskip then if next and getspecification(next) == notopskip then setglue(current) -- zero end if snap then local s = getattr(current,a_snapmethod) if s and s ~= 0 then setattr(current,a_snapmethod,0) local sv = snapmethods[s] local w, cw = snap_topskip(current,sv) if trace_vsnapping then report_snapper("topskip snapped from %p to %p for %a",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 = getnext(current) elseif subtype == abovedisplayskip_code and remove_math_skips then -- if trace then trace_skip("above display skip (normal)",sc,so,sp,current) end flush("above display skip (normal)") current = getnext(current) -- elseif subtype == belowdisplayskip_code and remove_math_skips then -- if trace then trace_skip("below display skip (normal)",sc,so,sp,current) end flush("below display skip (normal)") current = getnext(current) -- elseif subtype == abovedisplayshortskip_code and remove_math_skips then -- if trace then trace_skip("above display skip (short)",sc,so,sp,current) end flush("above display skip (short)") current = getnext(current) -- elseif subtype == belowdisplayshortskip_code and remove_math_skips then -- if trace then trace_skip("below display skip (short)",sc,so,sp,current) end flush("below display skip (short)") current = getnext(current) -- else -- other glue if snap and trace_vsnapping then local w = getwidth(current) if w ~= 0 then report_snapper("glue %p of type %a kept",w,gluecodes[subtype]) end end if trace then trace_skip(formatters["glue of type %a"](subtype),sc,so,sp,current) end flush("some glue") current = getnext(current) end else flush(trace and formatters["node with id %a"](id) or "other node") current = getnext(current) 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 = new_penalty(penalty_data) setvisual(p) if trace then trace_done("result",p) end setlink(tail,p) -- if penalty_data > special_penalty_min and penalty_data < special_penalty_max then local props = properties[p] if props then props.special_penalty = special_penalty or penalty_data else properties[p] = { special_penalty = special_penalty or penalty_data } end -- end 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,getwidth(glue_data),"after",trace) flushnode(glue_data) glue_data = nil elseif tail then setlink(tail,glue_data) setnext(glue_data) else head = glue_data end -- texsetnest("top","prevdepth",0) texnest[texnest.ptr].prevdepth = 0 -- appending to the list bypasses tex's prevdepth handler 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 %a to %a",nodecodes[getid(oldhead)],nodecodes[getid(head)]) end end return head end -- local function collapser(head,...) -- local current = head -- while current do -- local id = getid(current) -- if id == glue_code then -- if getsubtype(current) == userskip_code then -- local glue_data -- head, current, glue_data = remove_node(head,current) -- head, current = insertnodebefore(head,current,glue_data) -- end -- end -- current = getnext(current) -- end -- return head -- end local stackhead, stacktail, stackhack = nil, nil, false local function report(message,where,lst) if lst and where then report_vspacing(message,where,count_nodes(lst,true),nodeidstostring(lst)) else report_vspacing(message,count_nodes(lst,true),nodeidstostring(lst)) end end -- This really need a rework. although, it has now been stable for 15 years so why -- mess with it. We get sequences like these where penalties come from the par -- builder: -- -- glue -- glue hlist -- glue hlist penalty glue hlist penalty glue hlist -- -- and in context we never use penalties other than spacing related local forceflush = false function vspacing.pagehandler(newhead,where) if newhead then local newtail = find_node_tail(newhead) -- best pass that tail, known anyway local flush = false stackhack = true -- todo: only when grid snapping once enabled -- local t = { } for n, id in nextnode, newhead do t[#t+1] = nodecodes[id] end report_vspacing("page handler: % t",t) for n, id, subtype in nextnode, newhead do if id ~= glue_code then flush = true elseif subtype == userskip_code then -- local sc = getattr(n,a_skipcategory) local sc = getspecification(n) if sc then stackhack = true else flush = true end elseif subtype == parskip_code then -- if where == new_graf then ... end if texgetcount(c_spac_vspacing_ignore_parskip) > 0 then setglue(n) -- maybe removenode end end end texsetcount(c_spac_vspacing_ignore_parskip,0) if forceflush then forceflush = false flush = true end if flush then if stackhead then if trace_collect_vspacing then report("%s > appending %s nodes to stack (final): %s",where,newhead) end setlink(stacktail,newhead) newhead = stackhead stackhead = nil stacktail = nil end if stackhack then stackhack = false if trace_collect_vspacing then report("%s > processing %s nodes: %s",where,newhead) end newhead = collapser(newhead,"page",where,trace_page_vspacing,true,a_snapmethod) else if trace_collect_vspacing then report("%s > flushing %s nodes: %s",where,newhead) end end return newhead else if stackhead then if trace_collect_vspacing then report("%s > appending %s nodes to stack (intermediate): %s",where,newhead) end setlink(stacktail,newhead) else if trace_collect_vspacing then report("%s > storing %s nodes in stack (initial): %s",where,newhead) end stackhead = newhead end stacktail = newtail end end return nil end function vspacing.pageoverflow() local h = 0 if stackhead then for n, id in nextnode, stackhead do if id == glue_code then h = h + getwidth(n) elseif id == kern_code then h = h + getkern(n) end end end return h end function vspacing.forcepageflush() forceflush = true end local ignored = table.tohash { "splitkeep", "splitoff", -- "insert", } function vspacing.vboxhandler(head,where) if head and not ignored[where] and getnext(head) then -- if getid(head) == glue_code and getsubtype(head) == topskip_code then head = collapser(head,"vbox",where,trace_vbox_vspacing,true,a_snapvbox) -- todo: local snapper end return head end function vspacing.collapsevbox(n,aslist) -- for boxes but using global a_snapmethod local box = getbox(n) if box then local list = getlist(box) if list then list = collapser(list,"snapper","vbox",trace_vbox_vspacing,true,a_snapmethod) if aslist then setlist(box,list) -- beware, dimensions of box are wrong now else setlist(box,vpack_node(list)) end end end end end -- This one is needed to prevent bleeding of prevdepth to the next page -- which doesn't work well with forced skips. I'm not that sure if the -- following is a good way out. do local outer = texnest[0] local enabled = true local trace = false local report = logs.reporter("vspacing") trackers.register("vspacing.synchronizepage",function(v) trace = v end) directives.register("vspacing.synchronizepage",function(v) enabled = v end) local function ignoredepth() return texgetdimen("ignoredepthcriterion") -- -65536000 end -- A previous version analyzed the number of lines moved to the next page in -- synchronizepage because prevgraf is unreliable in that case. However, we cannot -- tweak that parameter because it is also used in postlinebreak and hangafter, so -- there is a danger for interference. Therefore we now do it dynamically. -- We can also support other lists but there prevgraf probably is ok. function vspacing.getnofpreviouslines(head) if enabled then if not thead then head = texlists.pagehead end local noflines = 0 if head then local tail = find_node_tail(tonut(head)) while tail do local id = getid(tail) if id == hlist_code then if getsubtype(tail) == linelist_code then noflines = noflines + 1 else break end elseif id == vlist_code then break elseif id == glue_code then local subtype = getsubtype(tail) if subtype == baselineskip_code or subtype == lineskip_code then -- we're ok elseif subtype == parskip_code then if getwidth(tail) > 0 then break else -- we assume we're ok end end elseif id == penalty_code then -- we're probably ok elseif id == rule_code or id == kern_code then break else -- ins, mark, boundary, whatsit end tail = getprev(tail) end end return noflines end end interfaces.implement { name = "getnofpreviouslines", public = true, actions = vspacing.getnofpreviouslines, } function vspacing.synchronizepage() if enabled then if trace then local newdepth = outer.prevdepth local olddepth = newdepth if not texlists.pagehead then newdepth = ignoredepth() texset("prevdepth",newdepth) outer.prevdepth = newdepth end report("page %i, prevdepth %p => %p",texgetcount("realpageno"),olddepth,newdepth) -- report("list %s",nodes.idsandsubtypes(head)) else if not texlists.pagehead then local newdepth = ignoredepth() texset("prevdepth",newdepth) outer.prevdepth = newdepth end end end end local trace = false local abs = math.abs -- local last = nil local vmode_code = tex.modelevels.vertical local temp_code = nodecodes.temp local texgetnest = tex.getnest local texgetlist = tex.getlist local getnodetail = nodes.tail -- trackers.register("vspacing.forcestrutdepth",function(v) trace = v end) -- abs : negative is inner function vspacing.checkstrutdepth(depth) local nest = texgetnest() if abs(nest.mode) == vmode_code and nest.head then local tail = nest.tail local id = tail.id if id == hlist_code then if tail.depth < depth then tail.depth = depth end nest.prevdepth = depth elseif id == temp_code and texgetnest("ptr") == 0 then local head = texgetlist("pagehead") if head then tail = getnodetail(head) if tail and tail.id == hlist_code then if tail.depth < depth then tail.depth = depth end nest.prevdepth = depth -- only works in lmtx texset("pagedepth",depth) end end end end end interfaces.implement { name = "checkstrutdepth", arguments = "dimension", actions = vspacing.checkstrutdepth, } -- function vspacing.forcestrutdepth(n,depth,trace_mode,plus) -- local box = texgetbox(n) -- if box then -- box = tonut(box) -- local head = getlist(box) -- if head then -- local tail = find_node_tail(head) -- if tail then -- if getid(tail) == hlist_code then -- local dp = getdepth(tail) -- if dp < depth then -- setdepth(tail,depth) -- outer.prevdepth = depth -- if trace or trace_mode > 0 then -- nuts.setvisual(tail,"depth") -- end -- end -- end -- -- last = nil -- -- if plus then -- -- -- penalty / skip ... -- -- local height = 0 -- -- local sofar = 0 -- -- local same = false -- -- local seen = false -- -- local list = { } -- -- last = nil -- -- while tail do -- -- local id = getid(tail) -- -- if id == hlist_code or id == vlist_code then -- -- local w, h, d = getwhd(tail) -- -- height = height + h + d + sofar -- -- sofar = 0 -- -- last = tail -- -- elseif id == kern_code then -- -- sofar = sofar + getkern(tail) -- -- elseif id == glue_code then -- -- if seen then -- -- sofar = sofar + getwidth(tail) -- -- seen = false -- -- else -- -- break -- -- end -- -- elseif id == penalty_code then -- -- local p = getpenalty(tail) -- -- if p >= 10000 then -- -- same = true -- -- seen = true -- -- else -- -- break -- -- end -- -- else -- -- break -- -- end -- -- tail = getprev(tail) -- -- end -- -- texsetdimen("global","d_spac_prevcontent",same and height or 0) -- -- end -- end -- end -- end -- end local hlist_code = nodes.nodecodes.hlist local insert_code = nodes.nodecodes.insert local mark_code = nodes.nodecodes.mark local line_code = nodes.listcodes.line -- local nuts = nodes.nuts -- local getid = nuts.getid -- local getsubtype = nuts.getsubtype -- local getdepth = nuts.getdepth -- local setdepth = nuts.setdepth local gettotal = nuts.gettotal local getspeciallist = nuts.getspeciallist local setspeciallist = nuts.setspeciallist local triggerbuildpage = tex.triggerbuildpage -- local texgetdimen = tex.getdimen -- local texsetdimen = tex.setdimen local texgetnest = tex.getnest -- local texget = tex.get -- local texset = tex.set local trace = false trackers.register("otr.forcestrutdepth", function(v) trace = v and function(n) setvisual(nuts.tonut(n),nodes.visualizers.modes.depth) end end) local treversenode = nuts.treversers.node local function flushcontributions() if texgetnest("ptr") == 0 then -- this flushes the contributions local prev = nil local cycle = 1 while cycle <= 10 do local head = getspeciallist("contributehead") if head == prev then -- This can happen .. maybe 10 is already too much ... e.g. -- extreme side float case in m4all. cycle = cycle + 1 else triggerbuildpage() prev = head end end return true else return false end end vspacing.flushcontributions = flushcontributions function vspacing.forcestrutdepth() -- check if in mvl if flushcontributions() then -- now we consult the last line (if present) local head, tail = getspeciallist("pagehead") if tail then for n, id, subtype in treversenode, tail do if id == hlist_code then if subtype == line_code then local strutdp = texgetdimen(d_strutdp) local delta = strutdp - getdepth(n) if delta > 0 then --- also pagelastdepth setdepth(n,strutdp) texset("pagetotal",texget("pagetotal") + delta) texset("pagedepth",strutdp) if trace then trace(n) end end end break elseif id == insert_code or id == mark_code then -- prev else -- if id == glue_code then -- print(gluecodes[subtype],nuts.getwidth(n)) -- else break -- end end end end else local nest = texgetnest() -- if abs(nest.mode) == vmode_code and nest.head then local tail = nest.tail if tail.id == hlist_code and tail.subtype == line_code then local strutdp = texgetdimen(d_strutdp) if tail.depth < strutdp then tail.depth = strutdp end nest.prevdepth = strutdp if trace then trace(tail) end end -- end end end -- highly experimental, only for m4all now; todo: tracing local setbox = nuts.setbox function vspacing.interceptsamepagecontent(box) if vspacing.flushcontributions() then -- now we consult the last line (if present) local head, tail = getspeciallist("pagehead") if tail and getid(tail) == glue_code then local prev = getprev(tail) if prev and getid(prev) == penalty_code then if getpenalty(prev) >= 10000 then local state = nil local first = nil local last = tail local c = getprev(prev) while c do if getid(c) == glue_code then local p = getprev(c) if p and getid(p) == penalty_code then if getpenalty(p) < 10000 then state = 1 end else state = 2 break end end first = c c = getprev(c) end if first and first ~= head then setnext(getprev(first)) setprev(first) local vbox = vpack_node(first) setvisual(vbox) setbox(box,vbox) report_vspacing("same page intercept, case %i") end end end end end end interfaces.implement { name = "interceptsamepagecontent", arguments = "integer", actions = vspacing.interceptsamepagecontent, } -- interfaces.implement { -- name = "removelastline", -- actions = function() -- local h, t = getspeciallist("pagehead") -- if t and getid(t) == hlist_code and getsubtype(t) == line_code then -- local total = gettotal(t) -- h = remove_node(h,t,true) -- setspeciallist(h) -- texset("pagetotal",texget("pagetotal") - total) -- -- set prevdepth -- end -- end -- } function vspacing.pushatsame() -- needs better checking ! if last then -- setsplit nuts.setnext(getprev(last)) nuts.setprev(last) end end function vspacing.popatsame() -- needs better checking ! nuts.write(last) end end -- interface do local implement = interfaces.implement implement { name = "injectvspacing", actions = vspacing.inject, arguments = { "integer", "string" }, } implement { name = "injectvpenalty", actions = vspacing.injectpenalty, arguments = "integer", } implement { name = "injectvskip", actions = vspacing.injectskip, arguments = "dimension", } implement { name = "injectdisable", actions = vspacing.injectdisable, } -- implement { name = "synchronizepage", actions = vspacing.synchronizepage, scope = "private" } -- implement { -- name = "forcestrutdepth", -- arguments = { "integer", "dimension", "integer" }, -- actions = vspacing.forcestrutdepth, -- scope = "private" -- } -- implement { -- name = "forcestrutdepthplus", -- arguments = { "integer", "dimension", "integer", true }, -- actions = vspacing.forcestrutdepth, -- scope = "private" -- } implement { name = "forcestrutdepth", public = true, protected = true, actions = vspacing.forcestrutdepth, } implement { name = "pushatsame", actions = vspacing.pushatsame, scope = "private" } implement { name = "popatsame", actions = vspacing.popatsame, scope = "private" } implement { name = "vspacingsetamount", actions = vspacing.setskip, scope = "private", arguments = "string", } implement { name = "vspacingdefine", actions = vspacing.setmap, scope = "private", arguments = "2 strings", } implement { name = "vspacingcollapse", actions = vspacing.collapsevbox, scope = "private", arguments = "integer" } implement { name = "vspacingcollapseonly", actions = vspacing.collapsevbox, scope = "private", arguments = { "integer", true } } implement { name = "vspacingsnap", actions = vspacing.snapbox, scope = "private", arguments = "2 integers", } implement { name = "definesnapmethod", actions = vspacing.definesnapmethod, scope = "private", arguments = "2 strings", } -- local remove_node = nodes.remove -- local find_node_tail = nodes.tail -- -- interfaces.implement { -- name = "fakenextstrutline", -- actions = function() -- local head = texlists.pagehead -- if head then -- local head = remove_node(head,find_node_tail(head),true) -- texlists.pagehead = head -- buildpage() -- end -- end -- } implement { name = "removelastline", actions = function() local head = texlists.pagehead if head then local tail = find_node_tail(head) if tail then -- maybe check for hlist subtype 1 local head = remove_node(head,tail,true) texlists.pagehead = head buildpage() end end end } implement { name = "showpagelist", -- will improve actions = function() local head = texlists.pagehead if head then print("start") while head do print(" " .. tostring(head)) head = head.next end end end } implement { name = "pageoverflow", actions = { vspacing.pageoverflow, context } } implement { name = "forcepageflush", actions = vspacing.forcepageflush } implement { name = "injectzerobaselineskip", protected = true, public = true, actions = { nodes.pool.baselineskip, context }, } end