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