if not modules then modules = { } end modules ['typo-mar'] = { version = 1.001, comment = "companion to typo-mar.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- todo: -- -- * autoleft/right depending on available space (or distance to margin) -- * floating margin data, with close-to-call anchoring local format, validstring = string.format, string.valid local insert, remove, sortedkeys, fastcopy = table.insert, table.remove, table.sortedkeys, table.fastcopy local setmetatable, next, tonumber = setmetatable, next, tonumber local formatters = string.formatters local toboolean = toboolean local settings_to_hash = utilities.parsers.settings_to_hash local attributes = attributes local nodes = nodes local variables = variables local context = context local trace_margindata = false trackers.register("typesetters.margindata", function(v) trace_margindata = v end) local trace_marginstack = false trackers.register("typesetters.margindata.stack", function(v) trace_marginstack = v end) local trace_margingroup = false trackers.register("typesetters.margindata.group", function(v) trace_margingroup = v end) local report_margindata = logs.reporter("margindata") local tasks = nodes.tasks local prependaction = tasks.prependaction local disableaction = tasks.disableaction local enableaction = tasks.enableaction local variables = interfaces.variables local conditionals = tex.conditionals local systemmodes = tex.systemmodes local v_top = variables.top local v_depth = variables.depth local v_local = variables["local"] local v_global = variables["global"] local v_left = variables.left local v_right = variables.right local v_inner = variables.inner local v_outer = variables.outer local v_margin = variables.margin local v_edge = variables.edge local v_default = variables.default local v_normal = variables.normal local v_yes = variables.yes local v_continue = variables.continue local v_first = variables.first local v_text = variables.text local v_paragraph = variables.paragraph local v_line = variables.line local nuts = nodes.nuts local tonode = nuts.tonode local hpacknodes = nuts.hpack local traverseid = nuts.traverseid local flushnodelist = nuts.flushlist local getnext = nuts.getnext local getprev = nuts.getprev local getid = nuts.getid local getattr = nuts.getattr local setattr = nuts.setattr local getsubtype = nuts.getsubtype local getlist = nuts.getlist local getwhd = nuts.getwhd local setlist = nuts.setlist local setlink = nuts.setlink local getshift = nuts.getshift local setshift = nuts.setshift local getwidth = nuts.getwidth local setwidth = nuts.setwidth local getheight = nuts.getheight local setattrlist = nuts.setattrlist local getbox = nuts.getbox local takebox = nuts.takebox local setprop = nuts.setprop local getprop = nuts.getprop local nodecodes = nodes.nodecodes local listcodes = nodes.listcodes local whatsitcodes = nodes.whatsitcodes local hlist_code = nodecodes.hlist local vlist_code = nodecodes.vlist local whatsit_code = nodecodes.whatsit local userdefined_code = whatsitcodes.userdefined local nodepool = nuts.pool local new_hlist = nodepool.hlist local new_usernode = nodepool.usernode local latelua = nodepool.latelua local texgetdimen = tex.getdimen local texgetcount = tex.getcount local texget = tex.get local isleftpage = layouts.status.isleftpage local registertogether = builders.paragraphs.registertogether local paragraphs = typesetters.paragraphs local addtoline = paragraphs.addtoline local moveinline = paragraphs.moveinline local calculatedelta = paragraphs.calculatedelta local a_linenumber = attributes.private('linenumber') local inline_mark = nodepool.userids["margins.inline"] local jobpositions = job.positions local getposition = jobpositions.get local setposition = jobpositions.set local getreserved = jobpositions.getreserved local margins = { } typesetters.margins = margins local locations = { v_left, v_right, v_inner, v_outer } -- order might change local categories = { } local displaystore = { } -- [category][location][scope] local inlinestore = { } -- [number] local nofsaved = 0 local nofstored = 0 local nofinlined = 0 local nofdelayed = 0 local nofinjected = 0 local h_anchors = 0 local v_anchors = 0 local mt1 = { __index = function(t,location) local v = { [v_local] = { }, [v_global] = { } } t[location] = v return v end } local mt2 = { __index = function(stores,category) categories[#categories+1] = category local v = { } setmetatable(v,mt1) stores[category] = v return v end } setmetatable(displaystore,mt2) local defaults = { __index = { location = v_left, align = v_normal, -- not used method = "", name = "", threshold = 0, -- .25ex margin = v_normal, scope = v_global, distance = 0, hoffset = 0, voffset = 0, category = v_default, line = 0, vstack = 0, dy = 0, baseline = false, inline = false, leftskip = 0, rightskip = 0, option = { } } } local enablelocal, enableglobal -- forward reference (delayed initialization) local function showstore(store,banner,location) if next(store) then for i, si in table.sortedpairs(store) do local si =store[i] report_margindata("%s: stored in %a at %s: %a => %s",banner,location,i,validstring(si.name,"no name"),nodes.toutf(getlist(si.box))) end else report_margindata("%s: nothing stored in location %a",banner,location) end end function margins.save(t) setmetatable(t,defaults) local content = takebox(t.number) local location = t.location local category = t.category local inline = t.inline local scope = t.scope local name = t.name local option = t.option local stack = t.stack if option then option = settings_to_hash(option) t.option = option end if not content then report_margindata("ignoring empty margin data %a",location or "unknown") return end setprop(content,"specialcontent","margindata") local store if inline then store = inlinestore else store = displaystore[category][location] if not store then report_margindata("invalid location %a",location) return end store = store[scope] end if not store then report_margindata("invalid scope %a",scope) return end if enablelocal and scope == v_local then enablelocal() if enableglobal then enableglobal() -- is the fallback end elseif enableglobal and scope == v_global then enableglobal() end nofsaved = nofsaved + 1 nofstored = nofstored + 1 if trace_marginstack then showstore(store,"before",location) end if name and name ~= "" then -- this can be used to overload if inlinestore then -- todo: inline store has to be done differently (not sparse) local t = sortedkeys(store) for j=#t,1,-1 do local i = t[j] local si = store[i] if si.name == name then local s = remove(store,i) flushnodelist(s.box) end end else for i=#store,1,-1 do local si = store[i] if si.name == name then local s = remove(store,i) flushnodelist(s.box) end end end if trace_marginstack then showstore(store,"between",location) end end if t.number then local leftmargindistance = texgetdimen("naturalleftmargindistance") local rightmargindistance = texgetdimen("naturalrightmargindistance") local strutht = texgetdimen("strutht") local strutdp = texgetdimen("strutdp") -- better make a new table and make t entry in t t.box = content t.n = nofsaved -- used later (we will clean up this natural mess later) -- nice is to make a special status table mechanism t.strutheight = strutht t.strutdepth = strutdp -- beware: can be different from the applied one (we're not in forgetall) t.leftskip = texget("leftskip",false) t.rightskip = texget("rightskip",false) -- t.leftmargindistance = leftmargindistance -- todo:layoutstatus table t.rightmargindistance = rightmargindistance t.leftedgedistance = texgetdimen("naturalleftedgedistance") + texgetdimen("leftmarginwidth") + leftmargindistance t.rightedgedistance = texgetdimen("naturalrightedgedistance") + texgetdimen("rightmarginwidth") + rightmargindistance t.lineheight = texgetdimen("lineheight") -- -- t.realpageno = texgetcount("realpageno") if inline then local n = new_usernode(inline_mark,nofsaved) setattrlist(n,true) context(tonode(n)) -- or use a normal node store[nofsaved] = t -- no insert nofinlined = nofinlined + 1 else insert(store,t) end end if trace_marginstack then showstore(store,"after",location) end if trace_margindata then report_margindata("saved %a, location %a, scope %a, inline %a",nofsaved,location,scope,inline) end end -- Actually it's an advantage to have them all anchored left (tags and such) -- we could keep them in store and flush in stage two but we might want to -- do more before that so we need the content to be there unless we can be -- sure that we flush this first which might not be the case in the future. -- -- When the prototype inner/outer code that was part of this proved to be -- okay it was moved elsewhere. local function realign(current,candidate) local location = candidate.location local margin = candidate.margin local hoffset = candidate.hoffset local distance = candidate.distance local hsize = candidate.hsize local width = candidate.width local align = candidate.align local inline = candidate.inline local anchor = candidate.anchor local hook = candidate.hook local scope = candidate.scope local option = candidate.option local reverse = hook.reverse local atleft = true local hmove = 0 local delta = 0 local leftpage = isleftpage() local leftdelta = 0 local rightdelta = 0 local leftdistance = distance local rightdistance = distance -- if not anchor or anchor == "" then anchor = v_text -- this has to become more clever: region:0|column:n|column end if margin == v_normal then -- elseif margin == v_local then leftdelta = - candidate.leftskip rightdelta = candidate.rightskip elseif margin == v_margin then leftdistance = candidate.leftmargindistance rightdistance = candidate.rightmargindistance elseif margin == v_edge then leftdistance = candidate.leftedgedistance rightdistance = candidate.rightedgedistance end if leftpage then leftdistance, rightdistance = rightdistance, leftdistance end if location == v_right then atleft = false elseif location == v_inner then if leftpage then atleft = false end elseif location == v_outer then if not leftpage then atleft = false end else -- v_left end local islocal = scope == v_local local area = (not islocal or option[v_text]) and anchor or nil if atleft then delta = hoffset + leftdelta + leftdistance else delta = hoffset + rightdelta + rightdistance end local delta, hmove = calculatedelta ( hook, -- the line width, -- width of object delta, -- offset atleft, islocal, -- islocal option[v_paragraph], -- followshape area -- relative to area ) if hmove ~= 0 then delta = delta + hmove if trace_margindata then report_margindata("realigned %a, location %a, margin %a, move %p",candidate.n,location,margin,hmove) end else if trace_margindata then report_margindata("realigned %a, location %a, margin %a",candidate.n,location,margin) end end moveinline(hook,candidate.node,delta) end local function realigned(current,candidate) realign(current,candidate) nofdelayed = nofdelayed - 1 setprop(current,"margindata",false) return true end -- Stacking is done in two ways: the v_yes option stacks per paragraph (or line, -- depending on what gets by) and mostly concerns margin data dat got set at more or -- less the same time. The v_continue option uses position tracking and works on -- larger range. However, crossing pages is not part of it. Anyway, when you have -- such messed up margin data you'd better think twice. -- -- The stacked table keeps track (per location) of the offsets (the v_yes case). This -- table gets saved when the v_continue case is active. We use a special variant -- of position tracking, after all we only need the page number and vertical position. local validstacknames = { [v_left ] = v_left , [v_right] = v_right, [v_inner] = v_inner, [v_outer] = v_outer, } local cache = { } local stacked = { [v_yes] = { }, [v_continue] = { } } local anchors = { [v_yes] = { }, [v_continue] = { } } local function resetstacked(all) stacked[v_yes] = { } anchors[v_yes] = { } if all then stacked[v_continue] = { } anchors[v_continue] = { } end end -- anchors are only set for lines that have a note local function sa(specification) -- maybe l/r keys ipv left/right keys local tag = specification.tag local p = cache[tag] if p then if trace_marginstack then report_margindata("updating anchor %a",tag) end p.p = true p.y = true -- maybe settobesaved first setposition("md:v",tag,p) cache[tag] = nil -- do this later, per page a cleanup end end local function setanchor(v_anchor) -- freezes the global here return latelua { action = sa, tag = v_anchor } end local function aa(specification) -- maybe l/r keys ipv left/right keys local tag = specification.tag local n = specification.n local p = jobpositions.gettobesaved('md:v',tag) if p then if trace_marginstack then report_margindata("updating injected %a",tag) end local pages = p.pages if not pages then pages = { } p.pages = pages end pages[n] = texgetcount("realpageno") elseif trace_marginstack then report_margindata("not updating injected %a",tag) end end local function addtoanchor(v_anchor,n) -- freezes the global here return latelua { action = aa, tag = v_anchor, n = n } end local function markovershoot(current) -- todo: alleen als offset > line v_anchors = v_anchors + 1 cache[v_anchors] = fastcopy(stacked) local anchor = setanchor(v_anchors) -- local list = hpacknodes(setlink(anchor,getlist(current))) -- not ok, we need to retain width -- local list = setlink(anchor,getlist(current)) -- why not this ... better play safe local list = hpacknodes(setlink(anchor,getlist(current)),getwidth(current),"exactly")-- if trace_marginstack then report_margindata("marking anchor %a",v_anchors) end setlist(current,list) end local function inject(parent,head,candidate) local box = candidate.box if not box then return head, nil, false -- we can have empty texts end local width, height, depth = getwhd(box) local shift = getshift(box) local stack = candidate.stack local stackname = candidate.stackname local location = candidate.location local method = candidate.method local voffset = candidate.voffset local line = candidate.line local baseline = candidate.baseline local strutheight = candidate.strutheight local strutdepth = candidate.strutdepth local inline = candidate.inline local psubtype = getsubtype(parent) -- This stackname is experimental and therefore undocumented and basically -- unsupported. It was introduced when we needed to support overlapping -- of different anchors. if not stackname or stackname == "" then stackname = location else stackname = validstacknames[stackname] or location end local isstacked = stack == v_continue or stack == v_yes local offset = isstacked and stacked[stack][stackname] local firstonstack = offset == false or offset == nil nofinjected = nofinjected + 1 nofdelayed = nofdelayed + 1 -- yet untested baseline = tonumber(baseline) if not baseline then baseline = toboolean(baseline) end -- if baseline == true then baseline = false else baseline = tonumber(baseline) if not baseline or baseline <= 0 then -- in case we have a box of width 0 that is not analyzed baseline = false -- strutheight -- actually a hack end end candidate.width = width candidate.hsize = getwidth(parent) -- we can also pass textwidth candidate.psubtype = psubtype candidate.stackname = stackname if trace_margindata then report_margindata("processing, index %s, height %p, depth %p, parent %a, method %a",candidate.n,height,depth,listcodes[psubtype],method) end -- Overlap detection is somewhat complex because we have display and inline -- notes mixed as well as inner and outer positioning. We do need to -- handle it in the stream because we also keep lines together so we keep -- track of page numbers of notes. if isstacked then firstonstack = true local anchor = getposition("md:v") if anchor and (location == v_inner or location == v_outer) then local pages = anchor.pages if pages then local page = pages[nofinjected] if page then if isleftpage(page) then stackname = location == v_inner and v_right or v_left else stackname = location == v_inner and v_left or v_right end candidate.stackname = stackname offset = stack and stack ~= "" and stacked[stack][stackname] end end end local current = v_anchors + 1 local previous = anchors[stack][stackname] if trace_margindata then report_margindata("anchor %i, offset so far %p",current,offset or 0) end local ap = anchor and anchor[previous] local ac = anchor and anchor[current] if not previous then elseif previous == current then firstonstack = false elseif ap and ac and ap.p == ac.p then local distance = (ap.y or 0) - (ac.y or 0) if trace_margindata then report_margindata("distance %p",distance) end if offset > distance then -- we already overflow offset = offset - distance firstonstack = false else offset = 0 end else -- what to do end anchors[v_yes] [stackname] = current anchors[v_continue][stackname] = current if firstonstack then offset = 0 end offset = offset + candidate.dy -- always shift = shift + offset else if firstonstack then offset = 0 end offset = offset + candidate.dy -- always shift = shift + offset end -- Maybe we also need to patch offset when we apply methods, but how ... -- This needs a bit of playing as it depends on the stack setting of the -- following which we don't know yet ... so, consider stacking partially -- experimental. if method == v_top then local delta = height - getheight(parent) if trace_margindata then report_margindata("top aligned by %p",delta) end if delta < candidate.threshold then -- often we need a negative threshold here shift = shift + voffset + delta end elseif method == v_line then local _, ph, pd = getwhd(parent) if pd == 0 then local delta = height - ph if trace_margindata then report_margindata("top aligned by %p (no depth)",delta) end if delta < candidate.threshold then -- often we need a negative threshold here shift = shift + voffset + delta end end elseif method == v_first then if baseline then shift = shift + voffset + height - baseline -- option else shift = shift + voffset -- normal end if trace_margindata then report_margindata("first aligned") end elseif method == v_depth then local delta = strutdepth if trace_margindata then report_margindata("depth aligned by %p",delta) end shift = shift + voffset + delta elseif method == v_height then local delta = - strutheight if trace_margindata then report_margindata("height aligned by %p",delta) end shift = shift + voffset + delta elseif voffset ~= 0 then if trace_margindata then report_margindata("voffset %p applied",voffset) end shift = shift + voffset end -- -- -- if line ~= 0 then local delta = line * candidate.lineheight if trace_margindata then report_margindata("offset %p applied to line %s",delta,line) end shift = shift + delta offset = offset + delta end setshift(box,shift) setwidth(box,0) -- not needed when wrapped -- if isstacked then setlink(box,addtoanchor(v_anchors,nofinjected)) box = new_hlist(box) -- set height / depth ? end -- candidate.hook, candidate.node = addtoline(parent,box) -- setprop(box,"margindata",candidate) if trace_margindata then report_margindata("injected, location %a, stack %a, shift %p",location,stackname,shift) end -- we need to add line etc to offset as well offset = offset + depth local room = { height = height, depth = offset, slack = candidate.bottomspace, -- todo: 'depth' => strutdepth lineheight = candidate.lineheight, -- only for tracing stacked = inline and isstacked, } offset = offset + height -- we need a restart ... when there is no overlap at all stacked[v_yes] [stackname] = offset stacked[v_continue][stackname] = offset -- todo: if no real depth then zero if trace_margindata then report_margindata("status, offset %s",offset) end return getlist(parent), room, inline and isstacked or (stack == v_continue) end local function flushinline(parent,head) local current = head local done = false local continue = false local room, don, con, list while current and nofinlined > 0 do local id = getid(current) if id == whatsit_code then if getsubtype(current) == userdefined_code and getprop(current,"id") == inline_mark then local n = getprop(current,"data") local candidate = inlinestore[n] if candidate then -- no vpack, as we want to realign inlinestore[n] = nil nofinlined = nofinlined - 1 head, room, con = inject(parent,head,candidate) -- maybe return applied offset done = true continue = continue or con nofstored = nofstored - 1 if room and room.stacked then -- for now we also check for inline+yes/continue, maybe someday no such check -- will happen; we can assume most inlines are one line heigh; also this -- together feature can become optional registertogether(parent,room) end end end elseif id == hlist_code or id == vlist_code then -- optional (but sometimes needed) list, don, con = flushinline(current,getlist(current)) setlist(current,list) continue = continue or con done = done or don end current = getnext(current) end return head, done, continue end local function flushed(scope,parent) -- current is hlist local head = getlist(parent) local done = false local continue = false local room, con, don for c=1,#categories do local category = categories[c] for l=1,#locations do local location = locations[l] local store = displaystore[category][location][scope] if store then while true do local candidate = remove(store,1) -- brr, local stores are sparse if candidate then -- no vpack, as we want to realign head, room, con = inject(parent,head,candidate) done = true continue = continue or con nofstored = nofstored - 1 if room then registertogether(parent,room) end else break end end else -- report_margindata("fatal error: invalid category %a",category or "?") end end end if nofinlined > 0 then if done then setlist(parent,head) end head, don, con = flushinline(parent,head) continue = continue or con done = done or don end if done then local a = getattr(head,a_linenumber) -- hack .. we need a more decent critical attribute inheritance mechanism if false then local l = hpacknodes(head,getwidth(parent),"exactly") setlist(parent,l) if a then setattr(l,a_linenumber,a) end else -- because packing messes up profiling setlist(parent,head) if a then setattr(parent,a_linenumber,a) end end end return done, continue end -- only when group : vbox|vmode_par -- only when subtype : line, box (no indent alignment cell) local function handler(scope,head,group) if nofstored > 0 then if trace_margindata then report_margindata("flushing stage one, stored %s, scope %s, delayed %s, group %a",nofstored,scope,nofdelayed,group) end local current = head local done = false -- for tracing only while current do local id = getid(current) if (id == vlist_code or id == hlist_code) and getprop(current,"margindata") == nil then local don, continue = flushed(scope,current) if don then done = true setprop(current,"margindata",false) -- signal to prevent duplicate processing if continue then markovershoot(current) end if nofstored <= 0 then break end end end current = getnext(current) end if trace_margindata then if done then report_margindata("flushing stage one, done, %s left",nofstored) else report_margindata("flushing stage one, nothing done, %s left",nofstored) end end resetstacked() end return head end local trialtypesetting = context.trialtypesetting -- maybe change this to an action applied to the to be shipped out box (that is -- the mvl list in there so that we don't need to traverse global function margins.localhandler(head,group) -- sometimes group is "" which is weird if trialtypesetting() then return head end local inhibit = conditionals.inhibitmargindata if inhibit then if trace_margingroup then report_margindata("ignored 3, group %a, stored %s, inhibit %a",group,nofstored,inhibit) end return head end if nofstored > 0 then return handler(v_local,head,group) end if trace_margingroup then report_margindata("ignored 4, group %a, stored %s, inhibit %a",group,nofstored,inhibit) end return head end function margins.globalhandler(head,group) -- check group if trialtypesetting() then return head, false end local inhibit = conditionals.inhibitmargindata if inhibit or nofstored == 0 then if trace_margingroup then report_margindata("ignored 1, group %a, stored %s, inhibit %a",group,nofstored,inhibit) end return head elseif group == "hmode_par" then return handler(v_global,head,group) elseif group == "vmode_par" then -- experiment (for alignments) return handler(v_global,head,group) -- this needs checking as we then get quite some one liners to process and -- we cannot look ahead then: elseif group == "box" then -- experiment (for alignments) return handler(v_global,head,group) elseif group == "alignment" then -- experiment (for alignments) return handler(v_global,head,group) else if trace_margingroup then report_margindata("ignored 2, group %a, stored %s, inhibit %a",group,nofstored,inhibit) end return head end end local function finalhandler(head) if nofdelayed > 0 then local current = head while current and nofdelayed > 0 do local id = getid(current) if id == hlist_code then -- only lines? local a = getprop(current,"margindata") if not a then finalhandler(getlist(current)) elseif realigned(current,a) then if nofdelayed == 0 then return head, true end end elseif id == vlist_code then finalhandler(getlist(current)) end current = getnext(current) end end return head end function margins.finalhandler(head) if nofdelayed > 0 then if trace_margindata then report_margindata("flushing stage two, instore: %s, delayed: %s",nofstored,nofdelayed) end head = finalhandler(head) resetstacked(nofdelayed==0) else resetstacked() end return head end -- Somehow the vbox builder (in combinations) gets pretty confused and decides to -- go horizontal. So this needs more testing. enablelocal = function() enableaction("finalizers", "typesetters.margins.localhandler") enableaction("shipouts", "typesetters.margins.finalhandler") enablelocal = nil end enableglobal = function() enableaction("mvlbuilders", "typesetters.margins.globalhandler") enableaction("shipouts", "typesetters.margins.finalhandler") enableglobal = nil end statistics.register("margin data", function() if nofsaved > 0 then return format("%s entries, %s pending",nofsaved,nofdelayed) else return nil end end) interfaces.implement { name = "savemargindata", actions = margins.save, arguments = { { { "location" }, { "method" }, { "category" }, { "name" }, { "scope" }, { "number", "integer" }, { "margin" }, { "distance", "dimen" }, { "hoffset", "dimen" }, { "voffset", "dimen" }, { "dy", "dimen" }, { "bottomspace", "dimen" }, { "baseline"}, -- dimen or string or { "threshold", "dimen" }, { "inline", "boolean" }, { "anchor" }, -- { "leftskip", "dimen" }, -- { "rightskip", "dimen" }, { "align" }, { "option" }, { "line", "integer" }, { "index", "integer" }, { "stackname" }, { "stack" }, } } }