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