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