typo-mar.lua /size: 33 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['typo-mar'] = {
2    version   = 1.001,
3    comment   = "companion to typo-mar.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- todo:
10--
11-- * autoleft/right depending on available space (or distance to margin)
12-- * floating margin data, with close-to-call anchoring
13
14local format, validstring = string.format, string.valid
15local insert, remove, sortedkeys, fastcopy = table.insert, table.remove, table.sortedkeys, table.fastcopy
16local setmetatable, next, tonumber = setmetatable, next, tonumber
17local formatters = string.formatters
18local toboolean = toboolean
19local settings_to_hash = utilities.parsers.settings_to_hash
20
21local attributes         = attributes
22local nodes              = nodes
23local variables          = variables
24local context            = context
25
26local trace_margindata   = false  trackers.register("typesetters.margindata",       function(v) trace_margindata  = v end)
27local trace_marginstack  = false  trackers.register("typesetters.margindata.stack", function(v) trace_marginstack = v end)
28local trace_margingroup  = false  trackers.register("typesetters.margindata.group", function(v) trace_margingroup = v end)
29
30local report_margindata  = logs.reporter("margindata")
31
32local tasks              = nodes.tasks
33local prependaction      = tasks.prependaction
34local disableaction      = tasks.disableaction
35local enableaction       = tasks.enableaction
36
37local variables          = interfaces.variables
38
39local conditionals       = tex.conditionals
40local systemmodes        = tex.systemmodes
41
42local v_top              = variables.top
43local v_depth            = variables.depth
44local v_local            = variables["local"]
45local v_global           = variables["global"]
46local v_left             = variables.left
47local v_right            = variables.right
48local v_inner            = variables.inner
49local v_outer            = variables.outer
50local v_margin           = variables.margin
51local v_edge             = variables.edge
52local v_default          = variables.default
53local v_normal           = variables.normal
54local v_yes              = variables.yes
55local v_continue         = variables.continue
56local v_first            = variables.first
57local v_text             = variables.text
58local v_paragraph        = variables.paragraph
59local v_line             = variables.line
60
61local nuts               = nodes.nuts
62local tonode             = nuts.tonode
63
64local hpacknodes         = nuts.hpack
65local traverseid         = nuts.traverseid
66local flushnodelist      = nuts.flushlist
67
68local getnext            = nuts.getnext
69local getprev            = nuts.getprev
70local getid              = nuts.getid
71local getattr            = nuts.getattr
72local setattr            = nuts.setattr
73local getsubtype         = nuts.getsubtype
74local getlist            = nuts.getlist
75local getwhd             = nuts.getwhd
76local setlist            = nuts.setlist
77local setlink            = nuts.setlink
78local getshift           = nuts.getshift
79local setshift           = nuts.setshift
80local getwidth           = nuts.getwidth
81local setwidth           = nuts.setwidth
82local getheight          = nuts.getheight
83
84local setattrlist        = nuts.setattrlist
85
86local getbox             = nuts.getbox
87local takebox            = nuts.takebox
88
89local setprop            = nuts.setprop
90local getprop            = nuts.getprop
91
92local nodecodes          = nodes.nodecodes
93local listcodes          = nodes.listcodes
94local whatsitcodes       = nodes.whatsitcodes
95
96local hlist_code         = nodecodes.hlist
97local vlist_code         = nodecodes.vlist
98local whatsit_code       = nodecodes.whatsit
99local userdefined_code   = whatsitcodes.userdefined
100
101local nodepool           = nuts.pool
102
103local new_hlist          = nodepool.hlist
104local new_usernode       = nodepool.usernode
105local latelua            = nodepool.latelua
106
107local texgetdimen        = tex.getdimen
108local texgetcount        = tex.getcount
109local texget             = tex.get
110
111local isleftpage         = layouts.status.isleftpage
112local registertogether   = builders.paragraphs.registertogether
113
114local paragraphs         = typesetters.paragraphs
115local addtoline          = paragraphs.addtoline
116local moveinline         = paragraphs.moveinline
117local calculatedelta     = paragraphs.calculatedelta
118
119local a_linenumber       = attributes.private('linenumber')
120
121local inline_mark        = nodepool.userids["margins.inline"]
122
123local jobpositions       = job.positions
124local getposition        = jobpositions.get
125local setposition        = jobpositions.set
126local getreserved        = jobpositions.getreserved
127
128local margins            = { }
129typesetters.margins      = margins
130
131local locations          = { v_left, v_right, v_inner, v_outer } -- order might change
132local categories         = { }
133local displaystore       = { } -- [category][location][scope]
134local inlinestore        = { } -- [number]
135local nofsaved           = 0
136local nofstored          = 0
137local nofinlined         = 0
138local nofdelayed         = 0
139local nofinjected        = 0
140local h_anchors          = 0
141local v_anchors          = 0
142
143local mt1 = {
144    __index = function(t,location)
145        local v = { [v_local] = { }, [v_global] = { } }
146        t[location] = v
147        return v
148    end
149}
150
151local mt2 = {
152    __index = function(stores,category)
153        categories[#categories+1] = category
154        local v = { }
155        setmetatable(v,mt1)
156        stores[category] = v
157        return v
158    end
159}
160
161setmetatable(displaystore,mt2)
162
163local defaults = {
164    __index  = {
165        location  = v_left,
166        align     = v_normal, -- not used
167        method    = "",
168        name      = "",
169        threshold = 0, -- .25ex
170        margin    = v_normal,
171        scope     = v_global,
172        distance  = 0,
173        hoffset   = 0,
174        voffset   = 0,
175        category  = v_default,
176        line      = 0,
177        vstack    = 0,
178        dy        = 0,
179        baseline  = false,
180        inline    = false,
181        leftskip  = 0,
182        rightskip = 0,
183        option    = { }
184    }
185}
186
187local enablelocal, enableglobal -- forward reference (delayed initialization)
188
189local function showstore(store,banner,location)
190    if next(store) then
191        for i, si in table.sortedpairs(store) do
192            local si =store[i]
193            report_margindata("%s: stored in %a at %s: %a => %s",banner,location,i,validstring(si.name,"no name"),nodes.toutf(getlist(si.box)))
194        end
195    else
196        report_margindata("%s: nothing stored in location %a",banner,location)
197    end
198end
199
200function margins.save(t)
201    setmetatable(t,defaults)
202    local content  = takebox(t.number)
203    local location = t.location
204    local category = t.category
205    local inline   = t.inline
206    local scope    = t.scope
207    local name     = t.name
208    local option   = t.option
209    local stack    = t.stack
210    if option then
211        option   = settings_to_hash(option)
212        t.option = option
213    end
214    if not content then
215        report_margindata("ignoring empty margin data %a",location or "unknown")
216        return
217    end
218    setprop(content,"specialcontent","margindata")
219    local store
220    if inline then
221        store = inlinestore
222    else
223        store = displaystore[category][location]
224        if not store then
225            report_margindata("invalid location %a",location)
226            return
227        end
228        store = store[scope]
229    end
230    if not store then
231        report_margindata("invalid scope %a",scope)
232        return
233    end
234    if enablelocal and scope == v_local then
235        enablelocal()
236        if enableglobal then
237            enableglobal() -- is the fallback
238        end
239    elseif enableglobal and scope == v_global then
240        enableglobal()
241    end
242    nofsaved  = nofsaved + 1
243    nofstored = nofstored + 1
244    if trace_marginstack then
245        showstore(store,"before",location)
246    end
247    if name and name ~= "" then
248        -- this can be used to overload
249        if inlinestore then -- todo: inline store has to be done differently (not sparse)
250            local t = sortedkeys(store) for j=#t,1,-1 do local i = t[j]
251                local si = store[i]
252                if si.name == name then
253                    local s = remove(store,i)
254                    flushnodelist(s.box)
255                end
256            end
257        else
258            for i=#store,1,-1 do
259                local si = store[i]
260                if si.name == name then
261                    local s = remove(store,i)
262                    flushnodelist(s.box)
263                end
264            end
265        end
266        if trace_marginstack then
267            showstore(store,"between",location)
268        end
269    end
270    if t.number then
271        local leftmargindistance  = texgetdimen("naturalleftmargindistance")
272        local rightmargindistance = texgetdimen("naturalrightmargindistance")
273        local strutht = texgetdimen("strutht")
274        local strutdp = texgetdimen("strutdp")
275        -- better make a new table and make t entry in t
276        t.box                 = content
277        t.n                   = nofsaved
278        -- used later (we will clean up this natural mess later)
279        -- nice is to make a special status table mechanism
280        t.strutheight         = strutht
281        t.strutdepth          = strutdp
282        -- beware: can be different from the applied one (we're not in forgetall)
283        t.leftskip            = texget("leftskip",false)
284        t.rightskip           = texget("rightskip",false)
285        --
286        t.leftmargindistance  = leftmargindistance -- todo:layoutstatus table
287        t.rightmargindistance = rightmargindistance
288        t.leftedgedistance    = texgetdimen("naturalleftedgedistance")
289                              + texgetdimen("leftmarginwidth")
290                              + leftmargindistance
291        t.rightedgedistance   = texgetdimen("naturalrightedgedistance")
292                              + texgetdimen("rightmarginwidth")
293                              + rightmargindistance
294        t.lineheight          = texgetdimen("lineheight")
295        --
296     -- t.realpageno          = texgetcount("realpageno")
297        if inline then
298            local n = new_usernode(inline_mark,nofsaved)
299            setattrlist(n,true)
300            context(tonode(n)) -- or use a normal node
301            store[nofsaved] = t -- no insert
302            nofinlined = nofinlined + 1
303        else
304            insert(store,t)
305        end
306    end
307    if trace_marginstack then
308        showstore(store,"after",location)
309    end
310    if trace_margindata then
311        report_margindata("saved %a, location %a, scope %a, inline %a",nofsaved,location,scope,inline)
312    end
313end
314
315-- Actually it's an advantage to have them all anchored left (tags and such)
316-- we could keep them in store and flush in stage two but we might want to
317-- do more before that so we need the content to be there unless we can be
318-- sure that we flush this first which might not be the case in the future.
319--
320-- When the prototype inner/outer code that was part of this proved to be
321-- okay it was moved elsewhere.
322
323local function realign(current,candidate)
324    local location      = candidate.location
325    local margin        = candidate.margin
326    local hoffset       = candidate.hoffset
327    local distance      = candidate.distance
328    local hsize         = candidate.hsize
329    local width         = candidate.width
330    local align         = candidate.align
331    local inline        = candidate.inline
332    local anchor        = candidate.anchor
333    local hook          = candidate.hook
334    local scope         = candidate.scope
335    local option        = candidate.option
336    local reverse       = hook.reverse
337    local atleft        = true
338    local hmove         = 0
339    local delta         = 0
340    local leftpage      = isleftpage()
341    local leftdelta     = 0
342    local rightdelta    = 0
343    local leftdistance  = distance
344    local rightdistance = distance
345    --
346    if not anchor or anchor == "" then
347        anchor = v_text -- this has to become more clever: region:0|column:n|column
348    end
349    if margin == v_normal then
350        --
351    elseif margin == v_local then
352        leftdelta  = - candidate.leftskip
353        rightdelta =   candidate.rightskip
354    elseif margin == v_margin then
355        leftdistance  = candidate.leftmargindistance
356        rightdistance = candidate.rightmargindistance
357    elseif margin == v_edge then
358        leftdistance  = candidate.leftedgedistance
359        rightdistance = candidate.rightedgedistance
360    end
361    if leftpage then
362        leftdistance, rightdistance = rightdistance, leftdistance
363    end
364    if location == v_right then
365        atleft = false
366    elseif location == v_inner then
367        if leftpage then
368            atleft = false
369        end
370    elseif location == v_outer then
371        if not leftpage then
372            atleft = false
373        end
374    else
375        -- v_left
376    end
377
378    local islocal = scope == v_local
379    local area    = (not islocal or option[v_text]) and anchor or nil
380
381    if atleft then
382        delta = hoffset + leftdelta  + leftdistance
383    else
384        delta = hoffset + rightdelta + rightdistance
385    end
386
387    local delta, hmove = calculatedelta (
388        hook,                -- the line
389        width,               -- width of object
390        delta,               -- offset
391        atleft,
392        islocal,             -- islocal
393        option[v_paragraph], -- followshape
394        area                 -- relative to area
395    )
396
397    if hmove ~= 0 then
398        delta = delta + hmove
399        if trace_margindata then
400            report_margindata("realigned %a, location %a, margin %a, move %p",candidate.n,location,margin,hmove)
401        end
402    else
403        if trace_margindata then
404            report_margindata("realigned %a, location %a, margin %a",candidate.n,location,margin)
405        end
406    end
407    moveinline(hook,candidate.node,delta)
408end
409
410local function realigned(current,candidate)
411    realign(current,candidate)
412    nofdelayed = nofdelayed - 1
413    setprop(current,"margindata",false)
414    return true
415end
416
417-- Stacking is done in two ways: the v_yes option stacks per paragraph (or line,
418-- depending on what gets by) and mostly concerns margin data dat got set at more or
419-- less the same time. The v_continue option uses position tracking and works on
420-- larger range. However, crossing pages is not part of it. Anyway, when you have
421-- such messed up margin data you'd better think twice.
422--
423-- The stacked table keeps track (per location) of the offsets (the v_yes case). This
424-- table gets saved when the v_continue case is active. We use a special variant
425-- of position tracking, after all we only need the page number and vertical position.
426
427local validstacknames = {
428    [v_left ] = v_left ,
429    [v_right] = v_right,
430    [v_inner] = v_inner,
431    [v_outer] = v_outer,
432}
433
434local cache   = { }
435local stacked = { [v_yes] = { }, [v_continue] = { } }
436local anchors = { [v_yes] = { }, [v_continue] = { } }
437
438local function resetstacked(all)
439    stacked[v_yes] = { }
440    anchors[v_yes] = { }
441    if all then
442        stacked[v_continue] = { }
443        anchors[v_continue] = { }
444    end
445end
446
447-- anchors are only set for lines that have a note
448
449local function sa(specification) -- maybe l/r keys ipv left/right keys
450    local tag = specification.tag
451    local p   = cache[tag]
452    if p then
453        if trace_marginstack then
454            report_margindata("updating anchor %a",tag)
455        end
456        p.p = true
457        p.y = true
458        -- maybe settobesaved first
459        setposition("md:v",tag,p)
460        cache[tag] = nil -- do this later, per page a cleanup
461    end
462end
463
464local function setanchor(v_anchor) -- freezes the global here
465    return latelua { action = sa, tag = v_anchor }
466end
467
468local function aa(specification) -- maybe l/r keys ipv left/right keys
469    local tag = specification.tag
470    local n   = specification.n
471    local p   = jobpositions.gettobesaved('md:v',tag)
472    if p then
473        if trace_marginstack then
474            report_margindata("updating injected %a",tag)
475        end
476        local pages = p.pages
477        if not pages then
478            pages = { }
479            p.pages = pages
480        end
481        pages[n] = texgetcount("realpageno")
482    elseif trace_marginstack then
483        report_margindata("not updating injected %a",tag)
484    end
485end
486
487local function addtoanchor(v_anchor,n) -- freezes the global here
488    return latelua { action = aa, tag = v_anchor, n = n }
489end
490
491local function markovershoot(current) -- todo: alleen als offset > line
492    v_anchors = v_anchors + 1
493    cache[v_anchors] = fastcopy(stacked)
494    local anchor = setanchor(v_anchors)
495 -- local list = hpacknodes(setlink(anchor,getlist(current))) -- not ok, we need to retain width
496 -- local list = setlink(anchor,getlist(current)) -- why not this ... better play safe
497    local list = hpacknodes(setlink(anchor,getlist(current)),getwidth(current),"exactly")--
498    if trace_marginstack then
499        report_margindata("marking anchor %a",v_anchors)
500    end
501    setlist(current,list)
502end
503
504local function inject(parent,head,candidate)
505    local box = candidate.box
506    if not box then
507        return head, nil, false -- we can have empty texts
508    end
509    local width, height, depth
510                       = getwhd(box)
511    local shift        = getshift(box)
512    local stack        = candidate.stack
513    local stackname    = candidate.stackname
514    local location     = candidate.location
515    local method       = candidate.method
516    local voffset      = candidate.voffset
517    local line         = candidate.line
518    local baseline     = candidate.baseline
519    local strutheight  = candidate.strutheight
520    local strutdepth   = candidate.strutdepth
521    local inline       = candidate.inline
522    local psubtype     = getsubtype(parent)
523    -- This stackname is experimental and therefore undocumented and basically
524    -- unsupported. It was introduced when we needed to support overlapping
525    -- of different anchors.
526    if not stackname or stackname == "" then
527        stackname = location
528    else
529        stackname = validstacknames[stackname] or location
530    end
531    local isstacked    = stack == v_continue or stack == v_yes
532    local offset       = isstacked and stacked[stack][stackname]
533    local firstonstack = offset == false or offset == nil
534    nofinjected        = nofinjected + 1
535    nofdelayed         = nofdelayed + 1
536    -- yet untested
537    baseline = tonumber(baseline)
538    if not baseline then
539        baseline = toboolean(baseline)
540    end
541    --
542    if baseline == true then
543        baseline = false
544    else
545        baseline = tonumber(baseline)
546        if not baseline or baseline <= 0 then
547            -- in case we have a box of width 0 that is not analyzed
548            baseline = false -- strutheight -- actually a hack
549        end
550    end
551    candidate.width     = width
552    candidate.hsize     = getwidth(parent) -- we can also pass textwidth
553    candidate.psubtype  = psubtype
554    candidate.stackname = stackname
555    if trace_margindata then
556        report_margindata("processing, index %s, height %p, depth %p, parent %a, method %a",candidate.n,height,depth,listcodes[psubtype],method)
557    end
558    -- Overlap detection is somewhat complex because we have display and inline
559    -- notes mixed as well as inner and outer positioning. We do need to
560    -- handle it in the stream because we also keep lines together so we keep
561    -- track of page numbers of notes.
562
563    if isstacked then
564        firstonstack = true
565        local anchor = getposition("md:v")
566        if anchor and (location == v_inner or location == v_outer) then
567            local pages = anchor.pages
568            if pages then
569                local page = pages[nofinjected]
570                if page then
571                    if isleftpage(page) then
572                        stackname = location == v_inner and v_right or v_left
573                    else
574                        stackname = location == v_inner and v_left or v_right
575                    end
576                    candidate.stackname = stackname
577                    offset              = stack and stack ~= "" and stacked[stack][stackname]
578                end
579            end
580        end
581        local current  = v_anchors + 1
582        local previous = anchors[stack][stackname]
583        if trace_margindata then
584            report_margindata("anchor %i, offset so far %p",current,offset or 0)
585        end
586        local ap = anchor and anchor[previous]
587        local ac = anchor and anchor[current]
588        if not previous then
589        elseif previous == current then
590            firstonstack = false
591        elseif ap and ac and ap.p == ac.p then
592            local distance = (ap.y or 0) - (ac.y or 0)
593            if trace_margindata then
594                report_margindata("distance %p",distance)
595            end
596            if offset > distance then
597                -- we already overflow
598                offset = offset - distance
599                firstonstack = false
600            else
601                offset = 0
602            end
603        else
604            -- what to do
605        end
606        anchors[v_yes]     [stackname] = current
607        anchors[v_continue][stackname] = current
608        if firstonstack then
609            offset = 0
610        end
611        offset = offset + candidate.dy -- always
612        shift  = shift + offset
613    else
614        if firstonstack then
615            offset = 0
616        end
617        offset = offset + candidate.dy -- always
618        shift  = shift + offset
619    end
620    -- Maybe we also need to patch offset when we apply methods, but how ...
621    -- This needs a bit of playing as it depends on the stack setting of the
622    -- following which we don't know yet ... so, consider stacking partially
623    -- experimental.
624    if method == v_top then
625        local delta = height - getheight(parent)
626        if trace_margindata then
627            report_margindata("top aligned by %p",delta)
628        end
629        if delta < candidate.threshold then -- often we need a negative threshold here
630            shift = shift + voffset + delta
631        end
632    elseif method == v_line then
633        local _, ph, pd = getwhd(parent)
634        if pd == 0 then
635            local delta = height - ph
636            if trace_margindata then
637                report_margindata("top aligned by %p (no depth)",delta)
638            end
639            if delta < candidate.threshold then -- often we need a negative threshold here
640                shift = shift + voffset + delta
641            end
642        end
643    elseif method == v_first then
644        if baseline then
645            shift = shift + voffset + height - baseline -- option
646        else
647            shift = shift + voffset -- normal
648        end
649        if trace_margindata then
650            report_margindata("first aligned")
651        end
652    elseif method == v_depth then
653        local delta = strutdepth
654        if trace_margindata then
655            report_margindata("depth aligned by %p",delta)
656        end
657        shift = shift + voffset + delta
658    elseif method == v_height then
659        local delta = - strutheight
660        if trace_margindata then
661            report_margindata("height aligned by %p",delta)
662        end
663        shift = shift + voffset + delta
664    elseif voffset ~= 0 then
665        if trace_margindata then
666            report_margindata("voffset %p applied",voffset)
667        end
668        shift = shift + voffset
669    end
670    -- -- --
671    if line ~= 0 then
672        local delta = line * candidate.lineheight
673        if trace_margindata then
674            report_margindata("offset %p applied to line %s",delta,line)
675        end
676        shift  = shift + delta
677        offset = offset + delta
678    end
679    setshift(box,shift)
680    setwidth(box,0) -- not needed when wrapped
681    --
682    if isstacked then
683        setlink(box,addtoanchor(v_anchors,nofinjected))
684        box = new_hlist(box)
685        -- set height / depth ?
686    end
687    --
688    candidate.hook, candidate.node = addtoline(parent,box)
689    --
690    setprop(box,"margindata",candidate)
691    if trace_margindata then
692        report_margindata("injected, location %a, stack %a, shift %p",location,stackname,shift)
693    end
694    -- we need to add line etc to offset as well
695    offset = offset + depth
696    local room = {
697        height     = height,
698        depth      = offset,
699        slack      = candidate.bottomspace, -- todo: 'depth' => strutdepth
700        lineheight = candidate.lineheight,  -- only for tracing
701        stacked    = inline and isstacked,
702    }
703    offset = offset + height
704    -- we need a restart ... when there is no overlap at all
705    stacked[v_yes]     [stackname] = offset
706    stacked[v_continue][stackname] = offset
707    -- todo: if no real depth then zero
708    if trace_margindata then
709        report_margindata("status, offset %s",offset)
710    end
711    return getlist(parent), room, inline and isstacked or (stack == v_continue)
712end
713
714local function flushinline(parent,head)
715    local current = head
716    local done = false
717    local continue = false
718    local room, don, con, list
719    while current and nofinlined > 0 do
720        local id = getid(current)
721        if id == whatsit_code then
722            if getsubtype(current) == userdefined_code and getprop(current,"id") == inline_mark then
723                local n = getprop(current,"data")
724                local candidate = inlinestore[n]
725                if candidate then -- no vpack, as we want to realign
726                    inlinestore[n] = nil
727                    nofinlined = nofinlined - 1
728                    head, room, con = inject(parent,head,candidate) -- maybe return applied offset
729                    done      = true
730                    continue  = continue or con
731                    nofstored = nofstored - 1
732                    if room and room.stacked then
733                        -- for now we also check for inline+yes/continue, maybe someday no such check
734                        -- will happen; we can assume most inlines are one line heigh; also this
735                        -- together feature can become optional
736                        registertogether(parent,room)
737                    end
738                end
739            end
740        elseif id == hlist_code or id == vlist_code then
741            -- optional (but sometimes needed)
742            list, don, con = flushinline(current,getlist(current))
743            setlist(current,list)
744            continue = continue or con
745            done = done or don
746        end
747        current = getnext(current)
748    end
749    return head, done, continue
750end
751
752local function flushed(scope,parent) -- current is hlist
753    local head = getlist(parent)
754    local done = false
755    local continue = false
756    local room, con, don
757    for c=1,#categories do
758        local category = categories[c]
759        for l=1,#locations do
760            local location = locations[l]
761            local store = displaystore[category][location][scope]
762            if store then
763                while true do
764                    local candidate = remove(store,1) -- brr, local stores are sparse
765                    if candidate then -- no vpack, as we want to realign
766                        head, room, con = inject(parent,head,candidate)
767                        done      = true
768                        continue  = continue or con
769                        nofstored = nofstored - 1
770                        if room then
771                            registertogether(parent,room)
772                        end
773                    else
774                        break
775                    end
776                end
777            else
778             -- report_margindata("fatal error: invalid category %a",category or "?")
779            end
780        end
781    end
782    if nofinlined > 0 then
783        if done then
784            setlist(parent,head)
785        end
786        head, don, con = flushinline(parent,head)
787        continue = continue or con
788        done = done or don
789    end
790    if done then
791        local a = getattr(head,a_linenumber) -- hack .. we need a more decent critical attribute inheritance mechanism
792        if false then
793            local l = hpacknodes(head,getwidth(parent),"exactly")
794            setlist(parent,l)
795            if a then
796                setattr(l,a_linenumber,a)
797            end
798        else
799            -- because packing messes up profiling
800            setlist(parent,head)
801            if a then
802                setattr(parent,a_linenumber,a)
803            end
804        end
805    end
806    return done, continue
807end
808
809-- only when group   : vbox|vmode_par
810-- only when subtype : line, box (no indent alignment cell)
811
812local function handler(scope,head,group)
813   if nofstored > 0 then
814        if trace_margindata then
815            report_margindata("flushing stage one, stored %s, scope %s, delayed %s, group %a",nofstored,scope,nofdelayed,group)
816        end
817        local current = head
818        local done    = false -- for tracing only
819        while current do
820            local id = getid(current)
821            if (id == vlist_code or id == hlist_code) and getprop(current,"margindata") == nil then
822                local don, continue = flushed(scope,current)
823                if don then
824                    done = true
825                    setprop(current,"margindata",false) -- signal to prevent duplicate processing
826                    if continue then
827                        markovershoot(current)
828                    end
829                    if nofstored <= 0 then
830                        break
831                    end
832                end
833            end
834            current = getnext(current)
835        end
836        if trace_margindata then
837            if done then
838                report_margindata("flushing stage one, done, %s left",nofstored)
839            else
840                report_margindata("flushing stage one, nothing done, %s left",nofstored)
841            end
842        end
843        resetstacked()
844    end
845    return head
846end
847
848local trialtypesetting = context.trialtypesetting
849
850-- maybe change this to an action applied to the to be shipped out box (that is
851-- the mvl list in there so that we don't need to traverse global
852
853function margins.localhandler(head,group) -- sometimes group is "" which is weird
854
855    if trialtypesetting() then
856        return head
857    end
858
859    local inhibit = conditionals.inhibitmargindata
860    if inhibit then
861        if trace_margingroup then
862            report_margindata("ignored 3, group %a, stored %s, inhibit %a",group,nofstored,inhibit)
863        end
864        return head
865    end
866    if nofstored > 0 then
867        return handler(v_local,head,group)
868    end
869    if trace_margingroup then
870        report_margindata("ignored 4, group %a, stored %s, inhibit %a",group,nofstored,inhibit)
871    end
872    return head
873end
874
875function margins.globalhandler(head,group) -- check group
876
877    if trialtypesetting() then
878        return head, false
879    end
880
881    local inhibit = conditionals.inhibitmargindata
882    if inhibit or nofstored == 0 then
883        if trace_margingroup then
884            report_margindata("ignored 1, group %a, stored %s, inhibit %a",group,nofstored,inhibit)
885        end
886        return head
887    elseif group == "hmode_par" then
888        return handler(v_global,head,group)
889    elseif group == "vmode_par" then              -- experiment (for alignments)
890        return handler(v_global,head,group)
891     -- this needs checking as we then get quite some one liners to process and
892     -- we cannot look ahead then:
893    elseif group == "box" then                    -- experiment (for alignments)
894        return handler(v_global,head,group)
895    elseif group == "alignment" then              -- experiment (for alignments)
896        return handler(v_global,head,group)
897    else
898        if trace_margingroup then
899            report_margindata("ignored 2, group %a, stored %s, inhibit %a",group,nofstored,inhibit)
900        end
901        return head
902    end
903end
904
905local function finalhandler(head)
906    if nofdelayed > 0 then
907        local current = head
908        while current and nofdelayed > 0 do
909            local id = getid(current)
910            if id == hlist_code then -- only lines?
911                local a = getprop(current,"margindata")
912                if not a then
913                    finalhandler(getlist(current))
914                elseif realigned(current,a) then
915                    if nofdelayed == 0 then
916                        return head, true
917                    end
918                end
919            elseif id == vlist_code then
920                finalhandler(getlist(current))
921            end
922            current = getnext(current)
923        end
924    end
925    return head
926end
927
928function margins.finalhandler(head)
929    if nofdelayed > 0 then
930        if trace_margindata then
931            report_margindata("flushing stage two, instore: %s, delayed: %s",nofstored,nofdelayed)
932        end
933        head = finalhandler(head)
934        resetstacked(nofdelayed==0)
935    else
936        resetstacked()
937    end
938    return head
939end
940
941-- Somehow the vbox builder (in combinations) gets pretty confused and decides to
942-- go horizontal. So this needs more testing.
943
944enablelocal = function()
945    enableaction("finalizers", "typesetters.margins.localhandler")
946    enableaction("shipouts",   "typesetters.margins.finalhandler")
947    enablelocal = nil
948end
949
950enableglobal = function()
951    enableaction("mvlbuilders", "typesetters.margins.globalhandler")
952    enableaction("shipouts",    "typesetters.margins.finalhandler")
953    enableglobal = nil
954end
955
956statistics.register("margin data", function()
957    if nofsaved > 0 then
958        return format("%s entries, %s pending",nofsaved,nofdelayed)
959    else
960        return nil
961    end
962end)
963
964interfaces.implement {
965    name      = "savemargindata",
966    actions   = margins.save,
967    arguments = {
968        {
969           { "location" },
970           { "method" },
971           { "category" },
972           { "name" },
973           { "scope" },
974           { "number", "integer" },
975           { "margin" },
976           { "distance", "dimen" },
977           { "hoffset", "dimen" },
978           { "voffset", "dimen" },
979           { "dy", "dimen" },
980           { "bottomspace", "dimen" },
981           { "baseline"}, -- dimen or string or
982           { "threshold", "dimen" },
983           { "inline", "boolean" },
984           { "anchor" },
985        -- { "leftskip", "dimen" },
986        -- { "rightskip", "dimen" },
987           { "align" },
988           { "option" },
989           { "line", "integer" },
990           { "index", "integer" },
991           { "stackname" },
992           { "stack" },
993        }
994    }
995}
996