anch-pos.lmt /size: 67 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. In the meantime these
11-- files have become quite large. In some cases that get noticed by a hickup in the
12-- start and/or finish, but that is the price we pay for progress.
13--
14-- This was the last module that got rid of directly setting scanners, with a little
15-- performance degradation but not that noticeable. It is also a module that has been
16-- on the (partial) redo list for a while.
17--
18-- We can gain a little when we group positions but then we still have to deal with
19-- regions and cells so we either end up with lots of extra small tables pointing to
20-- them and/or assembling/disassembling. I played with that and rejected the idea
21-- until I ran into a test case where we had 50.000 one line paragraphs in an eight
22-- columns setup, and there we save 25 M on a 75 M tuc file. So, I played a bit more
23-- and we can have a solution that is of similar performance for regular documents
24-- (in spite of the extra overhead) but also works ok for the large files. In normal
25-- documents it is never a problem, but there are always exceptions to the normal and
26-- often these are also cases where positions are not really used but end up in the
27-- tuc file anyway.
28--
29-- Currently (because we never had split tags) we do splitting at access time, which
30-- is sort of inefficient but still ok. Much of this mechanism comes from MkII where
31-- TeX was the bottleneck.
32--
33-- By grouping we need to set more metatable so in the end that is also overhead
34-- but on the average we're okay because excessive serialization also comes at a price
35-- and this way we also delay some activities till the moment it is realy used (which
36-- is not always the case with positional information. We will make this transition
37-- stepwise so till we're done there will be inefficiencies and overhead.
38--
39-- The pure hash based variant combined with filtering is in anch-pos.lua and previous
40-- lmt versions! That is the reference.
41
42local tostring, next, setmetatable, tonumber, rawget, rawset = tostring, next, setmetatable, tonumber, rawget, rawset
43local sort, sortedhash = table.sort, table.sortedhash
44local format, gmatch = string.format, string.gmatch
45local P, R, C, Cc, lpegmatch = lpeg.P, lpeg.R, lpeg.C, lpeg.Cc, lpeg.match
46local insert, remove = table.insert, table.remove
47local allocate = utilities.storage.allocate
48local setmetatableindex, setmetatablenewindex = table.setmetatableindex, table.setmetatablenewindex
49
50local report            = logs.reporter("positions")
51
52local scanners          = tokens.scanners
53local scanstring        = scanners.string
54local scaninteger       = scanners.integer
55local scandimen         = scanners.dimen
56
57local implement         = interfaces.implement
58
59local commands          = commands
60local context           = context
61
62local ctx_latelua       = context.latelua
63
64local ctx_doif          = commands.doif
65local ctx_doifelse      = commands.doifelse
66
67local tex               = tex
68local texgetdimen       = tex.getdimen
69local texgetcount       = tex.getcount
70local texgetinteger     = tex.getintegervalue or tex.getcount
71local texiscount        = tex.iscount
72local texisdimen        = tex.isdimen
73local texsetcount       = tex.setcount
74local texget            = tex.get
75local texsp             = tex.sp
76----- texsp             = string.todimen -- because we cache this is much faster but no rounding
77local texgetnest        = tex.getnest
78local texgetparstate    = tex.getparstate
79
80local nuts              = nodes.nuts
81local tonut             = nodes.tonut
82
83local setlink           = nuts.setlink
84local getlist           = nuts.getlist
85local setlist           = nuts.setlist
86local getbox            = nuts.getbox
87local getid             = nuts.getid
88local getwhd            = nuts.getwhd
89local setprop           = nuts.setprop
90
91local getparstate       = nuts.getparstate
92
93local hlist_code        = nodes.nodecodes.hlist
94local par_code          = nodes.nodecodes.par
95
96local find_tail         = nuts.tail
97----- hpack             = nuts.hpack
98
99local new_latelua       = nuts.pool.latelua
100
101local variables         = interfaces.variables
102local v_text            = variables.text
103local v_column          = variables.column
104
105local pt                = number.dimenfactors.pt
106local pts               = number.pts
107local formatters        = string.formatters
108
109local collected         = allocate()
110local tobesaved         = allocate()
111local positionsused     = nil
112
113local jobpositions = {
114    collected = collected,
115    tobesaved = tobesaved,
116}
117
118job.positions = jobpositions
119
120local default = { -- not r and paragraphs etc
121    __index = {
122        x   = 0,     -- x position baseline
123        y   = 0,     -- y position baseline
124        w   = 0,     -- width
125        h   = 0,     -- height
126        d   = 0,     -- depth
127        p   = 0,     -- page
128        n   = 0,     -- paragraph
129        ls  = 0,     -- leftskip
130        rs  = 0,     -- rightskip
131        hi  = 0,     -- hangindent
132        ha  = 0,     -- hangafter
133        hs  = 0,     -- hsize
134        pi  = 0,     -- parindent
135        ps  = false, -- parshape
136        dir = 0,     -- obsolete
137        r2l = false, -- righttoleft
138    }
139}
140
141local f_b_tag      = formatters["b:%s"]
142local f_e_tag      = formatters["e:%s"]
143local f_p_tag      = formatters["p:%s"]
144----- f_w_tag      = formatters["w:%s"]
145
146local f_region     = formatters["region:%s"]
147
148local f_tag_three  = formatters["%s:%s:%s"]
149local f_tag_two    = formatters["%s:%s"]
150
151local c_realpageno = texiscount("realpageno")
152local d_strutht    = texisdimen("strutht")
153local d_strutdp    = texisdimen("strutdp")
154
155-- Because positions are set with a delay we cannot yet make the tree -- so that
156-- is a finalizer step. But, we already have a dual split.
157
158local treemode = false
159local treemode = true
160
161local function checkshapes(s)
162    for p, data in next, s do
163        local n = #data
164        if n > 1 then
165            local d1 = data[1]
166            local ph = d1[2]
167            local pd = d1[3]
168            local xl = d1[4]
169            local xr = d1[5]
170            for i=2,n do
171                local di = data[i]
172                local h = di[2]
173                local d = di[3]
174                local l = di[4]
175                local r = di[5]
176                if r == xr then
177                    di[5] = nil
178                    if l == xl then
179                        di[4] = nil
180                        if d == pd then
181                            di[3] = nil
182                            if h == ph then
183                                di[2] = nil
184                            else
185                                ph = h
186                            end
187                        else
188                            pd, ph = d, h
189                        end
190                    else
191                        ph, pd, xl = h, d, l
192                    end
193                else
194                    ph, pd, xl, xr = h, d, l, r
195                end
196            end
197        end
198    end
199end
200
201local columndata = { }
202local freedata   = { }       -- we can make these weak
203local syncdata   = { }       -- we can make these weak
204local columndone = false
205
206if treemode then
207
208    -- At some point we can install extra ones. I actually was halfway making a more
209    -- general installer but we have quite some distinct handling down here and it
210    -- became messy. So I rolled that back. Also, users and modules will quite likely
211    -- stay in the "user" namespace.
212
213    -- syncpos    : indirect access via helper, todo after we switch: direct setters
214    -- free       : indirect access via helper, todo after we switch: direct setters
215    -- columnarea : indirect access via helper, todo after we switch: direct setters
216
217    -- todo: keep track of total and check that against # (sanity check)
218
219    local prefix_number       = { "text", "textarea", "page", "p", "free", "columnarea" }
220    local prefix_label_number = { "syncpos" }
221    local prefix_number_rest  = { "region", "b", "e" }
222
223    -- no need to split: syncpos free columnarea (textarea?)
224
225    local function splitter_pattern()
226        local p_number = R("09")^1/tonumber
227        local p_colon  = P(":")
228        local p_label  = C(P(1 - p_colon)^0)
229        local p_rest   = C(P(1)^0)
230        return
231            C(lpeg.utfchartabletopattern(prefix_number      )) * p_colon * p_number * P(-1)
232          + C(lpeg.utfchartabletopattern(prefix_label_number)) * p_colon * (p_number + p_label) * p_colon * p_number * P(-1)
233          + C(lpeg.utfchartabletopattern(prefix_number_rest )) * p_colon * (p_number + p_rest)
234          + Cc("user")                                         * p_rest
235    end
236
237    -- In the end these metatable entries are not more efficient than copying
238    -- but it's all about making sure that the tuc file doesn't explode.
239
240    columndata = { }
241    columndone = false
242
243    local deltapacking = true -- so we can see the difference
244--     local deltapacking = false -- so we can see the difference
245
246    local function checkcommondata(v,common)
247        if common then
248            local i = v.i
249            local t = common[i]
250            if t then
251v.i = nil
252                local m = t.mt
253                if not m then
254                    setmetatable(t,default)
255                    m = { __index = t }
256                    t.mt = m
257                end
258                setmetatable(v,m)
259                return
260            end
261        end
262        setmetatable(v,default)
263    end
264
265    local function initializer()
266        tobesaved = jobpositions.tobesaved
267        collected = jobpositions.collected
268        --
269        local p_splitter = splitter_pattern()
270        --
271        local list = nil
272        --
273        local shared        = setmetatableindex(rawget(collected,"shared"),"table")
274        local x_y_w_h_list  = shared.x_y_w_h
275        local y_w_h_d_list  = shared.y_w_h_d
276        local x_h_d_list    = shared.x_h_d
277        local x_h_d_hs_list = shared.x_h_d_hs
278        --
279        columndata = setmetatableindex(function(t,k)
280            setmetatableindex(t,"table")
281            list = rawget(collected,"columnarea")
282            if list then
283             -- for tag, data in next, list do
284                for i=1,#list do
285                    local data = list[i]
286                    columndata[data.p or 0][data.c or 0] = data
287                    checkcommondata(data,y_w_h_d_list)
288                end
289            end
290            columndone = true
291            return t[k]
292        end)
293        --
294        -- todo: use a raw collected and a weak proxy
295        --
296        setmetatableindex(collected,function(t,k)
297            if k ~= true then
298                local prefix, one, two = lpegmatch(p_splitter,k)
299                local list = rawget(t,prefix)
300                if list and type(list) == "table" then
301                    local v = list[one] or false
302                    if v then
303                        if prefix == "p" then
304                         -- if deltapacking and type(v) == "number" then
305                            if type(v) == "number" then
306                                for i=one,1,-1 do
307                                    local l = list[i]
308                                    if type(l) ~= "number" then
309                                        if not getmetatable(l) then
310                                            checkcommondata(l,x_h_d_hs_list)
311                                        end
312                                        v = setmetatable({ y = v }, { __index = l })
313                                        list[one] = v
314                                        break
315                                    end
316                                end
317                            else
318                                checkcommondata(v,x_h_d_hs_list)
319                            end
320                        elseif prefix == "text" or prefix == "textarea" then
321                            if type(v) == "number" then
322                                for i=one,1,-1 do
323                                    local l = list[i]
324                                    if type(l) ~= "number" then
325                                        if not getmetatable(l) then
326                                            checkcommondata(l,x_y_w_h_list)
327                                        end
328                                        v = setmetatable({ p = v }, { __index = l })
329                                        list[one] = v
330                                        break
331                                    end
332                                end
333                            else
334                                checkcommondata(v,x_y_w_h_list)
335                            end
336                        elseif prefix == "columnarea" then
337                            if not columndone then
338                                checkcommondata(v,y_w_h_d_list)
339                            end
340                        elseif prefix == "syncpos" then
341                            -- will become an error
342                            if two then
343                             -- v = syncdata[one][two] or { }
344                                v = v[two] or { }
345                            else
346                                v = { }
347                            end
348                         -- for j=1,#v do
349                         --     checkcommondata(v[j],x_h_d_list)
350                         -- end
351                        elseif prefix == "free" then
352                            -- will become an error
353                        elseif prefix == "page" then
354                            checkcommondata(v)
355                        else
356                            checkcommondata(v)
357                        end
358                    else
359                        if prefix == "page" then
360                            for i=one,1,-1 do
361                                local data = list[i]
362                                if data then
363                                    v = setmetatableindex({ free = free or false, p = p },last)
364                                    list[one] = v
365                                    break
366                                end
367                            end
368                        end
369                    end
370                    t[k] = v
371                    return v
372                end
373            end
374            t[k] = false
375            return false
376        end)
377        --
378        setmetatableindex(tobesaved,function(t,k)
379            local prefix, one, two = lpegmatch(p_splitter,k)
380            local v = rawget(t,prefix)
381            if v and type(v) == "table" then
382                v = v[one]
383                if v and two then
384                    v = v[two]
385                end
386                return v -- or default
387            else
388             -- return default
389            end
390        end)
391        --
392        setmetatablenewindex(tobesaved,function(t,k,v)
393            local prefix, one, two = lpegmatch(p_splitter,k)
394            local p = rawget(t,prefix)
395            if not p then
396                p = { }
397                rawset(t,prefix,p)
398            end
399            if type(one) == "number" then -- maybe Cc(0 1 2)
400                if #p < one then
401                    for i=#p+1,one-1 do
402                        p[i] = { } -- false
403                    end
404                end
405            end
406            if two then
407                local pone = p[one]
408                if not pone then
409                    pone = { }
410                    p[one] = pone
411                end
412                if type(two) == "number" then -- maybe Cc(0 1 2)
413                    if #pone < two then
414                        for i=#pone+1,two-1 do
415                            pone[i] = { } -- false
416                        end
417                    end
418                end
419                pone[two] = v
420            else
421                p[one] = v
422            end
423        end)
424        --
425        syncdata = setmetatableindex(function(t,category)
426            -- p's and y's are not shared so no need to resolve
427            local list = rawget(collected,"syncpos")
428            local tc = list and rawget(list,category)
429            if tc then
430                sort(tc,function(a,b)
431                    local ap = a.p
432                    local bp = b.p
433                    if ap == bp then
434                        return b.y < a.y
435                    else
436                        return ap < bp
437                    end
438                end)
439                tc.start = 1
440                for i=1,#tc do
441                    checkcommondata(tc[i],x_h_d_list)
442                end
443            else
444                tc = { }
445            end
446            t[category] = tc
447            return tc
448        end)
449    end
450
451    local function finalizer()
452
453        -- We make the (possible extensive) shape lists sparse working from the end. We
454        -- could also drop entries here that have l and r the same which saves testing
455        -- later on.
456
457        local nofpositions = 0
458        local nofpartials  = 0
459        local nofdeltas    = 0
460        --
461        local x_y_w_h_size = 0
462        local x_y_w_h_list = { }
463        local x_y_w_h_hash = setmetatableindex(function(t,x)
464            local y = setmetatableindex(function(t,y)
465                local w = setmetatableindex(function(t,w)
466                    local h = setmetatableindex(function(t,h)
467                        x_y_w_h_size = x_y_w_h_size + 1
468                        t[h] = x_y_w_h_size
469                        x_y_w_h_list[x_y_w_h_size] = { x = x, y = y, w = w, h = h }
470                        return x_y_w_h_size
471                    end)
472                    t[w] = h
473                    return h
474                end)
475                t[y] = w
476                return w
477            end)
478            t[x] = y
479            return y
480        end)
481        --
482        local y_w_h_d_size = 0
483        local y_w_h_d_list = { }
484        local y_w_h_d_hash = setmetatableindex(function(t,y)
485            local w = setmetatableindex(function(t,w)
486                local h = setmetatableindex(function(t,h)
487                    local d = setmetatableindex(function(t,d)
488                        y_w_h_d_size = y_w_h_d_size + 1
489                        t[d] = y_w_h_d_size
490                        y_w_h_d_list[y_w_h_d_size] = { y = y, w = w, h = h, d = d }
491                        return y_w_h_d_size
492                    end)
493                    t[h] = d
494                    return d
495                end)
496                t[w] = h
497                return h
498            end)
499            t[y] = w
500            return w
501        end)
502        --
503        local x_h_d_size = 0
504        local x_h_d_list = { }
505        local x_h_d_hash = setmetatableindex(function(t,x)
506            local h = setmetatableindex(function(t,h)
507                local d = setmetatableindex(function(t,d)
508                    x_h_d_size = x_h_d_size + 1
509                    t[d] = x_h_d_size
510                    x_h_d_list[x_h_d_size] = { x = x, h = h, d = d }
511                    return x_h_d_size
512                end)
513                t[h] = d
514                return d
515            end)
516            t[x] = h
517            return h
518        end)
519        --
520        local x_h_d_hs_size = 0
521        local x_h_d_hs_list = { }
522        local x_h_d_hs_hash = setmetatableindex(function(t,x)
523            local h = setmetatableindex(function(t,h)
524                local d = setmetatableindex(function(t,d)
525                    local hs = setmetatableindex(function(t,hs)
526                        x_h_d_hs_size = x_h_d_hs_size + 1
527                        t[hs] = x_h_d_hs_size
528                        x_h_d_hs_list[x_h_d_hs_size] = { x = x, h = h, d = d, hs = hs }
529                        return x_h_d_hs_size
530                    end)
531                    t[d] = hs
532                    return hs
533                end)
534                t[h] = d
535                return d
536            end)
537            t[x] = h
538            return h
539        end)
540        --
541        rawset(tobesaved,"shared", {
542            x_y_w_h  = x_y_w_h_list,
543            y_w_h_d  = y_w_h_d_list,
544            x_h_d    = x_h_d_list,
545            x_h_d_hs = x_h_d_hs_list,
546        })
547        --
548        -- If fonts can use crazy and hard to grasp packing tricks so can we. The "i" field
549        -- refers to a shared set of values. In addition we pack some sequences.
550        --
551        -- how about free
552        --
553        for k, v in sortedhash(tobesaved) do
554            if k == "p" then
555                -- numeric
556                local n = #v
557                for i=1,n do
558                    local t = v[i]
559                    local hsh = x_h_d_hs_hash[t.x or 0][t.h or 0][t.d or 0][t.hs or 0]
560                    t.x = nil
561                    t.h = nil
562                    t.d = nil
563                    t.hs = nil -- not in syncpos
564                    t.i = hsh
565                    local s = t.s
566                    if s then
567                        checkshapes(s)
568                    end
569                end
570                if deltapacking then
571                    -- delta packing (y)
572                    local last
573                    local current
574                    for i=1,n do
575                        current = v[i]
576                        if last then
577                            for k, v in next, last do
578                                if k ~= "y" and v ~= current[k] then
579                                    goto DIFFERENT
580                                end
581                            end
582                            for k, v in next, current do
583                                if k ~= "y" and v ~= last[k] then
584                                    goto DIFFERENT
585                                end
586                            end
587                            v[i] = current.y or 0
588                            nofdeltas = nofdeltas + 1
589                            goto CONTINUE
590                        end
591                      ::DIFFERENT::
592                        last = current
593                      ::CONTINUE::
594                    end
595                end
596                --
597                nofpositions = nofpositions + n
598                nofpartials  = nofpartials  + n
599            elseif k == "syncpos" then
600                -- hash
601                for k, t in next, v do
602                    -- numeric
603                    local n = #t
604                    for j=1,n do
605                        local t = t[j]
606                        local hsh = x_h_d_hash[t.x or 0][t.h or 0][t.d or 0]
607                        t.x = nil
608                        t.h = nil
609                        t.d = nil
610                        t.i = hsh
611                    end
612                    nofpositions = nofpositions + n
613                    nofpartials  = nofpartials  + n
614                end
615            elseif k == "text" or k == "textarea" then
616                -- numeric
617                local n = #v
618                for i=1,n do
619                    local t = v[i]
620                    local hsh = x_y_w_h_hash[t.x or 0][t.y or 0][t.w or 0][t.h or 0]
621                    t.x = nil
622                    t.y = nil
623                    t.w = nil
624                    t.h = nil
625                    t.i = hsh
626                end
627                nofpositions = nofpositions + n
628                nofpartials  = nofpartials  + n
629                if deltapacking then
630                    -- delta packing (p)
631                    local last
632                    local current
633                    for i=1,n do
634                        current = v[i]
635                        if last then
636                            for k, v in next, last do
637                                if k ~= "p" and v ~= current[k] then
638                                    goto DIFFERENT
639                                end
640                            end
641                            for k, v in next, current do
642                                if k ~= "p" and v ~= last[k] then
643                                    goto DIFFERENT
644                                end
645                            end
646                            v[i] = current.p or 0
647                            nofdeltas = nofdeltas + 1
648                            goto CONTINUE
649                        end
650                      ::DIFFERENT::
651                        last = current
652                      ::CONTINUE::
653                    end
654                end
655            elseif k == "columnarea" then
656                -- numeric
657                local n = #v
658                for i=1,n do
659                    local t = v[i]
660                    local hsh = y_w_h_d_hash[t.y or 0][t.w or 0][t.h or 0][t.d or 0]
661                    t.y = nil
662                    t.w = nil
663                    t.h = nil
664                    t.d = nil
665                    t.i = hsh
666                end
667                nofpositions = nofpositions + n
668                nofpartials  = nofpartials  + n
669            else -- probably only b has shapes
670                for k, t in next, v do -- no need to sort
671                    local s = t.s
672                    if s then
673                        checkshapes(s)
674                    end
675                    nofpositions = nofpositions + 1
676                end
677            end
678        end
679
680        statistics.register("positions", function()
681            if nofpositions > 0 then
682                return format("%s collected, %i deltas, %i shared partials, %i partial entries",
683                    nofpositions, nofdeltas, nofpartials,
684                    x_y_w_h_size + y_w_h_d_size + x_h_d_size + x_h_d_hs_size
685                )
686            else
687                return nil
688            end
689        end)
690
691    end
692
693    freedata = setmetatableindex(function(t,page)
694        local list = rawget(collected,"free")
695        local free = { }
696        if list then
697            local size = 0
698            for i=1,#list do
699                local l = list[i]
700                if l.p == page then
701                    size = size + 1
702                    free[size] = l
703                    checkcommondata(l)
704                end
705            end
706            sort(free,function(a,b) return b.y < a.y end) -- order matters !
707        end
708        t[page] = free
709        return free
710    end)
711
712    job.register('job.positions.collected', tobesaved, initializer, finalizer)
713
714else
715
716    columndata = setmetatableindex("table") -- per page
717    freedata   = setmetatableindex("table") -- per page
718
719    local function initializer()
720        tobesaved = jobpositions.tobesaved
721        collected = jobpositions.collected
722        --
723        local pagedata   = { }
724        local p_splitter = lpeg.splitat(":",true)
725
726        for tag, data in next, collected do
727            local prefix, rest = lpegmatch(p_splitter,tag)
728            if prefix == "page" then
729                pagedata[tonumber(rest) or 0] = data
730            elseif prefix == "free" then
731                local t = freedata[data.p or 0]
732                t[#t+1] = data
733            elseif prefix == "columnarea" then
734                columndata[data.p or 0][data.c or 0] = data
735            end
736            setmetatable(data,default)
737        end
738        local pages = structures.pages.collected
739        if pages then
740            local last = nil
741            for p=1,#pages do
742                local region = "page:" .. p
743                local data   = pagedata[p]
744                local free   = freedata[p]
745                if free then
746                    sort(free,function(a,b) return b.y < a.y end) -- order matters !
747                end
748                if data then
749                    last      = data
750                    last.free = free
751                elseif last then
752                    local t = setmetatableindex({ free = free, p = p },last)
753                    if not collected[region] then
754                        collected[region] = t
755                    else
756                        -- something is wrong
757                    end
758                    pagedata[p] = t
759                end
760            end
761        end
762        jobpositions.pagedata = pagedata -- never used
763
764    end
765
766    local function finalizer()
767
768        -- We make the (possible extensive) shape lists sparse working from the end. We
769        -- could also drop entries here that have l and r the same which saves testing
770        -- later on.
771
772        local nofpositions = 0
773
774        for k, v in next, tobesaved do
775            local s = v.s
776            if s then
777                checkshapes(s)
778            end
779            nofpositions = nofpositions + 1
780        end
781
782        statistics.register("positions", function()
783            if nofpositions > 0 then
784                return format("%s collected",nofpositions)
785            else
786                return nil
787            end
788        end)
789
790    end
791
792    local p_number = lpeg.patterns.cardinal/tonumber
793    local p_tag    = P("syncpos:") * p_number * P(":") * p_number
794
795    syncdata = setmetatableindex(function(t,category)
796        setmetatable(t,nil)
797        for tag, pos in next, collected do
798            local c, n = lpegmatch(p_tag,tag)
799            if c then
800                local tc = t[c]
801                if tc then
802                    tc[n] = pos
803                else
804                    t[c] = { [n] = pos }
805                end
806            end
807        end
808        for k, list in next, t do
809            sort(list,function(a,b)
810                local ap = a.p
811                local bp = b.p
812                if ap == bp then
813                    return b.y < a.y
814                else
815                    return ap < bp
816                end
817            end)
818            list.start = 1
819        end
820        return t[category]
821    end)
822
823    job.register('job.positions.collected', tobesaved, initializer, finalizer)
824
825end
826
827function jobpositions.used()
828    if positionsused == nil then
829        positionsused = false
830        for k, v in next, collected do
831            if k ~= "shared" and type(v) == "table" and next(v) then
832                positionsused = true
833                break
834            end
835        end
836    end
837    return positionsused
838end
839
840function jobpositions.getfree(page)
841    return freedata[page]
842end
843
844function jobpositions.getsync(category)
845    return syncdata[category] or { }
846end
847
848local regions    = { }
849local nofregions = 0
850local region     = nil
851
852local columns    = { }
853local nofcolumns = 0
854local column     = nil
855
856local nofpages   = nil
857
858-- beware ... we're not sparse here as lua will reserve slots for the nilled
859
860local getpos, gethpos, getvpos, getrpos
861
862function jobpositions.registerhandlers(t)
863    getpos  = t and t.getpos  or function() return 0, 0 end
864    getrpos = t and t.getrpos or function() return 0, 0, 0 end
865    gethpos = t and t.gethpos or function() return 0 end
866    getvpos = t and t.getvpos or function() return 0 end
867end
868
869function jobpositions.getpos () return getpos () end
870function jobpositions.getrpos() return getrpos() end
871function jobpositions.gethpos() return gethpos() end
872function jobpositions.getvpos() return getvpos() end
873
874-------- jobpositions.getcolumn() return column end
875
876jobpositions.registerhandlers()
877
878local function setall(name,p,x,y,w,h,d,extra)
879    tobesaved[name] = {
880        p = p,
881        x = x ~= 0 and x or nil,
882        y = y ~= 0 and y or nil,
883        w = w ~= 0 and w or nil,
884        h = h ~= 0 and h or nil,
885        d = d ~= 0 and d or nil,
886        e = extra ~= "" and extra or nil,
887        r = region,
888        c = column,
889        r2l = texgetinteger("inlinelefttoright") == 1 and true or nil,
890    }
891end
892
893local function enhance(data)
894    if not data then
895        return nil
896    end
897    if data.r == true then -- or ""
898        data.r = region
899    end
900    if data.x == true then
901        if data.y == true then
902            local x, y = getpos()
903            data.x = x ~= 0 and x or nil
904            data.y = y ~= 0 and y or nil
905        else
906            local x = gethpos()
907            data.x = x ~= 0 and x or nil
908        end
909    elseif data.y == true then
910        local y = getvpos()
911        data.y = y ~= 0 and y or nil
912    end
913    if data.p == true then
914        data.p = texgetcount(c_realpageno) -- we should use a variable set in otr
915    end
916    if data.c == true then
917        data.c = column
918    end
919    if data.w == 0 then
920        data.w = nil
921    end
922    if data.h == 0 then
923        data.h = nil
924    end
925    if data.d == 0 then
926        data.d = nil
927    end
928    return data
929end
930
931-- analyze some files (with lots if margindata) and then when one key optionally
932-- use that one instead of a table (so, a 3rd / 4th argument: key, e.g. "x")
933
934local function set(name,index,value) -- ,key
935    -- officially there should have been a settobesaved
936    local data = enhance(value or {})
937    if value then
938        container = tobesaved[name]
939        if not container then
940            tobesaved[name] = {
941                [index] = data
942            }
943        else
944            container[index] = data
945        end
946    else
947        tobesaved[name] = data
948    end
949end
950
951local function setspec(specification)
952    local name  = specification.name
953    local index = specification.index
954    local value = specification.value
955    local data  = enhance(value or {})
956    if value then
957        container = tobesaved[name]
958        if not container then
959            tobesaved[name] = {
960                [index] = data
961            }
962        else
963            container[index] = data
964        end
965    else
966        tobesaved[name] = data
967    end
968end
969
970local function get(id,index)
971    if index then
972        local container = collected[id]
973        return container and container[index]
974    else
975        return collected[id]
976    end
977end
978
979------------.setdim  = setdim
980jobpositions.setall  = setall
981jobpositions.set     = set
982jobpositions.setspec = setspec
983jobpositions.get     = get
984
985implement {
986    name      = "dosaveposition",
987    public    = true,
988    protected = true,
989    arguments = { "argument", "integerargument", "dimenargument", "dimenargument" },
990    actions   = setall, -- name p x y
991}
992
993implement {
994    name      = "dosavepositionwhd",
995    public    = true,
996    protected = true,
997    arguments = { "argument", "integerargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument" },
998    actions   = setall, -- name p x y w h d
999}
1000
1001implement {
1002    name      = "dosavepositionplus",
1003    public    = true,
1004    protected = true,
1005    arguments = { "argument", "integerargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "argument" },
1006    actions   = setall,  -- name p x y w h d extra
1007}
1008
1009-- will become private table (could also become attribute driven but too nasty
1010-- as attributes can bleed e.g. in margin stuff)
1011
1012-- not much gain in keeping stack (inc/dec instead of insert/remove)
1013
1014local function b_column(specification)
1015    local tag = specification.tag
1016    local x = gethpos()
1017    tobesaved[tag] = {
1018        r = true,
1019        x = x ~= 0 and x or nil,
1020     -- w = 0,
1021    }
1022    insert(columns,tag)
1023    column = tag
1024end
1025
1026local function e_column()
1027    local t = tobesaved[column]
1028    if not t then
1029        -- something's wrong
1030    else
1031        local x = gethpos() - t.x
1032        t.w = x ~= 0 and x or nil
1033        t.r = region
1034    end
1035    remove(columns)
1036    column = columns[#columns]
1037end
1038
1039jobpositions.b_column = b_column
1040jobpositions.e_column = e_column
1041
1042implement {
1043    name      = "bposcolumn",
1044    arguments = "string",
1045    actions   = function(tag)
1046        insert(columns,tag)
1047        column = tag
1048    end
1049}
1050
1051implement {
1052    name      = "bposcolumnregistered",
1053    arguments = "string",
1054    actions   = function(tag)
1055        insert(columns,tag)
1056        column = tag
1057        ctx_latelua { action = b_column, tag = tag }
1058    end
1059}
1060
1061implement {
1062    name    = "eposcolumn",
1063    actions = function()
1064        remove(columns)
1065        column = columns[#columns]
1066    end
1067}
1068
1069implement {
1070    name    = "eposcolumnregistered",
1071    actions = function()
1072        ctx_latelua { action = e_column }
1073        remove(columns)
1074        column = columns[#columns]
1075    end
1076}
1077
1078-- regions
1079
1080local function b_region(specification)
1081    local tag  = specification.tag or specification
1082    local last = tobesaved[tag]
1083    if last then
1084        local x, y = getpos()
1085        last.x = x ~= 0 and x or nil
1086        last.y = y ~= 0 and y or nil
1087        last.p = texgetcount(c_realpageno)
1088        insert(regions,tag) -- todo: fast stack
1089        region = tag
1090    end
1091end
1092
1093local function e_region(specification)
1094    local last = tobesaved[region]
1095    if last then
1096        local y = getvpos()
1097        if specification.correct then
1098            local h = (last.y or 0) - y
1099            last.h = h ~= 0 and h or nil
1100        end
1101        last.y = y ~= 0 and y or nil
1102        remove(regions) -- todo: fast stack
1103        region = regions[#regions]
1104    end
1105end
1106
1107jobpositions.b_region = b_region
1108jobpositions.e_region = e_region
1109
1110local lastregion
1111
1112local function setregionbox(n,tag,index,k,lo,ro,to,bo,column) -- kind
1113    if not tag or tag == "" then
1114        nofregions = nofregions + 1
1115        tag   = "region"
1116        index = nofregions
1117    elseif index ~= 0 then
1118        -- So we can cheat and pass a zero index and enforce tag as is needed in
1119        -- cases where we fallback on automated region tagging (framed).
1120        tag = tag .. ":" .. index
1121    end
1122    local box = getbox(n)
1123    local w, h, d = getwhd(box)
1124    -- We could set directly but then we also need to check for gaps but as this
1125    -- is direct is is unlikely that we get a gap. We then also need to intecept
1126    -- these auto regions (comning from framed). Too messy and the split in the
1127    -- setter is fast enough.
1128    tobesaved[tag] = {
1129     -- p  = texgetcount(c_realpageno), -- we copy them
1130        x  = 0,
1131        y  = 0,
1132        w  = w  ~= 0 and w  or nil,
1133        h  = h  ~= 0 and h  or nil,
1134        d  = d  ~= 0 and d  or nil,
1135        k  = k  ~= 0 and k  or nil,
1136        lo = lo ~= 0 and lo or nil,
1137        ro = ro ~= 0 and ro or nil,
1138        to = to ~= 0 and to or nil,
1139        bo = bo ~= 0 and bo or nil,
1140        c  = column         or nil,
1141    }
1142    lastregion = tag
1143    return tag, box
1144end
1145
1146-- we can have a finalizer property that we catch in the backend but that demands
1147-- a check for property for each list .. what is the impact
1148
1149-- textarea operates *inside* a box so experiments with pre/post hooks in the
1150-- backend driver didn't work out (because a box can be larger)
1151--
1152-- it also gives no gain to split prefix and number here because in the end we
1153-- push and pop tags as strings, but it save a little on expansion so we do it
1154-- in the interface
1155
1156local function markregionbox(n,tag,index,correct,...) -- correct needs checking
1157    local tag, box = setregionbox(n,tag,index,...)
1158     -- todo: check if tostring is needed with formatter
1159    local push = new_latelua { action = b_region, tag = tag }
1160    local pop  = new_latelua { action = e_region, correct = correct }
1161    -- maybe we should construct a hbox first (needs experimenting) so that we can avoid some at the tex end
1162    local head = getlist(box)
1163    -- no, this fails with \framed[region=...] .. needs thinking
1164 -- if getid(box) ~= hlist_code then
1165 --  -- report("mark region box assumes a hlist, fix this for %a",tag)
1166 --     head = hpack(head)
1167 -- end
1168    if head then
1169        local tail = find_tail(head)
1170        setlink(push,head)
1171        setlink(tail,pop)
1172    else -- we can have a simple push/pop
1173        setlink(push,pop)
1174    end
1175    setlist(box,push)
1176end
1177
1178jobpositions.markregionbox = markregionbox
1179jobpositions.setregionbox  = setregionbox
1180
1181function jobpositions.enhance(name)
1182    enhance(tobesaved[name])
1183end
1184
1185function jobpositions.gettobesaved(name,tag)
1186    local t = tobesaved[name]
1187    if t and tag then
1188        return t[tag]
1189    else
1190        return t
1191    end
1192end
1193
1194function jobpositions.settobesaved(name,tag,data)
1195    local t = tobesaved[name]
1196    if t and tag and data then
1197        t[tag] = data
1198    end
1199end
1200
1201do
1202
1203    local c_anch_positions_paragraph = texiscount("c_anch_positions_paragraph")
1204
1205    local nofparagraphs = 0
1206
1207    local function enhancepar_1(data)
1208        if data then
1209            local par   = data.par -- we can pass twice when we copy
1210            local state = par and getparstate(data.par,true)
1211            if state then
1212                local x, y = getpos()
1213                if x ~= 0 then
1214                    data.x  = x
1215                end
1216                if y ~= 0 then
1217                    data.y  = y
1218                end
1219                data.p = texgetcount(c_realpageno) -- we should use a variable set in otr
1220                if column then
1221                    data.c = column
1222                end
1223                if region then
1224                    data.r = region
1225                end
1226                --
1227                data.par         = nil
1228                local leftskip   = state.leftskip
1229                local rightskip  = state.rightskip
1230                local hangindent = state.hangindent
1231                local hangafter  = state.hangafter
1232                local parindent  = state.parindent
1233                local parshape   = state.parshape
1234                if hangafter ~= 0 and hangafter ~= 1 then
1235                    data.ha = hangafter
1236                end
1237                if hangindent ~= 0 then
1238                    data.hi = hangindent
1239                end
1240                data.hs = state.hsize
1241                if leftskip ~= 0 then
1242                    data.ls = leftskip
1243                end
1244                if parindent ~= 0 then
1245                    data.pi = parindent
1246                end
1247                if rightskip ~= 0 then
1248                    data.rs = rightskip
1249                end
1250                if parshape and #parshape > 0 then
1251                    data.ps = parshape
1252                end
1253            end
1254        end
1255        return data
1256    end
1257
1258    local function enhancepar_2(data)
1259        if data then
1260            local x, y = getpos()
1261            if x ~= 0 then
1262                data.x  = x
1263            end
1264            if y ~= 0 then
1265                data.y  = y
1266            end
1267            data.p = texgetcount(c_realpageno)
1268            if column then
1269                data.c = column
1270            end
1271            if region then
1272                data.r = region
1273            end
1274        end
1275        return data
1276    end
1277
1278    implement {
1279        name    = "parpos",
1280        actions = function()
1281            nofparagraphs = nofparagraphs + 1
1282            texsetcount("global",c_anch_positions_paragraph,nofparagraphs)
1283            local name = f_p_tag(nofparagraphs)
1284            local h = texgetdimen(d_strutht)
1285            local d = texgetdimen(d_strutdp)
1286            --
1287            local top = texgetnest("top","head")
1288            local nxt = top.next
1289            if nxt then
1290                nxt = tonut(nxt)
1291            end
1292            local data
1293            if nxt and getid(nxt) == par_code then -- todo: check node type
1294                local t = {
1295                    h   = h,
1296                    d   = d,
1297                    par = nxt,
1298                }
1299                tobesaved[name] = t
1300                ctx_latelua { action = enhancepar_1, specification = t }
1301            else
1302                -- This is kind of weird but it happens in tables (rows) so we probably
1303                -- need less.
1304                local state      = texgetparstate()
1305                local leftskip   = state.leftskip
1306                local rightskip  = state.rightskip
1307                local hangindent = state.hangindent
1308                local hangafter  = state.hangafter
1309                local parindent  = state.parindent
1310                local parshape   = state.parshape
1311                local t = {
1312                    p  = true,
1313                    c  = true,
1314                    r  = true,
1315                    x  = true,
1316                    y  = true,
1317                    h  = h,
1318                    d  = d,
1319                    hs = state.hsize, -- never 0
1320                }
1321                if leftskip ~= 0 then
1322                    t.ls = leftskip
1323                end
1324                if rightskip ~= 0 then
1325                    t.rs = rightskip
1326                end
1327                if hangindent ~= 0 then
1328                    t.hi = hangindent
1329                end
1330                if hangafter ~= 1 and hangafter ~= 0 then -- can not be zero .. so it needs to be 1 if zero
1331                    t.ha = hangafter
1332                end
1333                if parindent ~= 0 then
1334                    t.pi = parindent
1335                end
1336                if parshape and #parshape > 0 then
1337                    t.ps = parshape
1338                end
1339                tobesaved[name] = t
1340                ctx_latelua { action = enhancepar_2, specification = t }
1341            end
1342        end
1343    }
1344
1345    implement {
1346        name      = "dosetposition",
1347        arguments = "argument",
1348        public    = true,
1349        protected = true,
1350        actions   = function(name)
1351            local spec = {
1352                p   = true,
1353                c   = column,
1354                r   = true,
1355                x   = true,
1356                y   = true,
1357                n   = nofparagraphs > 0 and nofparagraphs or nil,
1358                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1359            }
1360            tobesaved[name] = spec
1361            ctx_latelua { action = enhance, specification = spec }
1362        end
1363    }
1364
1365    implement {
1366        name      = "dosetpositionwhd",
1367        arguments = { "argument", "dimenargument", "dimenargument", "dimenargument" },
1368        public    = true,
1369        protected = true,
1370        actions   = function(name,w,h,d)
1371            local spec = {
1372                p   = true,
1373                c   = column,
1374                r   = true,
1375                x   = true,
1376                y   = true,
1377                w   = w ~= 0 and w or nil,
1378                h   = h ~= 0 and h or nil,
1379                d   = d ~= 0 and d or nil,
1380                n   = nofparagraphs > 0 and nofparagraphs or nil,
1381                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1382            }
1383            tobesaved[name] = spec
1384            ctx_latelua { action = enhance, specification = spec }
1385        end
1386    }
1387
1388    implement {
1389        name      = "dosetpositionbox",
1390        arguments = { "argument", "integerargument" },
1391        public    = true,
1392        protected = true,
1393        actions   = function(name,n)
1394            local box  = getbox(n)
1395            local w, h, d = getwhd(box)
1396            local spec = {
1397                p = true,
1398                c = column,
1399                r = true,
1400                x = true,
1401                y = true,
1402                w = w ~= 0 and w or nil,
1403                h = h ~= 0 and h or nil,
1404                d = d ~= 0 and d or nil,
1405                n = nofparagraphs > 0 and nofparagraphs or nil,
1406                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1407            }
1408            tobesaved[name] = spec
1409            ctx_latelua { action = enhance, specification = spec }
1410        end
1411    }
1412
1413    implement {
1414        name      = "dosetpositionplus",
1415        arguments = { "argument", "dimenargument", "dimenargument", "dimenargument" },
1416        public    = true,
1417        protected = true,
1418        actions   = function(name,w,h,d)
1419            local spec = {
1420                p   = true,
1421                c   = column,
1422                r   = true,
1423                x   = true,
1424                y   = true,
1425                w   = w ~= 0 and w or nil,
1426                h   = h ~= 0 and h or nil,
1427                d   = d ~= 0 and d or nil,
1428                n   = nofparagraphs > 0 and nofparagraphs or nil,
1429                e   = scanstring(),
1430                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1431            }
1432            tobesaved[name] = spec
1433            ctx_latelua { action = enhance, specification = spec }
1434        end
1435    }
1436
1437    implement {
1438        name      = "dosetpositionstrut",
1439        arguments = "argument",
1440        public    = true,
1441        protected = true,
1442        actions   = function(name)
1443            local h = texgetdimen(d_strutht)
1444            local d = texgetdimen(d_strutdp)
1445            local spec = {
1446                p   = true,
1447                c   = column,
1448                r   = true,
1449                x   = true,
1450                y   = true,
1451                h   = h ~= 0 and h or nil,
1452                d   = d ~= 0 and d or nil,
1453                n   = nofparagraphs > 0 and nofparagraphs or nil,
1454                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1455            }
1456            tobesaved[name] = spec
1457            ctx_latelua { action = enhance, specification = spec }
1458        end
1459    }
1460
1461    implement {
1462        name      = "dosetpositionstrutkind",
1463        arguments = { "argument", "integerargument" },
1464        public    = true,
1465        protected = true,
1466        actions   = function(name,kind)
1467            local h = texgetdimen(d_strutht)
1468            local d = texgetdimen(d_strutdp)
1469            local spec = {
1470                k   = kind,
1471                p   = true,
1472                c   = column,
1473                r   = true,
1474                x   = true,
1475                y   = true,
1476                h   = h ~= 0 and h or nil,
1477                d   = d ~= 0 and d or nil,
1478                n   = nofparagraphs > 0 and nofparagraphs or nil,
1479                r2l = texgetinteger("inlinelefttoright") == 1 or nil,
1480            }
1481            tobesaved[name] = spec
1482            ctx_latelua { action = enhance, specification = spec }
1483        end
1484    }
1485
1486end
1487
1488function jobpositions.getreserved(tag,n)
1489    if tag == v_column then
1490        local fulltag = f_tag_three(tag,texgetcount(c_realpageno),n or 1)
1491        local data = collected[fulltag]
1492        if data then
1493            return data, fulltag
1494        end
1495        tag = v_text
1496    end
1497    if tag == v_text then
1498        local fulltag = f_tag_two(tag,texgetcount(c_realpageno))
1499        return collected[fulltag] or false, fulltag
1500    end
1501    return collected[tag] or false, tag
1502end
1503
1504function jobpositions.copy(target,source)
1505    collected[target] = collected[source]
1506end
1507
1508function jobpositions.replace(id,p,x,y,w,h,d)
1509    local c = collected[id]
1510    if c then
1511        c.p = p ; c.x = x ; c.y = y ; c.w = w ; c.h = h ; c.d = d ; -- c g
1512    else
1513        collected[i] = { p = p, x = x, y = y, w = w, h = h, d = d } -- c g
1514    end
1515end
1516
1517local function getpage(id)
1518    local jpi = collected[id]
1519    return jpi and jpi.p
1520end
1521
1522local function getcolumn(id)
1523    local jpi = collected[id]
1524    return jpi and jpi.c or false
1525end
1526
1527local function getparagraph(id)
1528    local jpi = collected[id]
1529    return jpi and jpi.n
1530end
1531
1532local function getregion(id)
1533    local jpi = collected[id]
1534    if jpi then
1535        local r = jpi.r
1536        if r then
1537            return r
1538        end
1539        local p = jpi.p
1540        if p then
1541            return "page:" .. p
1542        end
1543    end
1544    return false
1545end
1546
1547jobpositions.page      = getpage
1548jobpositions.column    = getcolumn
1549jobpositions.paragraph = getparagraph
1550jobpositions.region    = getregion
1551
1552jobpositions.p = getpage      -- not used, kind of obsolete
1553jobpositions.c = getcolumn    -- idem
1554jobpositions.n = getparagraph -- idem
1555jobpositions.r = getregion    -- idem
1556
1557function jobpositions.x(id)
1558    local jpi = collected[id]
1559    return jpi and jpi.x
1560end
1561
1562function jobpositions.y(id)
1563    local jpi = collected[id]
1564    return jpi and jpi.y
1565end
1566
1567function jobpositions.width(id)
1568    local jpi = collected[id]
1569    return jpi and jpi.w
1570end
1571
1572function jobpositions.height(id)
1573    local jpi = collected[id]
1574    return jpi and jpi.h
1575end
1576
1577function jobpositions.depth(id)
1578    local jpi = collected[id]
1579    return jpi and jpi.d
1580end
1581
1582function jobpositions.whd(id)
1583    local jpi = collected[id]
1584    if jpi then
1585        return jpi.h, jpi.h, jpi.d
1586    end
1587end
1588
1589function jobpositions.leftskip(id)
1590    local jpi = collected[id]
1591    return jpi and jpi.ls
1592end
1593
1594function jobpositions.rightskip(id)
1595    local jpi = collected[id]
1596    return jpi and jpi.rs
1597end
1598
1599function jobpositions.hsize(id)
1600    local jpi = collected[id]
1601    return jpi and jpi.hs
1602end
1603
1604function jobpositions.parindent(id)
1605    local jpi = collected[id]
1606    return jpi and jpi.pi
1607end
1608
1609function jobpositions.hangindent(id)
1610    local jpi = collected[id]
1611    return jpi and jpi.hi
1612end
1613
1614function jobpositions.hangafter(id)
1615    local jpi = collected[id]
1616    return jpi and jpi.ha or 1
1617end
1618
1619function jobpositions.xy(id)
1620    local jpi = collected[id]
1621    if jpi then
1622        return jpi.x, jpi.y
1623    else
1624        return 0, 0
1625    end
1626end
1627
1628function jobpositions.lowerleft(id)
1629    local jpi = collected[id]
1630    if jpi then
1631        return jpi.x, jpi.y - jpi.d
1632    else
1633        return 0, 0
1634    end
1635end
1636
1637function jobpositions.lowerright(id)
1638    local jpi = collected[id]
1639    if jpi then
1640        return jpi.x + jpi.w, jpi.y - jpi.d
1641    else
1642        return 0, 0
1643    end
1644end
1645
1646function jobpositions.upperright(id)
1647    local jpi = collected[id]
1648    if jpi then
1649        return jpi.x + jpi.w, jpi.y + jpi.h
1650    else
1651        return 0, 0
1652    end
1653end
1654
1655function jobpositions.upperleft(id)
1656    local jpi = collected[id]
1657    if jpi then
1658        return jpi.x, jpi.y + jpi.h
1659    else
1660        return 0, 0
1661    end
1662end
1663
1664function jobpositions.position(id)
1665    local jpi = collected[id]
1666    if jpi then
1667        return jpi.p, jpi.x, jpi.y, jpi.w, jpi.h, jpi.d
1668    else
1669        return 0, 0, 0, 0, 0, 0
1670    end
1671end
1672
1673local splitter = lpeg.splitat(",")
1674
1675function jobpositions.extra(id,n,default) -- assume numbers
1676    local jpi = collected[id]
1677    if jpi then
1678        local e = jpi.e
1679        if e then
1680            local split = jpi.split
1681            if not split then
1682                split = lpegmatch(splitter,jpi.e)
1683                jpi.split = split
1684            end
1685            return texsp(split[n]) or default -- watch the texsp here
1686        end
1687    end
1688    return default
1689end
1690
1691local function overlapping(one,two,overlappingmargin) -- hm, strings so this is wrong .. texsp
1692    one = collected[one]
1693    two = collected[two]
1694    if one and two and one.p == two.p then
1695        if not overlappingmargin then
1696            overlappingmargin = 2
1697        end
1698        local x_one = one.x
1699        local x_two = two.x
1700        local w_two = two.w
1701        local llx_one = x_one         - overlappingmargin
1702        local urx_two = x_two + w_two + overlappingmargin
1703        if llx_one > urx_two then
1704            return false
1705        end
1706        local w_one = one.w
1707        local urx_one = x_one + w_one + overlappingmargin
1708        local llx_two = x_two         - overlappingmargin
1709        if urx_one < llx_two then
1710            return false
1711        end
1712        local y_one = one.y
1713        local y_two = two.y
1714        local d_one = one.d
1715        local h_two = two.h
1716        local lly_one = y_one - d_one - overlappingmargin
1717        local ury_two = y_two + h_two + overlappingmargin
1718        if lly_one > ury_two then
1719            return false
1720        end
1721        local h_one = one.h
1722        local d_two = two.d
1723        local ury_one = y_one + h_one + overlappingmargin
1724        local lly_two = y_two - d_two - overlappingmargin
1725        if ury_one < lly_two then
1726            return false
1727        end
1728        return true
1729    end
1730end
1731
1732local function onsamepage(list,page)
1733    for id in gmatch(list,"([^,%s]+)") do
1734        local jpi = collected[id]
1735        if jpi then
1736            local p = jpi.p
1737            if not p then
1738                return false
1739            elseif not page then
1740                page = p
1741            elseif page ~= p then
1742                return false
1743            end
1744        end
1745    end
1746    return page
1747end
1748
1749local function columnofpos(realpage,xposition)
1750    local p = columndata[realpage]
1751    if p then
1752        for i=1,#p do
1753            local c = p[i]
1754            local x = c.x or 0
1755            local w = c.w or 0
1756            if xposition >= x and xposition <= (x + w) then
1757                return i
1758            end
1759        end
1760    end
1761    return 1
1762end
1763
1764local function getcolumndata(realpage,column)
1765    local p = columndata[realpage]
1766    if p then
1767        return p[column]
1768    end
1769end
1770
1771jobpositions.overlapping   = overlapping
1772jobpositions.onsamepage    = onsamepage
1773jobpositions.columnofpos   = columnofpos
1774jobpositions.getcolumndata = getcolumndata
1775
1776-- interface
1777
1778implement {
1779    name      = "replacepospxywhd",
1780    arguments = { "argument", "integerargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument", "dimenargument" },
1781    public    = true,
1782    protected = true,
1783    actions   = function(name,page,x,y,w,h,d)
1784        local c = collected[name]
1785        if c then
1786            c.p = page ; c.x = x ; c.y = y ; c.w = w ; c.h = h ; c.d = d ;
1787        else
1788            collected[name] = { p = page, x = x, y = y, w = w, h = h, d = d }
1789        end
1790    end
1791}
1792
1793implement {
1794    name      = "copyposition",
1795    arguments = "2 arguments",
1796    public    = true,
1797    protected = true,
1798    actions   = function(target,source)
1799        collected[target] = collected[source]
1800    end
1801}
1802
1803implement {
1804    name      = "MPp",
1805    arguments = "argument",
1806    public    = true,
1807    actions   = function(name)
1808        local jpi = collected[name]
1809        if jpi then
1810            local p = jpi.p
1811            if p and p ~= true then
1812                context(p)
1813                return
1814            end
1815        end
1816        context('0')
1817    end
1818}
1819
1820implement {
1821    name      = "MPx",
1822    arguments = "argument",
1823    public    = true,
1824    actions   = function(name)
1825        local jpi = collected[name]
1826        if jpi then
1827            local x = jpi.x
1828            if x and x ~= true and x ~= 0 then
1829                context("%.5Fpt",x*pt)
1830                return
1831            end
1832        end
1833        context('0pt')
1834    end
1835}
1836
1837implement {
1838    name      = "MPy",
1839    arguments = "argument",
1840    public    = true,
1841    actions   = function(name)
1842        local jpi = collected[name]
1843        if jpi then
1844            local y = jpi.y
1845            if y and y ~= true and y ~= 0 then
1846                context("%.5Fpt",y*pt)
1847                return
1848            end
1849        end
1850        context('0pt')
1851    end
1852}
1853
1854implement {
1855    name      = "MPw",
1856    arguments = "argument",
1857    public    = true,
1858    actions   = function(name)
1859        local jpi = collected[name]
1860        if jpi then
1861            local w = jpi.w
1862            if w and w ~= 0 then
1863                context("%.5Fpt",w*pt)
1864                return
1865            end
1866        end
1867        context('0pt')
1868    end
1869}
1870
1871implement {
1872    name      = "MPh",
1873    arguments = "argument",
1874    public    = true,
1875    actions   = function(name)
1876        local jpi = collected[name]
1877        if jpi then
1878            local h = jpi.h
1879            if h and h ~= 0 then
1880                context("%.5Fpt",h*pt)
1881                return
1882            end
1883        end
1884        context('0pt')
1885    end
1886}
1887
1888implement {
1889    name      = "MPd",
1890    arguments = "argument",
1891    public    = true,
1892    actions   = function(name)
1893        local jpi = collected[name]
1894        if jpi then
1895            local d = jpi.d
1896            if d and d ~= 0 then
1897                context("%.5Fpt",d*pt)
1898                return
1899            end
1900        end
1901        context('0pt')
1902    end
1903}
1904
1905implement {
1906    name      = "MPxy",
1907    arguments = "argument",
1908    public    = true,
1909    actions   = function(name)
1910        local jpi = collected[name]
1911        if jpi then
1912            context('(%.5Fpt,%.5Fpt)',
1913                jpi.x*pt,
1914                jpi.y*pt
1915            )
1916        else
1917            context('(0,0)')
1918        end
1919    end
1920}
1921
1922implement {
1923    name      = "MPwhd",
1924    arguments = "argument",
1925    public    = true,
1926    actions   = function(name)
1927        local jpi = collected[name]
1928        if jpi then
1929            local w = jpi.w or 0
1930            local h = jpi.h or 0
1931            local d = jpi.d or 0
1932            if w ~= 0 or h ~= 0 or d ~= 0 then
1933                context("%.5Fpt,%.5Fpt,%.5Fpt",w*pt,h*pt,d*pt)
1934                return
1935            end
1936        end
1937        context('0pt,0pt,0pt')
1938    end
1939}
1940
1941implement {
1942    name      = "MPll",
1943    arguments = "argument",
1944    public    = true,
1945    actions   = function(name)
1946        local jpi = collected[name]
1947        if jpi then
1948            context('(%.5Fpt,%.5Fpt)',
1949                 jpi.x       *pt,
1950                (jpi.y-jpi.d)*pt
1951            )
1952        else
1953            context('(0,0)') -- for mp only
1954        end
1955    end
1956}
1957
1958implement {
1959    name      = "MPlr",
1960    arguments = "argument",
1961    public    = true,
1962    actions   = function(name)
1963        local jpi = collected[name]
1964        if jpi then
1965            context('(%.5Fpt,%.5Fpt)',
1966                (jpi.x + jpi.w)*pt,
1967                (jpi.y - jpi.d)*pt
1968            )
1969        else
1970            context('(0,0)') -- for mp only
1971        end
1972    end
1973}
1974
1975implement {
1976    name      = "MPur",
1977    arguments = "argument",
1978    public    = true,
1979    actions   = function(name)
1980        local jpi = collected[name]
1981        if jpi then
1982            context('(%.5Fpt,%.5Fpt)',
1983                (jpi.x + jpi.w)*pt,
1984                (jpi.y + jpi.h)*pt
1985            )
1986        else
1987            context('(0,0)') -- for mp only
1988        end
1989    end
1990}
1991
1992implement {
1993    name      = "MPul",
1994    arguments = "argument",
1995    public    = true,
1996    actions   = function(name)
1997        local jpi = collected[name]
1998        if jpi then
1999            context('(%.5Fpt,%.5Fpt)',
2000                 jpi.x         *pt,
2001                (jpi.y + jpi.h)*pt
2002            )
2003        else
2004            context('(0,0)') -- for mp only
2005        end
2006    end
2007}
2008
2009local function MPpos(id)
2010    local jpi = collected[id]
2011    if jpi then
2012        local p = jpi.p
2013        if p then
2014            context("%s,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt",
2015                p,
2016                jpi.x*pt,
2017                jpi.y*pt,
2018                jpi.w*pt,
2019                jpi.h*pt,
2020                jpi.d*pt
2021            )
2022            return
2023        end
2024    end
2025    context('0,0,0,0,0,0') -- for mp only
2026end
2027
2028implement {
2029    name      = "MPpos",
2030    arguments = "argument",
2031    public    = true,
2032    actions   = MPpos
2033}
2034
2035implement {
2036    name      = "MPn",
2037    arguments = "argument",
2038    public    = true,
2039    actions   = function(name)
2040        local jpi = collected[name]
2041        if jpi then
2042            local n = jpi.n
2043            if n then
2044                context(n)
2045                return
2046            end
2047        end
2048        context(0)
2049    end
2050}
2051
2052implement {
2053    name      = "MPc",
2054    arguments = "argument",
2055    public    = true,
2056    actions   = function(name)
2057        local jpi = collected[name]
2058        if jpi then
2059            local c = jpi.c
2060            if c and c ~= true  then
2061                context(c)
2062                return
2063            end
2064        end
2065        context('0') -- okay ?
2066    end
2067}
2068
2069implement {
2070    name      = "MPr",
2071    arguments = "argument",
2072    public    = true,
2073    actions   = function(name)
2074        local jpi = collected[name]
2075        if jpi then
2076            local r = jpi.r
2077            if r and r ~= true  then
2078                context(r)
2079                return
2080            end
2081            local p = jpi.p
2082            if p and p ~= true then
2083                context("page:" .. p)
2084            end
2085        end
2086    end
2087}
2088
2089local function MPpardata(id)
2090    local t = collected[id]
2091    if not t then
2092        local tag = f_p_tag(id)
2093        t = collected[tag]
2094    end
2095    if t then
2096        context("%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%s,%.5Fpt",
2097            t.hs*pt,
2098            t.ls*pt,
2099            t.rs*pt,
2100            t.hi*pt,
2101            t.ha,
2102            t.pi*pt
2103        )
2104    else
2105        context("0,0,0,0,0,0") -- for mp only
2106    end
2107end
2108
2109implement {
2110    name      = "MPpardata",
2111    arguments = "argument",
2112    public    = true,
2113    actions   = MPpardata
2114}
2115
2116-- implement {
2117--     name      = "MPposset",
2118--     arguments = "argument",
2119--     public    = true,
2120--     actions   = function(name)
2121--         local b = f_b_tag(name)
2122--         local e = f_e_tag(name)
2123--         local w = f_w_tag(name)
2124--         local p = f_p_tag(getparagraph(b))
2125--         MPpos(b) context(",") MPpos(e) context(",") MPpos(w) context(",") MPpos(p) context(",") MPpardata(p)
2126--     end
2127-- }
2128
2129implement {
2130    name      = "MPls",
2131    arguments = "argument",
2132    public    = true,
2133    actions   = function(name)
2134        local jpi = collected[name]
2135        if jpi then
2136            context("%.5Fpt",jpi.ls*pt)
2137        else
2138            context("0pt")
2139        end
2140    end
2141}
2142
2143implement {
2144    name      = "MPrs",
2145    arguments = "argument",
2146    public    = true,
2147    actions   = function(name)
2148        local jpi = collected[name]
2149        if jpi then
2150            context("%.5Fpt",jpi.rs*pt)
2151        else
2152            context("0pt")
2153        end
2154    end
2155}
2156
2157local splitter = lpeg.tsplitat(",")
2158
2159implement {
2160    name      = "MPplus",
2161    arguments = { "argument", "integerargument", "argument" },
2162    public    = true,
2163    actions   = function(name,n,default)
2164        local jpi = collected[name]
2165        if jpi then
2166            local e = jpi.e
2167            if e then
2168                local split = jpi.split
2169                if not split then
2170                    split = lpegmatch(splitter,jpi.e)
2171                    jpi.split = split
2172                end
2173                context(split[n] or default)
2174                return
2175            end
2176        end
2177        context(default)
2178    end
2179}
2180
2181implement {
2182    name      = "MPrest",
2183    arguments = "2 arguments",
2184    public    = true,
2185    actions   = function(name,default)
2186        local jpi = collected[name]
2187        context(jpi and jpi.e or default)
2188    end
2189}
2190
2191implement {
2192    name      = "MPxywhd",
2193    arguments = "argument",
2194    public    = true,
2195    actions   = function(name)
2196        local jpi = collected[name]
2197        if jpi then
2198            context("%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt,%.5Fpt",
2199                jpi.x*pt,
2200                jpi.y*pt,
2201                jpi.w*pt,
2202                jpi.h*pt,
2203                jpi.d*pt
2204            )
2205        else
2206            context("0,0,0,0,0") -- for mp only
2207        end
2208    end
2209}
2210
2211implement {
2212    name      = "doifelseposition",
2213    arguments = "argument",
2214    public    = true,
2215    protected = true,
2216    actions   = function(name)
2217        ctx_doifelse(collected[name])
2218    end
2219}
2220
2221implement {
2222    name      = "doifposition",
2223    arguments = "argument",
2224    public    = true,
2225    protected = true,
2226    actions   = function(name)
2227        ctx_doif(collected[name])
2228    end
2229}
2230
2231implement {
2232    name      = "doifelsepositiononpage",
2233    arguments = { "string", "integerargument" },
2234    public    = true,
2235    protected = true,
2236    actions   = function(name,p)
2237        local c = collected[name]
2238        ctx_doifelse(c and c.p == p)
2239    end
2240}
2241
2242implement {
2243    name      = "doifelseoverlapping",
2244    arguments = "2 arguments",
2245    public    = true,
2246    protected = true,
2247    actions   = function(one,two)
2248        ctx_doifelse(overlapping(one,two))
2249    end
2250}
2251
2252implement {
2253    name      = "doifelsepositionsonsamepage",
2254    arguments = "argument", -- string
2255    public    = true,
2256    protected = true,
2257    actions   = function(list)
2258        ctx_doifelse(onsamepage(list))
2259    end
2260}
2261
2262implement {
2263    name      = "doifelsepositionsonthispage",
2264    arguments = "argument", -- string
2265    public    = true,
2266    protected = true,
2267    actions   = function(list)
2268        ctx_doifelse(onsamepage(list,tostring(texgetcount(c_realpageno))))
2269    end
2270}
2271
2272implement {
2273    name      = "doifelsepositionsused",
2274    public    = true,
2275    protected = true,
2276    actions   = function()
2277        ctx_doifelse(jobpositions.used())
2278    end
2279}
2280
2281implement {
2282    name      = "markregionbox",
2283    arguments = "2 integers",
2284    actions   = markregionbox
2285}
2286
2287implement {
2288    name      = "setregionbox",
2289    arguments = "2 integers",
2290    actions   = setregionbox
2291}
2292
2293implement {
2294    name      = "markregionboxtagged",
2295    arguments = { "integer", "string", "integer" },
2296    actions   = markregionbox
2297}
2298
2299implement {
2300    name      = "markregionboxtaggedn",
2301    arguments = { "integer", "string", "integer", "integer" },
2302    actions   = function(box,tag,index,n)
2303        markregionbox(box,tag,index,nil,nil,nil,nil,nil,nil,n)
2304    end
2305}
2306
2307implement {
2308    name      = "setregionboxtagged",
2309    arguments = { "integer", "string", "integer" },
2310    actions   = setregionbox
2311}
2312
2313implement {
2314    name      = "markregionboxcorrected",
2315    arguments = { "integer", "string", "integer", true },
2316    actions   = markregionbox
2317}
2318
2319implement {
2320    name      = "markregionboxtaggedkind",
2321    arguments = { "integer", "string", "integer", "integer", "dimen", "dimen", "dimen", "dimen" },
2322    actions   = function(box,tag,index,n,d1,d2,d3,d4)
2323        markregionbox(box,tag,index,nil,n,d1,d2,d3,d4)
2324    end
2325}
2326
2327implement {
2328    name    = "reservedautoregiontag",
2329    public  = true,
2330    actions = function()
2331        nofregions = nofregions + 1
2332        context(f_region(nofregions))
2333    end
2334}
2335
2336-- We support the low level positional commands too:
2337
2338local newsavepos = nodes.pool.savepos
2339
2340jobpositions.lastx = 0
2341jobpositions.lasty = 0
2342
2343implement { name = "savepos",  actions = function() context(newsavepos()) end }
2344implement { name = "lastxpos", actions = function() context(jobpositions.lastx) end }
2345implement { name = "lastypos", actions = function() context(jobpositions.lasty) end }
2346