anch-pos.lua /size: 39 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['anch-pos'] = {
2    version   = 1.001,
3    comment   = "companion to anch-pos.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--[[ldx--
10<p>We save positional information in the main utility table. Not only
11can we store much more information in <l n='lua'/> but it's also
12more efficient.</p>
13--ldx]]--
14
15-- plus (extra) is obsolete but we will keep it for a while
16--
17-- maybe replace texsp by our own converter (stay at the lua end)
18-- eventually mp will have large numbers so we can use sp there too
19--
20-- this is one of the first modules using scanners and we need to replace it by
21-- implement and friends
22--
23-- we could have namespaces, like p, page, region, columnarea, textarea but then
24-- we need virtual table accessors as well as have tag/id accessors ... we don't
25-- save much here (at least not now)
26--
27-- This was the last module that got rid of directly setting scanners, with a little
28-- performance degradation but not that noticeable.
29
30local tostring, next, setmetatable, tonumber = tostring, next, setmetatable, tonumber
31local sort = table.sort
32local format, gmatch = string.format, string.gmatch
33local lpegmatch = lpeg.match
34local insert, remove = table.insert, table.remove
35local allocate = utilities.storage.allocate
36
37local report            = logs.reporter("positions")
38
39local scanners          = tokens.scanners
40local scanstring        = scanners.string
41local scaninteger       = scanners.integer
42local scandimen         = scanners.dimen
43
44local implement         = interfaces.implement
45
46local commands          = commands
47local context           = context
48
49local ctx_latelua       = context.latelua
50
51local tex               = tex
52local texgetcount       = tex.getcount
53local texgetinteger     = tex.getintegervalue or tex.getcount
54local texsetcount       = tex.setcount
55local texget            = tex.get
56local texsp             = tex.sp
57----- texsp             = string.todimen -- because we cache this is much faster but no rounding
58
59local setmetatableindex    = table.setmetatableindex
60local setmetatablenewindex = table.setmetatablenewindex
61
62local nuts              = nodes.nuts
63
64local setlink           = nuts.setlink
65local getlist           = nuts.getlist
66local setlist           = nuts.setlist
67local getbox            = nuts.getbox
68local getid             = nuts.getid
69local getwhd            = nuts.getwhd
70
71local hlist_code        = nodes.nodecodes.hlist
72
73local find_tail         = nuts.tail
74----- hpack             = nuts.hpack
75
76local new_latelua       = nuts.pool.latelua
77
78local variables         = interfaces.variables
79local v_text            = variables.text
80local v_column          = variables.column
81
82local pt                = number.dimenfactors.pt
83local pts               = number.pts
84local formatters        = string.formatters
85
86local collected         = allocate()
87local tobesaved         = allocate()
88
89local jobpositions = {
90    collected = collected,
91    tobesaved = tobesaved,
92}
93
94job.positions = jobpositions
95
96local default = { -- not r and paragraphs etc
97    __index = {
98        x   = 0,     -- x position baseline
99        y   = 0,     -- y position baseline
100        w   = 0,     -- width
101        h   = 0,     -- height
102        d   = 0,     -- depth
103        p   = 0,     -- page
104        n   = 0,     -- paragraph
105        ls  = 0,     -- leftskip
106        rs  = 0,     -- rightskip
107        hi  = 0,     -- hangindent
108        ha  = 0,     -- hangafter
109        hs  = 0,     -- hsize
110        pi  = 0,     -- parindent
111        ps  = false, -- parshape
112        dir = 0,
113    }
114}
115
116local f_b_tag     = formatters["b:%s"]
117local f_e_tag     = formatters["e:%s"]
118local f_p_tag     = formatters["p:%s"]
119local f_w_tag     = formatters["w:%s"]
120
121local f_region    = formatters["region:%s"]
122
123local f_tag_three = formatters["%s:%s:%s"]
124local f_tag_two   = formatters["%s:%s"]
125
126local nofregular  = 0
127local nofspecial  = 0
128local splitter    = lpeg.splitat(":",true)
129
130local pagedata    = { }
131local columndata  = setmetatableindex("table") -- per page
132local freedata    = setmetatableindex("table") -- per page
133
134local function initializer()
135    tobesaved = jobpositions.tobesaved
136    collected = jobpositions.collected
137    for tag, data in next, collected do
138        local prefix, rest = lpegmatch(splitter,tag)
139        if prefix == "p" then
140            nofregular = nofregular + 1
141        elseif prefix == "page" then
142            nofregular = nofregular + 1
143            pagedata[tonumber(rest) or 0] = data
144        elseif prefix == "free" then
145            nofspecial = nofspecial + 1
146            local t = freedata[data.p or 0]
147            t[#t+1] = data
148        elseif prefix == "columnarea" then
149            columndata[data.p or 0][data.c or 0] = data
150        end
151        setmetatable(data,default)
152    end
153    --
154    local pages = structures.pages.collected
155    if pages then
156        local last = nil
157        for p=1,#pages do
158            local region = "page:" .. p
159            local data   = pagedata[p]
160            local free   = freedata[p]
161            if free then
162                sort(free,function(a,b) return b.y < a.y end) -- order matters !
163            end
164            if data then
165                last      = data
166                last.free = free
167            elseif last then
168                local t = setmetatableindex({ free = free, p = p },last)
169                if not collected[region] then
170                    collected[region] = t
171                else
172                    -- something is wrong
173                end
174                pagedata[p] = t
175            end
176        end
177    end
178    jobpositions.pagedata   = pagedata
179end
180
181function jobpositions.used()
182    return next(collected) -- we can safe it
183end
184
185function jobpositions.getfree(page)
186    return freedata[page]
187end
188
189-- we can gain a little when we group positions but then we still have to
190-- deal with regions and cells so we either end up with lots of extra small
191-- tables pointing to them and/or assembling/disassembling so in the end
192-- it makes no sense to do it (now) and still have such a mix
193--
194-- proof of concept code removed ... see archive
195
196local function finalizer()
197    -- We make the (possible extensive) shape lists sparse working
198    -- from the end. We could also drop entries here that have l and
199    -- r the same which saves testing later on.
200    for k, v in next, tobesaved do
201        local s = v.s
202        if s then
203            for p, data in next, s do
204                local n = #data
205                if n > 1 then
206                    local ph = data[1][2]
207                    local pd = data[1][3]
208                    local xl = data[1][4]
209                    local xr = data[1][5]
210                    for i=2,n do
211                        local di = data[i]
212                        local h = di[2]
213                        local d = di[3]
214                        local l = di[4]
215                        local r = di[5]
216                        if r == xr then
217                            di[5] = nil
218                            if l == xl then
219                                di[4] = nil
220                                if d == pd then
221                                    di[3] = nil
222                                    if h == ph then
223                                        di[2] = nil
224                                    else
225                                        ph = h
226                                    end
227                                else
228                                    pd, ph = d, h
229                                end
230                            else
231                                ph, pd, xl = h, d, l
232                            end
233                        else
234                            ph, pd, xl, xr = h, d, l, r
235                        end
236                    end
237                end
238            end
239        end
240    end
241end
242
243job.register('job.positions.collected', tobesaved, initializer, finalizer)
244
245local regions    = { }
246local nofregions = 0
247local region     = nil
248
249local columns    = { }
250local nofcolumns = 0
251local column     = nil
252
253local nofpages   = nil
254
255-- beware ... we're not sparse here as lua will reserve slots for the nilled
256
257local getpos, gethpos, getvpos, getrpos
258
259function jobpositions.registerhandlers(t)
260    getpos  = t and t.getpos  or function() return 0, 0 end
261    getrpos = t and t.getrpos or function() return 0, 0, 0 end
262    gethpos = t and t.gethpos or function() return 0 end
263    getvpos = t and t.getvpos or function() return 0 end
264end
265
266function jobpositions.getpos () return getpos () end
267function jobpositions.getrpos() return getrpos() end
268function jobpositions.gethpos() return gethpos() end
269function jobpositions.getvpos() return getvpos() end
270
271-------- jobpositions.getcolumn() return column end
272
273jobpositions.registerhandlers()
274
275local function setall(name,p,x,y,w,h,d,extra)
276    tobesaved[name] = {
277        p = p,
278        x = x ~= 0 and x or nil,
279        y = y ~= 0 and y or nil,
280        w = w ~= 0 and w or nil,
281        h = h ~= 0 and h or nil,
282        d = d ~= 0 and d or nil,
283        e = extra ~= "" and extra or nil,
284        r = region,
285        c = column,
286        r2l = texgetinteger("inlinelefttoright") == 1 and true or nil,
287    }
288end
289
290local function enhance(data)
291    if not data then
292        return nil
293    end
294    if data.r == true then -- or ""
295        data.r = region
296    end
297    if data.x == true then
298        if data.y == true then
299            local x, y = getpos()
300            data.x = x ~= 0 and x or nil
301            data.y = y ~= 0 and y or nil
302        else
303            local x = gethpos()
304            data.x = x ~= 0 and x or nil
305        end
306    elseif data.y == true then
307        local y = getvpos()
308        data.y = y ~= 0 and y or nil
309    end
310    if data.p == true then
311        data.p = texgetcount("realpageno") -- we should use a variable set in otr
312    end
313    if data.c == true then
314        data.c = column
315    end
316    if data.w == 0 then
317        data.w = nil
318    end
319    if data.h == 0 then
320        data.h = nil
321    end
322    if data.d == 0 then
323        data.d = nil
324    end
325    return data
326end
327
328-- analyze some files (with lots if margindata) and then when one key optionally
329-- use that one instead of a table (so, a 3rd / 4th argument: key, e.g. "x")
330
331local function set(name,index,value) -- ,key
332    -- officially there should have been a settobesaved
333    local data = enhance(value or {})
334    if value then
335        container = tobesaved[name]
336        if not container then
337            tobesaved[name] = {
338                [index] = data
339            }
340        else
341            container[index] = data
342        end
343    else
344        tobesaved[name] = data
345    end
346end
347
348local function setspec(specification)
349    local name  = specification.name
350    local index = specification.index
351    local value = specification.value
352    local data  = enhance(value or {})
353    if value then
354        container = tobesaved[name]
355        if not container then
356            tobesaved[name] = {
357                [index] = data
358            }
359        else
360            container[index] = data
361        end
362    else
363        tobesaved[name] = data
364    end
365end
366
367local function get(id,index)
368    if index then
369        local container = collected[id]
370        return container and container[index]
371    else
372        return collected[id]
373    end
374end
375
376------------.setdim  = setdim
377jobpositions.setall  = setall
378jobpositions.set     = set
379jobpositions.setspec = setspec
380jobpositions.get     = get
381
382implement {
383    name      = "dosaveposition",
384    arguments = { "string", "integer", "dimen", "dimen" },
385    actions   = setall, -- name p x y
386}
387
388implement {
389    name      = "dosavepositionwhd",
390    arguments = { "string", "integer", "dimen", "dimen", "dimen", "dimen", "dimen" },
391    actions   = setall, -- name p x y w h d
392}
393
394implement {
395    name     = "dosavepositionplus",
396    arguments = { "string", "integer", "dimen", "dimen", "dimen", "dimen", "dimen", "string" },
397    actions   = setall,  -- name p x y w h d extra
398}
399
400-- will become private table (could also become attribute driven but too nasty
401-- as attributes can bleed e.g. in margin stuff)
402
403-- not much gain in keeping stack (inc/dec instead of insert/remove)
404
405local function b_column(specification)
406    local tag = specification.tag
407    local x = gethpos()
408    tobesaved[tag] = {
409        r = true,
410        x = x ~= 0 and x or nil,
411     -- w = 0,
412    }
413    insert(columns,tag)
414    column = tag
415end
416
417local function e_column()
418    local t = tobesaved[column]
419    if not t then
420        -- something's wrong
421    else
422        local x = gethpos() - t.x
423        t.w = x ~= 0 and x or nil
424        t.r = region
425    end
426    remove(columns)
427    column = columns[#columns]
428end
429
430jobpositions.b_column = b_column
431jobpositions.e_column = e_column
432
433implement {
434    name      = "bposcolumn",
435    arguments = "string",
436    actions   = function(tag)
437        insert(columns,tag)
438        column = tag
439    end
440}
441
442implement {
443    name      = "bposcolumnregistered",
444    arguments = "string",
445    actions   = function(tag)
446        insert(columns,tag)
447        column = tag
448        ctx_latelua { action = b_column, tag = tag }
449    end
450}
451
452implement {
453    name    = "eposcolumn",
454    actions = function()
455        remove(columns)
456        column = columns[#columns]
457    end
458}
459
460implement {
461    name    = "eposcolumnregistered",
462    actions = function()
463        ctx_latelua { action = e_column }
464        remove(columns)
465        column = columns[#columns]
466    end
467}
468
469-- regions
470
471local function b_region(specification)
472    local tag  = specification.tag or specification
473    local last = tobesaved[tag]
474    local x, y = getpos()
475    last.x = x ~= 0 and x or nil
476    last.y = y ~= 0 and y or nil
477    last.p = texgetcount("realpageno")
478    insert(regions,tag) -- todo: fast stack
479    region = tag
480end
481
482local function e_region(specification)
483    local last = tobesaved[region]
484    local y = getvpos()
485    local x, y = getpos()
486    if specification.correct then
487        local h = (last.y or 0) - y
488        last.h = h ~= 0 and h or nil
489    end
490    last.y = y ~= 0 and y or nil
491    remove(regions) -- todo: fast stack
492    region = regions[#regions]
493end
494
495jobpositions.b_region = b_region
496jobpositions.e_region = e_region
497
498local lastregion
499
500local function setregionbox(n,tag,k,lo,ro,to,bo,column) -- kind
501    if not tag or tag == "" then
502        nofregions = nofregions + 1
503        tag = f_region(nofregions)
504    end
505    local box = getbox(n)
506    local w, h, d = getwhd(box)
507    tobesaved[tag] = {
508     -- p  = texgetcount("realpageno"), -- we copy them
509        x  = 0,
510        y  = 0,
511        w  = w  ~= 0 and w  or nil,
512        h  = h  ~= 0 and h  or nil,
513        d  = d  ~= 0 and d  or nil,
514        k  = k  ~= 0 and k  or nil,
515        lo = lo ~= 0 and lo or nil,
516        ro = ro ~= 0 and ro or nil,
517        to = to ~= 0 and to or nil,
518        bo = bo ~= 0 and bo or nil,
519        c  = column         or nil,
520    }
521    lastregion = tag
522    return tag, box
523end
524
525local function markregionbox(n,tag,correct,...) -- correct needs checking
526    local tag, box = setregionbox(n,tag,...)
527     -- todo: check if tostring is needed with formatter
528    local push = new_latelua { action = b_region, tag = tag }
529    local pop  = new_latelua { action = e_region, correct = correct }
530    -- maybe we should construct a hbox first (needs experimenting) so that we can avoid some at the tex end
531    local head = getlist(box)
532    -- no, this fails with \framed[region=...] .. needs thinking
533 -- if getid(box) ~= hlist_code then
534 --  -- report("mark region box assumes a hlist, fix this for %a",tag)
535 --     head = hpack(head)
536 -- end
537    if head then
538        local tail = find_tail(head)
539        setlink(push,head)
540        setlink(tail,pop)
541    else -- we can have a simple push/pop
542        setlink(push,pop)
543    end
544    setlist(box,push)
545end
546
547jobpositions.markregionbox = markregionbox
548jobpositions.setregionbox  = setregionbox
549
550function jobpositions.enhance(name)
551    enhance(tobesaved[name])
552end
553
554function jobpositions.gettobesaved(name,tag)
555    local t = tobesaved[name]
556    if t and tag then
557        return t[tag]
558    else
559        return t
560    end
561end
562
563function jobpositions.settobesaved(name,tag,data)
564    local t = tobesaved[name]
565    if t and tag and data then
566        t[tag] = data
567    end
568end
569
570local nofparagraphs = 0
571
572implement {
573    name    = "parpos",
574    actions = function()
575        nofparagraphs = nofparagraphs + 1
576        texsetcount("global","c_anch_positions_paragraph",nofparagraphs)
577        local box = getbox("strutbox")
578        local w, h, d = getwhd(box)
579        local t = {
580            p  = true,
581            c  = true,
582            r  = true,
583            x  = true,
584            y  = true,
585            h  = h,
586            d  = d,
587            hs = texget("hsize"), -- never 0
588        }
589        local leftskip   = texget("leftskip",false)
590        local rightskip  = texget("rightskip",false)
591        local hangindent = texget("hangindent")
592        local hangafter  = texget("hangafter")
593        local parindent  = texget("parindent")
594        local parshape   = texget("parshape")
595        if leftskip ~= 0 then
596            t.ls = leftskip
597        end
598        if rightskip ~= 0 then
599            t.rs = rightskip
600        end
601        if hangindent ~= 0 then
602            t.hi = hangindent
603        end
604        if hangafter ~= 1 and hangafter ~= 0 then -- can not be zero .. so it needs to be 1 if zero
605            t.ha = hangafter
606        end
607        if parindent ~= 0 then
608            t.pi = parindent
609        end
610        if parshape and #parshape > 0 then
611            t.ps = parshape
612        end
613        local name = f_p_tag(nofparagraphs)
614        tobesaved[name] = t
615        ctx_latelua { action = enhance, specification = t }
616    end
617}
618
619implement {
620    name      = "dosetposition",
621    arguments = "string",
622    actions   = function(name)
623        local spec = {
624            p   = true,
625            c   = column,
626            r   = true,
627            x   = true,
628            y   = true,
629            n   = nofparagraphs > 0 and nofparagraphs or nil,
630            r2l = texgetinteger("inlinelefttoright") == 1 or nil,
631        }
632        tobesaved[name] = spec
633        ctx_latelua { action = enhance, specification = spec }
634    end
635}
636
637implement {
638    name      = "dosetpositionwhd",
639    arguments = { "string", "dimen", "dimen", "dimen" },
640    actions   = function(name,w,h,d)
641        local spec = {
642            p   = true,
643            c   = column,
644            r   = true,
645            x   = true,
646            y   = true,
647            w   = w ~= 0 and w or nil,
648            h   = h ~= 0 and h or nil,
649            d   = d ~= 0 and d or nil,
650            n   = nofparagraphs > 0 and nofparagraphs or nil,
651            r2l = texgetinteger("inlinelefttoright") == 1 or nil,
652        }
653        tobesaved[name] = spec
654        ctx_latelua { action = enhance, specification = spec }
655    end
656}
657
658implement {
659    name      = "dosetpositionbox",
660    arguments = { "string", "integer" },
661    actions   = function(name,n)
662        local box  = getbox(n)
663        local w, h, d = getwhd(box)
664        local spec = {
665            p = true,
666            c = column,
667            r = true,
668            x = true,
669            y = true,
670            w = w ~= 0 and w or nil,
671            h = h ~= 0 and h or nil,
672            d = d ~= 0 and d or nil,
673            n = nofparagraphs > 0 and nofparagraphs or nil,
674            r2l = texgetinteger("inlinelefttoright") == 1 or nil,
675        }
676        tobesaved[name] = spec
677        ctx_latelua { action = enhance, specification = spec }
678    end
679}
680
681implement {
682    name      = "dosetpositionplus",
683    arguments = { "string", "dimen", "dimen", "dimen" },
684    actions   = function(name,w,h,d)
685        local spec = {
686            p   = true,
687            c   = column,
688            r   = true,
689            x   = true,
690            y   = true,
691            w   = w ~= 0 and w or nil,
692            h   = h ~= 0 and h or nil,
693            d   = d ~= 0 and d or nil,
694            n   = nofparagraphs > 0 and nofparagraphs or nil,
695            e   = scanstring(),
696            r2l = texgetinteger("inlinelefttoright") == 1 or nil,
697        }
698        tobesaved[name] = spec
699        ctx_latelua { action = enhance, specification = spec }
700    end
701}
702
703implement {
704    name      = "dosetpositionstrut",
705    arguments = "string",
706    actions   = function(name)
707        local box = getbox("strutbox")
708        local w, h, d = getwhd(box)
709        local spec = {
710            p   = true,
711            c   = column,
712            r   = true,
713            x   = true,
714            y   = true,
715            h   = h ~= 0 and h or nil,
716            d   = d ~= 0 and d or nil,
717            n   = nofparagraphs > 0 and nofparagraphs or nil,
718            r2l = texgetinteger("inlinelefttoright") == 1 or nil,
719        }
720        tobesaved[name] = spec
721        ctx_latelua { action = enhance, specification = spec }
722    end
723}
724
725implement {
726    name      = "dosetpositionstrutkind",
727    arguments = { "string", "integer" },
728    actions   = function(name,kind)
729        local box = getbox("strutbox")
730        local w, h, d = getwhd(box)
731        local spec = {
732            k   = kind,
733            p   = true,
734            c   = column,
735            r   = true,
736            x   = true,
737            y   = true,
738            h   = h ~= 0 and h or nil,
739            d   = d ~= 0 and d or nil,
740            n   = nofparagraphs > 0 and nofparagraphs or nil,
741            r2l = texgetinteger("inlinelefttoright") == 1 or nil,
742        }
743        tobesaved[name] = spec
744        ctx_latelua { action = enhance, specification = spec }
745    end
746}
747
748function jobpositions.getreserved(tag,n)
749    if tag == v_column then
750        local fulltag = f_tag_three(tag,texgetcount("realpageno"),n or 1)
751        local data = collected[fulltag]
752        if data then
753            return data, fulltag
754        end
755        tag = v_text
756    end
757    if tag == v_text then
758        local fulltag = f_tag_two(tag,texgetcount("realpageno"))
759        return collected[fulltag] or false, fulltag
760    end
761    return collected[tag] or false, tag
762end
763
764function jobpositions.copy(target,source)
765    collected[target] = collected[source]
766end
767
768function jobpositions.replace(id,p,x,y,w,h,d)
769    collected[id] = { p = p, x = x, y = y, w = w, h = h, d = d } -- c g
770end
771
772local function getpage(id)
773    local jpi = collected[id]
774    return jpi and jpi.p
775end
776
777local function getcolumn(id)
778    local jpi = collected[id]
779    return jpi and jpi.c or false
780end
781
782local function getparagraph(id)
783    local jpi = collected[id]
784    return jpi and jpi.n
785end
786
787local function getregion(id)
788    local jpi = collected[id]
789    if jpi then
790        local r = jpi.r
791        if r then
792            return r
793        end
794        local p = jpi.p
795        if p then
796            return "page:" .. p
797        end
798    end
799    return false
800end
801
802jobpositions.page      = getpage
803jobpositions.column    = getcolumn
804jobpositions.paragraph = getparagraph
805jobpositions.region    = getregion
806
807jobpositions.p = getpage      -- not used, kind of obsolete
808jobpositions.c = getcolumn    -- idem
809jobpositions.n = getparagraph -- idem
810jobpositions.r = getregion    -- idem
811
812function jobpositions.x(id)
813    local jpi = collected[id]
814    return jpi and jpi.x
815end
816
817function jobpositions.y(id)
818    local jpi = collected[id]
819    return jpi and jpi.y
820end
821
822function jobpositions.width(id)
823    local jpi = collected[id]
824    return jpi and jpi.w
825end
826
827function jobpositions.height(id)
828    local jpi = collected[id]
829    return jpi and jpi.h
830end
831
832function jobpositions.depth(id)
833    local jpi = collected[id]
834    return jpi and jpi.d
835end
836
837function jobpositions.whd(id)
838    local jpi = collected[id]
839    if jpi then
840        return jpi.h, jpi.h, jpi.d
841    end
842end
843
844function jobpositions.leftskip(id)
845    local jpi = collected[id]
846    return jpi and jpi.ls
847end
848
849function jobpositions.rightskip(id)
850    local jpi = collected[id]
851    return jpi and jpi.rs
852end
853
854function jobpositions.hsize(id)
855    local jpi = collected[id]
856    return jpi and jpi.hs
857end
858
859function jobpositions.parindent(id)
860    local jpi = collected[id]
861    return jpi and jpi.pi
862end
863
864function jobpositions.hangindent(id)
865    local jpi = collected[id]
866    return jpi and jpi.hi
867end
868
869function jobpositions.hangafter(id)
870    local jpi = collected[id]
871    return jpi and jpi.ha or 1
872end
873
874function jobpositions.xy(id)
875    local jpi = collected[id]
876    if jpi then
877        return jpi.x, jpi.y
878    else
879        return 0, 0
880    end
881end
882
883function jobpositions.lowerleft(id)
884    local jpi = collected[id]
885    if jpi then
886        return jpi.x, jpi.y - jpi.d
887    else
888        return 0, 0
889    end
890end
891
892function jobpositions.lowerright(id)
893    local jpi = collected[id]
894    if jpi then
895        return jpi.x + jpi.w, jpi.y - jpi.d
896    else
897        return 0, 0
898    end
899end
900
901function jobpositions.upperright(id)
902    local jpi = collected[id]
903    if jpi then
904        return jpi.x + jpi.w, jpi.y + jpi.h
905    else
906        return 0, 0
907    end
908end
909
910function jobpositions.upperleft(id)
911    local jpi = collected[id]
912    if jpi then
913        return jpi.x, jpi.y + jpi.h
914    else
915        return 0, 0
916    end
917end
918
919function jobpositions.position(id)
920    local jpi = collected[id]
921    if jpi then
922        return jpi.p, jpi.x, jpi.y, jpi.w, jpi.h, jpi.d
923    else
924        return 0, 0, 0, 0, 0, 0
925    end
926end
927
928local splitter = lpeg.splitat(",")
929
930function jobpositions.extra(id,n,default) -- assume numbers
931    local jpi = collected[id]
932    if jpi then
933        local e = jpi.e
934        if e then
935            local split = jpi.split
936            if not split then
937                split = lpegmatch(splitter,jpi.e)
938                jpi.split = split
939            end
940            return texsp(split[n]) or default -- watch the texsp here
941        end
942    end
943    return default
944end
945
946local function overlapping(one,two,overlappingmargin) -- hm, strings so this is wrong .. texsp
947    one = collected[one]
948    two = collected[two]
949    if one and two and one.p == two.p then
950        if not overlappingmargin then
951            overlappingmargin = 2
952        end
953        local x_one = one.x
954        local x_two = two.x
955        local w_two = two.w
956        local llx_one = x_one         - overlappingmargin
957        local urx_two = x_two + w_two + overlappingmargin
958        if llx_one > urx_two then
959            return false
960        end
961        local w_one = one.w
962        local urx_one = x_one + w_one + overlappingmargin
963        local llx_two = x_two         - overlappingmargin
964        if urx_one < llx_two then
965            return false
966        end
967        local y_one = one.y
968        local y_two = two.y
969        local d_one = one.d
970        local h_two = two.h
971        local lly_one = y_one - d_one - overlappingmargin
972        local ury_two = y_two + h_two + overlappingmargin
973        if lly_one > ury_two then
974            return false
975        end
976        local h_one = one.h
977        local d_two = two.d
978        local ury_one = y_one + h_one + overlappingmargin
979        local lly_two = y_two - d_two - overlappingmargin
980        if ury_one < lly_two then
981            return false
982        end
983        return true
984    end
985end
986
987local function onsamepage(list,page)
988    for id in gmatch(list,"(, )") do
989        local jpi = collected[id]
990        if jpi then
991            local p = jpi.p
992            if not p then
993                return false
994            elseif not page then
995                page = p
996            elseif page ~= p then
997                return false
998            end
999        end
1000    end
1001    return page
1002end
1003
1004local function columnofpos(realpage,xposition)
1005    local p = columndata[realpage]
1006    if p then
1007        for i=1,#p do
1008            local c = p[i]
1009            local x = c.x or 0
1010            local w = c.w or 0
1011            if xposition >= x and xposition <= (x + w) then
1012                return i
1013            end
1014        end
1015    end
1016    return 1
1017end
1018
1019jobpositions.overlapping = overlapping
1020jobpositions.onsamepage  = onsamepage
1021jobpositions.columnofpos = columnofpos
1022
1023-- interface
1024
1025implement {
1026    name      = "replacepospxywhd",
1027    arguments = { "string", "integer", "dimen", "dimen", "dimen", "dimen", "dimen" },
1028    actions   = function(name,page,x,y,w,h,d)
1029        collected[name] = {
1030            p = page,
1031            x = x,
1032            y = y,
1033            w = w,
1034            h = h,
1035            d = d,
1036        }
1037    end
1038}
1039
1040implement {
1041    name      = "copyposition",
1042    arguments = "2 strings",
1043    actions   = function(target,source)
1044        collected[target] = collected[source]
1045    end
1046}
1047
1048implement {
1049    name      = "MPp",
1050    arguments = "string",
1051    actions   = function(name)
1052        local jpi = collected[name]
1053        if jpi then
1054            local p = jpi.p
1055            if p and p ~= true then
1056                context(p)
1057                return
1058            end
1059        end
1060        context('0')
1061    end
1062}
1063
1064implement {
1065    name      = "MPx",
1066    arguments = "string",
1067    actions   = function(name)
1068        local jpi = collected[name]
1069        if jpi then
1070            local x = jpi.x
1071            if x and x ~= true and x ~= 0 then
1072                context("%.5Fpt",x*pt)
1073                return
1074            end
1075        end
1076        context('0pt')
1077    end
1078}
1079
1080implement {
1081    name      = "MPy",
1082    arguments = "string",
1083    actions   = function(name)
1084        local jpi = collected[name]
1085        if jpi then
1086            local y = jpi.y
1087            if y and y ~= true and y ~= 0 then
1088                context("%.5Fpt",y*pt)
1089                return
1090            end
1091        end
1092        context('0pt')
1093    end
1094}
1095
1096implement {
1097    name      = "MPw",
1098    arguments = "string",
1099    actions   = function(name)
1100        local jpi = collected[name]
1101        if jpi then
1102            local w = jpi.w
1103            if w and w ~= 0 then
1104                context("%.5Fpt",w*pt)
1105                return
1106            end
1107        end
1108        context('0pt')
1109    end
1110}
1111
1112implement {
1113    name      = "MPh",
1114    arguments = "string",
1115    actions   = function(name)
1116        local jpi = collected[name]
1117        if jpi then
1118            local h = jpi.h
1119            if h and h ~= 0 then
1120                context("%.5Fpt",h*pt)
1121                return
1122            end
1123        end
1124        context('0pt')
1125    end
1126}
1127
1128implement {
1129    name      = "MPd",
1130    arguments = "string",
1131    actions   = function(name)
1132        local jpi = collected[name]
1133        if jpi then
1134            local d = jpi.d
1135            if d and d ~= 0 then
1136                context("%.5Fpt",d*pt)
1137                return
1138            end
1139        end
1140        context('0pt')
1141    end
1142}
1143
1144implement {
1145    name      = "MPxy",
1146    arguments = "string",
1147    actions   = function(name)
1148        local jpi = collected[name]
1149        if jpi then
1150            context('(%.5Fpt,%.5Fpt)',
1151                jpi.x*pt,
1152                jpi.y*pt
1153            )
1154        else
1155            context('(0,0)')
1156        end
1157    end
1158}
1159
1160implement {
1161    name      = "MPwhd",
1162    arguments = "string",
1163    actions   = function(name)
1164        local jpi = collected[name]
1165        if jpi then
1166            local w = jpi.w or 0
1167            local h = jpi.h or 0
1168            local d = jpi.d or 0
1169            if w ~= 0 or h ~= 0 or d ~= 0 then
1170                context("%.5Fpt,%.5Fpt,%.5Fpt",w*pt,h*pt,d*pt)
1171                return
1172            end
1173        end
1174        context('0pt,0pt,0pt')
1175    end
1176}
1177
1178implement {
1179    name      = "MPll",
1180    arguments = "string",
1181    actions   = function(name)
1182        local jpi = collected[name]
1183        if jpi then
1184            context('(%.5Fpt,%.5Fpt)',
1185                 jpi.x       *pt,
1186                (jpi.y-jpi.d)*pt
1187            )
1188        else
1189            context('(0,0)') -- for mp only
1190        end
1191    end
1192}
1193
1194implement {
1195    name      = "MPlr",
1196    arguments = "string",
1197    actions   = function(name)
1198        local jpi = collected[name]
1199        if jpi then
1200            context('(%.5Fpt,%.5Fpt)',
1201                (jpi.x + jpi.w)*pt,
1202                (jpi.y - jpi.d)*pt
1203            )
1204        else
1205            context('(0,0)') -- for mp only
1206        end
1207    end
1208}
1209
1210implement {
1211    name      = "MPur",
1212    arguments = "string",
1213    actions   = function(name)
1214        local jpi = collected[name]
1215        if jpi then
1216            context('(%.5Fpt,%.5Fpt)',
1217                (jpi.x + jpi.w)*pt,
1218                (jpi.y + jpi.h)*pt
1219            )
1220        else
1221            context('(0,0)') -- for mp only
1222        end
1223    end
1224}
1225
1226implement {
1227    name      = "MPul",
1228    arguments = "string",
1229    actions   = function(name)
1230        local jpi = collected[name]
1231        if jpi then
1232            context('(%.5Fpt,%.5Fpt)',
1233                 jpi.x         *pt,
1234                (jpi.y + jpi.h)*pt
1235            )
1236        else
1237            context('(0,0)') -- for mp only
1238        end
1239    end
1240}
1241
1242local function MPpos(id)
1243    local jpi = collected[id]
1244    if jpi then
1245        local p = jpi.p
1246        if p then
1247            context("%s,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt",
1248                p,
1249                jpi.x*pt,
1250                jpi.y*pt,
1251                jpi.w*pt,
1252                jpi.h*pt,
1253                jpi.d*pt
1254            )
1255            return
1256        end
1257    end
1258    context('0,0,0,0,0,0') -- for mp only
1259end
1260
1261implement {
1262    name      = "MPpos",
1263    arguments = "string",
1264    actions   = MPpos
1265}
1266
1267implement {
1268    name      = "MPn",
1269    arguments = "string",
1270    actions   = function(name)
1271        local jpi = collected[name]
1272        if jpi then
1273            local n = jpi.n
1274            if n then
1275                context(n)
1276                return
1277            end
1278        end
1279        context(0)
1280    end
1281}
1282
1283implement {
1284    name      = "MPc",
1285    arguments = "string",
1286    actions   = function(name)
1287        local jpi = collected[name]
1288        if jpi then
1289            local c = jpi.c
1290            if c and c ~= true  then
1291                context(c)
1292                return
1293            end
1294        end
1295        context('0') -- okay ?
1296    end
1297}
1298
1299implement {
1300    name      = "MPr",
1301    arguments = "string",
1302    actions   = function(name)
1303        local jpi = collected[name]
1304        if jpi then
1305            local r = jpi.r
1306            if r and r ~= true  then
1307                context(r)
1308                return
1309            end
1310            local p = jpi.p
1311            if p and p ~= true then
1312                context("page:" .. p)
1313            end
1314        end
1315    end
1316}
1317
1318local function MPpardata(id)
1319    local t = collected[id]
1320    if not t then
1321        local tag = f_p_tag(id)
1322        t = collected[tag]
1323    end
1324    if t then
1325        context("%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%s,%.5Fpt",
1326            t.hs*pt,
1327            t.ls*pt,
1328            t.rs*pt,
1329            t.hi*pt,
1330            t.ha,
1331            t.pi*pt
1332        )
1333    else
1334        context("0,0,0,0,0,0") -- for mp only
1335    end
1336end
1337
1338implement {
1339    name      = "MPpardata",
1340    arguments = "string",
1341    actions   = MPpardata
1342}
1343
1344implement {
1345    name      = "MPposset",
1346    arguments = "string",
1347    actions   = function(name)
1348        local b = f_b_tag(name)
1349        local e = f_e_tag(name)
1350        local w = f_w_tag(name)
1351        local p = f_p_tag(getparagraph(b))
1352        MPpos(b) context(",") MPpos(e) context(",") MPpos(w) context(",") MPpos(p) context(",") MPpardata(p)
1353    end
1354}
1355
1356implement {
1357    name      = "MPls",
1358    arguments = "string",
1359    actions   = function(name)
1360        local jpi = collected[name]
1361        if jpi then
1362            context("%.5Fpt",jpi.ls*pt)
1363        else
1364            context("0pt")
1365        end
1366    end
1367}
1368
1369implement {
1370    name      = "MPrs",
1371    arguments = "string",
1372    actions   = function(name)
1373        local jpi = collected[name]
1374        if jpi then
1375            context("%.5Fpt",jpi.rs*pt)
1376        else
1377            context("0pt")
1378        end
1379    end
1380}
1381
1382local splitter = lpeg.tsplitat(",")
1383
1384implement {
1385    name      = "MPplus",
1386    arguments = { "string", "integer", "string" },
1387    actions   = function(name,n,default)
1388        local jpi = collected[name]
1389        if jpi then
1390            local e = jpi.e
1391            if e then
1392                local split = jpi.split
1393                if not split then
1394                    split = lpegmatch(splitter,jpi.e)
1395                    jpi.split = split
1396                end
1397                context(split[n] or default)
1398                return
1399            end
1400        end
1401        context(default)
1402    end
1403}
1404
1405implement {
1406    name      = "MPrest",
1407    arguments = { "string", "string" },
1408    actions   = function(name,default)
1409        local jpi = collected[name]
1410        context(jpi and jpi.e or default)
1411    end
1412}
1413
1414implement {
1415    name      = "MPxywhd",
1416    arguments = "string",
1417    actions   = function(name)
1418        local jpi = collected[name]
1419        if jpi then
1420            context("%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt",
1421                jpi.x*pt,
1422                jpi.y*pt,
1423                jpi.w*pt,
1424                jpi.h*pt,
1425                jpi.d*pt
1426            )
1427        else
1428            context("0,0,0,0,0") -- for mp only
1429        end
1430    end
1431}
1432
1433local doif     = commands.doif
1434local doifelse = commands.doifelse
1435
1436implement {
1437    name      = "doifelseposition",
1438    arguments = "string",
1439    actions   = function(name)
1440        doifelse(collected[name])
1441    end
1442}
1443
1444implement {
1445    name      = "doifposition",
1446    arguments = "string",
1447    actions   = function(name)
1448        doif(collected[name])
1449    end
1450}
1451
1452implement {
1453    name      = "doifelsepositiononpage",
1454    arguments = { "string", "integer" },
1455    actions   = function(name,p)
1456        local c = collected[name]
1457        doifelse(c and c.p == p)
1458    end
1459}
1460
1461implement {
1462    name      = "doifelseoverlapping",
1463    arguments = { "string", "string" },
1464    actions   = function(one,two)
1465        doifelse(overlapping(one,two))
1466    end
1467}
1468
1469implement {
1470    name      = "doifelsepositionsonsamepage",
1471    arguments = "string",
1472    actions   = function(list)
1473        doifelse(onsamepage(list))
1474    end
1475}
1476
1477implement {
1478    name      = "doifelsepositionsonthispage",
1479    arguments = "string",
1480    actions   = function(list)
1481        doifelse(onsamepage(list,tostring(texgetcount("realpageno"))))
1482    end
1483}
1484
1485implement {
1486    name      = "doifelsepositionsused",
1487    actions   = function()
1488        doifelse(next(collected))
1489    end
1490}
1491
1492implement {
1493    name      = "markregionbox",
1494    arguments = "integer",
1495    actions   = markregionbox
1496}
1497
1498implement {
1499    name      = "setregionbox",
1500    arguments = "integer",
1501    actions   = setregionbox
1502}
1503
1504implement {
1505    name      = "markregionboxtagged",
1506    arguments = { "integer", "string" },
1507    actions   = markregionbox
1508}
1509
1510implement {
1511    name      = "markregionboxtaggedn",
1512    arguments = { "integer", "string", "integer" },
1513    actions   = function(box,tag,n)
1514        markregionbox(box,tag,nil,nil,nil,nil,nil,nil,n)
1515    end
1516}
1517
1518implement {
1519    name      = "setregionboxtagged",
1520    arguments = { "integer", "string" },
1521    actions   = setregionbox
1522}
1523
1524implement {
1525    name      = "markregionboxcorrected",
1526    arguments = { "integer", "string", true },
1527    actions   = markregionbox
1528}
1529
1530implement {
1531    name      = "markregionboxtaggedkind",
1532    arguments = { "integer", "string", "integer", "dimen", "dimen", "dimen", "dimen" },
1533    actions   = function(box,tag,n,d1,d2,d3,d4)
1534        markregionbox(box,tag,nil,n,d1,d2,d3,d4)
1535    end
1536}
1537
1538implement {
1539    name    = "reservedautoregiontag",
1540    actions = function()
1541        nofregions = nofregions + 1
1542        context(f_region(nofregions))
1543    end
1544}
1545
1546-- statistics (at least for the moment, when testing)
1547
1548-- statistics.register("positions", function()
1549--     local total = nofregular + nofusedregions + nofmissingregions
1550--     if total > 0 then
1551--         return format("%s collected, %s regulars, %s regions, %s unresolved regions",
1552--             total, nofregular, nofusedregions, nofmissingregions)
1553--     else
1554--         return nil
1555--     end
1556-- end)
1557
1558statistics.register("positions", function()
1559    local total = nofregular + nofspecial
1560    if total > 0 then
1561        return format("%s collected, %s regular, %s special",total,nofregular,nofspecial)
1562    else
1563        return nil
1564    end
1565end)
1566
1567-- We support the low level positional commands too:
1568
1569local newsavepos = nodes.pool.savepos
1570
1571implement { name = "savepos",  actions = function() context(newsavepos()) end }
1572implement { name = "lastxpos", actions = function() context(gethpos()) end }
1573implement { name = "lastypos", actions = function() context(getvpos()) end }
1574